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