You are here

search_api_saved_searches.module in Search API Saved Searches 8

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

Allows visitors to bookmark searches and get notifications for new results.

File

search_api_saved_searches.module
View source
<?php

/**
 * @file
 * Allows visitors to bookmark searches and get notifications for new results.
 */
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityMalformedException;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api_saved_searches\Entity\SavedSearch;
use Drupal\search_api_saved_searches\Entity\SavedSearchAccessControlHandler;
use Drupal\search_api_saved_searches\Plugin\search_api_saved_searches\notification\Email;
use Drupal\search_api_saved_searches\SavedSearchesException;
use Drupal\user\UserInterface;

/**
 * Implements hook_cron().
 */
function search_api_saved_searches_cron() {
  \Drupal::getContainer()
    ->get('search_api_saved_searches.new_results_check')
    ->checkAll();
}

/**
 * Implements hook_query_TAG_alter() for "search_api_saved_search_access".
 */
function search_api_saved_searches_query_search_api_saved_search_access_alter(AlterableInterface $query) {

  // Read the account to use from the query, if provided.
  $account = $query
    ->getMetaData('account') ?: \Drupal::currentUser();
  $admin_permission = SavedSearchAccessControlHandler::ADMIN_PERMISSION;
  if ($account
    ->hasPermission($admin_permission)) {
    return;
  }

  // Non-admins can only see their own saved searches, anonymous users can't see
  // any saved search listing (only individual searches, using an access token).
  $uid = $account
    ->isAnonymous() ? -1 : $account
    ->id();
  if ($query instanceof QueryInterface) {
    $query
      ->condition('uid', $uid);
  }
  elseif ($query instanceof SelectInterface) {
    $search_table = NULL;
    foreach ($query
      ->getTables() as $key => $table_info) {
      if ($table_info['table'] === 'search_api_saved_search') {
        $search_table = $key;
        break;
      }
    }
    if (!$search_table) {
      $query
        ->where('1 <> 1');
      \Drupal::logger('search_api_saved_searches')
        ->error('Could not add access checks to Saved Search query: base table not found');
      return;
    }
    $query
      ->condition("{$search_table}.uid", $uid);
  }
  else {
    $args['@class'] = get_class($query);
    \Drupal::logger('search_api_saved_searches')
      ->error('Could not add access checks to Saved Search query: query is of unknown class @class', $args);
  }
}

/**
 * Implements hook_entity_field_storage_info().
 */
function search_api_saved_searches_entity_field_storage_info(EntityTypeInterface $entity_type) {
  if ($entity_type
    ->id() !== 'search_api_saved_search') {
    return [];
  }

  // Add field storage definitions for all notification plugin-provided fields.
  $fields = [];
  $bundles = \Drupal::getContainer()
    ->get('entity_type.bundle.info')
    ->getBundleInfo('search_api_saved_search');
  foreach (array_keys($bundles) as $bundle) {

    // We don't use the $base_field_definitions parameter in that method, so no
    // need to retrieve those for passing them here.
    $fields += SavedSearch::bundleFieldDefinitions($entity_type, $bundle, []);
  }
  return $fields;
}

/**
 * Implements hook_ENTITY_TYPE_delete() for type "search_api_index".
 *
 * Deletes all saved searches that used this index.
 */
function search_api_saved_searches_search_api_index_delete(IndexInterface $index) {
  _search_api_saved_searches_delete_searches('index_id', $index
    ->id());
}

/**
 * Implements hook_ENTITY_TYPE_insert() for type "user".
 *
 * If a new user already has saved searches with the same mail address,
 * associate them with the new user. However, only do this if the user is
 * already active.
 */
function search_api_saved_searches_user_insert(UserInterface $account) {
  _search_api_saved_searches_claim_anonymous_searches($account);
}

/**
 * Implements hook_ENTITY_TYPE_update() for type "user".
 *
 * If a user gets activated, associate saved searches with the same mail address
 * with them.
 *
 * If a user gets deactivated, disable all related saved searches.
 *
 * Also, change mail address of saved searches when the user mail address
 * changes (on behalf of the "E-mail" notification plugin).
 */
function search_api_saved_searches_user_update(UserInterface $account) {
  $original = $account->original;
  if (!$original instanceof UserInterface) {
    return;
  }

  // For newly activated users, transfer all saved searches with their mail
  // address to them.
  if ($account
    ->isActive() && !$original
    ->isActive()) {
    _search_api_saved_searches_claim_anonymous_searches($account);
  }

  // If an account gets deactivated/banned, disable all associated searches.
  if (!$account
    ->isActive() && $original
    ->isActive()) {
    _search_api_saved_searches_deactivate_searches($account);
  }

  // Addition on behalf of the "E-Mail" notification plugin: If the user's mail
  // address changed, also change the mail address of the user's saved searches
  // from previous (original) to current address.
  if ($account
    ->getEmail() !== $original
    ->getEmail()) {
    _search_api_saved_searches_adapt_mail($account, $original);
  }
}

/**
 * Reacts to the creation or activation of a new user account.
 *
 * Associates all anonymously created saved searches with the same mail address
 * with that user account.
 *
 * @param \Drupal\user\UserInterface $account
 *   The user account in question.
 */
function _search_api_saved_searches_claim_anonymous_searches(UserInterface $account) {
  if (!$account
    ->isActive()) {
    return;
  }

  // Special case: This will silently fail if no saved search types use the
  // "E-mail" plugin – which is fine by us.
  $searches = _search_api_saved_searches_load_searches(0, $account
    ->getEmail());

  /** @var \Drupal\search_api_saved_searches\SavedSearchInterface $search */
  foreach ($searches as $search) {
    $search
      ->setOwner($account);
    try {
      $search
        ->save();
    } catch (EntityStorageException $e) {
      $args['@search_id'] = $search
        ->id();
      watchdog_exception('search_api_saved_searches', $e, '%type while trying to save saved search #@search_id: @message in %function (line %line of %file).', $args);
    }
  }
}

/**
 * Deactivates all saved searches for a specific user account.
 *
 * @param \Drupal\user\UserInterface $account
 *   The user account in question.
 */
function _search_api_saved_searches_deactivate_searches(UserInterface $account) {
  $searches = _search_api_saved_searches_load_searches($account
    ->id());

  /** @var \Drupal\search_api_saved_searches\SavedSearchInterface $search */
  foreach ($searches as $search) {
    $search
      ->set('notify_interval', -1);
    try {
      $search
        ->save();
    } catch (EntityStorageException $e) {
      $args['@search_id'] = $search
        ->id();
      watchdog_exception('search_api_saved_searches', $e, '%type while trying to save saved search #@search_id: @message in %function (line %line of %file).', $args);
    }
  }
}

/**
 * Updates a user's saved searches to reflect a changed mail address.
 *
 * Only used for searches that use the "E-Mail" notification plugin.
 *
 * @param \Drupal\user\UserInterface $account
 *   The user account in question.
 * @param \Drupal\user\UserInterface $original
 *   The old version of the user account, with the old mail address.
 */
function _search_api_saved_searches_adapt_mail(UserInterface $account, UserInterface $original) {
  $searches = _search_api_saved_searches_load_searches($account
    ->id(), $original
    ->getEmail());

  /** @var \Drupal\search_api_saved_searches\Entity\SavedSearch $search */
  foreach ($searches as $search) {
    $search
      ->set('mail', $account
      ->getEmail());
    try {
      $search
        ->save();
    } catch (EntityStorageException $e) {
      $args['@search_id'] = $search
        ->id();
      watchdog_exception('search_api_saved_searches', $e, '%type while trying to save saved search #@search_id: @message in %function (line %line of %file).', $args);
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete() for type "user".
 *
 * Deletes all saved searches owned by the deleted user.
 */
function search_api_saved_searches_user_delete(UserInterface $account) {
  _search_api_saved_searches_delete_searches('uid', $account
    ->id());
}

/**
 * Loads all saved searches, optionally filtering by UID or e-mail address.
 *
 * @param int|null $uid
 *   (optional) The owner UID to filter for, if any.
 * @param string|null $mail
 *   (optional) The e-mail address to filter for, if any.
 *
 * @return \Drupal\search_api_saved_searches\SavedSearchInterface[]
 *   The requested saved searches.
 */
function _search_api_saved_searches_load_searches($uid = NULL, $mail = NULL) {
  try {
    $query = \Drupal::entityQuery('search_api_saved_search');
    if ($mail !== NULL) {
      $query
        ->condition('mail', $mail);
    }
    if ($uid !== NULL) {
      $query
        ->condition('uid', $uid);
    }
    $ids = $query
      ->accessCheck(FALSE)
      ->execute();
    if (!$ids) {
      return [];
    }

    /** @var \Drupal\search_api_saved_searches\SavedSearchInterface[] $searches */
    $searches = \Drupal::entityTypeManager()
      ->getStorage('search_api_saved_search')
      ->loadMultiple($ids);
    return $searches;
  } catch (\Exception $e) {
    watchdog_exception('search_api_saved_searches', $e);
    return [];
  }
}

/**
 * Deletes saved searches based on the specified criterion.
 *
 * @param string $field
 *   The saved search field to match against.
 * @param mixed $value
 *   The field value to look for.
 */
function _search_api_saved_searches_delete_searches($field, $value) {
  $ids = \Drupal::entityQuery('search_api_saved_search')
    ->condition($field, $value)
    ->accessCheck(FALSE)
    ->execute();
  if (!$ids) {
    return;
  }
  try {
    $storage = \Drupal::entityTypeManager()
      ->getStorage('search_api_saved_search');
    $searches = $storage
      ->loadMultiple($ids);
    if ($searches) {
      $storage
        ->delete($searches);
    }
  } catch (\Exception $e) {
    $args['@field'] = $field;
    $args['@value'] = $value;
    watchdog_exception('search_api_saved_searches', $e, '%type while trying to delete saved searches (condition: @field = @value): @message in %function (line %line of %file).', $args);
  }
}

/**
 * Implements hook_mail().
 *
 * Implemented on behalf of the "E-mail" notification plugin.
 *
 * @see \Drupal\search_api_saved_searches\Plugin\search_api_saved_searches\notification\Email
 */
function search_api_saved_searches_mail($key, &$message, $params) {
  if (empty($params['plugin'])) {
    return;
  }
  $plugin = $params['plugin'];
  if (!$plugin instanceof Email) {
    return;
  }
  switch ($key) {
    case Email::MAIL_ACTIVATE:
      $plugin
        ->getActivationMail($message, $params);
      break;
    case Email::MAIL_NEW_RESULTS:
      $plugin
        ->getNewResultsMail($message, $params);
      break;
  }
}

/**
 * Implements hook_ENTITY_TYPE_presave() for type "search_api_saved_search".
 *
 * Implemented on behalf of the "E-mail" notification plugin.
 *
 * @see \Drupal\search_api_saved_searches\Plugin\search_api_saved_searches\notification\Email
 */
function search_api_saved_searches_search_api_saved_search_presave(EntityInterface $search) {

  // Don't check searches that are already disabled.
  if (!$search
    ->get('status')->value) {
    return;
  }

  // Admins also generally don't have to activate saved searches they create.
  $admin_permission = SavedSearchAccessControlHandler::ADMIN_PERMISSION;
  if (\Drupal::currentUser()
    ->hasPermission($admin_permission)) {
    return;
  }
  try {

    /** @var \Drupal\search_api_saved_searches\SavedSearchInterface $search */
    $type = $search
      ->getType();

    // If the type doesn't use the "E-mail" notification plugin, we're done.
    if (!$type
      ->isValidNotificationPlugin('email')) {
      return;
    }

    // Otherwise, check whether the "Activation mail" setting is even enabled.
    $plugin = $type
      ->getNotificationPlugin('email');
    if (!$plugin
      ->getConfiguration()['activate']['send']) {
      return;
    }

    // Don't check searches that aren't new, unless the mail address changed.
    $mail = $search
      ->get('mail')->value;
    if (!$search
      ->isNew() && $mail == $search->original
      ->get('mail')->value) {
      return;
    }
    $owner = $search
      ->getOwner();

    // If we couldn't get the owner, we can't really check further, so bail.
    if (!$owner) {

      // To avoid having to duplicate the complicated logging logic below, just
      // throw an exception.
      throw new SavedSearchesException('Saved search does not specify a valid owner.');
    }

    // If the owner is a registered user and used their own e-mail address,
    // there's no need for an activation mail.
    if (!$owner
      ->isAnonymous() && $owner
      ->getEmail() === $mail) {
      return;
    }

    // De-activate the saved search.
    $search
      ->set('status', FALSE);

    // Unfortunately, we can't send the activation mail right away, as the saved
    // search doesn't have an ID set yet (unless this is an update), so we can't
    // get the activation URL. We therefore queue the mail to be sent at the end
    // of the page request.
    $params = [
      'search' => $search,
      'plugin' => $plugin,
    ];
    \Drupal::getContainer()
      ->get('search_api_saved_searches.email_queue')
      ->queueMail([
      'search_api_saved_searches',
      Email::MAIL_ACTIVATE,
      $mail,
      $owner
        ->getPreferredLangcode(),
      $params,
    ]);
  } catch (\Exception $e) {
    $context['%search_label'] = $search
      ->label();
    if (!$search
      ->isNew()) {
      $context['%search_label'] .= ' (#' . $search
        ->id() . ')';
      try {
        $context['link'] = $search
          ->toLink(t('View saved search'), 'edit-form')
          ->toString();
      } catch (EntityMalformedException $e) {

        // Ignore.
      }
    }
    watchdog_exception('search_api_saved_searches', $e, '%type while preprocessing saved search %search_label before saving: @message in %function (line %line of %file).', $context);
  }
}

Functions

Namesort descending Description
search_api_saved_searches_cron Implements hook_cron().
search_api_saved_searches_entity_field_storage_info Implements hook_entity_field_storage_info().
search_api_saved_searches_mail Implements hook_mail().
search_api_saved_searches_query_search_api_saved_search_access_alter Implements hook_query_TAG_alter() for "search_api_saved_search_access".
search_api_saved_searches_search_api_index_delete Implements hook_ENTITY_TYPE_delete() for type "search_api_index".
search_api_saved_searches_search_api_saved_search_presave Implements hook_ENTITY_TYPE_presave() for type "search_api_saved_search".
search_api_saved_searches_user_delete Implements hook_ENTITY_TYPE_delete() for type "user".
search_api_saved_searches_user_insert Implements hook_ENTITY_TYPE_insert() for type "user".
search_api_saved_searches_user_update Implements hook_ENTITY_TYPE_update() for type "user".
_search_api_saved_searches_adapt_mail Updates a user's saved searches to reflect a changed mail address.
_search_api_saved_searches_claim_anonymous_searches Reacts to the creation or activation of a new user account.
_search_api_saved_searches_deactivate_searches Deactivates all saved searches for a specific user account.
_search_api_saved_searches_delete_searches Deletes saved searches based on the specified criterion.
_search_api_saved_searches_load_searches Loads all saved searches, optionally filtering by UID or e-mail address.