class ContentAccess in Search API 8
Adds content access checks for nodes and comments.
Plugin annotation
@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,
},
)
Hierarchy
- class \Drupal\Component\Plugin\PluginBase implements DerivativeInspectionInterface, PluginInspectionInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
- class \Drupal\search_api\Plugin\HideablePluginBase implements HideablePluginInterface
- class \Drupal\search_api\Plugin\ConfigurablePluginBase implements ConfigurablePluginInterface uses PluginDependencyTrait
- class \Drupal\search_api\Plugin\IndexPluginBase implements IndexPluginInterface
- class \Drupal\search_api\Processor\ProcessorPluginBase implements ProcessorInterface
- class \Drupal\search_api\Plugin\search_api\processor\ContentAccess uses LoggerTrait
- class \Drupal\search_api\Processor\ProcessorPluginBase implements ProcessorInterface
- class \Drupal\search_api\Plugin\IndexPluginBase implements IndexPluginInterface
- class \Drupal\search_api\Plugin\ConfigurablePluginBase implements ConfigurablePluginInterface uses PluginDependencyTrait
- class \Drupal\search_api\Plugin\HideablePluginBase implements HideablePluginInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
Expanded class hierarchy of ContentAccess
File
- src/
Plugin/ search_api/ processor/ ContentAccess.php, line 36
Namespace
Drupal\search_api\Plugin\search_api\processorView source
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) {
$this
->addNodeAccess($query, $account);
}
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.
*/
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);
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
ConfigurablePluginBase:: |
public | function |
Calculates dependencies for the configured plugin. Overrides DependentPluginInterface:: |
6 |
ConfigurablePluginBase:: |
protected | function | Calculates and adds dependencies of a specific plugin instance. | |
ConfigurablePluginBase:: |
public | function |
Gets default configuration for this plugin. Overrides ConfigurableInterface:: |
11 |
ConfigurablePluginBase:: |
public | function |
Gets this plugin's configuration. Overrides ConfigurableInterface:: |
|
ConfigurablePluginBase:: |
public | function |
Returns the plugin's description. Overrides ConfigurablePluginInterface:: |
|
ConfigurablePluginBase:: |
protected | function | Calculates and returns dependencies of a specific plugin instance. | |
ConfigurablePluginBase:: |
public | function |
Returns the label for use on the administration pages. Overrides ConfigurablePluginInterface:: |
|
ConfigurablePluginBase:: |
protected | function | Wraps the module handler. | |
ConfigurablePluginBase:: |
public | function |
Informs the plugin that some of its dependencies are being removed. Overrides ConfigurablePluginInterface:: |
5 |
ConfigurablePluginBase:: |
public | function |
Sets the configuration for this plugin instance. Overrides ConfigurableInterface:: |
3 |
ConfigurablePluginBase:: |
protected | function | Wraps the theme handler. | |
ContentAccess:: |
protected | property | The current_user service used by this plugin. | |
ContentAccess:: |
protected | property | The database connection. | |
ContentAccess:: |
public | function |
Adds the values of properties defined by this processor to the item. Overrides ProcessorPluginBase:: |
|
ContentAccess:: |
protected | function | Adds a node access filter to a search query, if applicable. | |
ContentAccess:: |
public static | function |
Creates an instance of the plugin. Overrides ProcessorPluginBase:: |
|
ContentAccess:: |
public | function | Retrieves the current user. | |
ContentAccess:: |
public | function | Retrieves the database connection. | |
ContentAccess:: |
protected | function | Retrieves the node related to an indexed search object. | |
ContentAccess:: |
public | function |
Retrieves the properties this processor defines for the given datasource. Overrides ProcessorPluginBase:: |
|
ContentAccess:: |
public | function |
Preprocesses the search index entity before it is saved. Overrides ProcessorPluginBase:: |
|
ContentAccess:: |
public | function |
Preprocesses a search query. Overrides ProcessorPluginBase:: |
|
ContentAccess:: |
public | function | Sets the current user. | |
ContentAccess:: |
public | function | Sets the database connection. | |
ContentAccess:: |
public static | function |
Checks whether this processor is applicable for a certain index. Overrides ProcessorPluginBase:: |
|
DependencySerializationTrait:: |
protected | property | An array of entity type IDs keyed by the property name of their storages. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
DependencySerializationTrait:: |
public | function | 1 | |
DependencySerializationTrait:: |
public | function | 2 | |
DependencyTrait:: |
protected | property | The object's dependencies. | |
DependencyTrait:: |
protected | function | Adds multiple dependencies. | |
DependencyTrait:: |
protected | function | Adds a dependency. | |
IndexPluginBase:: |
protected | property | The index this processor is configured for. | |
IndexPluginBase:: |
public | function |
Retrieves the index this plugin is configured for. Overrides IndexPluginInterface:: |
|
IndexPluginBase:: |
public | function |
Sets the index this plugin is configured for. Overrides IndexPluginInterface:: |
|
IndexPluginBase:: |
public | function |
Constructs a \Drupal\Component\Plugin\PluginBase object. Overrides ConfigurablePluginBase:: |
2 |
LoggerTrait:: |
protected | property | The logging channel to use. | |
LoggerTrait:: |
public | function | Retrieves the logger. | |
LoggerTrait:: |
protected | function | Logs an exception. | |
LoggerTrait:: |
public | function | Sets the logger. | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
PluginBase:: |
protected | property | Configuration information passed into the plugin. | 1 |
PluginBase:: |
protected | property | The plugin implementation definition. | 1 |
PluginBase:: |
protected | property | The plugin_id. | |
PluginBase:: |
constant | A string which is used to separate base plugin IDs from the derivative ID. | ||
PluginBase:: |
public | function |
Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the definition of the plugin implementation. Overrides PluginInspectionInterface:: |
3 |
PluginBase:: |
public | function |
Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface:: |
|
PluginBase:: |
public | function | Determines if the plugin is configurable. | |
PluginDependencyTrait:: |
protected | function | Calculates and adds dependencies of a specific plugin instance. Aliased as: traitCalculatePluginDependencies | 1 |
PluginDependencyTrait:: |
protected | function | Calculates and returns dependencies of a specific plugin instance. Aliased as: traitGetPluginDependencies | |
PluginDependencyTrait:: |
protected | function | Wraps the module handler. Aliased as: traitModuleHandler | 1 |
PluginDependencyTrait:: |
protected | function | Wraps the theme handler. Aliased as: traitThemeHandler | 1 |
ProcessorInterface:: |
constant | Processing stage: add properties. | ||
ProcessorInterface:: |
constant | Processing stage: alter indexed items. | ||
ProcessorInterface:: |
constant | Processing stage: postprocess query. | ||
ProcessorInterface:: |
constant | Processing stage: preprocess index. | ||
ProcessorInterface:: |
constant | Processing stage: preprocess query. | ||
ProcessorInterface:: |
constant | Processing stage: preprocess index. | ||
ProcessorPluginBase:: |
protected | property | The fields helper. | 1 |
ProcessorPluginBase:: |
public | function |
Alter the items to be indexed. Overrides ProcessorInterface:: |
3 |
ProcessorPluginBase:: |
protected | function | Ensures that a field with certain properties is indexed on the index. | |
ProcessorPluginBase:: |
protected | function | Finds a certain field in the index. | |
ProcessorPluginBase:: |
public | function | Retrieves the fields helper. | 1 |
ProcessorPluginBase:: |
public | function |
Returns the weight for a specific processing stage. Overrides ProcessorInterface:: |
|
ProcessorPluginBase:: |
public | function |
Determines whether this plugin should be hidden in the UI. Overrides HideablePluginBase:: |
|
ProcessorPluginBase:: |
public | function |
Determines whether this processor should always be enabled. Overrides ProcessorInterface:: |
|
ProcessorPluginBase:: |
public | function |
Postprocess search results before they are returned by the query. Overrides ProcessorInterface:: |
2 |
ProcessorPluginBase:: |
public | function |
Preprocesses search items for indexing. Overrides ProcessorInterface:: |
5 |
ProcessorPluginBase:: |
public | function |
Determines whether re-indexing is required after a settings change. Overrides ProcessorInterface:: |
|
ProcessorPluginBase:: |
public | function | Sets the fields helper. | 1 |
ProcessorPluginBase:: |
public | function |
Sets the weight for a specific processing stage. Overrides ProcessorInterface:: |
|
ProcessorPluginBase:: |
public | function |
Checks whether this processor implements a particular stage. Overrides ProcessorInterface:: |
2 |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |