search_api-8.x-1.15/src/Plugin/search_api/processor/ContentAccess.php
src/Plugin/search_api/processor/ContentAccess.php
<?php namespace Drupal\search_api\Plugin\search_api\processor; use Drupal\comment\CommentInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Session\AnonymousUserSession; use Drupal\Core\TypedData\ComplexDataInterface; use Drupal\node\NodeInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\IndexInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\LoggerTrait; use Drupal\search_api\Processor\ProcessorPluginBase; use Drupal\search_api\Processor\ProcessorProperty; use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\SearchApiException; use Drupal\user\Entity\User; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Adds content access checks for nodes and comments. * * @SearchApiProcessor( * id = "content_access", * label = @Translation("Content access"), * description = @Translation("Adds content access checks for nodes and comments."), * stages = { * "add_properties" = 0, * "pre_index_save" = -10, * "preprocess_query" = -30, * }, * ) */ class ContentAccess extends ProcessorPluginBase { use LoggerTrait; /** * The database connection. * * @var \Drupal\Core\Database\Connection|null */ protected $database; /** * The current_user service used by this plugin. * * @var \Drupal\Core\Session\AccountProxyInterface|null */ protected $currentUser; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { /** @var static $processor */ $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition); $processor->setLogger($container->get('logger.channel.search_api')); $processor->setDatabase($container->get('database')); $processor->setCurrentUser($container->get('current_user')); return $processor; } /** * Retrieves the database connection. * * @return \Drupal\Core\Database\Connection * The database connection. */ public function getDatabase() { return $this->database ?: \Drupal::database(); } /** * Sets the database connection. * * @param \Drupal\Core\Database\Connection $database * The new database connection. * * @return $this */ public function setDatabase(Connection $database) { $this->database = $database; return $this; } /** * Retrieves the current user. * * @return \Drupal\Core\Session\AccountProxyInterface * The current user. */ public function getCurrentUser() { return $this->currentUser ?: \Drupal::currentUser(); } /** * Sets the current user. * * @param \Drupal\Core\Session\AccountProxyInterface $current_user * The current user. * * @return $this */ public function setCurrentUser(AccountProxyInterface $current_user) { $this->currentUser = $current_user; return $this; } /** * {@inheritdoc} */ public static function supportsIndex(IndexInterface $index) { foreach ($index->getDatasources() as $datasource) { if (in_array($datasource->getEntityTypeId(), ['node', 'comment'])) { return TRUE; } } return FALSE; } /** * {@inheritdoc} */ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) { $properties = []; if (!$datasource) { $definition = [ 'label' => $this->t('Node access information'), 'description' => $this->t('Data needed to apply node access.'), 'type' => 'string', 'processor_id' => $this->getPluginId(), 'hidden' => TRUE, 'is_list' => TRUE, ]; $properties['search_api_node_grants'] = new ProcessorProperty($definition); } return $properties; } /** * {@inheritdoc} */ public function addFieldValues(ItemInterface $item) { static $anonymous_user; if (!isset($anonymous_user)) { // Load the anonymous user. $anonymous_user = new AnonymousUserSession(); } // Only run for node and comment items. $entity_type_id = $item->getDatasource()->getEntityTypeId(); if (!in_array($entity_type_id, ['node', 'comment'])) { return; } // Get the node object. $node = $this->getNode($item->getOriginalObject()); if (!$node) { // Apparently we were active for a wrong item. return; } $fields = $item->getFields(); $fields = $this->getFieldsHelper() ->filterForPropertyPath($fields, NULL, 'search_api_node_grants'); foreach ($fields as $field) { // Collect grant records for the node. If there are none, use the pseudo // grant "node_access__all". $sql = 'SELECT gid, realm FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1'; $args = [':nid' => $node->id()]; $grant_records = $this->getDatabase()->query($sql, $args)->fetchAll(); if ($grant_records) { foreach ($grant_records as $grant) { $field->addValue("node_access_{$grant->realm}:{$grant->gid}"); } } else { // Add the generic pseudo view grant if we are not using node access. $field->addValue('node_access__all'); } } } /** * {@inheritdoc} */ public function preIndexSave() { foreach ($this->index->getDatasources() as $datasource_id => $datasource) { $entity_type = $datasource->getEntityTypeId(); if (in_array($entity_type, ['node', 'comment'])) { $this->ensureField($datasource_id, 'status', 'boolean'); if ($entity_type == 'node') { $this->ensureField($datasource_id, 'uid', 'integer'); } } } $field = $this->ensureField(NULL, 'search_api_node_grants', 'string'); $field->setHidden(); } /** * Retrieves the node related to an indexed search object. * * Will be either the node itself, or the node the comment is attached to. * * @param \Drupal\Core\TypedData\ComplexDataInterface $item * A search object that is being indexed. * * @return \Drupal\node\NodeInterface|null * The node related to that search object. */ protected function getNode(ComplexDataInterface $item) { $item = $item->getValue(); if ($item instanceof CommentInterface) { $item = $item->getCommentedEntity(); } if ($item instanceof NodeInterface) { return $item; } return NULL; } /** * {@inheritdoc} */ public function preprocessSearchQuery(QueryInterface $query) { if (!$query->getOption('search_api_bypass_access')) { $account = $query->getOption('search_api_access_account', $this->getCurrentUser()); if (is_numeric($account)) { $account = User::load($account); } if ($account instanceof AccountInterface) { try { $this->addNodeAccess($query, $account); } catch (SearchApiException $e) { $this->logException($e); } } else { $account = $query->getOption('search_api_access_account', $this->getCurrentUser()); if ($account instanceof AccountInterface) { $account = $account->id(); } if (!is_scalar($account)) { $account = var_export($account, TRUE); } $this->getLogger()->warning('An illegal user UID was given for node access: @uid.', ['@uid' => $account]); } } } /** * Adds a node access filter to a search query, if applicable. * * @param \Drupal\search_api\Query\QueryInterface $query * The query to which a node access filter should be added, if applicable. * @param \Drupal\Core\Session\AccountInterface $account * The user for whom the search is executed. * * @throws \Drupal\search_api\SearchApiException * Thrown if not all necessary fields are indexed on the index. */ protected function addNodeAccess(QueryInterface $query, AccountInterface $account) { // Don't do anything if the user can access all content. if ($account->hasPermission('bypass node access')) { return; } // Gather the affected datasources, grouped by entity type, as well as the // unaffected ones. $affected_datasources = []; $unaffected_datasources = []; foreach ($this->index->getDatasources() as $datasource_id => $datasource) { $entity_type = $datasource->getEntityTypeId(); if (in_array($entity_type, ['node', 'comment'])) { $affected_datasources[$entity_type][] = $datasource_id; } else { $unaffected_datasources[] = $datasource_id; } } // The filter structure we want looks like this: // [belongs to other datasource] // OR // ( // [is enabled (or was created by the user, if applicable)] // AND // [grants view access to one of the user's gid/realm combinations] // ) // If there are no "other" datasources, we don't need the nested OR, // however, and can add the inner conditions directly to the query. if ($unaffected_datasources) { $outer_conditions = $query->createConditionGroup('OR', ['content_access']); $query->addConditionGroup($outer_conditions); foreach ($unaffected_datasources as $datasource_id) { $outer_conditions->addCondition('search_api_datasource', $datasource_id); } $access_conditions = $query->createConditionGroup('AND'); $outer_conditions->addConditionGroup($access_conditions); } else { $access_conditions = $query; } if (!$account->hasPermission('access content')) { unset($affected_datasources['node']); } if (!$account->hasPermission('access comments')) { unset($affected_datasources['comment']); } // If the user does not have the permission to see any content at all, deny // access to all items from affected datasources. if (!$affected_datasources) { // If there were "other" datasources, the existing filter will already // remove all results of node or comment datasources. Otherwise, we should // not return any results at all. if (!$unaffected_datasources) { $query->abort($this->t('You have no access to any results in this search.')); } return; } // Collect all the required fields that need to be part of the index. $unpublished_own = $account->hasPermission('view own unpublished content'); $enabled_conditions = $query->createConditionGroup('OR', ['content_access_enabled']); foreach ($affected_datasources as $entity_type => $datasources) { foreach ($datasources as $datasource_id) { // If this is a comment datasource, or users cannot view their own // unpublished nodes, a simple filter on "status" is enough. Otherwise, // it's a bit more complicated. $status_field = $this->findField($datasource_id, 'status', 'boolean'); if ($status_field) { $enabled_conditions->addCondition($status_field->getFieldIdentifier(), TRUE); } if ($entity_type == 'node' && $unpublished_own) { $author_field = $this->findField($datasource_id, 'uid', 'integer'); if ($author_field) { $enabled_conditions->addCondition($author_field->getFieldIdentifier(), $account->id()); } } } } $access_conditions->addConditionGroup($enabled_conditions); // Filter by the user's node access grants. $node_grants_field = $this->findField(NULL, 'search_api_node_grants', 'string'); if (!$node_grants_field) { return; } $node_grants_field_id = $node_grants_field->getFieldIdentifier(); $grants_conditions = $query->createConditionGroup('OR', ['content_access_grants']); $grants = node_access_grants('view', $account); foreach ($grants as $realm => $gids) { foreach ($gids as $gid) { $grants_conditions->addCondition($node_grants_field_id, "node_access_$realm:$gid"); } } // Also add items that are accessible for everyone by checking the "access // all" pseudo grant. $grants_conditions->addCondition($node_grants_field_id, 'node_access__all'); $access_conditions->addConditionGroup($grants_conditions); } }