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 @@
 <?php
-// $Id: salesforce_api.admin.inc,v 1.2.2.10 2009/07/31 08:04:05 bevan Exp $
+// $Id$
 
 /**
  * @file
@@ -7,7 +7,9 @@
  *   for general settings and fieldmap administration.
  */
 
-// The settings form at admin/settings/salesforce.
+/**
+ * The settings form at admin/settings/salesforce.
+ */
 function salesforce_api_settings_form() {
   $form = array();
 
@@ -70,14 +72,39 @@ function salesforce_api_settings_form() 
     ),
     '#default_value' => 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('<none>');
+  $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'] = '<div class="error">';
+      $element['objects'][$i]['#suffix'] = '</div>';
+      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 @@
 <?php
-// $Id: salesforce_api.install,v 1.2.2.10 2009/08/31 15:00:22 bevan Exp $
+// $Id$
 
 /**
  * @file
@@ -20,6 +20,7 @@ function salesforce_api_install() {
 function salesforce_api_uninstall() {
   drupal_uninstall_schema('salesforce_api');
   db_query("DELETE FROM {variable} WHERE {name} LIKE 'salesforce%'");
+  db_query("DELETE FROM {cache} WHERE {cid} = 'salesforce_api_sf_objects'");
 }
 
 /**
@@ -27,6 +28,7 @@ function salesforce_api_uninstall() {
  */
 function salesforce_api_enable() {
   drupal_set_message(t('Salesforce API: Before making any Salesforce connections, please <a href="!url">enter your Salesforce API credentials</a>', 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 <a href="!url">Object setup</a> 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 @@
 <?php
-// $Id: salesforce_api.module,v 1.2.2.21 2009/08/31 15:00:22 bevan Exp $
+// $Id$
 
 /**
  * @file
@@ -17,10 +17,12 @@ define('SALESFORCE_DIR_TOOLKIT', SALESFO
 define('SALESFORCE_DIR_SOAPCLIENT', SALESFORCE_DIR_TOOLKIT .'/soapclient');
 define('SALESFORCE_DIR_WSDL', SALESFORCE_DIR .'/wsdl');
 
+
 // Define Drupal paths for various parts of the Salesforce UI.
 define('SALESFORCE_PATH_ADMIN', 'admin/settings/salesforce');
-define('SALESFORCE_PATH_FIELDMAPS', 'admin/settings/salesforce/fieldmap');
-define('SALESFORCE_PATH_DEMO', 'admin/settings/salesforce/demo');
+define('SALESFORCE_PATH_FIELDMAPS', SALESFORCE_PATH_ADMIN .'/fieldmap');
+define('SALESFORCE_PATH_DEMO', SALESFORCE_PATH_ADMIN .'/demo');
+define('SALESFORCE_PATH_OBJECT', SALESFORCE_PATH_ADMIN .'/object');
 
 // Define field importing requirements.
 // TODO: I believe we need to use bitwise operators instead.
@@ -111,7 +113,23 @@ function salesforce_api_menu() {
     'type' => 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 <a href="!url">current credentials</a>.', 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;
+}
+
