You are here

acquia_search.module in Acquia Search 6

Integration between Acquia Drupal and Acquia's hosted solr search service.

File

acquia_search.module
View source
<?php

// $Id$

/**
 * @file
 *   Integration between Acquia Drupal and Acquia's hosted solr search service.
 */
define('ACQUIA_SEARCH_VERSION', "6.x-1.5704");

/**
 * Implementation of hook_enable().
 */
function acquia_search_enable() {
  if (acquia_agent_subscription_is_active()) {

    // Send a heartbeat so the Acquia Network knows the module is enabled.
    acquia_agent_check_subscription();
    _acquia_search_set_variables();
  }
}

/**
 * Helper function - set apachesolr variables to Acquia values..
 */
function _acquia_search_set_variables() {
  $identifier = acquia_agent_settings('acquia_identifier');
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $search_host = variable_get('acquia_search_host', 'search.acquia.com');

  // @todo rework this logic once we have AH_SERVER_REGION
  // legacy_internal will be true if 'internal-' is the start of the host
  // name that's set in via Acquia Cloud platform or vset.
  $legacy_internal = preg_match('/internal[-.]/', $search_host);

  // Adding the subscription specific colony to the heartbeat data
  if (!empty($subscription['heartbeat_data']['search_service_colony'])) {
    $search_host = $subscription['heartbeat_data']['search_service_colony'];
    if ($legacy_internal) {

      // If we want to be using an internal host name, fix the one
      // from the hearbeat data here.
      $search_host = 'internal-' . $search_host;
    }
  }

  // Check if we are on Acquia Cloud hosting. @see NN-2503
  if (!empty($_ENV['AH_SITE_ENVIRONMENT']) && !empty($_ENV['AH_SERVER_REGION'])) {
    if ($_ENV['AH_SERVER_REGION'] == 'us-east-1' && $search_host == 'search.acquia.com') {
      $search_host = 'internal-search.acquia.com';
    }
    elseif (strpos($search_host, 'search-' . $_ENV['AH_SERVER_REGION']) === 0) {
      $search_host = 'internal-' . $search_host;
    }
  }
  variable_set('apachesolr_host', $search_host);
  variable_set('apachesolr_port', variable_get('acquia_search_port', '80'));
  variable_set('apachesolr_path', variable_get('acquia_search_path', '/solr/' . $identifier));
  variable_set('apachesolr_service_class', array(
    'acquia_search',
    'Acquia_Search_Service.php',
    'Acquia_Search_Service',
  ));
  if (!variable_get('apachesolr_failure', FALSE)) {
    variable_set('apachesolr_failure', 'show_drupal_results');
  }
  variable_set('apachesolr_search_make_default', 1);

  // Refresh the salt with the subscription data returned by the heartbeat
  // since it can change periodically.
  $salt = variable_get('acquia_search_derived_key_salt', '');
  if (isset($subscription['derived_key_salt']) && $salt != $subscription['derived_key_salt']) {
    variable_set('acquia_search_derived_key_salt', $subscription['derived_key_salt']);
  }
}

/**
 * Implementation of hook_disable().
 */
function acquia_search_disable() {
  acquia_search_delete_variables();
}

/**
 * Helper function to clear variables we may have set.
 */
function acquia_search_delete_variables() {
  variable_del('apachesolr_host');
  variable_del('apachesolr_port');
  variable_del('apachesolr_path');
  variable_del('apachesolr_service_class');
  variable_del('apachesolr_search_make_default');
  variable_del('acquia_search_derived_key_salt');
}

/**
 * Implementation of hook_menu_alter().
 */
function acquia_search_menu_alter(&$menu) {
  if (isset($menu['admin/settings/apachesolr'])) {
    $menu['admin/settings/apachesolr']['title'] = 'Acquia Search';
  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 */
function acquia_search_form_apachesolr_delete_index_form_alter(&$form, $form_state) {
  $form['markup']['#value'] = t('Acquia Search index controls');
}

/**
 * Implementation of hook_form_[form_id]_alter().
 */
function acquia_search_form_apachesolr_settings_alter(&$form, $form_state) {

  // Don't alter the form if there is no subscription.
  if (acquia_agent_subscription_is_active()) {
    $form['apachesolr_host'] = array(
      '#type' => 'item',
      '#title' => t("Search is being provided by the Acquia Search network service"),
      '#value' => variable_get('apachesolr_host', 'localhost'),
    );
    $form['apachesolr_port']['#type'] = 'value';
    $form['apachesolr_path']['#type'] = 'value';
    $form['apachesolr_failure']['#title'] = t("If your site cannot connect to the Acquia Search network service");
    $form['#submit'][] = 'acquia_search_settings_submit';
    $form['advanced']['acquia_search_edismax_default'] = array(
      '#type' => 'radios',
      '#title' => t('Allow advanced syntax for all searches'),
      '#default_value' => variable_get('acquia_search_edismax_default', 0),
      '#options' => array(
        0 => t('Disabled'),
        1 => t('Enabled'),
      ),
      '#description' => t('If enabled, all Acquia Search keyword searches may use advanced <a href="@url">Lucene syntax</a> such as wildcard searches, fuzzy searches, proximity searches, boolean operators and more via the Extended Dismax parser.', array(
        '@url' => 'http://lucene.apache.org/java/2_9_3/queryparsersyntax.html',
      )),
      '#weight' => 10,
    );
    $form['advanced']['acquia_search_request_timeout'] = array(
      '#type' => 'textfield',
      '#title' => t('Request timeout'),
      '#required' => TRUE,
      '#default_value' => variable_get('acquia_search_request_timeout', '20.0'),
      '#description' => t('A timeout in seconds that the entire request cannot exceeed. Usually this value will be significantly greater than the connection timeout.'),
      '#size' => 5,
      '#weight' => 15,
    );
    $form['advanced']['acquia_search_connect_timeout'] = array(
      '#type' => 'textfield',
      '#title' => t('Connection timeout'),
      '#required' => TRUE,
      '#default_value' => variable_get('acquia_search_connect_timeout', '3.0'),
      '#description' => t('A timeout in seconds that the connection alone cannot exceeed. It is usually safe to set this value is set between 1 and 5 seconds.'),
      '#size' => 5,
      '#weight' => 15,
    );
    $form['#validate'][] = 'acquia_search_settings_validate';
  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 */
function acquia_search_form_acquia_agent_settings_form_alter(&$form, $form_state) {
  if (isset($form['cs'])) {
    $form['cs']['buttons']['submit']['#submit'][] = 'acquia_search_settings_submit';
    $form['cs']['buttons']['delete']['#submit'][] = 'acquia_search_settings_submit';
  }
  else {

    // Older versions had a different form array.
    $form['buttons']['submit']['#submit'][] = 'acquia_search_settings_submit';
    $form['buttons']['delete']['#submit'][] = 'acquia_search_settings_submit';
  }
}

/**
 * Implementation of hook_flush_caches().
 */
function acquia_search_flush_caches() {

  // Make sure our settings are correct.
  acquia_search_settings_submit();
  return array();
}

/**
 * Added validate function for the apachesolr_settings form.
 */
function acquia_search_settings_validate(&$form, &$form_state) {
  if (!is_numeric($form_state['values']['acquia_search_request_timeout'])) {
    form_set_error('acquia_search_request_timeout', t('Request timeout must be numeric.'));
  }
  if (!is_numeric($form_state['values']['acquia_search_connect_timeout'])) {
    form_set_error('acquia_search_connect_timeout', t('Connection timeout must be numeric.'));
  }
}

/**
 * Added submit function for acquia_agent_settings and apachesolr_settings forms.
 */
function acquia_search_settings_submit() {
  if (acquia_agent_subscription_is_active()) {
    _acquia_search_set_variables();
  }
  else {
    acquia_search_delete_variables();
  }
}

/**
 * Implementation of hook_apachesolr_modify_query().
 *
 * Possibly alters the query type ('defType') param to edismax.
 */
function acquia_search_apachesolr_modify_query($query, &$params, $caller) {

  // @todo - does it make sense to check $caller too?
  if (isset($params['qt']) || isset($params['defType'])) {

    // This is a 'mlt' query or something else custom.
    return;
  }

  // Set the qt to edismax if we have keywords, and we always use it, or are
  // using a wildcard (* or ?).
  $keys = $query
    ->get_query_basic();
  if ($keys && (($wildcard = preg_match('/\\S+[*?]/', $keys)) || variable_get('acquia_search_edismax_default', 0))) {
    $params['defType'] = 'edismax';
    if ($wildcard) {
      $keys = preg_replace_callback('/(\\S+[*?]\\S*)/', '_acquia_search_lower', $keys);
      $query
        ->set_keys($keys);
    }
  }
}

/**
 * Convert to lower-case any keywords containing a wildcard.
 */
function _acquia_search_lower($matches) {
  return drupal_strtolower($matches[1]);
}

/**
 * Modify a solr base url and construct a hmac authenticator cookie.
 *
 * @param $url
 *  The solr url beng requested - passed by reference and may be altered.
 * @param $string
 *  A string - the data to be authenticated, or empty to just use the path
 *  and query from the url to build the authenticator.
 * @param $derived_key
 *  Optional string to supply the derived key.
 *
 * @return
 *  An array containing the string to be added as the content of the
 *  Cookie header to the request and the nonce.
 */
function acquia_search_auth_cookie(&$url, $string = '', $derived_key = NULL) {
  $uri = parse_url($url);

  // Add a scheme - should always be https if available.
  if (in_array('ssl', stream_get_transports(), TRUE) && !defined('ACQUIA_DEVELOPMENT_NOSSL')) {
    $scheme = 'https://';
    $port = '';
  }
  else {
    $scheme = 'http://';
    $port = isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : '';
  }
  $path = isset($uri['path']) ? $uri['path'] : '/';
  $query = isset($uri['query']) ? '?' . $uri['query'] : '';
  $url = $scheme . $uri['host'] . $port . $path . $query;
  $nonce = md5(acquia_agent_random_bytes(55));
  if ($string) {
    $auth_header = acquia_search_authenticator($string, $nonce, $derived_key);
  }
  else {
    $auth_header = acquia_search_authenticator($path . $query, $nonce, $derived_key);
  }
  return array(
    $auth_header,
    $nonce,
  );
}

/**
 * Returns the subscription's salt used to generate the derived key.
 *
 * The salt is stored in a system variable so that this module can continue
 * connecting to Acquia Search even when the subscription data is not available.
 * The most common reason for subscription data being unavailable is a failed
 * heartbeat connection to rpc.acquia.com.
 *
 * Acquia Connector versions <= 7.x-2.7 pulled the derived key salt directly
 * from the subscription data. In order to allow for seamless upgrades, this
 * function checks whether the system variable exists and sets it with the data
 * in the subscription if it doesn't.
 *
 * @return string
 *   The derived key salt.
 *
 * @see http://drupal.org/node/1784114
 */
function acquia_search_derived_key_salt() {
  $salt = variable_get('acquia_search_derived_key_salt', '');
  if (!$salt) {

    // If the variable doesn't exist, set it using the subscription data.
    $subscription = acquia_agent_settings('acquia_subscription_data');
    if (isset($subscription['derived_key_salt'])) {
      variable_set('acquia_search_derived_key_salt', $subscription['derived_key_salt']);
      $salt = $subscription['derived_key_salt'];
    }
  }
  return $salt;
}

/**
 * Derive a key for the solr hmac using the information shared with acquia.com.
 */
function _acquia_search_derived_key() {
  static $derived_key = NULL;
  if (!isset($derived_key) && acquia_agent_subscription_is_active()) {
    $key = acquia_agent_settings('acquia_key');
    $identifier = acquia_agent_settings('acquia_identifier');
    $derived_key_salt = acquia_search_derived_key_salt();

    // We use a salt from acquia.com in key derivation since this is a shared
    // value that we could change on the AN side if needed to force any
    // or all clients to use a new derived key.  We also use a string
    // ('solr') specific to the service, since we want each service using a
    // derived key to have a separate one.
    if (empty($derived_key_salt) || empty($key) || empty($identifier)) {

      // Expired or invalid subscription - don't continue.
      $derived_key = '';
    }
    else {
      $derivation_string = $identifier . 'solr' . $derived_key_salt;
      $derived_key = _acquia_search_hmac($key, str_pad($derivation_string, 80, $derivation_string));
    }
  }
  return $derived_key;
}

/**
 * Creates an authenticator based on a data string and HMAC-SHA1.
 */
function acquia_search_authenticator($string, $nonce, $derived_key = NULL) {
  if (empty($derived_key)) {
    $derived_key = _acquia_search_derived_key();
  }
  if (empty($derived_key)) {

    // Expired or invalid subscription - don't continue.
    return '';
  }
  else {
    $time = time();
    return 'acquia_solr_time=' . $time . '; acquia_solr_nonce=' . $nonce . '; acquia_solr_hmac=' . _acquia_search_hmac($derived_key, $time . $nonce . $string) . ';';
  }
}

/**
 * Validate the authenticity of returned data using a nonce and HMAC-SHA1.
 *
 * @return
 *  TRUE or FALSE.
 */
function acquia_search_valid_response($hmac, $nonce, $string, $derived_key = NULL) {
  if (empty($derived_key)) {
    $derived_key = _acquia_search_derived_key();
  }
  return $hmac == _acquia_search_hmac($derived_key, $nonce . $string);
}

/**
 * Look in the headers and get the hmac_digest out
 * @return string hmac_digest
 *
 */
function acquia_search_extract_hmac($http_response_header) {
  $reg = array();
  if (is_array($http_response_header)) {
    foreach ($http_response_header as $header) {
      if (preg_match("/Pragma:.*hmac_digest=(.+);/i", $header, $reg)) {
        return trim($reg[1]);
      }
    }
  }
  return '';
}

/**
 * Calculates a HMAC-SHA1 of a data string.
 *
 * See RFC2104 (http://www.ietf.org/rfc/rfc2104.txt). Note, the result of this
 * must be identical to using hash_hmac('sha1', $string, $key);  We don't use
 * that function since PHP can be missing it if it was compiled with the
 * --disable-hash switch. However, the hash extension is enabled by default
 * as of PHP 5.1.2, so we should consider requiring it and using the built-in
 * function since it is a little faster (~1.5x).
 */
function _acquia_search_hmac($key, $string) {
  return sha1((str_pad($key, 64, chr(0x0)) ^ str_repeat(chr(0x5c), 64)) . pack("H*", sha1((str_pad($key, 64, chr(0x0)) ^ str_repeat(chr(0x36), 64)) . $string)));
}

Functions

Namesort descending Description
acquia_search_apachesolr_modify_query Implementation of hook_apachesolr_modify_query().
acquia_search_authenticator Creates an authenticator based on a data string and HMAC-SHA1.
acquia_search_auth_cookie Modify a solr base url and construct a hmac authenticator cookie.
acquia_search_delete_variables Helper function to clear variables we may have set.
acquia_search_derived_key_salt Returns the subscription's salt used to generate the derived key.
acquia_search_disable Implementation of hook_disable().
acquia_search_enable Implementation of hook_enable().
acquia_search_extract_hmac Look in the headers and get the hmac_digest out
acquia_search_flush_caches Implementation of hook_flush_caches().
acquia_search_form_acquia_agent_settings_form_alter Implementation of hook_form_[form_id]_alter().
acquia_search_form_apachesolr_delete_index_form_alter Implementation of hook_form_[form_id]_alter().
acquia_search_form_apachesolr_settings_alter Implementation of hook_form_[form_id]_alter().
acquia_search_menu_alter Implementation of hook_menu_alter().
acquia_search_settings_submit Added submit function for acquia_agent_settings and apachesolr_settings forms.
acquia_search_settings_validate Added validate function for the apachesolr_settings form.
acquia_search_valid_response Validate the authenticity of returned data using a nonce and HMAC-SHA1.
_acquia_search_derived_key Derive a key for the solr hmac using the information shared with acquia.com.
_acquia_search_hmac Calculates a HMAC-SHA1 of a data string.
_acquia_search_lower Convert to lower-case any keywords containing a wildcard.
_acquia_search_set_variables Helper function - set apachesolr variables to Acquia values..

Constants

Namesort descending Description
ACQUIA_SEARCH_VERSION @file Integration between Acquia Drupal and Acquia's hosted solr search service.