You are here

salesforce_api.module in Salesforce Suite 7.2

Defines an API that enables modules to interact with the Salesforce server.

File

salesforce_api/salesforce_api.module
View source
<?php

/**
 * @file
 * Defines an API that enables modules to interact with the Salesforce server.
 *
 */
define('SALESFORCE_API_MINIMUM_VERSION', 1);
define('SALESFORCE_API_CURRENT_VERSION', 1);

// Define default directory paths for the Toolkit and WSDL files.
define('SALESFORCE_DIR', drupal_get_path('module', 'salesforce_api'));
define('SALESFORCE_DIR_TOOLKIT', salesforce_api_locate_toolkit());
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/config/services/salesforce');
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('SALESFORCE_PATH_UPDATE_WSDL', SALESFORCE_PATH_ADMIN . '/wsdl');

// Salesforce schema properties. Not all these are in use yet.
define('SALESFORCE_FIELD_CREATEABLE', 0x1);
define('SALESFORCE_FIELD_DEFAULTEDONCREATE', 0x2);
define('SALESFORCE_FIELD_DEPRECATEDANDHIDDEN', 0x4);
define('SALESFORCE_FIELD_IDLOOKUP', 0x8);
define('SALESFORCE_FIELD_NILLABLE', 0x10);
define('SALESFORCE_FIELD_RESTRICTEDPICKLIST', 0x20);
define('SALESFORCE_FIELD_UNIQUE', 0x40);
define('SALESFORCE_FIELD_UPDATEABLE', 0x80);
define('SALESFORCE_FIELD_SOURCE_ONLY', ~SALESFORCE_FIELD_CREATEABLE);

// Define reporting levels for watchdog messages.
define('SALESFORCE_LOG_NONE', 0);
define('SALESFORCE_LOG_SOME', 5);
define('SALESFORCE_LOG_ALL', 10);
define('SALESFORCE_AUTO_SYNC_OFF', 0x0);
define('SALESFORCE_AUTO_SYNC_CREATE', 0x1);
define('SALESFORCE_AUTO_SYNC_UPDATE', 0x2);
define('SALESFORCE_AUTO_SYNC_DELETE', 0x4);
define('SALESFORCE_DELETED_POLICY_NOOP', 0);
define('SALESFORCE_DELETED_POLICY_UPSERT', 1);
if (!function_exists('is_sfid')) {

  // Without a roundtrip to salesforce.com, checking the string length is the
  // best we can do to verify a Salesforce ID.
  function is_sfid($sfid) {
    if (!is_string($sfid)) {
      return FALSE;
    }
    if (strlen($sfid) == 15 || strlen($sfid) == 18) {
      return TRUE;
    }
    return FALSE;
  }
}

/**
 * Locates the Salesforce PHP Toolkit, if installed.
 *
 * @return string $toolkit_path
 *   The path to the Salesforce Toolkit, or an empty string if not found.
 */
function salesforce_api_locate_toolkit() {
  $toolkit_path = variable_get('salesforce_api_toolkit_path', '');

  // If the toolkit path is not set or is no longer valid, try to find it.
  if ($toolkit_path == '' || !file_exists($toolkit_path . '/soapclient/SforceEnterpriseClient.php')) {
    $profile = drupal_get_profile();
    $config = conf_path();

    // Use Libraries if it is available
    if (function_exists('libraries_get_path')) {
      $toolkit_path = libraries_get_path('salesforce') . '/toolkit';
    }
    else {
      if (file_exists("profiles/{$profile}/libraries/salesforce/toolkit")) {
        $toolkit_path = "profiles/{$profile}/libraries/salesforce/toolkit";
      }
      else {
        if (file_exists("sites/all/libraries/salesforce/toolkit")) {
          $toolkit_path = "sites/all/libraries/salesforce/toolkit";
        }
        else {
          if (file_exists("{$config}/libraries/salesforce/toolkit")) {
            $toolkit_path = "{$config}/libraries/salesforce/toolkit";
          }
          else {
            $toolkit_path = "";
          }
        }
      }
    }
    variable_set('salesforce_api_toolkit_path', $toolkit_path);
  }
  return $toolkit_path;
}

/**
 * Implements hook_init().
 * Checks to see if the Salesforce PHP Toolkit is installed, and warns if it is not.
 */
function salesforce_api_init() {
  if (!salesforce_api_toolkit_installed() && user_access('administer salesforce')) {
    drupal_set_message(t('Salesforce API installed, but missing Salesforce PHP Toolkit. Please make sure Salesforce PHP Toolkit is available at ' . SALESFORCE_DIR_SOAPCLIENT . '/SforceEnterpriseClient.php'), 'error');
    drupal_set_message(t('This message will appear as long as Salesforce API is enabled and the Salesforce PHP Toolkit is missing. Please refer to README.txt or INSTALL.txt for additional information.'), 'error');
  }
}

/**
 * Implements hook_help().
 */
function salesforce_api_help($section) {
  switch ($section) {
    case 'admin/help#salesforce_api':

      // Return a line-break version of the module README
      // @todo Determine the D7 equivalent of filter_filter with the process op and 1 parameter
      return file_get_contents(drupal_get_path('module', 'salesforce_api') . "/../README.txt");
  }
}

/**
 * Implements hook_features_api().
 */
function salesforce_api_features_api() {
  return array(
    'salesforce_fieldmap' => array(
      'name' => t('Salesforce Fieldmaps'),
      'feature_source' => TRUE,
      'default_hook' => 'default_salesforce_fieldmaps',
      'default_file' => FEATURES_DEFAULTS_INCLUDED,
      'file' => drupal_get_path('module', 'salesforce_api') . '/salesforce_api.features.inc',
    ),
  );
}

/**
 * Implements hook_menu().
 */
function salesforce_api_menu() {
  $map_id_arg = count(explode('/', SALESFORCE_PATH_FIELDMAPS));
  $items[SALESFORCE_PATH_ADMIN] = array(
    'title' => 'Salesforce',
    'description' => 'Administer settings related to your Salesforce integration.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_settings_form',
    ),
    'access arguments' => array(
      'administer salesforce',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'salesforce_api.admin.inc',
  );
  $items[SALESFORCE_PATH_ADMIN . '/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  $items[SALESFORCE_PATH_DEMO] = array(
    'title' => 'Test/Demo',
    'page callback' => 'salesforce_api_demo',
    'access arguments' => array(
      'administer salesforce',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
  );
  $items[SALESFORCE_PATH_FIELDMAPS] = array(
    'title' => 'Fieldmaps',
    'description' => 'Administer fieldmap relationships between Drupal objects and Salesforce objects.',
    'page callback' => 'salesforce_api_fieldmap_admin',
    'access arguments' => array(
      'administer salesforce',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'access arguments' => array(
      'administer salesforce',
    ),
    'weight' => 0,
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/add'] = array(
    'title' => 'Add',
    'description' => 'Create a new fieldmap.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_fieldmap_add_form',
    ),
    'access arguments' => array(
      'administer salesforce',
    ),
    'type' => MENU_LOCAL_TASK,
    'weight' => 5,
    'file' => 'salesforce_api.admin.inc',
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap'] = array(
    'title' => 'Edit',
    'description' => 'Edit an existing fieldmap.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_fieldmap_edit_form',
      $map_id_arg,
    ),
    'access callback' => '_salesforce_fieldmap_access',
    'access arguments' => array(
      'administer salesforce',
      'edit',
      $map_id_arg,
    ),
    'file' => 'salesforce_api.admin.inc',
    'weight' => 10,
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/edit'] = array(
    'title' => 'Edit',
    'description' => 'Edit an existing fieldmap.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_fieldmap_edit_form',
      $map_id_arg,
    ),
    'access callback' => '_salesforce_fieldmap_access',
    'access arguments' => array(
      'administer salesforce',
      'edit',
      $map_id_arg,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
    'weight' => 10,
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/remove/%'] = array(
    'title' => 'Edit',
    'description' => 'Edit an existing fieldmap.',
    'page callback' => 'salesforce_api_fieldmap_remove_field',
    'page arguments' => array(
      $map_id_arg,
      $map_id_arg + 2,
    ),
    'access callback' => '_salesforce_fieldmap_access',
    'access arguments' => array(
      'administer salesforce',
      'edit',
      $map_id_arg,
    ),
    'type' => MENU_CALLBACK,
    'file' => 'salesforce_api.admin.inc',
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/clone'] = array(
    'title' => 'Clone',
    'description' => 'Clone an existing fieldmap.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_fieldmap_clone_form',
      $map_id_arg,
    ),
    'access callback' => '_salesforce_fieldmap_access',
    'access arguments' => array(
      'administer salesforce',
      'clone',
      $map_id_arg,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
    'weight' => 15,
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/delete'] = array(
    'title' => 'Delete',
    'description' => 'Delete an existing fieldmap.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_fieldmap_delete_form',
      $map_id_arg,
    ),
    'access callback' => '_salesforce_fieldmap_delete_revert_access',
    'access arguments' => array(
      'administer salesforce',
      'delete',
      $map_id_arg,
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
    'weight' => 20,
  );
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/revert'] = $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/delete'];
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/revert']['title'] = 'Revert';
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/revert']['access callback'] = '_salesforce_fieldmap_delete_revert_access';
  $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/revert']['access arguments'] = array(
    'administer salesforce',
    'revert',
    $map_id_arg,
  );
  $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',
    ); */
  $items[SALESFORCE_PATH_UPDATE_WSDL] = array(
    'title' => 'WSDL',
    'description' => 'Upload a new WSDL XML file and set its location',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'salesforce_api_update_wsdl_form',
    ),
    'access arguments' => array(
      'administer salesforce',
    ),
    'type' => MENU_LOCAL_TASK,
    'file' => 'salesforce_api.admin.inc',
  );
  if (module_exists('ctools')) {
    $items[SALESFORCE_PATH_FIELDMAPS . '/%salesforce_api_fieldmap/export'] = array(
      'title' => 'Export',
      'description' => 'Export a fieldmap.',
      'page callback' => 'salesforce_api_export_salesforce_fieldmap',
      'page arguments' => array(
        $map_id_arg,
      ),
      'access callback' => '_salesforce_fieldmap_access',
      'access arguments' => array(
        'administer salesforce',
        'export',
        $map_id_arg,
      ),
      'type' => MENU_LOCAL_TASK,
      // 'file' => 'salesforce_api.admin.inc',
      'weight' => 25,
    );
    $items[SALESFORCE_PATH_FIELDMAPS . '/import'] = array(
      'title' => 'Import',
      'description' => 'Import a fieldmap.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'salesforce_api_import_salesforce_fieldmap',
      ),
      'access arguments' => array(
        'administer salesforce',
      ),
      'type' => MENU_LOCAL_TASK,
      'file' => 'salesforce_api.admin.inc',
      'weight' => 30,
    );
  }
  return $items;
}

/**
 * %wildcard_load implementation for %salesforce_api_fieldmap menu wildcard.
 * @see salesforce_api_salesforce_fieldmap_load
 */
function salesforce_api_fieldmap_load($name) {
  if (empty($name)) {
    return;
  }
  return salesforce_api_salesforce_fieldmap_load($name);
}

/**
 * Implements hook_permission().
 */
function salesforce_api_permission() {
  return array(
    // @todo: Make this permission more granular, so that users can import and export without having it.
    'administer salesforce' => array(
      'title' => t('administer salesforce'),
      'description' => t('Access Salesforce administration screens, manually export, import, unlink entities'),
      'restrict access' => TRUE,
    ),
    // @todo: Possibly move this into a separate module due to security implications.
    'use php for salesforce fixed values' => array(
      'title' => t('use php for salesforce fixed values'),
      'description' => t('Write PHP code in a fieldmap for exporting values. Has the potential to create fatal errors if done incorrectly.'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Access callback for fieldmap editing screens.
 *
 */
function _salesforce_fieldmap_access($perm, $op = 'edit', $map = NULL) {
  if (empty($map) || !is_object($map)) {
    return FALSE;
  }
  return user_access($perm);
}

/**
 * Access callback for delete / revert operations. Only code-based, overridden
 * maps can be reverted and only database-only maps can be deleted.
 */
function _salesforce_fieldmap_delete_revert_access($perm, $op = 'delete', $map = NULL) {
  if (empty($map) || !is_object($map)) {
    return FALSE;
  }
  switch ($map->type) {
    case 'Normal':
      return $op == 'delete' && user_access($perm);
    case 'Default':
      return FALSE;
    case 'Overridden':
      return $op == 'revert' && user_access($perm);
  }
}

/**
 * Creates an object used for communicating with the Salesforce server and
 *   performs a login to verify the API credentials.
 *
 * @param $username
 *   Username for Salesforce. An email address, most likely. If none passed,
 *     sitewide creds will be used
 * @param $password
 *   Password to Salesforce account.
 * @param $token
 *   Security token from Salesforce.
 * @param $reconnect
 *   By default, subsequent calls to this function will return the same, already
 *     connected Salesforce object as preceding calls. Setting this variable to
 *     TRUE will cause a new connection to be established instead.
 * @return
 *   The Salesforce Client object used to communicate with the Salesforce server
 *     if successful or FALSE if a connection could not be established.
 */
function salesforce_api_connect($username = FALSE, $password = FALSE, $token = FALSE, $reconnect = FALSE) {
  static $sf = FALSE;

  // Return the previously connected object.
  if ($sf && !$reconnect) {
    return $sf;
  }

  // Load up the sitewide API credentials if none were provided.
  $encrypted = variable_get('salesforce_api_encrypt', FALSE);
  $default_username = $encrypted ? salesforce_api_decrypt(variable_get('salesforce_api_username', '')) : variable_get('salesforce_api_username', '');
  $username = $username ? $username : $default_username;
  $password = $password ? $password : ($encrypted ? salesforce_api_decrypt(variable_get('salesforce_api_password', '')) : variable_get('salesforce_api_password', ''));
  $token = $token ? $token : ($encrypted ? salesforce_api_decrypt(variable_get('salesforce_api_token', '')) : variable_get('salesforce_api_token', ''));

  // Boolean, whether we are connecting with the default website user or not.
  $default_site_user = $username == $default_username;
  if (!salesforce_api_toolkit_installed()) {
    return FALSE;
  }

  // Fail early if we didn't receive an API username, password, or token.
  if (empty($username) || empty($password) || empty($token)) {
    salesforce_api_log(SALESFORCE_LOG_SOME, 'Connection to Salesforce
      failed because API credentials have not been set.', array(), WATCHDOG_ERROR);
    return FALSE;
  }

  // Attempt a login.
  $sf = salesforce_api_login($username, $password, $token);
  if ($sf) {

    // Mimic expired password state to debug.
    // $sf->login->passwordExpired = TRUE;
    if ($sf->login->passwordExpired) {
      if ($default_site_user) {
        salesforce_api_reset_expired_password($sf);
      }
      elseif (user_access('administer salesforce')) {
        drupal_set_message(t('Your Salesforce account password expired.  Please
         <a href="https://login.salesforce.com/">login to Salesforce.com</a> and
         change your password.'), 'error');
      }
      else {
        salesforce_api_log(SALESFORCE_LOG_SOME, 'Connection to Salesforce
          due to expired password for @user.', array(
          '@user' => $username,
        ), WATCHDOG_ERROR);
      }
    }
  }
  else {
    salesforce_api_log(SALESFORCE_LOG_SOME, 'Connection to Salesforce failed.', array(), WATCHDOG_ERROR);

    // Or return FALSE to indicate the failure.
    $sf = FALSE;
  }
  return $sf;
}

/**
 * Helper function for salesforce_api_connect(). You should probably not call
 * this function directly
 *
 * @param string $username
 * @param string $password
 * @param string $token
 * @return Salesforce Client object
 */
function salesforce_api_login($username, $password, $token) {
  require_once DRUPAL_ROOT . '/' . SALESFORCE_DIR_SOAPCLIENT . '/SforceEnterpriseClient.php';

  // Create a new Salesforce object with the API credentials.
  $sf = (object) array(
    'username' => $username,
    'password' => $password,
    'token' => $token,
    'client' => new SforceEnterpriseClient(),
  );

  // Default to the uploaded WSDL, then any wsdl in web path if available.
  if (!($dir = variable_get('salesforce_api_dir_wsdl', FALSE))) {
    $dir = SALESFORCE_DIR_WSDL;
  }
  $wsdl = $dir . '/enterprise.wsdl.xml';

  // Otherwise fall back to the one included with the Toolkit.
  if (!file_exists($wsdl)) {
    $wsdl = SALESFORCE_DIR_SOAPCLIENT . '/enterprise.wsdl.xml';
  }

  // Get proxy settings if a proxy is configured.
  $proxy = NULL;
  if (variable_get('salesforce_api_proxy', FALSE)) {
    $proxy = new ProxySettings();
    $proxy->host = variable_get('salesforce_api_proxy_host', '');
    $proxy->port = variable_get('salesforce_api_proxy_port', '');
    $proxy->login = variable_get('salesforce_api_proxy_login', '');
    $proxy->password = variable_get('salesforce_api_proxy_password', '');
  }

  // Connect to the server and login, logging any failures to the watchdog.
  try {

    // Connect to the server.
    // Ensure that the WSDL cache is not set.
    ini_set('soap.wsdl_cache_enabled', '0');
    ini_set('soap.wsdl_cache_ttl', '0');
    $sf->client
      ->createConnection($wsdl, $proxy);

    // Attempt a login with the credentials entered by the user.
    $sf->login = $sf->client
      ->login($username, $password . $token);

    // Log the login occurence.
    salesforce_api_log(SALESFORCE_LOG_ALL, '@user (@email) logged into Salesforce.', array(
      '@user' => $sf->login->userInfo->userFullName,
      '@email' => $sf->login->userInfo->userEmail,
    ));
  } catch (Exception $e) {

    // Log the error message.
    // @todo: Determine whether it is a security risk to show !debug, since it can contain a username, password, and security token.
    salesforce_api_log(SALESFORCE_LOG_SOME, 'Could not login to Salesforce: %message.', array(
      '%message' => $e->faultstring,
      '!debug' => check_plain($sf->client
        ->getLastRequest()),
    ), WATCHDOG_ERROR);

    // Indicate the failed login.
    return FALSE;
  }

  // Indicate the successful login.
  return $sf;
}

/**
 * Wraps watchdog(). Logs a message to the watchdog based on the Salesforce log
 * settings.
 *
 * @param $level
 * @param $message
 * @param $vars
 * @param $severity
 * @param $link
 */
function salesforce_api_log($level, $message, $vars = array(), $severity = WATCHDOG_NOTICE, $link = NULL) {

  // Log nothing for notices if the related log level is not greater than or
  // equal to the level of this message.
  switch ($severity) {
    case WATCHDOG_NOTICE:
      if (variable_get('salesforce_api_activity_log', SALESFORCE_LOG_SOME) < $level) {
        return;
      }
      break;
    case WATCHDOG_WARNING:
    case WATCHDOG_ERROR:
      if (variable_get('salesforce_api_error_log', SALESFORCE_LOG_ALL) < $level) {
        return;
      }
      break;
    default:
      break;
  }

  // Log the message to the watchdog.
  watchdog('salesforce', $message, $vars, $severity, $link);
}

/**
 * Helper function for salesforce_api_connect() to reset an expired password,
 *   for the website's default salesforce user only.
 *
 * @param $sf
 *   The Salesforce client object with an expired password.
 * @return
 *   void
 */
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';

  // setPassword() may throw InvalidIdFault or UnexpectedErrorFault exceptions.
  // @todo: If it may throw exceptions, should it be wrapped in a try/catch block?
  $sf->client
    ->setPassword($sf->login->userId, $new_password);
  variable_set('salesforce_api_password', $new_password);

  // Salesforce changes the security token when the password gets changed and
  // sends an email with the new security token.  The new security token can
  // not be retrieved via the API.
  variable_del('salesforce_api_token');

  // Log the event and alert admins about required steps to complete.
  $vars = array(
    '%user' => $sf->login->userInfo->userFullName,
    '%email' => $sf->login->userInfo->userEmail,
    '!uri' => url(SALESFORCE_PATH_ADMIN, array(
      'absolute' => TRUE,
    )),
  );
  salesforce_api_log(SALESFORCE_LOG_ALL, 'The password for the salesforce
    account %user expired.  Drupal changed it and saved the new password.
    Salesforce updated the security token and emailed it to %email.', $vars);
  salesforce_api_log(SALESFORCE_LOG_SOME, 'Provide the new security token
    at <a href="!uri">Drupal\'s Salesforce settings page</a>.  Salesforce
    emailed it to %email.', $vars, WATCHDOG_ALERT);

  // If salesforce connects on pages for anonymous users then these messages should not be displayed.
  if (user_access('administer salesforce')) {
    drupal_set_message(t('The password for the salesforce account %user expired.
      Drupal changed it and saved the new password.  Salesforce updated the
      security token and emailed it to %email.', $vars));
    drupal_set_message(t('Provide the new security token at
      <a href="!uri">Drupal\'s Salesforce settings page</a>.
      Salesforce emailed it to %email.', $vars), 'error');
  }
}

/**
 * Implements hook_fieldmap_objects().
 *
 * This will pull a cached version (if possible) of the available Salesforce fields for
 * the object(s) in question. This helps prevent the Salesforce API query limit from being reached.
 */
function salesforce_api_fieldmap_objects($type = 'salesforce') {
  $objects = array();

  // Define the data fields available for Salesforce objects.
  if ($type == 'salesforce') {
    $cache = cache_get('salesforce_api_sf_objects');

    // If nothing is in the cache, then fetch the objects from Salesforce.
    if (empty($cache->data)) {
      $objects = salesforce_api_cache_build();
    }
    else {
      $objects = $cache->data;
    }
  }

  // To mimic the structure for the $objects array for Drupal objects,
  // (i.e., $objects[$entity_name][$bundle_name][$field_name]),
  // wrap the return value in 'salesforce'.
  return array(
    'salesforce' => $objects,
  );
}

/**
 * Recreate the Salesforce object cache.
 *
 * @return array
 *  An array of Salesforce objects enabled for use in fieldmaps.
 */
function salesforce_api_cache_build() {

  // @todo: Change Campaign to Case to provide a better demo of a node export.
  $sf_objects = variable_get('salesforce_api_enabled_objects', array(
    'Campaign',
    'Contact',
    'Lead',
  ));
  $result = salesforce_api_describeSObjects($sf_objects);
  foreach ($sf_objects as $i => $obj) {
    $objects[$obj] = salesforce_api_object_to_fieldmap_fields($result[$obj]);
  }

  // Look up the cache expiration time.
  $lifetime = variable_get('salesforce_api_object_expire', CACHE_PERMANENT);
  $expire = $lifetime == CACHE_PERMANENT ? CACHE_PERMANENT : REQUEST_TIME + $lifetime;
  cache_set('salesforce_api_sf_objects', $objects, $table = 'cache', $expire, $headers = NULL);

  // Notify administrators when the Salesforce object cache has been refreshed.
  // This typically happens either when a manual refresh is requested, a new Salesforce object
  // is enabled for mapping, or Drupal does a flush of all caches.
  if (user_access('administer salesforce')) {
    drupal_set_message(t('Salesforce object cache has been refreshed.'));
  }
  return $objects;
}

/**
 * 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'),
    ),
    'IsDeleted' => array(
      'label' => t('Is the object deleted?'),
    ),
    'CreatedById' => array(
      'label' => t('User ID of the creator'),
    ),
    'CreatedDate' => array(
      'label' => t('Creation date and time'),
    ),
    'LastModifiedById' => array(
      'label' => t('User ID of the last modifier'),
    ),
    'LastModifiedDate' => array(
      'label' => t('Last user modification date and time'),
    ),
    'SystemModstamp' => array(
      'label' => t('Last user or system modification date and time'),
    ),
  );
  return $fields;
}

/**
 * Salesforce does not accept email addresses with relative domains, like
 * root@localhost. This function is based on Drupal's valid_email_address.
 * Greater men than I have tried and failed to capture valid email addresses
 * with simple regular expressions. This function merely tries to mimic
 * Salesforce's validation rules, NOT to capture all valid email addresses.
 */
function salesforce_api_valid_email_address($mail) {
  $user = '[a-zA-Z0-9_\\-\\.\\+\\^!#\\$%&*+\\/\\=\\?\\`\\|\\{\\}~\']+';
  $domain = '(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])';
  $tld = '(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])+';
  $domain = '(?:' . $domain . '\\.)+' . $tld;
  $ipv4 = '[0-9]{1,3}(\\.[0-9]{1,3}){3}';
  return preg_match("/^{$user}@({$domain}|(\\[({$ipv4})\\]))\$/", $mail);
}

/**
 * Saves a fieldmap to the database.
 *
 * @param $map
 *   An array containing the fieldmap data using the following keys and values:
 *   - fieldmap: the numeric index of the fieldmap. (Will not be present on creation.)
 *   - fieldmap_name: the machine-readable name of the fieldmap. (Only set on creation.)
 *   - drupal_entity: the kind of Drupal entity being mapped.
 *   - drupal_bundle: the name of the bundle for this entity. (Always 'user' for the user entity.)
 *   - salesforce: the name of a Salesforce object.
 *   - automatic: whether or not the sync should be automatic
 *   - description: a short title or description of the fieldmap
 *   - fields: an array that maps source fields (as keys) to their corresponding
 *       target fields (as values).
 */
function salesforce_api_fieldmap_save(&$map) {
  $primary_keys = array();
  if (!empty($map->fieldmap)) {
    $primary_keys = array(
      'name',
    );
  }
  if (empty($map->name)) {

    // If this fieldmap is being created, set its machine name to be human-readable if possible.
    if (!empty($map->fieldmap_name)) {
      $map->name = $map->fieldmap_name;

      // Unset the fieldmap_name, since it is not part of the database schema.
      unset($map->fieldmap_name);
    }
    else {
      $map->name = md5(microtime());
    }
  }
  drupal_write_record('salesforce_fieldmap', $map, $primary_keys);
}

/**
 * Remove a field from all fieldmaps. This is particularly useful for implementations
 * of hook_field_delete_instance. May be use to delete an occurrence
 * in a single fieldmap (by supplying drupal_entity, drupal_bundle and/or salesforce_type), or every
 * occurence in all fieldmaps (by supplying only the fieldname).
 *
 * @param string $fieldname
 *  The name of the field to be deleted. Either a Field API field, or a Salesforce field
 * @param array $conditions (optional)
 *  If given, limit deleting of the field to this Drupal entity, Drupal bundle, and/or Salesforce type.
 *  These are the fields in the {salesforce_fieldmap} table: drupal_entity, drupal_bundle, salesforce
 * @see sf_entity/sf_entity.module:sf_entity_field_delete_instance
 * @todo Ensure that this behaves correctly, especially for if it is a Salesforce field being deleted.
 */
function salesforce_api_fieldmap_field_delete($fieldname, $conditions = array()) {

  // If entity, bundle, and/or salesforce type are specified, load just the fieldmaps for that combination of conditions.
  if (isset($conditions['drupal_entity']) && isset($conditions['drupal_bundle']) && isset($conditions['salesforce']) || isset($conditions['drupal_entity']) || isset($conditions['salesforce'])) {
    $maps = salesforce_api_salesforce_fieldmap_load_by($conditions);
  }
  else {
    $maps = salesforce_api_salesforce_fieldmap_load_all();
  }
  foreach ($maps as $map) {

    // In the extremely unlikely event that a Salesforce field and a Drupal
    // field share the same name, this function handles both.
    if (isset($conditions['drupal_entity']) && !empty($conditions['drupal_entity'])) {
      $drupal_fieldname = array_search($fieldname, $map->fields);
      if ($drupal_fieldname) {
        unset($map->fields[$drupal_fieldname]);
        if (user_access('administer salesforce')) {
          drupal_set_message(t('Removed Drupal field from salesforce_api !link', array(
            '!link' => l('fieldmap ' . $fieldmap_id, SALESFORCE_PATH_OBJECT . '/' . $fieldmap_id),
          )));
        }
        salesforce_api_fieldmap_save($map);
      }
    }
    if (isset($conditions['salesforce'])) {
      $sf_fieldname = array_search($fieldname, $map->fields);
      if (!empty($map->fields[$sf_fieldname])) {
        unset($map->fields[$sf_fieldname]);
        if (user_access('administer salesforce')) {
          drupal_set_message(t('Removed Salesforce field from salesforce_api !link', array(
            '@link' => l('fieldmap ' . $fieldmap_id, SALESFORCE_PATH_OBJECT . '/' . $fieldmap_id),
          )));
        }
        salesforce_api_fieldmap_save($map);
      }
    }
  }
}

/**
 * Clones a fieldmap.
 *
 * @param $fieldmap
 *   The index or name of the fieldmap to clone.
 * @return
 *   The newly created fieldmap or FALSE if the clone failed.
 */
function salesforce_api_fieldmap_clone($fieldmap) {

  // Load the fieldmap from the database.
  $map = salesforce_api_salesforce_fieldmap_load($fieldmap);

  // Return FALSE if the source fieldmap does not exist.
  if (empty($map)) {
    return FALSE;
  }

  // Save the old fieldmap ids, save new ones, and return it.
  unset($map->fieldmap, $map->name);
  salesforce_api_fieldmap_save($map);
  return !empty($map->name) ? $map : FALSE;
}

/**
 * Deletes a fieldmap from the database.
 *
 * @param $fieldmap
 *   The name of the fieldmap to delete.
 * @return array
 *   The number of fieldmaps and objects that were deleted, or FALSE if none were.
 */
function salesforce_api_fieldmap_delete($fieldmap) {

  // Ensure that the fieldmap parameter is a string.
  if (is_string($fieldmap)) {
    $name = $fieldmap;
  }
  else {
    $name = db_query('SELECT name FROM {salesforce_fieldmap} WHERE fieldmap = :fieldmap', array(
      ':fieldmap' => $fieldmap,
    ))
      ->fetchField();
  }
  if (empty($name)) {
    return FALSE;
  }
  $fieldmaps_deleted = db_delete('salesforce_fieldmap')
    ->condition('name', $name)
    ->execute();
  $objects_deleted = db_delete('salesforce_object_map')
    ->condition('name', $name)
    ->execute();
  if (function_exists('sf_prematch_match_by_delete')) {
    sf_prematch_match_by_delete($name);
  }
  return array(
    'fieldmaps_deleted' => $fieldmaps_deleted,
    'objects_deleted' => $objects_deleted,
  );
}

/**
 * Given a Drupal entity type and Drupal object id, delete an object mapping
 *
 * @param string $oid
 * @param string $entity_name
 * @param string $bundle_name (optional)
 * @return int
 *   The number of object mappings deleted.
 */
function salesforce_api_delete_object_map($oid, $entity_name, $bundle_name = NULL) {
  $num_deleted = db_delete('salesforce_object_map');
  if (isset($bundle_name) && !empty($bundle_name)) {
    $num_deleted
      ->condition('drupal_bundle', $bundle_name);
  }
  $num_deleted
    ->condition('drupal_entity', $entity_name)
    ->condition('oid', $oid)
    ->execute();
  return $num_deleted;
}

/**
 * Returns an array of fieldmaps for use as options in the Forms API.
 *
 * @param $drupal_entity
 *   Filters the fieldmaps by Drupal object.
 * @param $drupal_bundle
 *   Filters the fieldmaps by Drupal bundle.
 * @param $salesforce
 *   Filters the fieldmaps by Salesforce object.
 * @param $automatic
 *   Optional: Filter the fieldmaps to only pull those marked automatic.
 * @return
 *   A FAPI options array of all the matching fieldmaps.
 */
function salesforce_api_fieldmap_options($drupal_entity = NULL, $drupal_bundle = NULL, $salesforce = NULL, $automatic = NULL) {
  $options = array();

  // This does not need to not be optimized for performance since it's only an admin interface.
  $maps = salesforce_api_salesforce_fieldmap_load_all();
  foreach ($maps as $map) {
    if ($drupal_entity && $map->drupal_entity != $drupal_entity) {
      continue;
    }
    if ($drupal_bundle && $map->drupal_bundle != $drupal_bundle) {
      continue;
    }
    if ($salesforce && $map->salesforce != $salesforce) {
      continue;
    }
    if (is_array($map)) {
      $map = (object) $map;
    }

    // Setup some replacement args for the label.
    $args = array(
      '@drupal' => salesforce_api_fieldmap_object_label('drupal', $map->drupal_entity, $map->drupal_bundle),
      '@salesforce' => salesforce_api_fieldmap_object_label('salesforce', 'salesforce', $map->salesforce),
    );
    $options[$map->name] = t('Drupal @drupal to Salesforce @salesforce', $args);
  }
  return $options;
}

/**
 * Returns all or a subset of the objects defined via hook_fieldmap_objects
 *   and hook_fieldmap_objects_alter().
 *
 * @param string $type
 *   valid values: 'drupal' or 'salesforce'
 *   Specify a type to filter the return value to objects of that type.
 * @param string $entity
 *   valid values: if $type == 'salesforce', this should also be 'salesforce'
 *   if $type == 'drupal', this should be a valid entity name
 *   Specify an entity name to filter the return value to that entity alone.
 *   If this parameter is supplied, you must specify a type.
 * @param string $bundle
 *   Specify a bundle name to further filter the return value by bundle.
 *   If this parameter is supplied, you must specify an entity.
 * @param bool $reset
 *   Whether to reset the cache of object definitions.
 * @return
 *  Return value structure depends on the arguments provided.
 *  If no arguments, all fieldmap objects will be returned.
 *  If $type is specified, only objects of that type will be returned, etc.
 */
function salesforce_api_fieldmap_objects_load($type = NULL, $entity = NULL, $bundle = NULL, $reset = FALSE) {
  static $objects = array();

  // If we have not yet cached the object definitions...
  if ($reset || empty($objects)) {

    // Find all the Drupal objects defined by hook_sf_fieldmap().
    $objects['drupal'] = module_invoke_all('fieldmap_objects', 'drupal');

    // Get all the Salesforce objects defined by hook_sf_fieldmap().
    $objects['salesforce'] = module_invoke_all('fieldmap_objects', 'salesforce');

    // Allow other modules to modify the object definitions.
    foreach (module_implements('fieldmap_objects_alter') as $module) {
      $function = $module . '_fieldmap_objects_alter';
      $function($objects);
    }
  }

  // If a particular object type was specified...
  if (!empty($type)) {

    // And a particular object was specified...
    if (!empty($entity)) {

      // Return that object definition if it exists or FALSE if it does not.
      if (!empty($bundle)) {
        if (isset($objects[$type][$entity][$bundle])) {
          return $objects[$type][$entity][$bundle];
        }
        else {
          return FALSE;
        }
      }
      else {
        if (isset($objects[$type][$entity])) {
          return $objects[$type][$entity];
        }
        else {
          return FALSE;
        }
      }
    }
    else {
      if (isset($objects[$type])) {
        return $objects[$type];
      }
      else {
        return FALSE;
      }
    }
  }
  return $objects;
}

/**
 * Simple check for Salesforce Toolkit.
 *
 * @return TRUE if toolkit is present, FALSE otherwise.
 */
function salesforce_api_toolkit_installed() {
  if (file_exists(SALESFORCE_DIR_SOAPCLIENT . '/SforceEnterpriseClient.php')) {
    return TRUE;
  }
  else {
    return FALSE;
  }
}

/**
 * Returns the label for the object of the specified type and name.
 * Note that both the $type and $entity parameters will be 'salesforce'
 * in the case of Salesforce objects.
 *
 * @param $type
 * @param $entity
 * @param $bundle
 * @return string
 *   The label for the object.
 */
function salesforce_api_fieldmap_object_label($type, $entity, $bundle) {

  // Get the object definition.
  $object = salesforce_api_fieldmap_objects_load($type, $entity, $bundle);

  // If no label is specified, return the object name.
  if (!isset($object['label']) && isset($object['fields']['name'])) {
    return check_plain($object['fields']['name']['label']);
  }
  return $object['label'];
}

/**
 * Returns a string of description text for the specified fieldmap.
 */
function salesforce_api_fieldmap_description($map) {
  return t('Maps Salesforce %salesforce objects to Drupal %drupal objects.', array(
    '%drupal' => salesforce_api_fieldmap_object_label('drupal', $map->drupal_entity, $map->drupal_bundle),
    '%salesforce' => salesforce_api_fieldmap_object_label('salesforce', 'salesforce', $map->salesforce),
  ));
}

/**
 * Returns a FAPI options array for specifying a field from the source object to
 *   associate with the target field.
 *
 * @param $object
 *   The source object whose fields we need to filter into the options array.
 * @param $type
 *   The type of the target field's object.
 * @param $name
 *   The name of the target object.
 * @param $field
 *   The name of the target field.
 * @return
 *   A FAPI options array of all the available fields that can map to the
 *     target field.
 */
function salesforce_api_fieldmap_field_options($object, $type = NULL, $name = NULL, $field = NULL) {

  // Define the options array.
  // (Don't add a blank value - we don't want that to be selectable in the fieldmapping UI.)
  $options = array();

  // TODO: Consider filtering these based on the object definition.  For now
  // this function simply uses any field defined for the source object.
  // Loop through all the fields of the source object.
  foreach ($object['fields'] as $key => $data) {

    // Add the field to the options array in the right options group.
    if (!empty($data['group'])) {
      $options[$data['group']][$key] = $data['label'];
    }
    else {
      $options[t('Core fields')][$key] = $data['label'];
    }
  }
  return $options;
}

/**
 * Creates an object for export to Salesforce based on the supplied Drupal
 *   object and fieldmap.
 *
 * @param $name
 *   The name of the fieldmap used to process the Drupal object for the export.
 * @param $drupal_data
 *   The Drupal object used to generate the export.
 * @return
 *   An object containing data ready for export to Salesforce or FALSE if
 *     the operation failed.
 */
function salesforce_api_fieldmap_export_create($name, $drupal_data = NULL) {

  // Load the fieldmap from the database.
  $map = salesforce_api_salesforce_fieldmap_load($name);

  // Fail if the fieldmap does not exist.
  if (!$map) {
    return FALSE;
  }
  $drupal_object_definition = salesforce_api_fieldmap_objects_load('drupal', $map->drupal_entity, $map->drupal_bundle);
  $sf_object_definition = salesforce_api_fieldmap_objects_load('salesforce', 'salesforce', $map->salesforce);
  $object = new stdClass();

  // Loop through the fields on the fieldmap.
  foreach ($map->fields as $sf_fieldname => $drupal_fieldname) {
    $value = '';
    $sf_field_definition = $sf_object_definition['fields'][$sf_fieldname];
    $updateable = $sf_field_definition['type'] & SALESFORCE_FIELD_UPDATEABLE;
    $createable = $sf_field_definition['type'] & SALESFORCE_FIELD_CREATEABLE;
    $nillable = $sf_field_definition['type'] & SALESFORCE_FIELD_NILLABLE;

    // Don't try to update or create fields to which those actions do not apply.
    if (!$updateable && !$createable || empty($drupal_data->salesforce->sfid) && !$createable || !empty($drupal_data->salesforce->sfid) && !$updateable) {
      continue;
    }

    // See if it's a special field
    if (is_array($map->fields[$sf_fieldname])) {
      switch ($map->fields[$sf_fieldname]['type']) {
        case 'fixed':
          if (isset($map->fields[$sf_fieldname]['value'])) {
            $value = $map->fields[$sf_fieldname]['value'];
          }
          break;
        case 'tokens':
          if (isset($map->fields[$sf_fieldname]['value'])) {
            $value = token_replace($map->fields[$sf_fieldname]['value'], array(
              $map->drupal_entity => $drupal_data,
            ), array(
              'clear' => TRUE,
              'sanitize' => FALSE,
            ));
          }
          break;
        case 'php':
          if (isset($map->fields[$sf_fieldname]['value'])) {
            $value = eval($map->fields[$sf_fieldname]['value']);
          }
          break;
      }
    }
    elseif (isset($drupal_object_definition['fields'][$drupal_fieldname]['export'])) {
      $drupal_field_export_handler = $drupal_object_definition['fields'][$drupal_fieldname]['export'];
      $drupal_field_definition = $drupal_object_definition['fields'][$drupal_fieldname];

      // Get the value for the field from the handler function.
      $value = $drupal_field_export_handler($drupal_data, $drupal_fieldname, $drupal_field_definition, $sf_field_definition);
    }
    elseif (isset($drupal_data->{$drupal_fieldname})) {
      $value = $drupal_data->{$drupal_fieldname};
    }

    // Ignore null values for non-nillable fields. We also explicitly set all
    // empty strings to NULL so they too can be subject to these checks.
    if ($value === "") {
      $value = NULL;
    }
    if (is_null($value) && !$nillable) {
      continue;
    }
    elseif (is_null($value) && $nillable && !empty($map->fields[$sf_fieldname])) {
      $fieldsToNull[] = $sf_fieldname;
      continue;
    }

    // Evaluate field-type-specific syntax rules. The point here is not to
    // massage data in any way - that should have been done in the export
    // handler. All we want to do is avoid creating a syntactically invalid
    // object for export. For example, an "id" or "reference" field must contain
    // a Salesforce ID. We do not check to make sure the given Salesforce ID
    // exists, merely that the format matches that of a Salesforce ID.
    //
    // For any errors, just continue to the next iteration and log the error.
    $type = $sf_field_definition['salesforce']['type'];
    $errors = array();
    switch ($type) {
      case 'boolean':
        if (empty($value)) {
          $object->{$sf_fieldname} = 0;
        }
        else {
          $object->{$sf_fieldname} = 1;
        }
        break;
      case 'time':
      case 'date':
      case 'datetime':
        $time = strtotime($value);
        if (empty($time)) {
          $errors[] = array(
            'message' => 'Salesforce cannot accept empty values for @type fields on fieldname @fieldname: "@value" value provided.',
            'vars' => array(
              '@type' => $type,
              '@value' => $value,
              '@fieldname' => $sf_fieldname,
            ),
          );
        }
        else {

          // The export handler should have handled this, but reformat to
          // DATE_ATOM, just in case.
          $object->{$sf_fieldname} = gmdate(DATE_ATOM, $time);
        }
        break;
      case 'email':

        // Remove spaces.
        $value = trim($value);
        if (salesforce_api_valid_email_address($value)) {
          $object->{$sf_fieldname} = $value;
        }
        else {
          $errors[] = array(
            'message' => 'Invalid email address provided for Salesforce export on fieldname @fieldname: @value',
            'vars' => array(
              '@fieldname' => $sf_fieldname,
              '@value' => $value,
            ),
          );
          continue;
        }
        break;
      case 'percent':
      case 'currency':
      case 'int':
      case 'double':
        if (is_numeric($value)) {
          $object->{$sf_fieldname} = $value;
        }
        else {
          $errors[] = array(
            'message' => 'Invalid value provided for Salesforce @type field on fieldname @fieldname: "@value".',
            'vars' => array(
              '@type' => $type,
              '@value' => $value,
              '@fieldname' => $sf_fieldname,
            ),
          );
        }
        break;
      case 'reference':
      case 'id':
        if (!is_sfid($value)) {
          $errors[] = array(
            'message' => 'Invalid value provided for Salesforce @type field on fieldname @fieldname: "@value".',
            'vars' => array(
              '@type' => $type,
              '@value' => $value,
              '@fieldname' => $sf_fieldname,
            ),
          );
        }
        else {
          $object->{$sf_fieldname} = $value;
        }
        break;
      case 'string':
      case 'picklist':
      case 'multipicklist':
      case 'combobox':
      case 'base64':
      case 'textarea':
      case 'phone':
      case 'url':
      case 'encryptedstring':
      default:
        $object->{$sf_fieldname} = $value;
        break;
    }
    $max_len = $sf_field_definition['salesforce']['length'];
    if ($max_len && isset($object->{$sf_fieldname}) && drupal_strlen($object->{$sf_fieldname}) > $max_len) {
      $object->{$sf_fieldname} = drupal_substr($object->{$sf_fieldname}, 0, $max_len);
    }
  }
  if (!empty($errors)) {
    foreach ($errors as $error) {
      salesforce_api_log(SALESFORCE_LOG_SOME, $error['message'], $error['vars'], WATCHDOG_ERROR);
    }
  }
  if (!empty($fieldsToNull)) {
    $object->fieldsToNull = $fieldsToNull;
  }
  return $object;
}

/**
 * Loads the Salesforce ID and fieldmap index of a Drupal object.
 *
 * @param $type
 *   The type of the Drupal object you are requesting data for; node or user.
 * @param $id
 *   The associated unique ID used to identify the object in Drupal.
 * @return
 *   An array containing the associated Salesforce object type and ID or an
 *     empty array if no data was found.
 */

// @todo: What to do if this returns multiple results?
function salesforce_api_id_load($oid, $entity_name, $bundle_name = NULL) {

  // Query the main ID table for the associated data.
  if (empty($bundle_name)) {
    $result = db_query("SELECT sfid, name FROM {salesforce_object_map} WHERE drupal_entity = :drupal_entity AND oid = :oid", array(
      ':drupal_entity' => $entity_name,
      ':oid' => $oid,
    ));
  }
  else {
    $result = db_query("SELECT sfid, name FROM {salesforce_object_map} WHERE drupal_entity = :drupal_entity AND drupal_bundle = :drupal_bundle AND oid = :oid", array(
      ':drupal_entity' => $entity_name,
      'drupal_bundle' => $bundle_name,
      ':oid' => $oid,
    ));
  }
  $data = $result
    ->fetchObject();

  // Return an empty array if no data was found.
  if (!$data) {
    return (object) array(
      'sfid' => NULL,
      'name' => NULL,
    );
  }
  else {

    // Otherwise return the Salesforce object type and ID.
    return $data;
  }
}

/**
 * Get an object id using the Salesforce id and fieldmap.
 *
 * @param $sfid
 *   A saleforce id
 * @param $name
 *   The name of the fieldmap for which data is being requested.
 * @return
 *   The associated unique ID used to identify the object in Drupal or FALSE.
 */
function salesforce_api_get_id_with_sfid($sfid, $name = NULL) {
  if (isset($name)) {
    $result = db_query("SELECT oid FROM {salesforce_object_map} WHERE sfid = :sfid AND name = :name", array(
      ':sfid' => $sfid,
      ':name' => $name,
    ));
  }
  else {
    $result = db_query("SELECT oid FROM {salesforce_object_map} WHERE sfid = :sfid", array(
      ':sfid' => $sfid,
    ));
  }
  return $result
    ->fetchField();
}

/**
 * Saves the Salesforce ID and fieldmap index of a Drupal object.
 * Also stores the timestamp of creation for the object mapping, and when the
 * object was last exported to Salesforce or imported to Drupal.
 *
 * @param int $oid
 *   The associated unique ID used to identify the object in Drupal.
 * @param string $sfid
 *   The Salesforce ID of the associated object in the Salesforce database.
 * @param string $name
 *   The name of the fieldmap used to generate the export.
 * @param string $entity_name
 *   The type of Drupal entity being saved.
 * @param string $bundle_name
 *   The Drupal bundle type being saved.
 * @param string $op_type
 *   The type of operation being performed. Possible values are 'import', 'export', and 'link'.
 * @return
 *   TRUE if was successful in saving the link, FALSE otherwise.
 * @todo salesforce_api_id_save_multi()
 */
function salesforce_api_id_save($oid = NULL, $sfid = NULL, $name = NULL, $entity_name = NULL, $bundle_name = NULL, $op_type = NULL) {

  // Allows other modules to respond to salesforce_api_id_save being called.
  foreach (module_implements('salesforce_api_id_save_alter') as $module) {
    $function = $module . '_salesforce_api_id_save_alter';
    $continue = $function($oid, $sfid, $name, $entity_name, $bundle_name, $op_type);
    if ($continue === FALSE) {
      return FALSE;
    }
  }
  if ($oid) {
    $oid = (int) $oid;
  }
  if ($oid && $sfid && $name && $entity_name && $bundle_name && $op_type) {
    $op_type == 'export' ? $fieldname = 'last_export' : ($fieldname = 'last_import');
    $result = db_merge('salesforce_object_map')
      ->key(array(
      'oid' => $oid,
      'name' => $name,
    ))
      ->insertFields(array(
      'oid' => $oid,
      'sfid' => $sfid,
      'name' => $name,
      'drupal_entity' => $entity_name,
      'drupal_bundle' => $bundle_name,
      'created' => REQUEST_TIME,
      $fieldname => REQUEST_TIME,
    ))
      ->updateFields(array(
      $fieldname => REQUEST_TIME,
    ))
      ->execute();

    // Log if an insert has been performed.
    if ($result == MergeQuery::STATUS_INSERT) {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'On !op, successfully linked Drupal !entity : !bundle !oid to Salesforce ID !sfid with fieldmap !name', array(
        '!op' => $op_type,
        '!entity' => $entity_name,
        '!bundle' => $bundle_name,
        '!oid' => $oid,
        '!sfid' => $sfid,
        '!name' => $name,
      ));
      return TRUE;
    }
    elseif ($result == MergeQuery::STATUS_UPDATE) {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'On !op, successfully re-saved linkage between Drupal !entity : !bundle !oid to Salesforce ID !sfid with fieldmap !name', array(
        '!op' => $op_type,
        '!entity' => $entity_name,
        '!bundle' => $bundle_name,
        '!oid' => $oid,
        '!sfid' => $sfid,
        '!name' => $name,
      ));
      return TRUE;
    }
    else {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'On !op, failed to link Drupal !entity : !bundle !oid to Salesforce ID !sfid with fieldmap !name', array(
        '!op' => $op_type,
        '!entity' => $entity_name,
        '!bundle' => $bundle_name,
        '!oid' => $oid,
        '!sfid' => $sfid,
        '!name' => $name,
      ), WATCHDOG_ERROR);
    }
  }
  else {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Attempted to save Drupal->Salesforce linkage with insufficient information. Drupal entity: !entity, Drupal bundle: !bundle, Drupal entity id: !oid, Salesforce ID: !sfid, Fieldmap: !name, Operation: !op', array(
      '!entity' => $entity_name,
      '!bundle' => $bundle_name,
      '!oid' => $oid,
      '!sfid' => $sfid,
      '!name' => $name,
      '!op' => $op_type,
    ), WATCHDOG_ERROR);
    return FALSE;
  }
}

/**
 * Removes a link between a Salesforce record and a Drupal object. Arguments
 * correspond to the columns in the salesforce_object_map table.
 *
 * @param array $args
 *   Associative array of criteria for deletion. These criteria will be AND'ed
 *   together to create a sql DELETE query. Keys are:
 *     - 'oid'
 *        drupal id of the object (nid, uid, etc).
 *
 *     - 'name'
 *        machine name of the fieldmap corresponding to this linkage.
 *
 *     - 'drupal_entity'
 *        The type of Drupal entity being exported.
 *
 *     - 'sfid'
 *        the salesforce id of the object
 *
 *  Keys can be supplied in various combinations, but $args must not be empty.
 *  EITHER "oid" must be set along with "name" or "drupal_type"
 *  OR
 *  "sfid" must be set
 *
 *  In other words, minimal valid key combinations are:
 *    - 'sfid'
 *    - 'name', 'oid'
 *    - 'drupal_entity', 'oid'
 */

// @todo: Need to add drupal_bundle in here as an option?
//        Would need to be combined with something, probably, in order to make it meaningful.
//        Also consider adding date-based unlinking (i.e., created, last_import, last_export.
function salesforce_api_id_unlink($args) {
  $valid_args = !empty($args['sfid']) || !empty($args['oid']) && (!empty($args['drupal_entity']) || !empty($args['name']));
  if (!$valid_args) {
    return FALSE;
  }
  $num_deleted = db_delete('salesforce_object_map');
  if (!empty($args['oid'])) {
    $num_deleted
      ->condition('oid', $args['oid']);
  }
  if (!empty($args['sfid'])) {
    $num_deleted
      ->condition('sfid', $args['sfid']);
  }
  if (!empty($args['drupal_entity'])) {
    $num_deleted
      ->condition('drupal_entity', $args['drupal_entity']);
  }
  if (!empty($args['name'])) {
    $num_deleted
      ->condition('name', $args['name']);
  }
  $num_deleted
    ->execute();
  module_invoke_all('salesforce_api_post_unlink', $args);
}

/**
 * Wrapper for SFBaseClient::delete
 *
 * @param string $sfid a Salesforce ID
 */
function salesforce_api_delete_salesforce_objects($sfids) {
  if (empty($sfids)) {
    return;
  }
  if (is_string($sfids)) {
    $sfids = array(
      $sfids,
    );
  }
  $real_sfids = array();
  foreach ($sfids as $i => $sfid) {
    if (is_sfid($sfid)) {
      $real_sfids[$i] = $sfid;
    }
  }
  if (empty($real_sfids)) {
    return FALSE;
  }
  try {
    $sf = salesforce_api_connect();
    if (!$sf) {
      throw new Exception('Unable to connect to Salesforce');
    }
    return $sf->client
      ->delete($real_sfids);
  } catch (Exception $e) {
    salesforce_api_log(SALESFORCE_LOG_SOME, 'Failed to delete Salesforce objects with ids %s : %s.', array(
      implode(', ', $real_sfids),
      $e
        ->getMessage(),
    ), WATCHDOG_ERROR);
    return FALSE;
  }
}

/**
 * Wrapper function for the sf_find_match hook, implemented by sf_match in the core Salesforce Suite.
 */
function salesforce_api_search_for_duplicates($direction, $entity_name, $bundle_name, $object, $fieldmap_name) {

  // Call hook_sf_find_match to give opportunity to try to match existing sf object instead
  // of creating a new one. No hook_sf_find_match is defined out of the box. Developers must
  // implement their own logic for this one.
  return module_invoke_all('sf_find_match', $direction, $entity_name, $bundle_name, $object, $fieldmap_name);
}

/**
 * Implements hook_theme().
 *
 * Registers theme callback for admin screen
 */
function salesforce_api_theme($existing, $type, $theme, $path) {
  return array(
    'salesforce_api_fieldmap_edit_form_table' => array(
      'file' => 'salesforce_api.admin.inc',
      'render element' => 'form',
    ),
    'salesforce_api_object_options' => array(
      'render element' => 'element',
    ),
    'salesforce_api_drupal_sfapi_automatic' => array(
      'render element' => 'element',
    ),
  );
}

/**
 * Wraps SforceBaseClient::query. Queries Salesforce for a record or set of records. For information about SOQL syntax, @see http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_soql_select.htm
 * @param string $query
 *  A SOQL query string.
 * @param array $options.
 *  An array of options for how the query should be done.
 *  Valid options are:
 *  queryAll (bool)
 *    Whether or not to include deleted records in this query. Default FALSE.
 *  queryMore (bool)
 *    Whether or not to include all records matching this query. Default FALSE.
 *  limit (integer)
 *    Set the SOQL Batch Size. This is NOT analagous to SOQL LIMIT (nor SQL
 *    LIMIT). Minimum value is 200. Maximum value is 2,000. Default varies. For
 *    more information about batch size, @see http://www.salesforce.com/us/developer/docs/api/Content/sforce_api_calls_soql_changing_batch_size.htm
 * @param object $sf
 *  A Salesforce connection. If not set, this function will connect.
 * @return object
 *  An array of matching records on success, or FALSE on failure.
 */
function salesforce_api_query($query, $options = array(), $sf = NULL) {

  // If not passed a Salesforce connection, then connect to Salesforce.
  if (!is_object($sf)) {
    $sf = salesforce_api_connect();

    // Return FALSE if could not connect to Salesforce.
    if ($sf == FALSE) {
      return FALSE;
    }
  }

  // Merge in defaults.
  $options += array(
    'queryAll' => FALSE,
    'queryMore' => FALSE,
    'limit' => NULL,
  );

  // Set a limit for the query if requested.
  // @todo: Determine why this limit is not being applied.
  if (is_numeric($options['limit']) && $options['limit'] > 0) {
    if ($options['limit'] > 2000) {
      $options['limit'] = 2000;
    }
    if ($options['limit'] < 200) {
      $options['limit'] = 200;
    }
    $queryOptions = new QueryOptions($options['limit']);
    $sf->client
      ->setQueryOptions($queryOptions);
  }

  // Execute the query. Include deleted records if set to queryAll.
  try {
    if (!$options['queryAll']) {
      $result = $sf->client
        ->query($query);
    }
    else {
      $query = preg_replace("@SELECT\\s+@si", "SELECT IsDeleted, ", $query);
      $result = $sf->client
        ->queryAll($query);
    }
  } catch (Exception $e) {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce query failed with exception: ' . $e
      ->getMessage(), array(), WATCHDOG_ERROR);
    return FALSE;
  }
  $records = $result->records;

  // If set to queryMore and this query hasn't retrieved all the results, then query for the rest.
  // @todo: Find a resultset large enough to test this.
  if (!$result->done && $options['queryMore']) {
    $moreRecords = _salesforce_api_querymore($result->queryLocator, $sf);
    if (is_array($moreRecords)) {
      $records = array_merge($records, $moreRecords);
    }
  }

  // Return records if any have been retrieved, or else return FALSE.
  return is_array($records) ? $records : FALSE;
}

/**
 * Wraps SforceBaseClient::queryMore. Needs a query locator for an active query
 * and a Salesforce connection, so this must only be called from salesforce_api_query().
 * Calls itself recursively until records are retrieved.
 *
 * @param object $queryLocator
 *  The position within the current query from which to begin.
 * @param object $sf
 *  The active Salesforce connection.
 * @return array
 *  An array of matching records on success, or FALSE on failure.
 */
function _salesforce_api_querymore($queryLocator, $sf) {
  if (!is_object($sf)) {
    return FALSE;
  }

  // Execute the queryMore
  try {
    $result = $sf->client
      ->queryMore($queryLocator);
  } catch (Exception $e) {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce queryMore failed with exception: ' . $e
      ->getMessage(), array(), WATCHDOG_ERROR);
    return FALSE;
  }

  // Recursively merge results
  $records = $result->records;
  if (!$result->done) {
    $moreRecords = _salesforce_api_querymore($result->queryLocator, $sf);
    if (is_array($moreRecords)) {
      $records = array_merge($records, $moreRecords);
    }
  }
  return $records;
}

/**
 * Wraps SforceBaseClient::upsert. Upserts a record in Salesforce.
 * If there is an existing record in Salesforce with the same ID, that record is
 * updated. Otherwise, a new record is created.
 * @param array $records
 *  Either an array of arrays of Salesforce fields, to be converted to sObjects, of the specified
 *  type, or else an array of sObjects.
 * @param string $type
 *  The type of sObject to update. Must either be a core sObject, or a custom type defined in your
 *  WSDL. Custom types all end in "__c". Default type is "Contact".
 * @param string $key
 *  The Salesforce field, or external ID, on which to upsert. Default is "Id".
 * @param object $sf
 *  A currently-active Salesforce connection. If none is passed in, one will be created.
 * @return array
 *  An array containing an array of Salesforce IDs successfully upserted, and the number of failures,
 *  or FALSE if no Salesforce connection could be made or an exception was thrown.
*/
function salesforce_api_upsert(array $records, $type = 'Contact', $key = 'Id', $sf = NULL) {

  // Connects to Salesforce if no existing connection supplied.
  if (!is_object($sf)) {
    $sf = salesforce_api_connect();

    // Return FALSE if could not connect to Salesforce.
    if ($sf == FALSE) {
      return FALSE;
    }
  }

  // Iterates through the records passed in and convert them to objects if necessary.
  $i = 0;
  foreach ($records as $record) {
    if (!is_object($record)) {
      $records[$i] = (object) $record;
    }
    $i++;
  }
  try {
    $results = $sf->client
      ->upsert($key, $records, $type);
  } catch (Exception $e) {
    salesforce_api_log(SALESFORCE_LOG_SOME, 'The following exception occurred while attempting to upsert records: <pre>%e</pre>', array(
      '%msg' => $e
        ->getMessage(),
      '%e' => print_r($e, TRUE),
    ), WATCHDOG_ERROR);
    return FALSE;
  }

  // Sets up the variables for the array of information about results of upsert operation.
  $success_ids = array();
  $failures = 0;
  $created_ids = array();
  $updated_ids = array();

  // $is_deleted_ids = array();
  // Iterate over the resultset.
  foreach ($results as $result) {

    // Handle any errors.
    // @todo: Log is_deleted errors separately, so they can be handled by unlink & upsert on
    // a successive call to this function.
    if (isset($result->errors) && is_array($result->errors)) {
      $err_msgs = array();
      $status_codes = array();

      // Log all errors to watchdog.
      // @todo: Present them more nicely than an array with print_r().
      foreach ($result->errors as $error) {
        $err_msgs[] = $error->message;
        $status_codes[] = $error->statusCode;
        if ($error->statusCode == 'ENTITY_IS_DELETED') {

          // @todo: Figure out a way to determine which one was deleted.
        }
      }
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Errors occurred while attempting to upsert record: <pre>%msgs</pre>', array(
        '%msgs' => print_r($err_msgs, TRUE),
        '%codes' => print_r($status_codes, TRUE),
      ), WATCHDOG_ERROR);

      // Increment the number of failures.
      $failures++;
    }
    elseif (isset($result->success) && $result->success == TRUE) {
      $success_ids[] = $result->id;

      // Separates successes into creates and updates.
      if (isset($result->created) && $result->created == TRUE) {
        $created_ids[] = $result->id;
      }
      else {
        $updated_ids[] = $result->id;
      }
    }
  }

  // Return the ids of results, grouped appropriately.
  $result_info = array(
    'successes' => $success_ids,
    'failures' => $failures,
    'created' => $created_ids,
    'updated' => $updated_ids,
  );
  return $result_info;
}

/**
 * Wraps SforceBaseClient::retrieve. Retrieve an object from Salesforce with
 * standard fields and any data in fields defined in the name object.
 *
 * @param $ids
 *   An array of Salesforce IDs for the objects to retrieve.
 * @param $name
 *   The name of the fieldmap that contains the fields to retrieve.
 * @return
 *   The single matching Salesforce objects or an array of all the objects
 *     if more than one are returned.
 */
function salesforce_api_retrieve($ids, $name) {
  $sf = salesforce_api_connect();
  if (!$sf) {

    // Let modules react to a failure to export this node.
    module_invoke_all('salesforce_api_export_connect_fail', NULL, $name, $ids);
    if (user_access('administer salesforce')) {
      drupal_set_message(t('Unable to connect to Salesforce using <a href="!url">current credentials</a>.', array(
        '!url' => url(SALESFORCE_PATH_ADMIN),
      )));
    }
    return FALSE;
  }

  // Load the fieldmap so we can get the object name.
  $map = salesforce_api_salesforce_fieldmap_load($name);
  $object = salesforce_api_fieldmap_objects_load('salesforce', 'salesforce', $map->salesforce);
  $fields = array_keys($object['fields']);
  return $sf->client
    ->retrieve(implode(', ', $fields), $map->salesforce, $ids);
}

/**
 * Wrapper for SOAP SforceBaseClient::getUpdated. Searches for records
 * updated/created between start and end date.
 * @param string $type
 *  The name of the Salesforce object for which to retrieve data, or
 *  a Salesforce fieldmap object.
 * @param int $start
 *  The timestamp for the beginning of the query.
 * @param int $end
 *  The timestamp for the end of the query.
 * @return FALSE if failed, or an object containing an array of Ids and the latest date covered.
 *  $response->ids = array of SFIDS
 *  $response->latestDateCovered = timestamp of latest updated Salesforce object
 */
function salesforce_api_get_updated($type, $start, $end) {
  if (!$type || !$start || !$end) {
    return FALSE;
  }

  // If $type is an object, we only need the Salesforce type.
  if (is_object($type) && $type->salesforce) {
    $type = $type->salesforce;
  }
  $sf = salesforce_api_connect();
  if ($sf) {
    try {
      $response = $sf->client
        ->getUpdated($type, (int) $start, (int) $end);
    } catch (Exception $e) {

      // Log the error message.
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Could not get updated records from Salesforce: %message.', array(
        '%message' => $e->faultstring,
        '!debug' => check_plain($sf->client
          ->getLastRequest()),
      ), WATCHDOG_ERROR);

      // Indicate the failed action.
      return FALSE;
    }
    if (isset($response->ids)) {
      return $response;
    }
    else {
      return FALSE;
    }
  }
}

/**
 * Wrapper for SOAP SforceBaseClient::getDeleted. Searches for records
 * deleted between start and end date.
 * @param string $type
 *  The name of the Salesforce object to retrieve data for.
 * @param int $start
 *  The timestamp for the beginning of the query.
 * @param int $end
 *  The timestamp for the end of the query.
 * @return FALSE if failed, or an object containing an array of Ids and the latest date covered.
 *  $response->deletedRecords = array of objects containing deletedDate and SFID
 *  $response->earliestDateAvailable = timestamp of the earliest available deleted object for the query
 *  $response->latestDateCovered = timestamp of latest deleted Salesforce object
 */
function salesforce_api_get_deleted($type, $start, $end) {
  if (!$type || !$start || !$end) {
    return FALSE;
  }
  $sf = salesforce_api_connect();
  if ($sf) {
    try {
      $response = $sf->client
        ->getDeleted($type, $start, $end);
    } catch (Exception $e) {

      // Log the error message.
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Could not get deleted records from Salesforce: %message.', array(
        '%message' => $e->faultstring,
        '!debug' => check_plain($sf->client
          ->getLastRequest()),
      ), WATCHDOG_ERROR);

      // Indicate the failed delete.
      return FALSE;
    }
    if ($response->deletedRecords) {
      return $response;
    }
    else {
      return FALSE;
    }
  }
}

/**
 * Wrapper for SOAP SforceBaseClient::describeGlobal
 * @return an SFQueryResult object (look at ->types for an array of SF object types)
 */
function salesforce_api_describeGlobal() {
  static $response;
  if (!empty($response)) {
    return $response;
  }
  $sf = salesforce_api_connect();
  if ($sf === FALSE) {
    $link = l('Please verify that you have completed your Salesforce credentials', SALESFORCE_PATH_ADMIN);
    if (user_access('administer salesforce')) {
      drupal_set_message(t('Unable to connect to Salesforce. !link', array(
        '!link' => $link,
      )), 'error');
    }
    return;
  }
  $response = $sf->client
    ->describeGlobal();
  if (isset($response->sobjects)) {
    $response->types = $response->sobjects;
    unset($response->sobjects);
  }
  return $response;
}

/**
 * Convert Salesforce object fields to fieldmap array for saving
 */
function salesforce_api_object_to_fieldmap_fields($object) {
  $fieldmap_object = array(
    'label' => $object->label,
    'fields' => array(),
  );
  if (!empty($object->fields) && is_array($object->fields)) {
    foreach ($object->fields as $field) {
      $properties = array(
        'name',
        'label',
        'type',
        'length',
        'soapType',
      );
      $booleans = array(
        'createable',
        'defaultedOnCreate',
        'deprecatedAndHidden',
        'idLookup',
        'nillable',
        'restrictedPicklist',
        'unique',
        'updateable',
      );
      $source = get_object_vars($field);
      $sf_definition = array_intersect_key($source, array_flip($properties));
      foreach ($booleans as $bool) {
        @($sf_definition['sf_type'] |= (int) $source[$bool] * constant('SALESFORCE_FIELD_' . strtoupper($bool)));
      }
      $fieldmap_object['fields'][$field->name] = array(
        'name' => $sf_definition['name'],
        'label' => $sf_definition['label'],
        'type' => $sf_definition['sf_type'],
        'salesforce' => $sf_definition,
      );
    }
  }
  return $fieldmap_object;
}

/**
 * Implements hook_cron().
 */
function salesforce_api_cron() {
  $cache = cache_get('salesforce_api_sf_objects');

  // Check to see if we can connect to Salesforce.
  $sf = salesforce_api_connect();
  if ($sf == TRUE) {

    // If the cache has already been cleared or is expired, then rebuild it.
    if (!$cache || REQUEST_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)) {
    if (user_access('administer salesforce')) {
      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 {
    if (user_access('administer salesforce')) {
      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)) {
    if (user_access('administer salesforce')) {
      drupal_set_message(t('DescribeSObjects expects an array. ' . gettype($types) . ' received.'), 'error');
    }
    return FALSE;
  }
  $types = array_filter($types);

  // 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);
      if (user_access('administer salesforce')) {
        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) {
    salesforce_api_log(SALESFORCE_LOG_SOME, '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;
}

/**
 * Compares mixed 15- and 18-character Salesforce IDs. Up-converts 15-character
 * strings for comparison when applicable. Based on Christian G. Warden's code
 * at http://xn.pinkhamster.net/blog/tech/salesforce/convert-15-character-salesforce-ids-to-18-characters-with-php.html
 *
 * @return TRUE if IDs match, or FALSE
 * @see http://salesforce-id.com
 */
function salesforce_api_id_compare($a, $b) {
  if (strlen($a) != strlen($b)) {
    if (strlen($a) == 15) {
      $a = salesforce_api_id_convert($a);
    }
    if (strlen($b) == 15) {
      $b = salesforce_api_id_convert($b);
    }
  }
  return $a == $b;
}

/**
 * Converts a 15-character Salesforce ID to 18-character ID.
 *
 * @param string $sfid15
 * @return case-insensitive 18-character Salesforce ID
 */
function salesforce_api_id_convert($sfid15) {
  if (strlen($sfid15) != 15) {
    return $sfid15;
  }
  $chunks = str_split($sfid15, 5);
  $extra = '';
  foreach ($chunks as $chunk) {
    $chars = str_split($chunk, 1);
    $bits = '';
    foreach ($chars as $char) {
      $bits .= !is_numeric($char) && $char == strtoupper($char) ? '1' : '0';
    }
    $map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ012345';
    $extra .= substr($map, base_convert(strrev($bits), 2, 10), 1);
  }
  return $sfid15 . $extra;
}

/**
 * Wrapper for Devel's dpm() function. Called in other Salesforce API modules.
 */
function sf_dpm($var, $show_msg = TRUE, $msg_level = 'error') {
  if (!variable_get('salesforce_api_debug', TRUE)) {
    return;
  }
  if (function_exists('dpm')) {
    dpm($var);
  }
  elseif ($show_msg) {
    drupal_set_message(check_plain(print_r($var, TRUE)), $msg_level);
  }
}

/**
 * Implements hook_views_api().
 */
function salesforce_api_views_api() {
  return array(
    'api' => 2,
  );
}

/**
 * @addtogroup exportables
 * @{
 */

/**
 * Loads all fieldmaps currently defined in the database or in code.
 * @return array
 *  An array of all defined fieldmaps.
 */
function salesforce_api_salesforce_fieldmap_load_all() {
  ctools_include('export');
  return ctools_export_load_object('salesforce_fieldmap');
}

/**
 * Loads a specific fieldmap by name.
 * @param string name
 *   The machine name of the fieldmap, or the numeric ID (in legacy code only).
 * @return object
 *   The specified fieldmap.
 */
function salesforce_api_salesforce_fieldmap_load($name) {
  ctools_include('export');
  $result = ctools_export_load_object('salesforce_fieldmap', 'names', array(
    $name,
  ));
  if (isset($result[$name])) {
    return $result[$name];
  }

  // For backwards compatibility, search on fieldmap column (numeric id).
  $result = ctools_export_load_object('salesforce_fieldmap', 'conditions', array(
    'fieldmap' => $name,
  ));
  if (!empty($result)) {

    // "fieldmap" column is serial, and thus is always unique - if not empty, this will always
    // contain a direct hit.
    return current($result);
  }
}

/**
 * Loads fieldmaps that match a particular set of conditions.
 * @param array $conditions
 *   An array of conditions on which to match fieldmaps, keyed by the fields of the
 *   {salesforce_fieldmap} table.
 * @return array
 *   An array of fieldmaps matching the provided conditions.
 */
function salesforce_api_salesforce_fieldmap_load_by($conditions) {
  ctools_include('export');
  $result = ctools_export_load_object('salesforce_fieldmap', 'conditions', $conditions);
  return $result;
}

/**
 * Generates a form with the export code for a given fieldmap.
 * @param string $fieldmap
 *   The name of the fieldmap to export.
 * @return array
 *   A CTools exportable form.
 */
function salesforce_api_export_salesforce_fieldmap($map) {
  drupal_set_title(t('Export Fieldmap'));
  $code = salesforce_api_salesforce_fieldmap_export($map);
  return drupal_get_form('ctools_export_form', $code, 'Export Fieldmap');
}

/**
 * Generates the export code for a given fieldmap.
 * @param object $map
 *   The fieldmap object to export.
 * @return string
 *   The code for the export.
 */
function salesforce_api_salesforce_fieldmap_export($map, $indent = '') {
  ctools_include('export');
  $output = ctools_export_object('salesforce_fieldmap', $map, $indent);
  return $output;
}

/**
 * Form builder function for a fieldmap import.
 * Makes it possible to save a fieldmap from generated code.
 */
function salesforce_api_import_salesforce_fieldmap($form, &$form_state) {
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Fieldmap'),
    '#description' => t('Enter the name of the new fieldmap. This is optional and is not necessary if you do not wish to rename the object. Lowercase letters, numbers, and underscores only please.'),
  );
  $form['object'] = array(
    '#type' => 'textarea',
    '#title' => t('Paste exported code here'),
    '#rows' => 15,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Import'),
  );
  return $form;
}

/**
 * Validation function for salesforce_api_import_fieldmap().
 * Makes sure that an import actually provides a handler.
 */
function salesforce_api_import_salesforce_fieldmap_validate($form, &$form_state) {

  // First, run the PHP and turn the input code into an object.
  $name = $form_state['values']['name'];
  if (preg_match('/[^a-z0-9_]/', $name)) {
    form_error($form['name'], t('Invalid name. Please use letters, numbers, or underscores only.'));
  }
  ob_start();
  eval($form_state['values']['object']);
  ob_end_clean();

  // The object should appear as $salesforce_fieldmap.
  // This was the "identifier" set in the export section of the {salesforce_fieldmap} schema.
  if (empty($salesforce_fieldmap)) {
    $errors = ob_get_contents();
    if (empty($errors)) {
      $errors = t('Could not load a fieldmap from this input. Check your code for errors.');
    }
    form_error($form['object'], t('Unable to get a fieldmap from the import. Errors reported: @errors', array(
      '@errors' => $errors,
    )));
  }
  if (empty($salesforce_fieldmap->drupal_entity) || empty($salesforce_fieldmap->drupal_bundle) || empty($salesforce_fieldmap->salesforce)) {
    form_error($form['object'], t('This fieldmap cannot be imported; the object definition is invalid.'));
    return;
  }
  if (!salesforce_api_fieldmap_source_entity_enabled($salesforce_fieldmap)) {
    form_error($form['object'], t('This fieldmap cannot be imported, because the module which supports the Drupal entity "%entity" cannot be found. Please make sure you have required any modules with which this fieldmap was built.', array(
      '%entity' => $salesforce_fieldmap->drupal_entity,
    )));
  }
  if (!salesforce_api_fieldmap_source_bundle_enabled($salesforce_fieldmap)) {
    form_error($form['object'], t('This fieldmap cannot be imported, because the module which supports the Drupal entity type "%bundle" cannot be found. Please make sure you have required any modules with which this fieldmap was built.', array(
      '%entity' => $salesforce_fieldmap->drupal_bundle,
    )));
  }

  // Try to enable the Salesforce object if it was not found.
  if (!salesforce_api_fieldmap_target_enabled($salesforce_fieldmap)) {
    form_error($form['object'], t('This fieldmap cannot be imported because the Salesforce API module cannot find a definition for the Salesforce object "%sfobj". Please verify your Salesforce connection and settings.', array(
      '%sfobj' => $salesforce_fieldmap->salesforce,
    )));
  }
  $form_state['obj'] = $salesforce_fieldmap;
}

/**
 * Helper function to determine whether the Drupal entity for a given
 * fieldmap is available.
 *
 * @param object $map
 * @return boolean
 */
function salesforce_api_fieldmap_source_entity_enabled($map) {
  $source = salesforce_api_fieldmap_objects_load('drupal', $map->drupal_entity);
  return !empty($source);
}

/**
 * Helper function to determine whether the bundle for the Drupal entity for a given
 * fieldmap is available.
 *
 * @param object $map
 * @return boolean
 */
function salesforce_api_fieldmap_source_bundle_enabled($map) {
  $source = salesforce_api_fieldmap_objects_load('drupal', $map->drupal_entity, $map->drupal_bundle);
  return !empty($source);
}

/**
 * Helper function to determine whether the Salesforce object (target) for a
 * given fieldmap is available.
 *
 * @param object $map
 * @param boolean $enable (optional) -
 *   if the object is not initially available, whether or not to try and enable
 *   it before returning.
 * @return boolean
 */
function salesforce_api_fieldmap_target_enabled($map, $enable = TRUE) {
  $sf_objects = variable_get('salesforce_api_enabled_objects', array(
    'Campaign',
    'Contact',
    'Lead',
  ));
  if ($enable && !in_array($map->salesforce, $sf_objects)) {
    $sf_objects[] = $map->salesforce;
    variable_set('salesforce_api_enabled_objects', array_filter($sf_objects));
    salesforce_api_cache_build();
  }

  // Load the Salesforce fieldmap objects fresh (not from cache), to see if the WSDL defines the object.
  $target = salesforce_api_fieldmap_objects_load('salesforce', 'salesforce', $map->salesforce, $reset = TRUE);
  return !empty($target);
}

/**
 * Submit handler for salesforce_api_import_salesforce_fieldmap().
 * Saves the imported object.
 */
function salesforce_api_import_salesforce_fieldmap_submit($form, &$form_state) {
  $salesforce_fieldmap = $form_state['obj'];

  // Allow a name to be specified for the fieldmap.
  if (!empty($form_state['values']['name'])) {
    $salesforce_fieldmap->name = $form_state['values']['name'];
  }
  salesforce_api_fieldmap_save($salesforce_fieldmap);
  $form_state['redirect'] = SALESFORCE_PATH_FIELDMAPS . '/' . $salesforce_fieldmap->name . '/edit';
}

/**
 * @} End of "addtogroup exportables".
 */

/**
 * @addtogroup encryption
 * @{
 */

/**
 * Wrappers for encryption lib. Right now only AES encryption is supported.
 * If/when other methods are supported, this abstraction layer will make the
 * transition easier.
 */

/**
 * Decrypt a specified value.
 */
function salesforce_api_decrypt($value) {
  return function_exists('aes_decrypt') ? aes_decrypt($value) : $value;
}

/**
 * Encrypt a specified value.
 */
function salesforce_api_encrypt($value) {
  return function_exists('aes_encrypt') ? aes_encrypt($value) : $value;
}

/**
 * Check whether encryption is available.
 */
function salesforce_api_encryption_available($options = array()) {
  $defaults = array(
    'check_config' => TRUE,
    'display_errors' => FALSE,
    'display_warnings' => FALSE,
    'display_all' => FALSE,
    'fail_threshold' => 'warnings',
  );
  $options = array_merge($defaults, $options);
  extract($options);
  $errors = array();
  $warnings = array();
  if (!module_exists('aes')) {
    $warnings[] = 'AES Encryption module is not installed.';
  }
  elseif ($check_config) {
    if (!variable_get('aes_key_path', FALSE) || variable_get('aes_key_storage_method', FALSE) != 'File') {
      $errors[] = 'AES Encryption is installed but not configured securely.
        Please go <a href="/admin/settings/aes">configure AES Encryption to use
        file storage</a> to enable encryption for Salesforce credentials.';
    }
  }
  if ($display_errors || $display_all) {
    foreach ($errors as $msg) {
      drupal_set_message(t($msg), 'error');
    }
  }
  switch ($fail_threshold) {
    case 'errors':
      if (empty($errors)) {
        return TRUE;
      }
    case 'warnings':
      if (empty($errors) && empty($warnings)) {
        return TRUE;
      }
  }
}

Functions

Namesort descending Description
salesforce_api_cache_build Recreate the Salesforce object cache.
salesforce_api_connect Creates an object used for communicating with the Salesforce server and performs a login to verify the API credentials.
salesforce_api_cron Implements hook_cron().
salesforce_api_decrypt Decrypt a specified value.
salesforce_api_delete_object_map Given a Drupal entity type and Drupal object id, delete an object mapping
salesforce_api_delete_salesforce_objects Wrapper for SFBaseClient::delete
salesforce_api_describeGlobal Wrapper for SOAP SforceBaseClient::describeGlobal
salesforce_api_describeSObject Wrapper for SOAP SforceBaseClient::describeSObject Given an sf object type, return the SF Object definition
salesforce_api_describeSObjects 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
salesforce_api_encrypt Encrypt a specified value.
salesforce_api_encryption_available Check whether encryption is available.
salesforce_api_export_salesforce_fieldmap Generates a form with the export code for a given fieldmap.
salesforce_api_features_api Implements hook_features_api().
salesforce_api_fieldmap_clone Clones a fieldmap.
salesforce_api_fieldmap_delete Deletes a fieldmap from the database.
salesforce_api_fieldmap_description Returns a string of description text for the specified fieldmap.
salesforce_api_fieldmap_export_create Creates an object for export to Salesforce based on the supplied Drupal object and fieldmap.
salesforce_api_fieldmap_field_delete Remove a field from all fieldmaps. This is particularly useful for implementations of hook_field_delete_instance. May be use to delete an occurrence in a single fieldmap (by supplying drupal_entity, drupal_bundle and/or salesforce_type), or…
salesforce_api_fieldmap_field_options Returns a FAPI options array for specifying a field from the source object to associate with the target field.
salesforce_api_fieldmap_load %wildcard_load implementation for %salesforce_api_fieldmap menu wildcard.
salesforce_api_fieldmap_objects Implements hook_fieldmap_objects().
salesforce_api_fieldmap_objects_load Returns all or a subset of the objects defined via hook_fieldmap_objects and hook_fieldmap_objects_alter().
salesforce_api_fieldmap_object_label Returns the label for the object of the specified type and name. Note that both the $type and $entity parameters will be 'salesforce' in the case of Salesforce objects.
salesforce_api_fieldmap_options Returns an array of fieldmaps for use as options in the Forms API.
salesforce_api_fieldmap_save Saves a fieldmap to the database.
salesforce_api_fieldmap_source_bundle_enabled Helper function to determine whether the bundle for the Drupal entity for a given fieldmap is available.
salesforce_api_fieldmap_source_entity_enabled Helper function to determine whether the Drupal entity for a given fieldmap is available.
salesforce_api_fieldmap_system_fields Returns an array of system fields that are retrievable from Salesforce.
salesforce_api_fieldmap_target_enabled Helper function to determine whether the Salesforce object (target) for a given fieldmap is available.
salesforce_api_get_deleted Wrapper for SOAP SforceBaseClient::getDeleted. Searches for records deleted between start and end date.
salesforce_api_get_id_with_sfid Get an object id using the Salesforce id and fieldmap.
salesforce_api_get_updated Wrapper for SOAP SforceBaseClient::getUpdated. Searches for records updated/created between start and end date.
salesforce_api_help Implements hook_help().
salesforce_api_id_compare Compares mixed 15- and 18-character Salesforce IDs. Up-converts 15-character strings for comparison when applicable. Based on Christian G. Warden's code at…
salesforce_api_id_convert Converts a 15-character Salesforce ID to 18-character ID.
salesforce_api_id_load
salesforce_api_id_save Saves the Salesforce ID and fieldmap index of a Drupal object. Also stores the timestamp of creation for the object mapping, and when the object was last exported to Salesforce or imported to Drupal.
salesforce_api_id_unlink
salesforce_api_import_salesforce_fieldmap Form builder function for a fieldmap import. Makes it possible to save a fieldmap from generated code.
salesforce_api_import_salesforce_fieldmap_submit Submit handler for salesforce_api_import_salesforce_fieldmap(). Saves the imported object.
salesforce_api_import_salesforce_fieldmap_validate Validation function for salesforce_api_import_fieldmap(). Makes sure that an import actually provides a handler.
salesforce_api_init Implements hook_init(). Checks to see if the Salesforce PHP Toolkit is installed, and warns if it is not.
salesforce_api_locate_toolkit Locates the Salesforce PHP Toolkit, if installed.
salesforce_api_log Wraps watchdog(). Logs a message to the watchdog based on the Salesforce log settings.
salesforce_api_login Helper function for salesforce_api_connect(). You should probably not call this function directly
salesforce_api_menu Implements hook_menu().
salesforce_api_object_to_fieldmap_fields Convert Salesforce object fields to fieldmap array for saving
salesforce_api_permission Implements hook_permission().
salesforce_api_query Wraps SforceBaseClient::query. Queries Salesforce for a record or set of records. For information about SOQL syntax,
salesforce_api_reset_expired_password Helper function for salesforce_api_connect() to reset an expired password, for the website's default salesforce user only.
salesforce_api_retrieve Wraps SforceBaseClient::retrieve. Retrieve an object from Salesforce with standard fields and any data in fields defined in the name object.
salesforce_api_salesforce_fieldmap_export Generates the export code for a given fieldmap.
salesforce_api_salesforce_fieldmap_load Loads a specific fieldmap by name.
salesforce_api_salesforce_fieldmap_load_all Loads all fieldmaps currently defined in the database or in code.
salesforce_api_salesforce_fieldmap_load_by Loads fieldmaps that match a particular set of conditions.
salesforce_api_search_for_duplicates Wrapper function for the sf_find_match hook, implemented by sf_match in the core Salesforce Suite.
salesforce_api_theme Implements hook_theme().
salesforce_api_toolkit_installed Simple check for Salesforce Toolkit.
salesforce_api_upsert Wraps SforceBaseClient::upsert. Upserts a record in Salesforce. If there is an existing record in Salesforce with the same ID, that record is updated. Otherwise, a new record is created.
salesforce_api_valid_email_address Salesforce does not accept email addresses with relative domains, like root@localhost. This function is based on Drupal's valid_email_address. Greater men than I have tried and failed to capture valid email addresses with simple regular…
salesforce_api_views_api Implements hook_views_api().
sf_dpm Wrapper for Devel's dpm() function. Called in other Salesforce API modules.
_salesforce_api_querymore Wraps SforceBaseClient::queryMore. Needs a query locator for an active query and a Salesforce connection, so this must only be called from salesforce_api_query(). Calls itself recursively until records are retrieved.
_salesforce_fieldmap_access Access callback for fieldmap editing screens.
_salesforce_fieldmap_delete_revert_access Access callback for delete / revert operations. Only code-based, overridden maps can be reverted and only database-only maps can be deleted.

Constants