You are here

acquia_search.module in Acquia Connector 7

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

File

acquia_search/acquia_search.module
View source
<?php

/**
 * @file
 *   Integration between Acquia Drupal and Acquia's hosted solr search service.
 */
define('ACQUIA_SEARCH_ENVIRONMENT_ID', 'acquia_search_server_1');

/**
 * Predefined Acquia Search network environment
 */
function acquia_search_get_environment($conf = array()) {
  if (!empty($conf['acquia_subscription_id'])) {
    $identifier = $conf['acquia_subscription_id'];
  }
  else {
    $identifier = acquia_agent_settings('acquia_identifier');
  }
  $environment = array(
    // @todo - server URL may depend on region info.
    'url' => variable_get('acquia_search_url', 'http://search.acquia.com/solr/' . $identifier),
    'service_class' => 'AcquiaSearchService',
  );
  return $environment;
}

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

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

/**
 * Implementation of hook_help().
 */
function acquia_search_help($path, $arg) {
  switch ($path) {
    case 'admin/config/search/apachesolr':
      $env_id = $arg[5] ? $arg[5] : variable_get('apachesolr_default_environment');
      $environment = apachesolr_environment_load($env_id);
      if (acquia_search_environment_connected($environment) && acquia_agent_subscription_is_active()) {
        $as_link = l(t('Acquia Search'), 'http://www.acquia.com/products-services/acquia-search');
        return t("Search is being provided by the !as network service.", array(
          '!as' => $as_link,
        ));
      }
      break;
  }
}

/**
 * Create a new record pointing to the Acquia apachesolr search server and set it as the default
 */
function acquia_search_enable_acquia_solr_environment() {

  // Creates the new environment
  $environment = apachesolr_environment_load(ACQUIA_SEARCH_ENVIRONMENT_ID);
  if (!$environment) {

    // Only set the default if we just created the environment.
    // This will almost always happen, unless the module was disabled via SQL.
    variable_set('apachesolr_default_environment', ACQUIA_SEARCH_ENVIRONMENT_ID);
    $environment['conf'] = array();
  }
  $environment = array_merge(acquia_search_get_environment(), $environment);
  $environment['env_id'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
  $environment['name'] = t('Acquia Search');
  apachesolr_environment_save($environment);
}

/**
 * Implementation of hook_disable().
 *
 * Helper function to clear variables we may have set.
 */
function acquia_search_disable() {

  // Remove the environment we added.
  apachesolr_environment_delete(ACQUIA_SEARCH_ENVIRONMENT_ID);

  // Unset all other acquia search environments
  $environments = apachesolr_load_all_environments();
  foreach ($environments as $environment) {
    if (acquia_search_environment_connected($environment)) {

      // remove traces of acquia_search
      // unset our acquia url and set it back to default
      $environment['url'] = 'http://localhost:8983/solr';
      if (variable_get('apachesolr_default_environment', '') == $environment['env_id']) {

        // Go back to the default.
        variable_del('apachesolr_default_environment');
      }

      // Emptying the service class, unsetting it would not work, since it would
      // not overwrite the old value
      $environment['service_class'] = '';
      apachesolr_environment_save($environment);
    }
  }
}

/**
 * Implementation of hook_menu_alter().
 */
function acquia_search_menu_alter(&$menu) {
  $delete_page = 'admin/config/search/apachesolr-env/%apachesolr_environment/delete';
  if (isset($menu[$delete_page])) {
    $menu[$delete_page]['access callback'] = 'acquia_search_environment_delete_access';
    $menu[$delete_page]['access arguments'] = array(
      4,
    );
  }
}

/**
 * Implementation of hook_cron().
 */
function acquia_search_cron() {

  // Cache the cersion in a variable so we can send it at not extra cost.
  $version = variable_get('acquia_search_version', '7.x');
  $info = system_get_info('module', 'acquia_search');

  // Send the version, or at least the core compatibility as a fallback.
  $new_version = isset($info['version']) ? (string) $info['version'] : (string) $info['core'];
  if ($version != $new_version) {
    variable_set('acquia_search_version', $new_version);
  }
}

/**
 * Tests whether the environment is connected to Acquia Search.
 */
function acquia_search_environment_connected($environment) {
  return $environment['service_class'] == 'AcquiaSearchService';
}

/**
 * Delete environment page access.
 */
function acquia_search_environment_delete_access($environment) {
  if (strstr($environment['env_id'], ACQUIA_SEARCH_ENVIRONMENT_ID)) {
    return FALSE;
  }

  // Fall back to the original check.
  return user_access('administer search');
}

/**
 * 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()) {

    // Don't show delete operation for the Default AS environment. This means
    // that cloned acquia search environments can be deleted
    foreach ($form['apachesolr_host_settings']['table']['#rows'] as &$row) {
      if (isset($row['data']['delete']['data']) && strpos($row['data']['delete']['data'], ACQUIA_SEARCH_ENVIRONMENT_ID . '/delete') !== FALSE) {
        $row['data']['delete']['data'] = '';
        break;
      }
    }
    $form['advanced']['acquia_search_edismax_default'] = array(
      '#type' => 'radios',
      '#title' => t('Always allow advanced syntax for Acquia Search'),
      '#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. If not enabled, this syntax wll only be used when needed to enable wildcard searches.', array(
        '@url' => 'http://lucene.apache.org/java/2_9_3/queryparsersyntax.html',
      )),
      '#weight' => 10,
    );
  }
}

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

  // Gets environment from form, gets connection status to Acquia Search.
  $env_id = isset($form['env_id']['#default_value']) ? $form['env_id']['#default_value'] : '';
  $environment = $env_id ? apachesolr_environment_load($env_id) : FALSE;
  if ($environment && acquia_search_environment_connected($environment)) {
    $form['url']['#disabled'] = TRUE;
    $form['env_id']['#disabled'] = TRUE;
  }

  // Don't let the user delete the initial environment.
  if ($env_id == ACQUIA_SEARCH_ENVIRONMENT_ID) {
    $form['name']['#disabled'] = TRUE;
    $form['actions']['delete']['#access'] = FALSE;
  }
  $form['actions']['save']['#validate'][] = 'acquia_search_environment_edit_form_validate';
}
function acquia_search_environment_edit_form_validate($form, &$form_state) {
  if ($form_state['values']['env_id'] == ACQUIA_SEARCH_ENVIRONMENT_ID) {

    // make sure that the environment parameters have not been changed
    $form_state['values'] = array_merge($form_state['values'], acquia_search_get_environment());
  }
}

/**
 * Implementation of hook_acquia_subscription_status().
 */
function acquia_search_acquia_subscription_status($active) {
  if ($active) {
    acquia_search_enable_acquia_solr_environment();
  }
  else {
    acquia_search_disable();
  }
}

/**
 * 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, $env_id = 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;

  // 32 character nonce.
  $nonce = base64_encode(drupal_random_bytes(24));
  if ($string) {
    $auth_header = acquia_search_authenticator($string, $nonce, $derived_key, $env_id);
  }
  else {
    $auth_header = acquia_search_authenticator($path . $query, $nonce, $derived_key, $env_id);
  }
  return array(
    $auth_header,
    $nonce,
  );
}

/**
 * Derive a key for the solr hmac using the information shared with acquia.com.
 */
function _acquia_search_derived_key($env_id = NULL) {
  static $derived_key = array();
  if (empty($env_id)) {
    $env_id = 0;
  }
  if (!isset($derived_key[$env_id])) {

    // If we set an explicit environment, check if this needs to overridden
    // Use the default
    $identifier = acquia_agent_settings('acquia_identifier');
    $key = acquia_agent_settings('acquia_key');

    // See if we need to overwrite these values
    if ($env_id) {

      // Load the explicit environment and a manually set search key.
      if ($search_key = apachesolr_environment_variable_get($env_id, 'acquia_search_key')) {
        $derived_key[$env_id] = $search_key;
      }
    }

    // In any case, this is equal for all subscriptions. Also
    // even if the search sub is different, the main subscription should be
    // active
    $subscription = acquia_agent_settings('acquia_subscription_data');

    // 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($subscription['active']) || empty($key) || empty($identifier)) {

      // Expired or invalid subscription - don't continue.
      $derived_key[$env_id] = '';
    }
    elseif (!isset($derived_key[$env_id])) {
      $salt = isset($subscription['derived_key_salt']) ? $subscription['derived_key_salt'] : '';
      $derivation_string = $identifier . 'solr' . $salt;
      $derived_key[$env_id] = hash_hmac('sha1', str_pad($derivation_string, 80, $derivation_string), $key);
    }
  }
  return $derived_key[$env_id];
}

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

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

/**
 * 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, $env_id = NULL) {
  if (empty($derived_key)) {
    $derived_key = _acquia_search_derived_key($env_id);
  }
  return $hmac == hash_hmac('sha1', $nonce . $string, $derived_key);
}

/**
 * Look in the headers and get the hmac_digest out
 * @return string hmac_digest
 *
 */
function acquia_search_extract_hmac($headers) {
  $reg = array();
  if (is_array($headers)) {
    foreach ($headers as $name => $value) {
      if (strtolower($name) == 'pragma' && preg_match("/hmac_digest=([^;]+);/i", $value, $reg)) {
        return trim($reg[1]);
      }
    }
  }
  return '';
}

/**
 * Implementation of hook_apachesolr_modify_query().
 *
 * Possibly alters the query type ('defType') param to edismax.
 */
function acquia_search_apachesolr_query_alter($query) {

  // @todo - does it make sense to check $caller too?
  if (!acquia_search_environment_connected($query
    ->solr('getId')) || $query
    ->getParam('qt') || $query
    ->getParam('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
    ->getParam('q');
  if ($keys && (($wildcard = preg_match('/\\S+[*?]/', $keys)) || variable_get('acquia_search_edismax_default', 0))) {
    $query
      ->addParam('defType', 'edismax');
    if ($wildcard) {
      $keys = preg_replace_callback('/(\\S+[*?]\\S*)/', '_acquia_search_lower', $keys);
      $query
        ->replaceParam('q', $keys);
    }
  }
}

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

Functions

Namesort descending Description
acquia_search_acquia_subscription_status Implementation of hook_acquia_subscription_status().
acquia_search_apachesolr_query_alter 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_cron Implementation of hook_cron().
acquia_search_disable Implementation of hook_disable().
acquia_search_enable Implementation of hook_enable().
acquia_search_enable_acquia_solr_environment Create a new record pointing to the Acquia apachesolr search server and set it as the default
acquia_search_environment_connected Tests whether the environment is connected to Acquia Search.
acquia_search_environment_delete_access Delete environment page access.
acquia_search_environment_edit_form_validate
acquia_search_extract_hmac Look in the headers and get the hmac_digest out
acquia_search_form_apachesolr_environment_edit_form_alter Implementation of hook_form_[form_id]_alter().
acquia_search_form_apachesolr_settings_alter Implementation of hook_form_[form_id]_alter().
acquia_search_get_environment Predefined Acquia Search network environment
acquia_search_help Implementation of hook_help().
acquia_search_menu_alter Implementation of hook_menu_alter().
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_lower Convert to lower-case any keywords containing a wildcard.

Constants

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