? salesforce-476978.patch ? salesforce_api.476978_1.patch ? salesforce_api/toolkit/soapclient Index: salesforce_api/salesforce_api.admin.inc =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/salesforce/salesforce_api/salesforce_api.admin.inc,v retrieving revision 1.2.2.10 diff -u -p -r1.2.2.10 salesforce_api.admin.inc --- salesforce_api/salesforce_api.admin.inc 31 Jul 2009 08:04:05 -0000 1.2.2.10 +++ salesforce_api/salesforce_api.admin.inc 29 Sep 2009 19:12:14 -0000 @@ -1,5 +1,5 @@ variable_get('salesforce_api_error_log', SALESFORCE_LOG_ALL), ); - + $form['objects'] = array( + '#type' => 'fieldset', + '#title' => t('Object settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => -9, + ); + $form['objects']['clear_cache'] = array( + '#type' => 'item', + '#value' => t('Caching data improves performance, however your Drupal site will be unaware of any alterations made to your Salesforce installation unless the cached data is refreshed. Select the lifetime the object data after which the cache will be automatically refreshed. To refresh all cached object data on your site, click the button below.'), + ); + $period = drupal_map_assoc(array(32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); + $period[CACHE_PERMANENT] = t(''); + $form['objects']['salesforce_api_object_expire'] = array( + '#type' => 'select', + '#title' => t('Cache lifetime'), + '#options' => $period, + '#default_value' => variable_get('salesforce_api_object_expire', CACHE_PERMANENT), + ); + $form['objects']['clear_cache_clear'] = array( + '#type' => 'submit', + '#value' => t('Clear cached object data'), + '#submit' => array('salesforce_api_cache_build'), + ); // Validate handler makes sure that the salesforce_api_password doesn't get set to null on accident $form['#validate'][] = 'salesforce_api_settings_form_validate'; $form['#submit'][] = 'salesforce_api_settings_form_submit'; return system_settings_form($form); } -// Settings form validate handler to verify new salesforce credentials before saving them. +/** + * Settings form validate handler to verify new salesforce credentials before saving them. + */ function salesforce_api_settings_form_validate(&$form, &$form_state) { $values = $form_state['values']; if (!salesforce_api_connect($values['salesforce_api_username'], $values['salesforce_api_password'], $values['salesforce_api_token'], TRUE)) { @@ -89,7 +116,9 @@ function salesforce_api_settings_form_va } } -// Settings form submit handler so that password doesn't get deleted. +/** + * Settings form submit handler so that password doesn't get deleted. + */ function salesforce_api_settings_form_submit(&$form, &$form_state) { // If the user hit "Save Configuration" and the required field // salesforce_api_password is blank, try to get it from variables @@ -101,7 +130,9 @@ function salesforce_api_settings_form_su } } -// Displays an admin table for fieldmaps. +/** + * Displays an admin table for fieldmaps. + */ function salesforce_api_fieldmap_admin() { // Define the header for the admin table. $header = array(t('Index'), t('Drupal object'), t('Salesforce object'), array('data' => t('Operations'), 'colspan' => 3)); @@ -130,7 +161,9 @@ function salesforce_api_fieldmap_admin() return theme('table', $header, $rows); } -// Displays the form to add a fieldmap. +/** + * Displays the form to add a fieldmap. + */ function salesforce_api_fieldmap_add_form(&$form_state) { $form = array(); @@ -180,7 +213,9 @@ function salesforce_api_fieldmap_add_for return $form; } -// Submit handler for a new fieldmap +/** + * FAPI submit handler for a new fieldmap + */ function salesforce_api_fieldmap_add_form_submit($form, &$form_state) { // Create the new fieldmap. $index = salesforce_api_fieldmap_create($form_state['values']['drupal_object'], $form_state['values']['salesforce_object'], $form_state['values']['automatic']); @@ -214,7 +249,9 @@ function salesforce_api_fieldmap_create( return $map['fieldmap']; } -// Displays the confirm form for deleting a fieldmap. +/** + * Displays the confirm form for deleting a fieldmap. + */ function salesforce_api_fieldmap_delete_form(&$form_state, $fieldmap) { // Load the fieldmap from the database. $map = salesforce_api_fieldmap_load($fieldmap); @@ -244,6 +281,9 @@ function salesforce_api_fieldmap_delete_ return confirm_form($form, t('Are you sure you want to delete this fieldmap?'), SALESFORCE_PATH_FIELDMAPS, $desc, t('Delete')); } +/** + * FAPI submit handler for deleting a fieldmap + */ function salesforce_api_fieldmap_delete_form_submit($form, &$form_state) { // Delete the specified fieldmap. salesforce_api_fieldmap_delete($form_state['values']['fieldmap_index']); @@ -254,7 +294,9 @@ function salesforce_api_fieldmap_delete_ $form_state['redirect'] = SALESFORCE_PATH_FIELDMAPS; } -// Displays the edit form for adding field associations to a fieldmap. +/** + * Displays the edit form for adding field associations to a fieldmap. + */ function salesforce_api_fieldmap_edit_form(&$form_state, $fieldmap) { // Load the fieldmap from the database. $map = salesforce_api_fieldmap_load($fieldmap); @@ -350,6 +392,9 @@ function salesforce_api_fieldmap_edit_fo return $form; } +/** + * FAPI submit handler for fieldmap editor + */ function salesforce_api_fieldmap_edit_form_submit($form, &$form_state) { // Load the fieldmap from the database. $map = salesforce_api_fieldmap_load($form_state['values']['fieldmap_index']); @@ -383,7 +428,9 @@ function salesforce_api_fieldmap_edit_fo $form_state['redirect'] = SALESFORCE_PATH_FIELDMAPS; } -// Themes the field associations on a fieldmap edit form into a table. +/** + * Themes the field associations on a fieldmap edit form into a table. + */ function theme_salesforce_api_fieldmap_edit_form_table($form) { // Build the header array. $header = array(); @@ -422,6 +469,115 @@ function theme_salesforce_api_fieldmap_e return theme('table', $header, $rows, $attributes, $caption); } + /** + * Ask salesforce for a list of objects and display a checklist for the user. + * Based on user selection, set up or tear down cached/synched Salesforce data. + * @TODO make this more user friendly. At the moment it's possible for an admin user to blow away + * their entire local SalesForce cache with a few clicks. This is not necessarily desirable. + * + * @param string $form_state + * @return void + * @author aaron + */ +function salesforce_api_admin_object(&$form_state) { + $objects = salesforce_api_describeGlobal(); + $cache = cache_get('salesforce_api_sf_objects'); + $defaults = is_array($cache->data) ? array_keys($cache->data) : array(); + + if (empty($objects->types)) { + drupal_set_message(t('There was an error retrieving the list of SalesForce objects. Please verify that your SalesForce instance is properly configured.'), 'error'); + return; + } + $options = array_combine($objects->types, $objects->types); + + // Disable any SF Object types currently in use by fieldmap(s). + $result = db_query('SELECT DISTINCT salesforce FROM {salesforce_field_map}'); + while ($type = db_result($result)) { + $disabled[$type] = $type; + } + + $fields = array( + 'objects' => array( + '#type' => 'checkboxes', + '#title' => 'SalesForce Objects', + '#description' => 'Check the SalesForce objects you would like to synchronize locally.', + '#options' => $options, + '#default_value' => $defaults, + ), + 'disabled_types' => array('#type' => 'value', '#value' => $disabled), + '#theme' => 'salesforce_api_object_options', + 'submit' => array( + '#type' => 'submit', + '#value' => 'Save' + ), + ); + return $fields; +} + +/** + * FAPI submit handler + * Gather enabled SF Objects and rebuild the cache. + */ +function salesforce_api_admin_object_submit($form, &$form_state) { + $values = $form_state['values']['objects']; + // Start off with all the SF Object types already in use. + $real_types = $form['disabled_types']['#value']; + foreach ($values as $i => $t) { + if (empty($t)) { + continue; + } + $real_types[$i] = $t; + } + $sf_objects = variable_set('salesforce_api_enabled_objects', $real_types); + $objects = salesforce_api_cache_build(); + return; +} + +/** + * Placeholder for per-object configuration settings. Any ideas? + */ +function salesforce_api_admin_object_settings($form_state, $type) { + return array('settings' => array( + '#type' => 'markup', + '#value' => 'Placeholder for per-object configuration settings.' + ) + ); +} + +/** + * Theming function for salesforce_api_admin_setup + * For locally-cached SF Objects, add a "configure" or "re-map" link next to the checkbox + */ +function theme_salesforce_api_object_options($element = NULL) { + if (empty($element['objects']['#options'])) { + return drupal_render($element); + } + $objects = $element['objects']; + $options = $objects['#options']; + + // Disable checkboxes for SF Types which are currently in use. + foreach ($options as $i) { + if (empty($objects[$i]['#value'])) { + continue; + } + if (!empty($element['disabled_types']['#value'][$i])) { + $element['objects'][$i]['#attributes']['disabled'] = 'disabled'; + $element['objects'][$i]['#description'] = + t('This object is in use by one or more fieldmaps and cannot be disabled.'); + } + $link = l('configure', SALESFORCE_PATH_OBJECT .'/'. $i); + if ($_SESSION['objects_error'][$i]) { + $element['objects'][$i]['#prefix'] = '
'; + $element['objects'][$i]['#suffix'] = '
'; + unset($_SESSION['objects_error'][$i]); + $link = l('re-map fields', SALESFORCE_PATH_OBJECT .'/'. $i); + } + $element['objects'][$i]['#title'] .= ' | '. $link; + } + unset($_SESSION['objects_error']); + return drupal_render($element); +} + /** * Demonstrates some of the API functionality through the Salesforce class and * fieldmap functionality. @@ -449,7 +605,7 @@ function salesforce_api_demo($demo = NUL switch ($demo) { case 'data-structure': if ($arg) { - $response = $sf->client->describeSObjects(array(check_plain($arg))); + $response = salesforce_api_describeSObjects(array(check_plain($arg))); if (function_exists('dpm')) { dpm($response); } Index: salesforce_api/salesforce_api.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/salesforce/salesforce_api/salesforce_api.install,v retrieving revision 1.2.2.10 diff -u -p -r1.2.2.10 salesforce_api.install --- salesforce_api/salesforce_api.install 31 Aug 2009 15:00:22 -0000 1.2.2.10 +++ salesforce_api/salesforce_api.install 29 Sep 2009 19:12:14 -0000 @@ -1,5 +1,5 @@ enter your Salesforce API credentials', array('!url' => url(SALESFORCE_PATH_ADMIN))), 'warning'); +drupal_set_message(t('Salesforce API: The default Salesforce object have been enabled, to export/import any other objects see the Object setup page.', array('!url' => url(SALESFORCE_PATH_OBJECT))), 'warning'); } /** Index: salesforce_api/salesforce_api.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/salesforce/salesforce_api/salesforce_api.module,v retrieving revision 1.2.2.21 diff -u -p -r1.2.2.21 salesforce_api.module --- salesforce_api/salesforce_api.module 31 Aug 2009 15:00:22 -0000 1.2.2.21 +++ salesforce_api/salesforce_api.module 29 Sep 2009 19:12:14 -0000 @@ -1,5 +1,5 @@ MENU_CALLBACK, 'file' => 'salesforce_api.admin.inc', ); - + $items[SALESFORCE_PATH_OBJECT] = array( + 'title' => 'Object setup', + 'description' => 'Define which SalesForce objects you would like to be available in your Drupal site.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('salesforce_api_admin_object'), + 'access arguments' => array('administer salesforce'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'salesforce_api.admin.inc', + ); + $items[SALESFORCE_PATH_OBJECT .'/%'] = array( + 'title' => 'Object setup', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('salesforce_api_admin_object_settings', count(explode('/', SALESFORCE_PATH_OBJECT))), + 'access arguments' => array('administer salesforce'), + 'type' => MENU_CALLBACK, + 'file' => 'salesforce_api.admin.inc', + ); return $items; } @@ -164,7 +182,7 @@ function salesforce_api_connect($usernam } // Include the file that defines the class. - require_once(drupal_get_path('module', 'salesforce_api') . '/salesforce.class.inc'); + require_once(drupal_get_path('module', 'salesforce_api') .'/salesforce.class.inc'); // Create a new Salesforce object with the API credentials. $sf = new DrupalSalesforce($username, $password, $token); @@ -201,7 +219,7 @@ function salesforce_api_connect($usernam function salesforce_api_reset_expired_password($sf) { // Append one letter and one digit to the password to make sure we meet // salesforce's password validation requirements. - $new_password = user_password() . 'z9'; + $new_password = user_password() .'z9'; // setPassword() may throw InvalidIdFault or UnexpectedErrorFault exceptions. $sf->client->setPassword($sf->login->userId, $new_password); variable_set('salesforce_api_password', $new_password); @@ -239,42 +257,36 @@ function salesforce_api_fieldmap_objects if ($type == 'salesforce') { $cache = cache_get('salesforce_api_sf_objects'); if ($cache->data == '') { - $sf_objects = variable_get('salesforce_api_enabled_objects', array('Campaign', 'Contact', 'Lead')); - $sf = salesforce_api_connect(); - if (!$sf) { - drupal_set_message(t('Unable to connect to Salesforce using current credentials.', array('!url' => url(SALESFORCE_PATH_ADMIN)))); - return array(); - } - - $result = $sf->client->describeSObjects($sf_objects); - foreach ($result as $key => $object) { - $objects[$sf_objects[$key]] = array( - 'label' => t($sf_objects[$key]), - 'fields' => array(), - ); - foreach ($object->fields as $field) { - $objects[$sf_objects[$key]]['fields'][$field->name] = array( - 'label' => t($field->label), - ); - if ($field->createable != 1) { - $objects[$sf_objects[$key]]['fields'][$field->name]['type'] = SALESFORCE_FIELD_SOURCE_ONLY; - } - elseif ($field->nillable != 1 && $field->defaultedOnCreate != 1) { - $objects[$sf_objects[$key]]['fields'][$field->name]['type'] = SALESFORCE_FIELD_REQUIRED; - } - } - } - cache_set('salesforce_api_sf_objects', $objects); + $objects = salesforce_api_cache_build(); } else { $objects = $cache->data; } } + return $objects; +} +/** + * Recreate the salesforce object cache + */ +function salesforce_api_cache_build() { + $sf_objects = variable_get('salesforce_api_enabled_objects', array('Campaign', 'Contact', 'Lead')); + $sf = salesforce_api_connect(); + $result = salesforce_api_describeSObjects($sf_objects); + foreach ($result as $key => $object) { + $objects[$sf_objects[$key]] = salesforce_api_object_to_fieldmap_fields($object); + } + // find the expiry time + $lifetime = variable_get('salesforce_api_object_expire', CACHE_PERMANENT); + $expire = ($lifetime == CACHE_PERMANENT) ? CACHE_PERMANENT : time() + $lifetime; + cache_set('salesforce_api_sf_objects', $objects, $table = 'cache', $expire, $headers = NULL); + drupal_set_message(t('Salesforce object cache has been refreshed.')); return $objects; } -// Returns an array of system fields that are retrievable from Salesforce. +/** + * Returns an array of system fields that are retrievable from Salesforce. + */ function salesforce_api_fieldmap_system_fields() { $fields = array( 'Id' => array('label' => t('Salesforce ID')), @@ -484,7 +496,9 @@ function salesforce_api_fieldmap_objects return $objects; } -// Returns the label for the object of the specified type and name. +/** + * Returns the label for the object of the specified type and name. + */ function salesforce_api_fieldmap_object_label($type, $name) { // Get the object definition. $object = salesforce_api_fieldmap_objects_load($type, $name); @@ -641,5 +655,146 @@ function salesforce_api_theme($existing, 'file' => 'salesforce_api.admin.inc', 'arguments' => array('form' => NULL), ), + 'salesforce_api_object_options' => array( + 'arguments' => array('element' => NULL) + ), + ); +} + +/** + * Wrapper for SOAP SforceBaseClient::describeGlobal + * @return an SFQueryResult object (look at ->types for an array of SF object types) + */ +function salesforce_api_describeGlobal() { + static $objects; + if (!empty($objects)) { + return $objects; + } + $sf = salesforce_api_connect(); + if ($sf === FALSE) { + $link = l('Please verify that you have completed your SalesForce credentials', SALESFORCE_PATH_ADMIN); + drupal_set_message(t('Unable to connect to SalesForce. !link', array('!link' => $link)), 'error'); + return; + } + $objects = $sf->client->describeGlobal(); + return $objects; +} + +/** + * Convert Salesforce object fields to fieldmap array for saving + */ +function salesforce_api_object_to_fieldmap_fields($object) { + $fieldmap_object = array( + 'label' => t($object->label), + 'fields' => array() ); + + foreach ($object->fields as $field) { + $fieldmap_object['fields'][$field->name]['label'] = t($field->label); + if ($field->createable != 1) { + $fieldmap_object['fields'][$field->name]['type'] = SALESFORCE_FIELD_SOURCE_ONLY; + } + elseif (!$field->nillable && !$field->defaultedOnCreate) { + $fieldmap_object['fields'][$field->name]['type'] = SALESFORCE_FIELD_REQUIRED; + } + else { + $fieldmap_object['fields'][$field->name]['type'] = SALESFORCE_FIELD_OPTIONAL; + } + } + return $fieldmap_object; +} + +/** + * Implementation of hook_cron(). + */ +function salesforce_api_cron() { + $cache = cache_get('salesforce_api_sf_objects'); + // if the cache has already been delete or is expired then rebuild + if (!$cache || time() > $cache->expire) salesforce_api_cache_build(); + return; } + + +/** + * Wrapper for SOAP SforceBaseClient::describeSObject + * Given an sf object type, return the SF Object definition + * @param string type : the machine-readable name of the SF object type +**/ +function salesforce_api_describeSObject($type) { + if (!is_string($type)) { + drupal_set_message(t('DescribeSObject expects a string. ' . gettype($type) . ' received.'), 'error'); + return false; + } + + $objects = salesforce_api_describeSObjects($type); + if(!empty($objects[$type])) { + return $objects[$type]; + } else { + drupal_set_message(t('DescribeSObject failed to find ' . $type), 'error'); + return false; + } +} + +/** + * Wrapper for SOAP SforceBaseClient::describeSObjects + * Given an array of sf object type, return an associative, normalized array of + * SF object definitions, indexed on machine-readable names of SObjects + * @param array types : an array of machine-readable names to SObjects + */ +function salesforce_api_describeSObjects($types) { + static $objects; + if (is_string($types)) { + $types = array($types); + } + if (!is_array($types)) { + drupal_set_message(t('DescribeSObjects expects an array. ' . gettype($types) . ' received.'), 'error'); + return false; + } + + // There is no reason to describe the same object twice in one HTTP request. + // Use a static cache to save API calls and bandwidth. + if (!empty($objects)) { + $outstanding = array_diff($types, array_keys($objects)); + if (empty($outstanding)) { + $ret = array(); + foreach ($types as $k) { + $ret[$k] = $objects[$k]; + } + return $ret; + } + } + + if (is_string($types)) { + $types = array($types); + } + + try { + $sf = salesforce_api_connect(); + if ($sf === FALSE) { + $link = l('Please verify that you have completed your SalesForce credentials', SALESFORCE_PATH_ADMIN); + drupal_set_message(t('Unable to connect to SalesForce. !link', array('!link' => $link)), 'error'); + return; + } + $objects = $sf->client->describeSObjects(array_values($types)); + } catch (Exception $e) { + watchdog('salesforce', 'Unable to establish Salesforce connection while issuing describeSObjects API call.', array(), WATCHDOG_ERROR); + } + if (empty($objects)) { + return array(); + } + + // This is the normalization part: If only one object was described, SalesForce + // returned an object instead of an array. ALWAYS return an array of objects. + if (is_object($objects)) { + $objects = array($objects); + } + + // And make it an associative array for good measure. + $tmp = array(); + foreach ($objects as $o) { + $tmp[$o->name] = $o; + } + $objects = $tmp; + return $objects; +} +