You are here

sf_notifications.module in Salesforce Suite 7.2

Same filename and directory in other branches
  1. 6.2 sf_notifications/sf_notifications.module

File

sf_notifications/sf_notifications.module
View source
<?php

define('SALESFORCE_PATH_NOTIFICATIONS_ADMIN', SALESFORCE_PATH_ADMIN . '/notifications');
define('SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT', 'sf_notifications/endpoint');

/**
 * Implements hook_menu.
 */
function sf_notifications_menu() {
  return array(
    SALESFORCE_PATH_NOTIFICATIONS_ADMIN => array(
      'title' => 'Notifications',
      'description' => 'Salesforce Notifications configuration settings - allowed IPs, active fieldmaps',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'sf_notifications_settings_form',
      ),
      'access arguments' => array(
        'administer salesforce',
      ),
      'type' => MENU_LOCAL_TASK,
      'file' => 'sf_notifications.admin.inc',
    ),
    SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT => array(
      'title' => FALSE,
      'page callback' => 'sf_notifications_endpoint',
      'access callback' => TRUE,
      'type' => MENU_CALLBACK,
    ),
    SALESFORCE_PATH_NOTIFICATIONS_ADMIN . '/queue' => array(
      'title' => 'Manage Notifications queue',
      'description' => 'Salesforce Notifications queue view and actions.',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'sf_notifications_queue_form',
      ),
      'access arguments' => array(
        'administer salesforce',
      ),
      'type' => MENU_CALLBACK,
      'file' => 'sf_notifications.admin.inc',
    ),
    SALESFORCE_PATH_NOTIFICATIONS_ADMIN . '/queue/process' => array(
      'title' => 'Process Notifications queue',
      'description' => 'Process Salesforce notifications queue',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'sf_notifications_process_confirm_form',
      ),
      'access arguments' => array(
        'administer salesforce',
      ),
      'type' => MENU_CALLBACK,
      'file' => 'sf_notifications.admin.inc',
    ),
    SALESFORCE_PATH_NOTIFICATIONS_ADMIN . '/queue/empty' => array(
      'title' => 'Process Notifications queue',
      'description' => 'Empty Salesforce notifications queue',
      'page callback' => 'drupal_get_form',
      'page arguments' => array(
        'sf_notifications_empty_confirm_form',
      ),
      'access arguments' => array(
        'administer salesforce',
      ),
      'type' => MENU_CALLBACK,
      'file' => 'sf_notifications.admin.inc',
    ),
  );
}

/**
 * Access callback for SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT
 *
 * @return TRUE if IP is in whitelist and FALSE if not.
 */
function sf_notifications_allowed_ips() {
  $ip = $_SERVER['REMOTE_ADDR'];
  $ips = variable_get('sf_notifications_allowed_ips', FALSE);
  $allowed_ips = $ips === FALSE ? sf_notifications_default_allowed_ips() : explode("\n", $ips);
  $access = FALSE;
  if (in_array($ip, $allowed_ips, TRUE)) {
    $access = TRUE;
  }
  else {
    foreach ($allowed_ips as $range) {
      if (_sf_notifications_cidr_match($ip, $range)) {
        $access = TRUE;
      }
    }
  }
  if ($access) {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications: IP address @ip accessed @endpoint successfully.', array(
      '@ip' => $_SERVER['REMOTE_ADDR'],
      '@endpoint' => SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT,
    ));
  }
  else {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications: Access denied to IP address @ip in attempt to access @endpoint.', array(
      '@ip' => $_SERVER['REMOTE_ADDR'],
      '@endpoint' => SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT,
    ));
  }
  return $access;
}

/**
 * Given a CIDR mask and an IP address, return TRUE or FALSE if the IP address
 * matches or doesn't match the CIDR mask.
 * Adapted from http://stackoverflow.com/questions/594112
 */
function _sf_notifications_cidr_match($ip, $range) {
  list($subnet, $bits) = explode('/', $range);
  $ip = ip2long($ip);
  $subnet = ip2long($subnet);

  // Sanity check: ip2long() returns FALSE for an invalid IP address.
  if (empty($subnet) || empty($bits) || empty($ip)) {
    return FALSE;
  }
  $mask = -1 << 32 - $bits;
  $subnet &= $mask;

  // in case the supplied subnet wasn't correctly aligned
  return ($ip & $mask) == $subnet;
}

/**
 * Return an array of CIDR notation masks for allowed Salesforce IPs.
 * These are taken from Knowledge Article #102757.
 * https://help.salesforce.com/apex/HTViewSolution?id=102757&language=en
 */
function sf_notifications_default_allowed_ips() {
  return array(
    '204.14.232.0/23',
    '204.14.237.0/24',
    '96.43.144.0/22',
    '96.43.148.0/22',
    '204.14.234.0/23',
    '204.14.238.0/23',
    '202.129.242.0/25',
  );
}

/**
 * Access callback for the notifications per-fieldmap settings.
 */
function sf_notifications_fieldmap_settings_access($fieldmap_id, $perm) {
  $active = variable_get('sf_notifications_active_maps', array());
  if (!empty($active[$fieldmap_id])) {
    return user_access($perm);
  }
  return FALSE;
}

/**
 * Menu callback for Salesforce notifications endpoint
 * @todo Add authentication. see "Downloading the Salesforce.com Client Certificate" at
 * http://www.salesforce.com/us/developer/docs/ajax/Content/sforce_api_ajax_queryresultiterator.htm
 */
function sf_notifications_endpoint() {

  // If the request is coming from outside the defined range of
  // Salesforce IPs, then do not continue.
  if (sf_notifications_allowed_ips() == FALSE) {
    exit;
  }
  $content = file_get_contents('php://input');
  if (empty($content)) {
    salesforce_api_log(SALESFORCE_LOG_SOME, 'Salesforce Notifications: Empty request.');
    drupal_exit();
  }
  salesforce_api_log(SALESFORCE_LOG_ALL, 'New outbound message received from Salesforce. Contents: <pre>%content</pre>', array(
    '%content' => print_r($content, TRUE),
  ));

  // If enabled, try to place the item in the notifications queue and exit. If not
  // succesful, log it, and try to process directly.
  if (variable_get('sf_notifications_use_queue', 0)) {
    $queue = DrupalQueue::get('sf_notifications_queue');
    if ($queue
      ->createItem($content)) {
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Outbound message from Salesforce pushed to queue.', array());
      _sf_notifications_soap_respond('true');
      drupal_exit();
    }
    else {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Unable to store outbound message from Salesforce in queue, attempting to process directly. Contents: <pre>%content</pre>', array(
        '%content' => print_r($content, TRUE),
      ), WATCHDOG_ERROR);
    }
  }
  $ret = _sf_notifications_parse_handle_message($content);

  // Sends SOAP response to SFDC.
  if ($ret) {
    _sf_notifications_soap_respond('true');
  }
  else {
    _sf_notifications_soap_respond('false');
  }
  drupal_exit();
}

/**
 * Loop through an array of SObjects from Salesforce and save them according to
 * any existing sf fieldmaps, notification settings, and data.
 *
 * @param array $objects
 *  A numerically indexed array of SObjects (as returned by
 *  _sf_notifications_parse_message())
 * @return (boolean) FALSE if there were errors. TRUE otherwise.
 * @see sf_notifications_fieldmap_settings()
 * @see sf_notifications_settings_form()
 */
function _sf_notifications_handle_message($objects) {
  $success = TRUE;

  // For each object received from Salesforce, gather all relevant fieldmaps.
  // For each relevant fieldmap, perform the appropriate C(r)UD operation.
  $new_records = $objects['salesforce'];
  $active = variable_get('sf_notifications_active_maps', array());
  $active = array_filter($active);
  foreach ($objects['drupal'] as $object_record) {
    $sfid = $object_record['sfid'];
    $obj = $objects['salesforce'][$sfid];

    // We'll handle inserts later on
    unset($new_records[$sfid]);

    // Break on fieldmap-specific conditions
    $map = salesforce_api_salesforce_fieldmap_load($object_record['name']);
    if (empty($active[$map->name])) {
      continue;
    }
    $operation = $obj->fields->IsDeleted == 'true' ? 'delete' : 'update';
    $object_record['fields'] = $obj->fields;
    $object_record['operation'] = $operation;

    // Check if any implementations of hook_sf_notifications_check_condition()
    // tell us we can't process this item with this map.
    $results = module_invoke_all('sf_notifications_check_condition', $operation, $object_record, $map);
    foreach ($results as $result) {
      if (!$result) {
        continue 2;
      }
    }
    switch ($operation) {
      case 'delete':
        $success = $success && sf_notifications_delete_record($object_record);
        break;
      case 'update':
        $success = $success && sf_notifications_update_record($object_record);
        break;
    }
  }
  foreach ($new_records as $sfid => $obj) {
    $maps = salesforce_api_salesforce_fieldmap_load_by(array(
      'salesforce' => $obj->type,
    ));
    if (empty($maps)) {
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Salesforce Notifications: No fieldmap found.
            <pre>' . print_r($obj, TRUE) . '</pre>');
      $success = FALSE;
      continue;
    }

    // For each map, check active, check conditions and insert.
    foreach ($maps as $map) {
      if (empty($active[$map->name])) {
        continue;
      }

      // Forge an object record to proceed.
      // Insert is the same as update, just without an oid.
      $object_record = array(
        'oid' => NULL,
        'name' => $map->name,
        'drupal_entity' => $map->drupal_entity,
        'drupal_bundle' => $map->drupal_bundle,
        'fields' => $obj->fields,
        'operation' => 'insert',
      );

      // Check if any implementations of hook_sf_notifications_check_condition()
      // tell us we can't process this item with this map.
      $results = module_invoke_all('sf_notifications_check_condition', 'insert', $object_record, $map);
      foreach ($results as $result) {
        if (!$result) {
          continue 2;
        }
      }
      $success = $success && sf_notifications_update_record($object_record);
    }
  }
  return $success;
}

/**
 * Helper function for _sf_notifications_handle_message() - attempt to delete
 * the local object data, given the salesforce object_record.
 */
function sf_notifications_delete_record($object_record) {

  // Try to delete the local record. Since the record is no more, in this
  // case we're agnostic to the drupal_entity ("node" or "user").
  $success = TRUE;
  switch ($object_record['drupal_entity']) {
    case 'user':
      user_cancel(array(), $object_record['oid'], $method = 'user_cancel_block');

      // Invoke hook_sf_notifications_processed().
      module_invoke_all('sf_notifications_processed', 'delete', $object_record);
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications deleted user ' . $object_record['oid'] . ' sfid ' . $sfid);
      break;
    case 'node':

      // Can't use node_delete() since it's wrapped in node_access and we're
      // probably anonymous. The following is adapted from node_delete().
      $node = node_load($nid, NULL, TRUE);
      db_delete('node')
        ->condition('nid', $node->nid)
        ->execute();
      db_delete('node_revisions')
        ->condition('nid', $node->nid)
        ->execute();

      // Call the node-specific callback (if any):
      node_invoke($node, 'delete');
      module_invoke_all('node_delete', $node);
      search_wipe($node->nid, 'node');

      // Invoke hook_sf_notifications_processed().
      module_invoke_all('sf_notifications_processed', 'delete', $object_record);
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications deleted node ' . $object_record['oid'] . ' sfid ' . $sfid);
      break;
    default:
      if (function_exists($object_record['drupal_entity'] . '_delete')) {
        $function = $object_record['drupal_entity'] . '_delete';
        $function($object_record['oid']);

        // Invoke hook_sf_notifications_processed().
        module_invoke_all('sf_notifications_processed', 'delete', $object_record);
        salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications deleted ' . $object_record['drupal_type'] . ' ' . $object_record['oid'] . ' sfid ' . $sfid);
      }
      else {
        salesforce_api_log(SALESFORCE_LOG_SOME, ' Salesforce Notifications: Could not find delete handler for deleted
          Salesforce record <pre>' . print_r($object_record, TRUE) . '</pre>');
        $success = FALSE;
      }
      break;
  }
  return $success;
}

/**
 * Helper function for _sf_notifications_handle_message() - attempt to update
 * (or insert if $object_record['oid'] is empty) the local object data, given
 * the salesforce object_record.
 */
function sf_notifications_update_record($object_record) {
  $success = TRUE;
  $drupal_entity = $object_record['drupal_entity'];
  if (empty($drupal_entity)) {
    $map = salesforce_api_salesforce_fieldmap_load($object_record['name']);
    if (empty($map)) {
      $success = FALSE;
    }
    else {
      $drupal_entity = $map->drupal_entity;
    }
  }

  // Check to see if the drupal_entity is one handled by the sf_entity module.
  $entities = field_info_bundles();
  $entity_names = array_keys($entities);
  if (in_array($drupal_entity, $entity_names)) {
    $function = 'sf_entity_import';
  }
  else {
    $function = 'sf_' . $drupal_entity . '_import';
  }
  if (function_exists($function)) {
    $drupal_id = $function($object_record['fields'], $object_record['name'], $object_record['oid']);
    if ($drupal_id) {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications successful ' . $object_record['operation'] . ' of ' . $drupal_entity . ' ' . $drupal_id);

      // Invoke hook_sf_notifications_processed().
      module_invoke_all('sf_notifications_processed', $object_record['operation'], $object_record, $drupal_id);
    }
    else {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications failed to update ' . $object_record['drupal_entity'] . ' from record.
        <pre>' . print_r($object_record, TRUE) . '</pre>');
      $success = FALSE;
    }
  }
  else {
    salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications: Import handler ' . $function . ' undefined.
        Drupal ' . $object_record['drupal_entity'] . ' with id ' . $object_record['oid'] . ' was not updated.');
    $success = FALSE;
  }
  return $success;
}

/**
 * Parse SOAP message into its component args.
 *
 * @param (object) $domDoc
 *  A DOMDocument representation of the outbound SOAP message from Salesforce.
 * @return (array) $matches
 *  An array with two sub-arrays, keyed as:
 *  'drupal':
 *    A sequential array containing relevant salesforce_ids records.
 *    We don't index on drupal_id because there could be overlap.
 *  'salesforce':
 *    An indexed array mapping sfids to SObject records from Salesforce.
 */
function _sf_notifications_parse_message($domDoc) {

  // Needed for the reference to SObject, since the Enterprise Toolkit doesn't have an SObject class.
  require_once DRUPAL_ROOT . '/' . SALESFORCE_DIR_SOAPCLIENT . '/SforcePartnerClient.php';
  $matches = array(
    'salesforce' => array(),
    'drupal' => array(),
  );
  $sfids = array();

  // Create sObject array and fill fields provided in notification
  $objects = $domDoc
    ->getElementsByTagName('sObject');
  foreach ($objects as $sObjectNode) {
    $sObjType = $sObjectNode
      ->getAttribute('xsi:type');
    if (substr_count($sObjType, 'sf:')) {
      $sObjType = substr($sObjType, 3);
    }
    $obj = new SObject();
    $obj->type = $sObjType;
    $elements = $sObjectNode
      ->getElementsByTagNameNS('urn:sobject.enterprise.soap.sforce.com', '*');
    $obj->fieldnames = array();
    foreach ($elements as $node) {
      if ($node->localName == 'Id') {

        // "Id" is a property of the SObject as well as SObject->fields
        $sfids[] = $obj->Id = $node->textContent;
      }
      $fieldname = $node->localName;
      $obj->fields->{$fieldname} = $node->nodeValue;
      array_push($obj->fieldnames, $fieldname);
    }
    $matches['salesforce'][$obj->Id] = $obj;
  }
  $result = db_query('SELECT name, oid, sfid, drupal_entity, drupal_bundle FROM {salesforce_object_map} WHERE sfid IN (:sfids)', array(
    ':sfids' => $sfids,
  ));
  while ($row = $result
    ->fetchAssoc()) {
    $matches['drupal'][] = $row;
  }
  salesforce_api_log(SALESFORCE_LOG_ALL, 'Salesforce Notifications found the following matches for the outbound message: ' . '<pre>' . print_r($matches, TRUE) . '</pre>');
  return $matches;
}

/**
 * Format and send a SOAP response message.
 *
 * @param boolean $tf
 * @return void
 **/
function _sf_notifications_soap_respond($tf = 'true') {
  print '<?xml version = "1.0" encoding = "utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <soapenv:Body>
      <notifications xmlns="http://soap.sforce.com/2005/09/outbound">
        <Ack>' . $tf . '</Ack>
      </notifications>
    </soapenv:Body>
</soapenv:Envelope>
';
}

/**
 * Parses and handles an outbound message from Salesforce.
 *
 * @param string $content The XML contents of the outbound message from Salesforce.
 * @return (boolean) FALSE if there were errors. TRUE otherwise.
 */
function _sf_notifications_parse_handle_message($content) {
  $dom = new DOMDocument();
  $dom
    ->loadXML($content);
  if (empty($dom) || !$dom
    ->hasChildNodes()) {
    salesforce_api_log(SALESFORCE_LOG_NONE, 'Salesforce Notifications: Failed to parse into DOM Document.
      <pre>' . print_r($content) . '</pre>', array(), WATCHDOG_ERROR);
    return FALSE;
  }
  $resultArray = _sf_notifications_parse_message($dom);
  $ret = _sf_notifications_handle_message($resultArray);
  return $ret;
}

/**
 * Implements hook_cron().
 *
 * We claim an item for 60 seconds. If it processes, we remove it from the queue.
 * If it doesn't, we leave it. We don't relinquish the claim so we can move past it
 * and continue processing other queue items. We log a watchdog items for the
 * admin's notice.
 */
function sf_notifications_cron() {

  // Release any expired claims so they are available to be re-claimed.
  sf_notifications_release_expired_claims();
  $queue = DrupalQueue::get('sf_notifications_queue');
  $end = time() + 60;
  while (time() < $end && ($item = $queue
    ->claimItem(60))) {
    $ret = _sf_notifications_parse_handle_message($item->data);
    if ($ret) {
      salesforce_api_log(SALESFORCE_LOG_SOME, 'Queued notification processed. Contents: <pre>%content</pre>', array(
        '%content' => print_r($item->data, TRUE),
      ));
      $queue
        ->deleteItem($item);
    }
    else {
      salesforce_api_log(SALESFORCE_LOG_ALL, 'Queued notification processing failed. Contents: <pre>%content</pre>', array(
        '%content' => print_r($item->data, TRUE),
      ), WATCHDOG_ERROR);
    }
  }
}

/**
 * Release items which have already had their claims expired, but have not been
 * released. Usually, this happens on cron, but we want to do it before processing
 * clearing the queue.
 */
function sf_notifications_release_expired_claims() {

  // Reset expired sf_notifications_queue items in the queue table.
  db_update('queue')
    ->fields(array(
    'expire' => 0,
  ))
    ->condition('expire', 0, '<>')
    ->condition('expire', REQUEST_TIME, '<')
    ->condition('name', 'sf_notifications_queue')
    ->execute();
}

Functions

Namesort descending Description
sf_notifications_allowed_ips Access callback for SALESFORCE_PATH_NOTIFICATIONS_ENDPOINT
sf_notifications_cron Implements hook_cron().
sf_notifications_default_allowed_ips Return an array of CIDR notation masks for allowed Salesforce IPs. These are taken from Knowledge Article #102757. https://help.salesforce.com/apex/HTViewSolution?id=102757&language=en
sf_notifications_delete_record Helper function for _sf_notifications_handle_message() - attempt to delete the local object data, given the salesforce object_record.
sf_notifications_endpoint Menu callback for Salesforce notifications endpoint @todo Add authentication. see "Downloading the Salesforce.com Client Certificate" at http://www.salesforce.com/us/developer/docs/ajax/Content/sforce_api_ajax...
sf_notifications_fieldmap_settings_access Access callback for the notifications per-fieldmap settings.
sf_notifications_menu Implements hook_menu.
sf_notifications_release_expired_claims Release items which have already had their claims expired, but have not been released. Usually, this happens on cron, but we want to do it before processing clearing the queue.
sf_notifications_update_record Helper function for _sf_notifications_handle_message() - attempt to update (or insert if $object_record['oid'] is empty) the local object data, given the salesforce object_record.
_sf_notifications_cidr_match Given a CIDR mask and an IP address, return TRUE or FALSE if the IP address matches or doesn't match the CIDR mask. Adapted from http://stackoverflow.com/questions/594112
_sf_notifications_handle_message Loop through an array of SObjects from Salesforce and save them according to any existing sf fieldmaps, notification settings, and data.
_sf_notifications_parse_handle_message Parses and handles an outbound message from Salesforce.
_sf_notifications_parse_message Parse SOAP message into its component args.
_sf_notifications_soap_respond Format and send a SOAP response message.

Constants