You are here

acquia_search.module in Acquia Connector 7.3

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.
 */
use Drupal\acquia_search\PreferredSearchCoreService;

// Original env ID for earlier versions of Acquia Search.
define('ACQUIA_SEARCH_ENVIRONMENT_ID', 'acquia_search_server_1');

// Env ID for V3 environments.
define('ACQUIA_SEARCH_V3_ENVIRONMENT_ID', 'acquia_search_server_3');
define('ACQUIA_SEARCH_OVERRIDE_AUTO_SET', 1);
define('ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY', 2);
define('ACQUIA_SEARCH_EXISTING_OVERRIDE', 3);
define('ACQUIA_SEARCH_SOLR_CORE_V2', "2");
define('ACQUIA_SEARCH_SOLR_CORE_V3', "3");
module_load_include('inc', 'acquia_search', 'includes/acquia_search_solr.helpers');

/**
 * Implements hook_init().
 */
function acquia_search_init() {

  // Add apachesolr module connection overrides if applicable.
  $auto_switch_disabled = variable_get('acquia_search_disable_auto_switch', 0);
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $sub_active = !empty($subscription['active']);
  if (!$auto_switch_disabled && $sub_active && !module_exists('acquia_search_multi_subs')) {
    module_load_include('php', 'acquia_search', 'src/PreferredSearchCoreService');
    global $conf;
    $acquia_identifier = acquia_agent_settings('acquia_identifier');
    $ah_env = isset($_SERVER['AH_SITE_ENVIRONMENT']) ? $_SERVER['AH_SITE_ENVIRONMENT'] : '';
    $sites_foldername = substr(conf_path(), strrpos(conf_path(), '/') + 1);
    $ah_db_name = isset($conf['acquia_hosting_site_info']['db']['name']) ? $conf['acquia_hosting_site_info']['db']['name'] : '';
    $available_cores = acquia_search_available_cores($subscription);
    $environments = apachesolr_load_all_environments();
    $core_service = new PreferredSearchCoreService($acquia_identifier, $ah_env, $sites_foldername, $ah_db_name, $available_cores);
    $overrode = acquia_search_add_apachesolr_overrides($core_service, $environments);
    if ($overrode) {

      // If an override was applied, then clear the corresponding cache item.
      cache_clear_all('apachesolr:environments', 'cache_apachesolr');
    }
  }
}

/**
 * Overrides apachesolr configs to talk to the proper Acquia search core.
 *
 * It determines proper (preferred) Acquia search core to which the site should
 * be connected and overrides the apachesolr config accordingly. If a proper
 * search core is not found, it enables the read-only mode so that the site
 * does not unintentionally connect to the wrong core and corrupts its index.
 *
 * @param Drupal\acquia_search\PreferredSearchCoreService $core_service
 *   Core service.
 * @param array $environments
 *   The apachesolr environments.
 *
 * @return bool|null
 *   True if preferred core has been found in the list of available cores and
 *   it was used to override the search settings.
 */
function acquia_search_add_apachesolr_overrides(PreferredSearchCoreService $core_service, array $environments) {
  global $conf;
  $overrode = FALSE;
  foreach ($environments as $acquia_env_id => $environment) {

    // Override Acquia search environments only.
    if ($environment['service_class'] != 'AcquiaSearchService') {
      continue;
    }

    // Skip if the Acquia search environment has already been overridden.
    if (!empty($conf['apachesolr_environments'][$acquia_env_id])) {
      continue;
    }
    $conf['apachesolr_environments'][$acquia_env_id]['acquia_search_possible_cores'] = $core_service
      ->getListOfPossibleCores();
    if ($core_service
      ->isPreferredCoreAvailable()) {
      $derived_key = acquia_search_get_derived_key_for_core($core_service
        ->getPreferredCoreId());
      $conf['apachesolr_environments'][$acquia_env_id]['url'] = $core_service
        ->getPreferredCoreUrl();
      $conf['apachesolr_environments'][$acquia_env_id]['conf'] = array(
        'acquia_search_key' => $derived_key,
      );
      $conf['apachesolr_environments'][$acquia_env_id]['overridden_by_acquia_search'] = ACQUIA_SEARCH_OVERRIDE_AUTO_SET;
      drupal_static_reset('apachesolr_load_all_environments');
      $overrode = TRUE;
      continue;
    }

    // At this point none of the available search cores match to what is
    // expected. Switch the search into read-only mode.
    if (!variable_get('acquia_search_disable_auto_read_only', 0)) {
      $conf['apachesolr_environments'][$acquia_env_id]['conf'] = array(
        'apachesolr_read_only' => 1,
      );
      $conf['apachesolr_environments'][$acquia_env_id]['overridden_by_acquia_search'] = ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY;
      drupal_static_reset('apachesolr_load_all_environments');
      $overrode = TRUE;
    }
  }
  return $overrode;
}

/**
 * Returns derived key for the given core ID.
 *
 * @param string $core_id
 *   Core id.
 *
 * @return string
 *   Derived key.
 */
function acquia_search_get_derived_key_for_core($core_id) {
  $salt = acquia_search_derived_key_salt();
  $key = acquia_agent_settings('acquia_key');
  return _acquia_search_create_derived_key($salt, $core_id, $key);
}

/**
 * Retrieves all available search cores as set in the subscription.
 *
 * @param array $subscription
 *   Acquia Subscription array.
 *
 * @return array
 *   Search cores.
 */
function acquia_search_available_cores(array $subscription) {
  if (!empty($subscription['heartbeat_data']['search_cores'])) {
    return $subscription['heartbeat_data']['search_cores'];
  }
}

/**
 * Pings the search core.
 *
 * @param string $env_id
 *   Env id.
 * @param string $op
 *   Op.
 *
 * @return bool
 *   Success.
 */
function acquia_search_ping($env_id, $op = 'ping') {
  try {
    return (bool) apachesolr_get_solr($env_id)
      ->{$op}();
  } catch (Exception $e) {
    watchdog_exception('acquia_search', $e, 'Exception thrown when calling @op on Apachesolr. %type: !message in %function (line %line of %file).', array(
      '@op' => $op,
    ));
  }
}

/**
 * Adds Acquia search connection details to the given form.
 *
 * @param array $form
 *   Form.
 * @param array $form_state
 *   Form state.
 * @param array $environment
 *   An Apache Solr module environment.
 */
function acquia_search_add_form_status_message(array &$form, array &$form_state, array $environment) {
  $form['acquia_search_message'] = array(
    '#type' => 'fieldset',
    '#title' => t('Acquia Search status for this connection'),
    '#collapsible' => FALSE,
    '#weight' => -10,
  );
  $form['acquia_search_message']['message'] = array(
    '#markup' => acquia_search_get_search_status_message($environment),
  );
}

/**
 * Returns formatted message about read-only mode.
 *
 * @param array $environment
 *   An Apache Solr module environment.
 * @param string $t
 *   Message.
 *
 * @return string
 *   Formatted message.
 */
function acquia_search_get_read_only_mode_warning(array $environment, $t = 't') {
  $msg = $t('To protect your data, Acquia Search module is enforcing
    read-only mode, because it could not figure out what Acquia-hosted Solr
    index to connect to. This helps you avoid writing to a production index
    if you copy your site to a development or other environment(s).');
  if (!empty($environment['acquia_search_possible_cores'])) {
    $list = theme('item_list', array(
      'items' => $environment['acquia_search_possible_cores'],
    ));
    $msg .= '<p>';
    $msg .= $t('These index IDs would have worked, but could not be found on
      your Acquia subscription: !list', array(
      '!list' => $list,
    ));
    $msg .= '</p>';
  }
  $msg .= $t('To fix this problem, please read <a href="@url">our documentation</a>.', array(
    '@url' => 'https://docs.acquia.com/acquia-search/multiple-cores',
  ));
  return $msg;
}

/**
 * Returns formatted message about Acquia Search connection details.
 *
 * @param array $environment
 *   An Apache Solr module environment.
 *
 * @return string
 *   Formatted message.
 */
function acquia_search_get_search_status_message(array $environment) {
  $items = array(
    t('apachesolr.module environment ID: @env', array(
      '@env' => $environment['env_id'],
    )),
    t('URL: @url', array(
      '@url' => $environment['url'],
    )),
  );
  if (acquia_search_ping($environment['env_id'])) {
    $items[] = t('Solr index is currently reachable and up.');
  }
  else {

    // Add message with error class.
    $items[] = array(
      'data' => t('Solr index is currently unreachable.'),
      'class' => array(
        'error',
      ),
    );
  }

  // Deep-ping the Solr index to ensure authentication is working.
  if (acquia_search_ping($environment['env_id'], 'getFields')) {
    $items[] = t('Requests to Solr index are passing authentication checks.');
  }
  else {

    // Add message with error class.
    $items[] = array(
      'data' => t('Solr core authentication check fails.'),
      'class' => array(
        'error',
      ),
    );
  }
  return t('Connection managed by Acquia Search module.') . theme('item_list', array(
    'items' => $items,
  ));
}

/**
 * Predefined Acquia Search network environment.
 */
function acquia_search_get_environment($conf = array()) {
  if (!empty($conf['acquia_subscription_id']) && !empty($conf['acquia_subscription_key'])) {
    $identifier = $conf['acquia_subscription_id'];
    $key = $conf['acquia_subscription_key'];
    $subscription = acquia_agent_get_subscription($params = array(), $identifier, $key);
  }
  else {
    $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;
    }
  }
  $environment = array(
    'url' => 'http://' . $search_host . variable_get('acquia_search_path', '/solr/' . $identifier),
    'service_class' => 'AcquiaSearchService',
  );
  return $environment;
}

/**
 * Implements hook_help().
 */
function acquia_search_help($path, $arg) {
  switch ($path) {
    case 'admin/config/search/apachesolr':
      $env_id = isset($arg[5]) ? $arg[5] : apachesolr_default_environment();
      $environment = apachesolr_environment_load($env_id);
      $subscription = acquia_agent_settings('acquia_subscription_data');
      $sub_active = !empty($subscription['active']);
      if (acquia_search_environment_connected($environment) && $sub_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.
 *
 * 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) {
    $environment['conf'] = array();

    // Copy the bundles from the previous default environment.
    $orig_env_id = apachesolr_default_environment();
    $orig_env = apachesolr_environment_load($orig_env_id);
    $environment['index_bundles'] = $orig_env['index_bundles'];

    // Also make sure that the default search page has Acquia Search as its
    // default environment.
    $default_search_page_id = apachesolr_search_default_search_page();
    $default_search_page = apachesolr_search_page_load($default_search_page_id);
    if (!empty($default_search_page) && $default_search_page['env_id'] != ACQUIA_SEARCH_ENVIRONMENT_ID) {
      $default_search_page['env_id'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
      apachesolr_search_page_save($default_search_page);
    }

    // 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);

    // Make sure apachesolr search is the default search module.
    variable_set('search_default_module', 'apachesolr_search');
  }
  $acquia_environment = acquia_search_get_environment();

  // Override default values.
  foreach ($acquia_environment as $key => $value) {
    $environment[$key] = $value;
  }
  $environment['env_id'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
  $environment['name'] = t('Acquia Search');

  // Allow other modules to override this.
  drupal_alter('acquia_search_enable', $environment);
  apachesolr_environment_save($environment);
}

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

/**
 * Helper function to cache the Acquia Search version.
 */
function _acquia_search_set_version() {

  // Cache the version 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) {
  if (in_array($environment['service_class'], [
    AcquiaSearchService::class,
    AcquiaSearchSolrService::class,
  ])) {
    return TRUE;
  }
  $acquia_search_key = apachesolr_environment_variable_get($environment['env_id'], 'acquia_search_key');
  if (!empty($acquia_search_key)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Delete environment page access.
 */
function acquia_search_environment_delete_access($environment) {
  if (empty($environment['env_id'])) {
    return FALSE;
  }
  if ($environment['env_id'] === ACQUIA_SEARCH_V3_ENVIRONMENT_ID || $environment['env_id'] === ACQUIA_SEARCH_ENVIRONMENT_ID) {
    return FALSE;
  }

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

/**
 * Implements 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.
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $sub_active = !empty($subscription['active']);
  if ($sub_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,
    );
  }
}

/**
 * Implements 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;

  // Add message to Acquia-connected indexes.
  if ($environment && acquia_search_environment_connected($environment)) {
    acquia_search_add_form_status_message($form, $form_state, $environment);
    if (acquia_search_is_environment_overridden_to_read_only($environment)) {
      $dsm = acquia_search_get_read_only_mode_warning($environment);
      drupal_set_message($dsm, 'warning', FALSE);

      // Avoid accidental saving of overridden values into the Database.
      $form['conf']['apachesolr_read_only']['#disabled'] = TRUE;
      $form['conf']['apachesolr_read_only']['#type'] = 'hidden';
    }
    $form['url']['#disabled'] = TRUE;
    $form['env_id']['#disabled'] = TRUE;
  }

  // Don't let the user delete the initial environment.
  $acquia_solr_environment = FALSE;
  if ($env_id == ACQUIA_SEARCH_ENVIRONMENT_ID || $env_id == ACQUIA_SEARCH_V3_ENVIRONMENT_ID) {
    $form['name']['#disabled'] = TRUE;
    $form['actions']['delete']['#access'] = FALSE;
    $acquia_solr_environment = TRUE;
  }

  // Adding solr core select option.
  $solr_core = '';
  if ($environment['env_id'] == ACQUIA_SEARCH_ENVIRONMENT_ID) {
    $solr_core = ACQUIA_SEARCH_SOLR_CORE_V2;
  }
  elseif ($environment['env_id'] == ACQUIA_SEARCH_V3_ENVIRONMENT_ID) {
    $solr_core = ACQUIA_SEARCH_SOLR_CORE_V3;
  }
  $form_element['acquia_search_api']['acquia_search_solr_core'] = [
    '#title' => t('Acquia Search Solr core'),
    '#type' => 'select',
    '#options' => [
      ACQUIA_SEARCH_SOLR_CORE_V2 => 'Solr 6 and below',
      ACQUIA_SEARCH_SOLR_CORE_V3 => 'Solr 7 and above',
    ],
    '#empty_value' => '',
    '#empty_option' => t('- None -'),
    '#default_value' => $solr_core,
    '#description' => t('Select version of Solr Core.'),
    '#disabled' => $acquia_solr_environment,
    '#ajax' => [
      'callback' => '_acquia_make_environment_url',
      'event' => 'change',
      'wrapper' => 'acquia_search_environment_values',
    ],
  ];
  array_unshift($form, $form_element['acquia_search_api']);
  $form['#prefix'] = '<div id="acquia_search_environment_values">';
  $form['#suffix'] = '</div>';
  $acquia_environment = acquia_search_get_environment();

  // Override default values.
  foreach ($acquia_environment as $key => $value) {
    $environment[$key] = $value;
  }
  $environment['env_id'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
  $environment['name'] = t('Acquia Search');
  $form['actions']['save']['#submit'][] = 'acquia_search_solr_settings_form_new_environment_submit';
  $form['actions']['save_edit']['#submit'] = [
    'acquia_search_solr_settings_form_new_environment_submit',
  ];
  $form['actions']['save']['#validate'][] = 'acquia_search_environment_edit_form_validate';
}

/**
 * Ajjax callback to set enviroment fields in form.
 *
 * @param mixed $form
 *   Form array.
 * @param mixed $form_state
 *   Form state array.
 *
 * @return mixed
 *   Returns Form array with field values.
 */
function _acquia_make_environment_url($form, &$form_state) {
  $solr_version = $form_state['values']['acquia_search_solr_core'];
  variable_set('acquia_search_solr_core', $form_state['values']['acquia_search_solr_core']);
  if ($solr_version == ACQUIA_SEARCH_SOLR_CORE_V2) {
    if (apachesolr_environment_load(ACQUIA_SEARCH_ENVIRONMENT_ID)) {
      $message = t('Search environment already exists.');
      drupal_set_message($message, 'warning');
    }
    $acquia_environment = acquia_search_get_environment();

    // Override default values.
    foreach ($acquia_environment as $key => $value) {
      $environment[$key] = $value;
    }
    $form['name']['#value'] = t('Acquia Search');
    $form['url']['#value'] = $environment['url'];
    $form['env_id']['#value'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
    $form['service_class']['#value'] = $environment['service_class'];

    // Allow other modules to override this.
    drupal_alter('acquia_search_enable', $environment);
  }
  elseif ($solr_version == ACQUIA_SEARCH_SOLR_CORE_V3) {
    $api = _acquia_search_solr_get_api();
    if (empty($api)) {
      $form['name']['#value'] = '';
      $form['url']['#value'] = '';
      $form['env_id']['#value'] = '';
      $form['service_class']['#value'] = '';
      $form_state['rebuild'] = TRUE;
      return $form;
    }
    if ($env = apachesolr_environment_load(ACQUIA_SEARCH_V3_ENVIRONMENT_ID)) {
      if (!empty($env['service_class'])) {
        $message = t('Search environment already exists.');
        drupal_set_message($message, 'warning');
      }
    }
    $preferred_index_service = $api
      ->getPreferredIndexService();
    if (!$preferred_index_service
      ->isPreferredIndexAvailable()) {
      $message = AcquiaSearchSolrMessages::getNoPreferredIndexError($preferred_index_service
        ->getAvailableIndexesIds());
      drupal_set_message($message, 'warning');
      $form['name']['#value'] = '';
      $form['url']['#value'] = '';
      $form['env_id']['#value'] = '';
      $form['service_class']['#value'] = '';
      $form_state['rebuild'] = TRUE;
      return $form;
    }
    $environment = apachesolr_environment_load(ACQUIA_SEARCH_V3_ENVIRONMENT_ID);
    if (!$environment) {
      $environment = [
        'env_id' => ACQUIA_SEARCH_V3_ENVIRONMENT_ID,
        'conf' => [],
      ];

      // Copy bundles from the previous default environment.
      $orig_env_id = apachesolr_default_environment();
      $orig_env = apachesolr_environment_load($orig_env_id);
      $environment['index_bundles'] = $orig_env['index_bundles'];
    }
    $environment['name'] = t('Acquia Search');
    $environment['service_class'] = AcquiaSearchSolrService::class;
    $preferred_index = $preferred_index_service
      ->getPreferredIndex();
    $environment['url'] = sprintf('https://%s', $preferred_index['data']['index_url']);
    $form['#environmet'] = $environment;
    $form['name']['#value'] = $environment['name'];
    $form['url']['#value'] = $environment['url'];
    $form['env_id']['#value'] = ACQUIA_SEARCH_V3_ENVIRONMENT_ID;
    $form['service_class']['#value'] = $environment['service_class'];
  }
  else {
    $form['name']['#value'] = '';
    $form['url']['#value'] = '';
    $form['env_id']['#value'] = '';
    $form['service_class']['#value'] = '';
  }
  $form_state['rebuild'] = TRUE;
  return $form;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function acquia_search_form_apachesolr_index_action_form_alter(&$form, $form_state) {
  $env_id = isset($form['action']['env_id']['#value']) ? $form['action']['env_id']['#value'] : '';
  $environment = $env_id ? apachesolr_environment_load($env_id) : FALSE;

  // Add message to Acquia-connected indexes.
  if ($environment && acquia_search_environment_connected($environment)) {
    acquia_search_add_form_status_message($form, $form_state, $environment);
    if (acquia_search_is_environment_overridden_to_read_only($environment)) {
      $dsm = acquia_search_get_read_only_mode_warning($environment);
      drupal_set_message($dsm, 'warning', FALSE);
    }
  }
}

/**
 * Determines whether the given environment is in the read-only mode.
 *
 * @param array $environment
 *   Apachesolr environment.
 *
 * @return bool
 *   True if the environment has been switched to read-only mode by Acquia
 *   Search.
 */
function acquia_search_is_environment_overridden_to_read_only(array $environment) {
  return isset($environment['overridden_by_acquia_search']) && $environment['overridden_by_acquia_search'] == ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY;
}

/**
 * Ensures environment parameters are unchanged.
 *
 * @param mixed $form
 *   Form array.
 * @param mixed $form_state
 *   Form state array.
 */
function acquia_search_environment_edit_form_validate($form, &$form_state) {
  if ($form_state['values']['acquia_search_solr_core'] === ACQUIA_SEARCH_SOLR_CORE_V2) {

    // Make sure that the environment parameters have not been changed.
    $form_state['values']['env_id'] = ACQUIA_SEARCH_ENVIRONMENT_ID;
    $form_state['values'] = array_merge($form_state['values'], acquia_search_get_environment());
  }
  if ($form_state['values']['acquia_search_solr_core'] === ACQUIA_SEARCH_SOLR_CORE_V3) {

    // Make sure that the environment parameters have not been changed.
    $form_state['values'] = array_merge($form_state['values'], [
      'service_class' => AcquiaSearchSolrService::class,
      'env_id' => ACQUIA_SEARCH_V3_ENVIRONMENT_ID,
    ]);
  }
}

/**
 * Modify a solr base url and construct a hmac authenticator cookie.
 *
 * @param string $url
 *   The solr url beng requested - passed by reference and may be altered.
 * @param string $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 string $derived_key
 *   Optional string to supply the derived key.
 *
 * @return array
 *   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,
  );
}

/**
 * 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;
}

/**
 * Get derived key for 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.
    $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[$env_id] = '';
    }
    elseif (!isset($derived_key[$env_id])) {

      // Get the Solr core identifier from the URL, to build the correct derived
      // key.
      $environment = apachesolr_environment_load($env_id);
      if (isset($environment['url'])) {
        $core_id = acquia_search_extract_core_id_from_environment_url($environment['url']);
        $derived_key[$env_id] = _acquia_search_create_derived_key($derived_key_salt, $core_id, $key);
      }
    }
  }
  return $derived_key[$env_id];
}

/**
 * Extracts and return a core ID from given URL.
 *
 * Returns the last part of URL within given environment URL which represents
 * the core ID. E.g. for 'http://useast1-c1.acquia-search.com/solr/GHTV-36910'
 * it returns 'GHTV-36910'.
 * For 'http://useast1-c1.acquia-search.com/solr/GHTV-36910.dev.mysitedev'
 * it returns 'GHTV-36910.dev.mysitedev'.
 *
 * @param string $url
 *   URL.
 *
 * @return string
 *   Core id.
 */
function acquia_search_extract_core_id_from_environment_url($url) {
  return substr($url, strrpos($url, '/') + 1);
}

/**
 * Derive a key for the solr hmac using a salt, id and key.
 */
function _acquia_search_create_derived_key($salt, $id, $key) {
  $derivation_string = $id . 'solr' . $salt;
  return hash_hmac('sha1', str_pad($derivation_string, 80, $derivation_string), $key);
}

/**
 * Creates an authenticator based on a data string and HMAC-SHA1.
 */
function acquia_search_authenticator($string, $nonce, $derived_key = NULL, $env_id = NULL, $time = 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 {

    // @see http://stackoverflow.com/questions/2524680/check-whether-the-string-is-a-unix-timestamp
    if (!(is_numeric($time) && (int) $time == $time)) {

      // Use time() instead of REQUEST_TIME so that long-running operations like
      // `drush solr-index` continually have fresh request times. Use of
      // REQUEST_TIME will cause Acquia Search to respond with a 403 Forbidden
      // after the acquia_solr_time value is older than 15 minutes.
      $time = 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 bool
 *   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 '';
}

/**
 * Implements hook_apachesolr_modify_query().
 *
 * Possibly alters the query type ('defType') param to edismax.
 */
function acquia_search_apachesolr_query_alter($query) {
  $environment = apachesolr_environment_load($query
    ->solr('getId'));

  // @todo - does it make sense to check $caller too?
  if (!acquia_search_environment_connected($environment) || $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);
    }
  }
  if ($environment['service_class'] === AcquiaSearchSolrService::class) {

    // Converts qf parameter to required format.
    $qf = $query
      ->getParam('qf');
    if (!empty($qf) && is_array($qf)) {
      $qf_string = implode(' ', $qf);
      $query
        ->replaceParam('qf', $qf_string);
    }
  }
}

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

/**
 * Additional submit handler which creates preconfigured search environment.
 *
 * @param array $form
 *   Form array.
 * @param array $form_state
 *   Form state array.
 */
function acquia_search_solr_settings_form_new_environment_submit(array $form, array &$form_state) {
  variable_set('acquia_search_solr_core', $form_state['values']['acquia_search_solr_core']);
  if (!acquia_agent_subscription_is_active()) {
    return;
  }

  // Checking Solr Core version.
  if (variable_get('acquia_search_solr_core', ACQUIA_SEARCH_SOLR_CORE_V2) === ACQUIA_SEARCH_SOLR_CORE_V3) {

    // Make sure acquia search is the default search module.
    variable_set('search_default_module', 'acquia_search');
  }
  else {

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

Functions

Namesort descending Description
acquia_search_add_apachesolr_overrides Overrides apachesolr configs to talk to the proper Acquia search core.
acquia_search_add_form_status_message Adds Acquia search connection details to the given form.
acquia_search_apachesolr_query_alter Implements 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_available_cores Retrieves all available search cores as set in the subscription.
acquia_search_derived_key_salt Returns the subscription's salt used to generate the derived key.
acquia_search_enable_acquia_solr_environment Create a new record pointing to the Acquia apachesolr search server.
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 Ensures environment parameters are unchanged.
acquia_search_extract_core_id_from_environment_url Extracts and return a core ID from given URL.
acquia_search_extract_hmac Look in the headers and get the hmac_digest out.
acquia_search_form_apachesolr_environment_edit_form_alter Implements hook_form_FORM_ID_alter().
acquia_search_form_apachesolr_index_action_form_alter Implements hook_form_FORM_ID_alter().
acquia_search_form_apachesolr_settings_alter Implements hook_form_FORM_ID_alter().
acquia_search_get_derived_key_for_core Returns derived key for the given core ID.
acquia_search_get_environment Predefined Acquia Search network environment.
acquia_search_get_read_only_mode_warning Returns formatted message about read-only mode.
acquia_search_get_search_status_message Returns formatted message about Acquia Search connection details.
acquia_search_help Implements hook_help().
acquia_search_init Implements hook_init().
acquia_search_is_environment_overridden_to_read_only Determines whether the given environment is in the read-only mode.
acquia_search_menu_alter Implements hook_menu_alter().
acquia_search_ping Pings the search core.
acquia_search_solr_settings_form_new_environment_submit Additional submit handler which creates preconfigured search environment.
acquia_search_valid_response Validate the authenticity of returned data using a nonce and HMAC-SHA1.
_acquia_make_environment_url Ajjax callback to set enviroment fields in form.
_acquia_search_create_derived_key Derive a key for the solr hmac using a salt, id and key.
_acquia_search_derived_key Get derived key for solr hmac using the information shared with acquia.com.
_acquia_search_lower Convert to lower-case any keywords containing a wildcard.
_acquia_search_set_version Helper function to cache the Acquia Search version.

Constants