acquia_search.module in Acquia Connector 8
Same filename and directory in other branches
Integration between Acquia Drupal and Acquia's hosted solr search service.
File
acquia_search/acquia_search.moduleView source
<?php
/**
* @file
* Integration between Acquia Drupal and Acquia's hosted solr search service.
*/
use Drupal\acquia_connector\Helper\Storage;
use Drupal\acquia_connector\Subscription;
use Drupal\acquia_search\AcquiaSearchV3ApiClient;
use Drupal\acquia_search\PreferredSearchCoreService;
use Drupal\Core\Database\Database;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\Markup;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Entity\Server;
define('ACQUIA_SEARCH_OVERRIDE_AUTO_SET', 1);
define('ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY', 2);
define('ACQUIA_SEARCH_EXISTING_OVERRIDE', 3);
/**
* Implements hook_acquia_subscription_status().
*/
function acquia_search_acquia_subscription_status($subscription = FALSE) {
if (!empty($subscription['active'])) {
// Refresh the salt with the subscription data returned by the heartbeat
// since it can change periodically.
$salt = \Drupal::config('acquia_search.settings')
->get('derived_key_salt');
if (isset($subscription['derived_key_salt']) && $salt != $subscription['derived_key_salt']) {
\Drupal::configFactory()
->getEditable('acquia_search.settings')
->set('derived_key_salt', $subscription['derived_key_salt'])
->save();
}
// Enable search.
/** @var \Drupal\search_api\Entity\Server $server */
if ($server = Server::load('acquia_search_server')) {
$server
->set('status', TRUE);
$server
->save();
}
/** @var \Drupal\search_api\Entity\Index $index */
if ($index = Index::load('acquia_search_index')) {
$index
->set('status', TRUE);
$index
->save();
}
}
elseif (is_int($subscription)) {
// @todo: Maybe we don't want to switch off search/index because there could
// be an override in place. But perhaps we need to log it or show a message.
switch ($subscription) {
case Subscription::NOT_FOUND:
case Subscription::EXPIRED:
// Disable search.
/** @var \Drupal\search_api\Entity\Server $server */
if ($server = Server::load('acquia_search_server')) {
$server
->set('status', FALSE);
$server
->save();
}
/** @var \Drupal\search_api\Entity\Index $index */
if ($index = Index::load('acquia_search_index')) {
$index
->set('status', FALSE);
$index
->save();
}
break;
}
}
}
/**
* Acquia Search helper function. Returns search host.
*
* @param array $subscription
* Acquia Subscription.
*
* @return string
* Search server url.
*/
function acquia_search_get_search_host(array $subscription = []) {
if (empty($subscription)) {
$subscription = \Drupal::state()
->get('acquia_subscription_data');
}
$search_host = \Drupal::config('acquia_search.settings')
->get('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'];
}
// Check if we are on Acquia Cloud hosting. @see NN-2503.
if (!empty($_ENV['AH_SITE_ENVIRONMENT']) && !empty($_ENV['AH_CURRENT_REGION'])) {
if ($_ENV['AH_CURRENT_REGION'] == 'us-east-1' && $search_host == 'search.acquia.com') {
$search_host = 'internal-search.acquia.com';
}
elseif (strpos($search_host, 'search-' . $_ENV['AH_CURRENT_REGION']) === 0) {
$search_host = 'internal-' . $search_host;
}
}
return $search_host;
}
/**
* Implements hook_entity_operation_alter().
*
* Don't allow delete default server and index.
*/
function acquia_search_entity_operation_alter(array &$operations, EntityInterface $entity) {
if (empty($operations['delete'])) {
return;
}
$do_not_delete = [
'acquia_search_server',
'acquia_search_index',
];
if (array_search($entity
->id(), $do_not_delete) !== FALSE) {
unset($operations['delete']);
}
}
/**
* Determine whether search core auto switch functionality is disabled.
*
* @return bool
* TRUE if the auto switch feature disabled via configuration, FALSE
* otherwise.
*/
function acquia_search_is_auto_switch_disabled() {
return !empty(\Drupal::config('acquia_search.settings')
->get('disable_auto_switch'));
}
/**
* Determine whether search config has been overridden via settings.php.
*
* @return bool
* TRUE if a search config has been overridden, FALSE otherwise.
*/
function acquia_search_is_connection_config_overridden() {
$overrides = \Drupal::config('acquia_search.settings')
->get('connection_override');
if (!$overrides) {
return FALSE;
}
$is_overridden_and_valid = !empty($overrides['host']) && !empty($overrides['scheme']) && !empty($overrides['port']) && !empty($overrides['index_id']) && !empty($overrides['derived_key']);
if ($is_overridden_and_valid) {
return TRUE;
}
\Drupal::logger('acquia search')
->notice("Invalid config override detected for\n acquia_search.settings.connection_override. It should include host, index_id,\n scheme, port and derived_key.");
return FALSE;
}
/**
* Determine if we should enforce read-only mode.
*
* @return bool
* TRUE if acquia_search module should enforce the read-only mode, FALSE
* otherwise.
*/
function acquia_search_should_set_read_only_mode() {
// If search config is overridden in settings.php we can't enforce anything.
if (acquia_search_is_connection_config_overridden()) {
return FALSE;
}
// Check if auto-switch or read-only modes are disabled in settings.
$auto_switch_disabled = \Drupal::config('acquia_search.settings')
->get('disable_auto_switch');
$disable_auto_read_only = \Drupal::config('acquia_search.settings')
->get('disable_auto_read_only');
if ($auto_switch_disabled || $disable_auto_read_only) {
return FALSE;
}
// If subscription is expired, then DO enforce read-only mode.
$subscription = new Subscription();
if (!$subscription
->isActive()) {
return TRUE;
}
// If there is no preferred core, then DO enforce read-only mode.
$core_service = acquia_search_get_core_service();
if (!$core_service
->isPreferredCoreAvailable()) {
return TRUE;
}
return FALSE;
}
/**
* Implements hook_search_api_server_load().
*
* Flag when a certain server should be enforcing read-only mode.
*
* @throws \Drupal\search_api\SearchApiException
*/
function acquia_search_search_api_server_load($entities) {
$acquia_servers = array_filter($entities, function ($server) {
/** @var \Drupal\search_api\Entity\Server $server */
return acquia_search_is_acquia_server($server
->getBackendConfig());
});
$core_service = acquia_search_get_core_service();
/** @var \Drupal\search_api\Entity\Server $server */
foreach ($acquia_servers as $server) {
/** @var \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend $backend */
$backend = $server
->getBackend();
$connector_config = $backend
->getSolrConnector()
->getConfiguration();
// Set a list of eligible cores.
$connector_config['acquia_search_possible_cores'] = $core_service
->getListOfPossibleCores();
unset($connector_config['overridden_by_acquia_search']);
if (acquia_search_should_set_read_only_mode()) {
$connector_config['overridden_by_acquia_search'] = ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY;
}
$backend
->getSolrConnector()
->setConfiguration($connector_config);
}
}
/**
* Determine whether given server config belongs to an Acquia search server.
*
* @param array $backend_config
* An array of data obtained from
* \Drupal\search_api\Entity\Server->getBackendConfig()
*
* @return bool
* TRUE if the provided config belongs to an Acquia Search server.
*/
function acquia_search_is_acquia_server(array $backend_config) {
return !empty($backend_config['connector']) && $backend_config['connector'] === 'solr_acquia_connector';
}
/**
* Implements hook_search_api_index_load().
*
* This takes care of marking indexes as read-only mode under the right
* conditions (@see acquia_search_search_api_server_load()).
*/
function acquia_search_search_api_index_load($entities) {
// Loop through the Index entities.
foreach ($entities as &$index) {
// Check for server-less indexes.
// @see https://www.drupal.org/project/acquia_connector/issues/2956737
$serverId = $index
->getServerId();
if (!isset($serverId) || $serverId == '') {
continue;
}
// Checking for serverless indexes.
$serverId = $index
->getServerId();
if (!$serverId) {
continue;
}
// Check for non-existent servers.
/** @var \Drupal\search_api\Entity\Index $index */
$server = Server::load($serverId);
if (!$server) {
continue;
}
if (!acquia_search_is_acquia_server($server
->getBackendConfig())) {
continue;
}
// Reset the overridden_by_acquia_search option.
$options = $index
->getOptions();
if (!empty($options['overridden_by_acquia_search'])) {
unset($options['overridden_by_acquia_search']);
$index
->setOptions($options);
}
if (acquia_search_should_set_read_only_mode()) {
// Set this index to read-only mode.
$index
->set('read_only', TRUE);
// Flag this index as having been altered by this module.
\Drupal::state()
->set('acquia_search_index.' . $index
->id() . '.overridden_by_acquia_search', ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY);
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Alters the Search API server's status form and displays a warning.
*/
function acquia_search_form_search_api_server_status_alter(&$form) {
$server = !empty($form['#server']) ? $form['#server'] : NULL;
if (!is_object($server) || get_class($server) !== 'Drupal\\search_api\\Entity\\Server') {
return;
}
if (!acquia_search_is_acquia_server($server
->getBackendConfig())) {
return;
}
if (!acquia_search_should_set_read_only_mode()) {
return;
}
// Show read-only warning and disable the "Delete all indexed
// data on this server" action.
acquia_search_server_show_read_only_mode_warning();
$form['actions']['clear']['#disabled'] = TRUE;
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Display the read-only warning.
*/
function acquia_search_form_search_api_server_edit_form_alter(&$form) {
$server = Server::load($form['id']['#default_value']);
if (!$server) {
return;
}
if (!acquia_search_is_acquia_server($server
->getBackendConfig())) {
return;
}
if (!acquia_search_should_set_read_only_mode()) {
return;
}
acquia_search_server_show_read_only_mode_warning();
}
/**
* Implements hook_form_FORM_ID_alter().
*
* Shows message if we are editing a Search API server's configuration.
*/
function acquia_search_form_search_api_index_edit_form_alter(&$form) {
/** @var \Drupal\search_api\Entity\Server $server */
$server = Server::load($form['server']['#default_value']);
if (!$server) {
return;
}
if (!acquia_search_is_acquia_server($server
->getBackendConfig())) {
return;
}
if (!acquia_search_should_set_read_only_mode()) {
return;
}
acquia_search_server_show_read_only_mode_warning();
$form['options']['read_only']['#disabled'] = TRUE;
}
/**
* Generates DSM with read-only message warning.
*/
function acquia_search_server_show_read_only_mode_warning() {
$message = acquia_search_get_read_only_mode_warning();
\Drupal::messenger()
->addWarning($message);
}
/**
* Returns formatted message about read-only mode.
*
* @return \Drupal\Component\Render\MarkupInterface|string
* Renderable array or translatable markup.
*/
function acquia_search_get_read_only_mode_warning() {
$msg = t('To protect your data, the Acquia Search 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).');
$core_service = acquia_search_get_core_service();
if ($core_service
->getListOfPossibleCores()) {
$item_list = [
'#theme' => 'item_list',
'#items' => $core_service
->getListOfPossibleCores(),
];
$list = render($item_list);
$msg .= '<p>';
$msg .= t('The following Acquia Search Solr index IDs would have worked for your current environment, but could not be found on your Acquia subscription: @list', [
'@list' => $list,
]);
$msg .= '</p>';
}
$msg .= PHP_EOL . t('To fix this problem, please read <a href="@url">our documentation</a>.', [
'@url' => 'https://docs.acquia.com/acquia-search/multiple-cores',
]);
return Markup::create((string) $msg);
}
/**
* Returns formatted message about Acquia Search connection details.
*
* @param \Drupal\search_api\Entity\Server $server
* Server entity.
*
* @return \Drupal\Component\Render\MarkupInterface|string
* Renderable array or translatable markup.
*
* @throws \Drupal\search_api\SearchApiException
*/
function acquia_search_get_search_status_message(Server $server) {
/** @var \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend $backend */
$backend = $server
->getBackend();
$configuration = $backend
->getSolrConnector()
->getConfiguration();
$items[] = acquia_search_get_server_id_message($server
->id());
$items[] = acquia_search_get_server_url_message($configuration);
// Report on the behavior chosen.
if (isset($configuration['overridden_by_acquia_search'])) {
$items[] = acquia_search_get_overridden_mode_message($configuration['overridden_by_acquia_search']);
}
$items[] = acquia_search_get_server_availability_message($server);
$items[] = acquia_search_get_server_auth_check_message($server);
$list = [
'#theme' => 'item_list',
'#items' => $items,
];
$list = \Drupal::service('renderer')
->renderRoot($list);
$msg = t('Connection managed by Acquia Search module.') . $list;
return Markup::create((string) $msg);
}
/**
* Get text describing the current override mode.
*
* @param int $override
* Override mode. Read only, core auto selected or using existing overrides.
*
* @return array|\Drupal\Core\StringTranslation\TranslatableMarkup
* Renderable array or translatable markup.
*/
function acquia_search_get_overridden_mode_message($override) {
switch ($override) {
case ACQUIA_SEARCH_AUTO_OVERRIDE_READ_ONLY:
return [
'#markup' => '<span class="color-warning">' . t('Acquia Search module automatically enforced read-only mode on this connection.') . '</span>',
];
case ACQUIA_SEARCH_OVERRIDE_AUTO_SET:
return t('Acquia Search module automatically selected the proper Solr connection based on the detected environment.');
case ACQUIA_SEARCH_EXISTING_OVERRIDE:
return [
'#markup' => '<span class="color-warning">' . t('Acquia Search module used overrides set from the <strong>acquia_search.settings</strong> configuration object instead of automatically selecting an available Acquia Search Solr connection.') . '</span>',
];
}
}
/**
* Get text showing the current URL based on configuration.
*
* @param array $configuration
* A configuration array containing scheme, host, port and path.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* Translatable markup.
*/
function acquia_search_get_server_url_message(array $configuration) {
$url = $configuration['scheme'] . '://' . $configuration['host'] . ':' . $configuration['port'] . $configuration['path'];
return t('URL: @url', [
'@url' => $url,
]);
}
/**
* Get text describing current server ID.
*
* @param string $server_id
* Server ID.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* Translatable markup.
*/
function acquia_search_get_server_id_message($server_id) {
return t('search_api_solr.module server ID: @id', [
'@id' => $server_id,
]);
}
/**
* Get message describing authentication status for the given server.
*
* @param \Drupal\search_api\Entity\Server $server
* Server entity.
*
* @return array|\Drupal\Core\StringTranslation\TranslatableMarkup
* Renderable array or translatable markup.
*
* @throws \Drupal\search_api\SearchApiException
*/
function acquia_search_get_server_auth_check_message(Server $server) {
if ($server
->getBackend()
->getSolrConnector()
->pingServer()) {
return t('Requests to Solr core are passing authentication checks.');
}
return [
'#markup' => '<span class="color-error">' . t('Solr core authentication check fails.') . '</span>',
];
}
/**
* Get text describing availability for the given server.
*
* @param \Drupal\search_api\Entity\Server $server
* Server entity.
*
* @return array|\Drupal\Core\StringTranslation\TranslatableMarkup
* Renderable array or translatable markup.
*
* @throws \Drupal\search_api\SearchApiException
*/
function acquia_search_get_server_availability_message(Server $server) {
if ($server
->getBackend()
->getSolrConnector()
->pingCore()) {
return t('Solr core is currently reachable and up.');
}
return [
'#markup' => '<span class="color-error">' . t('Solr index is currently unreachable.') . '</span>',
];
}
/**
* Instantiates the PreferredSearchCoreService class.
*
* The PreferredSearchCoreService class, helps to determines which search core
* should be used and whether it is available within the subscription.
*
* @return \Drupal\acquia_search\PreferredSearchCoreService
* Preferred Search Core Service.
*/
function acquia_search_get_core_service() {
static $core_service;
if (isset($core_service)) {
return $core_service;
}
$storage = new Storage();
$acquia_identifier = $storage
->getIdentifier();
$ah_env = isset($_ENV['AH_SITE_ENVIRONMENT']) ? $_ENV['AH_SITE_ENVIRONMENT'] : '';
$ah_site_name = isset($_ENV['AH_SITE_NAME']) ? $_ENV['AH_SITE_NAME'] : '';
$ah_site_group = isset($_ENV['AH_SITE_GROUP']) ? $_ENV['AH_SITE_GROUP'] : '';
$conf_path = \Drupal::service('site.path');
$sites_foldername = substr($conf_path, strrpos($conf_path, '/') + 1);
$ah_db_name = '';
if ($ah_env && $ah_site_name && $ah_site_group) {
$options = Database::getConnection()
->getConnectionOptions();
$ah_db_name = $options['database'];
}
$subscription = \Drupal::state()
->get('acquia_subscription_data');
$available_cores = [];
if (!empty($subscription['heartbeat_data']['search_cores'])) {
$available_cores = $subscription['heartbeat_data']['search_cores'];
}
$search_v3_cores = acquia_search_get_v3_cores($acquia_identifier);
$available_cores = array_merge($available_cores, $search_v3_cores);
$core_service = new PreferredSearchCoreService($acquia_identifier, $ah_env, $sites_foldername, $ah_db_name, $available_cores);
return $core_service;
}
/**
* Implements hook_theme_registry_alter().
*
* Helps us alter some Search API status pages.
*
* @see acquia_search_theme_search_api_index()
*/
function acquia_search_theme_registry_alter(&$theme_registry) {
$module_handler = \Drupal::moduleHandler();
$module_path = $module_handler
->getModule('acquia_search')
->getPath();
$theme_registry['search_api_index']['variables']['acquia_search_info_box'] = NULL;
$theme_registry['search_api_index']['path'] = $module_path . '/templates';
$theme_registry['search_api_server']['variables']['acquia_search_info_box'] = NULL;
$theme_registry['search_api_server']['path'] = $module_path . '/templates';
}
/**
* Implements hook_preprocess_HOOK().
*
* Theme override for Search API index status page.
*
* @see acquia_search_theme_registry_alter()
*/
function acquia_search_preprocess_search_api_index(&$variables) {
/** @var \Drupal\search_api\Entity\Index $index */
$index = $variables['index'];
/** @var \Drupal\search_api\Entity\Server $server */
$server = Server::load($index
->get('server'));
if (!$server || !acquia_search_is_acquia_server($server
->getBackendConfig())) {
return;
}
if (acquia_search_should_set_read_only_mode()) {
acquia_search_server_show_read_only_mode_warning();
}
$variables['acquia_search_info_box'] = [
'#type' => 'fieldset',
'#title' => t('Acquia Search status for this connection'),
'#markup' => acquia_search_get_search_status_message($server),
];
}
/**
* Implements hook_preprocess_HOOK().
*
* Theme override for Search API server status page.
*
* @see acquia_search_theme_registry_alter()
*/
function acquia_search_preprocess_search_api_server(array &$variables) {
/** @var \Drupal\search_api\Entity\Server $server */
$server = $variables['server'];
if (!acquia_search_is_acquia_server($server
->getBackendConfig())) {
return;
}
if (acquia_search_should_set_read_only_mode()) {
acquia_search_server_show_read_only_mode_warning();
}
$variables['acquia_search_info_box'] = [
'#type' => 'fieldset',
'#title' => t('Acquia Search status for this connection'),
'#markup' => acquia_search_get_search_status_message($server),
];
}
/**
* Initializes and returns an instance of AcquiaSearchV3ApiClient.
*
* @return \Drupal\acquia_search\AcquiaSearchV3ApiClient|false
* Acquia search V3 API Client or false on failure.
*/
function acquia_search_get_v3_client() {
$search_v3_host = \Drupal::state()
->get('acquia_search.v3_api_host') ? \Drupal::state()
->get('acquia_search.v3_api_host') : 'https://api.sr.acquia.com';
$search_v3_api_key = \Drupal::state()
->get('acquia_search.v3_api_key');
$drupal_http_client = \Drupal::service('http_client');
$cache = \Drupal::cache();
// If any of these variables are empty return FALSE.
if (empty($search_v3_host) || empty($search_v3_api_key)) {
return FALSE;
}
return new AcquiaSearchV3ApiClient($search_v3_host, $search_v3_api_key, $drupal_http_client, $cache);
}
/**
* Retrieves list of search v3 cores.
*
* @param string $acquia_identifier
* Acquia identifier (Subscription network id).
*
* @return array
* Array of Acquia search V3 cores. Empty array if cores aren't available.
*/
function acquia_search_get_v3_cores($acquia_identifier) {
$search_v3_enabled = \Drupal::config('acquia_search.settings')
->get('search_v3_enabled');
if (!$search_v3_enabled) {
return [];
}
$search_v3_client = acquia_search_get_v3_client();
if (!$search_v3_client) {
return [];
}
$search_v3_cores = $search_v3_client
->getSearchV3Indexes($acquia_identifier);
if (!is_array($search_v3_cores) || empty($search_v3_cores)) {
return [];
}
return $search_v3_cores;
}
/**
* Disable Search API Solr Warning/Error messages regarding EOL.
* @param $variables
*/
function acquia_search_preprocess_status_messages(&$variables) {
$current_path = \Drupal::service('path.current')
->getPath();
if (strpos((string) $current_path, "admin/config/search/search-api") == TRUE) {
if (isset($variables['message_list'])) {
foreach ($variables['message_list'] as $msg_type => $messages) {
foreach ($messages as $message_key => $message) {
if (strpos((string) $message, "Search API Solr 8.x-1.x") !== FALSE) {
unset($variables['message_list'][$msg_type][$message_key]);
}
if (empty($variables['message_list'][$msg_type])) {
unset($variables['message_list'][$msg_type]);
}
}
}
}
}
}