You are here

search_api_acquia.module in Acquia Search for Search API 7.2

Same filename and directory in other branches
  1. 7 search_api_acquia.module

Provides integration between your Drupal site and Acquia's hosted search service via the Search API Solr module.

File

search_api_acquia.module
View source
<?php

/**
 * @file
 * Provides integration between your Drupal site and Acquia's hosted search
 * service via the Search API Solr module.
 */
use Drupal\search_api_acquia\SAPIPreferredSearchCoreService;
define('SEARCH_API_ACQUIA_OVERRIDE_AUTO_SET', 1);
define('SEARCH_API_ACQUIA_AUTO_OVERRIDE_READ_ONLY', 2);
define('SEARCH_API_ACQUIA_EXISTING_OVERRIDE', 3);
define('SEARCH_API_ACQUIA_AUTO_SHOULD_OVERRIDE_READ_ONLY', 4);

/**
 * Implements hook_init().
 */
function search_api_acquia_init() {
  $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')) {
    $servers = search_api_server_load_multiple(FALSE, array(
      'class' => 'acquia_search_service',
    ));
    $result = search_api_acquia_add_solr_overrides($servers);

    // Store result in global for this request. @see search_api_acquia_requirements()
    $GLOBALS['conf']['search_api_acquia_init_result'] = $result;
  }
}

/**
 * Helper function that instantiates and caches the preferred search core service.
 */
function search_api_acquia_get_core_service($acquia_search_api_version = SearchApiAcquiaSearchService::ACQUIA_SEARCH_API_V2) {
  static $core_services;
  if (isset($core_services[$acquia_search_api_version])) {
    return $core_services[$acquia_search_api_version];
  }
  if ($acquia_search_api_version === SearchApiAcquiaSearchService::ACQUIA_SEARCH_API_V3) {
    $core_services[$acquia_search_api_version] = SearchApiAcquiaApi::getFromSettings()
      ->getPreferredCoreService();
    return $core_services[$acquia_search_api_version];
  }
  module_load_include('php', 'search_api_acquia', 'src/SAPIPreferredSearchCoreService');
  global $conf;
  $acquia_identifier = acquia_agent_settings('acquia_identifier');
  $ah_env = isset($_ENV['AH_SITE_ENVIRONMENT']) ? $_ENV['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'] : '';
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $available_cores = search_api_acquia_available_cores($subscription);
  $core_services[$acquia_search_api_version] = new SAPIPreferredSearchCoreService($acquia_identifier, $ah_env, $sites_foldername, $ah_db_name, $available_cores);
  return $core_services[$acquia_search_api_version];
}

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

/**
 * Overrides search_api_solr 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 Search API 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 array $servers
 *   The Search API Solr servers.
 *
 * @return bool
 *   True if preferred core has been found in the list of available cores and
 *   it was used to override the search settings.
 */
function search_api_acquia_add_solr_overrides($servers) {
  $override = FALSE;

  /**
   * @var string $acquia_server_id
   * @var SearchApiServer $server
   */
  foreach ($servers as $acquia_server_id => $server) {
    $core_service = search_api_acquia_get_core_service($server
      ->getAcquiaSearchApiVersion());
    $override |= search_api_acquia_override_server($core_service, $acquia_server_id);
  }
  return $override;
}

/**
 * Overrides settings for a server.
 */
function search_api_acquia_override_server($core_service, $server_id) {
  global $conf;

  // Skip if the Search API server has already been overridden.
  if (!empty($conf['search_api_acquia_overrides'][$server_id])) {
    if (empty($conf['search_api_acquia_overrides'][$server_id]['overridden_by_acquia_search'])) {
      $conf['search_api_acquia_overrides'][$server_id]['overridden_by_acquia_search'] = SEARCH_API_ACQUIA_EXISTING_OVERRIDE;
    }
    return FALSE;
  }
  $conf['search_api_acquia_overrides'][$server_id]['acquia_search_possible_cores'] = $core_service
    ->getListOfPossibleCores();
  if ($core_service
    ->isPreferredCoreAvailable()) {
    $derived_key = search_api_acquia_get_derived_key_for_core($core_service
      ->getPreferredCoreId());
    $conf['search_api_acquia_overrides'][$server_id]['path'] = '/solr/' . $core_service
      ->getPreferredCoreId();
    $conf['search_api_acquia_overrides'][$server_id]['host'] = $core_service
      ->getPreferredCoreHostname();
    $conf['search_api_acquia_overrides'][$server_id]['derived_key'] = $derived_key;
    $conf['search_api_acquia_overrides'][$server_id]['overridden_by_acquia_search'] = SEARCH_API_ACQUIA_OVERRIDE_AUTO_SET;
    return TRUE;
  }

  // At this point none of the available search cores match to what is
  //   expected, so read-only mode should be set.
  // Read only mode is actually set per-index during
  //   search_api_acquia_search_api_index_load(), however we add a diagnostic
  //   value here for testing.
  if (!variable_get('acquia_search_disable_auto_read_only', 0)) {
    $conf['search_api_acquia_overrides'][$server_id]['overridden_by_acquia_search'] = SEARCH_API_ACQUIA_AUTO_SHOULD_OVERRIDE_READ_ONLY;
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_search_api_index_load()
 *
 * This takes care of enforcing read-only mode, because that happens at the
 * Search API index (and not at the server).
 *
 * @param array $indexes
 *   Array of Search API index entities.
 */
function search_api_acquia_search_api_index_load($indexes) {
  global $conf;
  $auto_switch_disabled = variable_get('acquia_search_disable_auto_switch', 0);
  $read_only_switch_disabled = variable_get('acquia_search_disable_auto_read_only', 0);
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $sub_active = !empty($subscription['active']);
  if (!$auto_switch_disabled && !$read_only_switch_disabled && $sub_active && !module_exists('acquia_search_multi_subs')) {
    foreach ($indexes as &$index) {
      if (empty($index->server)) {

        // This covers circumstances where the Acquia Search service hasn't
        // been completely set up. Preventing an empty server machine name
        // from loading prevents array_flip errors in entity_load that are
        // otherwise hard to debug.
        continue;
      }
      $server = search_api_server_load($index->server);
      if (!method_exists($server, 'getAcquiaSearchApiVersion') || !search_api_acquia_get_core_service($server
        ->getAcquiaSearchApiVersion())
        ->isPreferredCoreAvailable()) {
        continue;
      }
      if ($server && $server->class == 'acquia_search_service') {
        $index->read_only = '1';
        $conf['search_api_acquia_overrides'][$index->server]['overridden_by_acquia_search'] = SEARCH_API_ACQUIA_AUTO_OVERRIDE_READ_ONLY;
      }
    }
  }
}

/**
 * Returns derived key for the given core ID.
 *
 * @param string $core_id
 *
 * @return string
 */
function search_api_acquia_get_derived_key_for_core($core_id) {
  $subscription = acquia_agent_settings('acquia_subscription_data');
  $derived_key_salt = $subscription['derived_key_salt'] ?? '';
  $key = acquia_agent_settings('acquia_key');
  $derivation_string = $core_id . 'solr' . $derived_key_salt;
  return hash_hmac('sha1', str_pad($derivation_string, 80, $derivation_string), $key);
}

/**
 * Implements hook_form_[form_id]_alter().
 *
 * Adds extra information to some Search API edit forms.
 * @param $form
 * @param $form_state
 */
function search_api_acquia_form_search_api_admin_server_edit_alter(&$form, &$form_state) {
  $server = $form_state['server'];
  if ($server->class == 'acquia_search_service') {
    if (!search_api_acquia_get_core_service($server
      ->getAcquiaSearchApiVersion())
      ->isPreferredCoreAvailable()) {
      drupal_set_message(search_api_acquia_get_read_only_mode_warning($server), 'warning');
    }
  }
}

/**
 * Implements hook_form_[form_id]_alter().
 *
 * Adds extra information to some Search API edit forms.
 * @param array $form
 * @param $form_state
 */
function search_api_acquia_form_search_api_admin_index_edit_alter(&$form, &$form_state) {
  global $conf;
  $server = search_api_index_get_server($form_state['index']);
  if (is_object($server) && $server->class == 'acquia_search_service') {
    search_api_acquia_add_form_status_message($form, $form_state, $server);

    // Show message if in read-only mode.
    if ($conf['search_api_acquia_overrides'][$server->machine_name]['overridden_by_acquia_search'] == SEARCH_API_ACQUIA_AUTO_OVERRIDE_READ_ONLY) {
      drupal_set_message(search_api_acquia_get_read_only_mode_warning($server), 'warning');
    }
  }
}

/**
 * Implements hook_block_view_MODULE_DELTA_alter().
 *
 * Add extra information onto some Search API admin pages.
 *
 * @param array $data
 * @param array $block
 */
function search_api_acquia_block_view_system_main_alter(&$data, $block) {
  global $conf;
  if (isset($data['content']['view'])) {
    $section = $data['content']['view'];

    // #theme tells us what we're rendering right now.
    if (!isset($section['#theme'])) {
      return;
    }
    if ($section['#theme'] == 'search_api_server') {
      $server = search_api_server_load($section['#machine_name']);
    }
    if ($section['#theme'] == 'search_api_index') {
      $server = $section['#server'];
    }
    if (isset($server->class) && $server->class == 'acquia_search_service') {
      $data['content']['acquia_search_message'] = array(
        '#type' => 'fieldset',
        '#title' => t('Acquia Search status for this connection'),
        '#collapsible' => FALSE,
        '#weight' => -10,
      );
      $data['content']['acquia_search_message']['message'] = array(
        '#markup' => search_api_acquia_get_search_status_message($server),
      );
      if (isset($conf['search_api_acquia_overrides'][$server->machine_name]['overridden_by_acquia_search']) && $conf['search_api_acquia_overrides'][$server->machine_name]['overridden_by_acquia_search'] == SEARCH_API_ACQUIA_AUTO_OVERRIDE_READ_ONLY) {
        drupal_set_message(search_api_acquia_get_read_only_mode_warning($server), 'warning');
      }
    }
  }
}

/**
 * Pings the search core.
 *
 * @param string $server_id
 *   Search API server id
 * @param string $op
 *
 * @return bool
 */
function search_api_acquia_ping($server_id, $op = 'ping') {
  $solr = search_api_server_load($server_id, true)
    ->getSolrConnection();
  if (empty($solr)) {
    return FALSE;
  }
  if ($op == 'ping') {
    try {
      return (bool) $solr
        ->{$op}();
    } catch (Exception $e) {
      watchdog_exception('search_api_acquia', $e, 'Exception thrown when calling @op on Search API Solr connection. %type: !message in %function (line %line of %file).', array(
        '@op' => $op,
      ));
    }
  }
  if ($op == 'deep-ping') {
    try {
      $result = $solr
        ->makeServletRequest('admin/luke', array(
        'numTerms' => 0,
      ));
      if ($result->code == 200) {
        return TRUE;
      }
    } catch (Exception $e) {
      watchdog_exception('search_api_acquia', $e, 'Exception thrown when calling @op on Search API Solr connection. %type: !message in %function (line %line of %file).', array(
        '@op' => $op,
      ));
    }
  }
  return FALSE;
}

/**
 * Adds Acquia search connection details to the given form.
 *
 * @param array $form
 * @param array $form_state
 * @param SearchApiServer $server
 */
function search_api_acquia_add_form_status_message(&$form, &$form_state, $server) {
  $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' => search_api_acquia_get_search_status_message($server),
  );
}

/**
 * Returns formatted message about read-only mode.
 *
 * @param SearchApiServer $server
 * @param string $t
 *
 * @return string
 */
function search_api_acquia_get_read_only_mode_warning($server, $t = 't') {
  global $conf;
  $msg = $t('To protect your data, the Search API Acquia module is enforcing
    read-only mode on the Search API indexes, 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($conf['search_api_acquia_overrides'][$server->machine_name]['acquia_search_possible_cores'])) {
    $list = theme('item_list', array(
      'items' => $conf['search_api_acquia_overrides'][$server->machine_name]['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 .= PHP_EOL . $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 SearchApiServer $server
 * @return string
 */
function search_api_acquia_get_search_status_message($server) {
  global $conf;
  $options = $server->options;

  // Apply overrides if they exist.
  if (isset($conf['search_api_acquia_overrides'][$server->machine_name])) {
    $options = array_merge($options, $conf['search_api_acquia_overrides'][$server->machine_name]);
  }
  $url = $options['scheme'] . '://' . $options['host'] . ':' . $options['port'] . $options['path'];
  $items = array(
    t('search_api_solr.module server ID: @env', array(
      '@env' => $server->machine_name,
    )),
    t('URL: @url', array(
      '@url' => $url,
    )),
  );
  if (search_api_acquia_ping($server->machine_name)) {
    $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 (search_api_acquia_ping($server->machine_name, 'deep-ping')) {
    $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 Search API Acquia module.') . theme('item_list', array(
    'items' => $items,
  ));
}

/**
 * Implements hook_enable().
 */
function search_api_acquia_enable() {
  search_api_acquia_set_version();
}

/**
 * Implements hook_cron().
 */
function search_api_acquia_cron() {
  search_api_acquia_set_version();
  search_api_acquia_cron_optimize();
}

/**
 * Runs optimize during cron runs.
 *
 * @see search_api_solr_cron()
 */
function search_api_acquia_cron_optimize() {
  $action = variable_get('search_api_acquia_cron_action', 'spellcheck');

  // We treat all unknown action settings as "none". However, we turn a blind
  // eye for Britons and other people who can spell.
  if (!in_array($action, array(
    'spellcheck',
    'optimize',
    'optimise',
  ))) {
    return;
  }

  // 86400 seconds is one day. We use slightly less here to allow for some
  // variation in the request time of the cron run, so that the time of day will
  // (more or less) stay the same.
  if (REQUEST_TIME - variable_get('search_api_acquia_last_optimize', 0) > 86340) {
    variable_set('search_api_acquia_last_optimize', REQUEST_TIME);
    $conditions = array(
      'class' => 'acquia_search_service',
      'enabled' => TRUE,
    );
    $count = 0;
    foreach (search_api_server_load_multiple(FALSE, $conditions) as $server) {
      try {
        $solr = $server
          ->getSolrConnection();
        if ($action != 'spellcheck') {
          $solr
            ->optimize(FALSE);
        }
        else {
          $params['rows'] = 0;
          $params['spellcheck'] = 'true';
          $params['spellcheck.build'] = 'true';
          $solr
            ->search(NULL, $params);
        }
        ++$count;
      } catch (SearchApiException $e) {
        watchdog_exception('search_api_acquia', $e, '%type while optimizing Solr server @server: !message in %function (line %line of %file).', array(
          '@server' => $server->name,
        ));
      }
    }
    if ($count) {
      $vars['@count'] = $count;
      if ($action != 'spellcheck') {
        watchdog('search_api_acquia', 'Optimized @count Solr server(s).', $vars, WATCHDOG_INFO);
      }
      else {
        watchdog('search_api_acquia', 'Rebuilt spellcheck dictionary on @count Solr server(s).', $vars, WATCHDOG_INFO);
      }
    }
  }
}

/**
 * Sets the version of this module in a system variable.
 *
 * The version is used to construct the User Agent in requests to the backend.
 * This allows Acquia to determine which module is being used to generate the
 * search query which helps in debugging.
 */
function search_api_acquia_set_version() {

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

  // 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('search_api_acquia_version', $new_version);
  }
}

/**
 * Implements hook_search_api_service_info().
 */
function search_api_acquia_search_api_service_info() {
  return array(
    'acquia_search_service' => array(
      'name' => t('Acquia Search'),
      'description' => t('<p>Index items using the Acquia Search service.<p>'),
      'class' => 'SearchApiAcquiaSearchService',
    ),
  );
}

Functions

Namesort descending Description
search_api_acquia_add_form_status_message Adds Acquia search connection details to the given form.
search_api_acquia_add_solr_overrides Overrides search_api_solr configs to talk to the proper Acquia search core.
search_api_acquia_available_cores Retrieves all available search cores as set in the subscription.
search_api_acquia_block_view_system_main_alter Implements hook_block_view_MODULE_DELTA_alter().
search_api_acquia_cron Implements hook_cron().
search_api_acquia_cron_optimize Runs optimize during cron runs.
search_api_acquia_enable Implements hook_enable().
search_api_acquia_form_search_api_admin_index_edit_alter Implements hook_form_[form_id]_alter().
search_api_acquia_form_search_api_admin_server_edit_alter Implements hook_form_[form_id]_alter().
search_api_acquia_get_core_service Helper function that instantiates and caches the preferred search core service.
search_api_acquia_get_derived_key_for_core Returns derived key for the given core ID.
search_api_acquia_get_read_only_mode_warning Returns formatted message about read-only mode.
search_api_acquia_get_search_status_message Returns formatted message about Acquia Search connection details.
search_api_acquia_init Implements hook_init().
search_api_acquia_override_server Overrides settings for a server.
search_api_acquia_ping Pings the search core.
search_api_acquia_search_api_index_load Implements hook_search_api_index_load()
search_api_acquia_search_api_service_info Implements hook_search_api_service_info().
search_api_acquia_set_version Sets the version of this module in a system variable.

Constants