contrib modules updates

This commit is contained in:
2019-02-27 10:39:59 +01:00
parent 04a4b8895d
commit e3cf889820
579 changed files with 18343 additions and 4076 deletions
@@ -1,87 +0,0 @@
<?php
namespace Drupal\entity\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Checks if the current user has delete access to the items of the tempstore.
*/
class EntityDeleteMultipleAccessCheck implements AccessInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityTypeManager;
/**
* The tempstore service.
*
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
*/
protected $tempStore;
/**
* Request stack service.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new EntityDeleteMultipleAccessCheck.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, RequestStack $request_stack) {
$this->entityTypeManager = $entity_type_manager;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
$this->requestStack = $request_stack;
}
/**
* Checks if the user has delete access for at least one item of the store.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
* @param string $entity_type_id
* Entity type ID.
*
* @return \Drupal\Core\Access\AccessResult
* Allowed or forbidden, neutral if tempstore is empty.
*/
public function access(AccountInterface $account, $entity_type_id) {
if (!$this->requestStack->getCurrentRequest()->getSession()) {
return AccessResult::neutral();
}
$selection = $this->tempStore->get($account->id() . ':' . $entity_type_id);
if (empty($selection) || !is_array($selection)) {
return AccessResult::neutral();
}
$entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($selection));
foreach ($entities as $entity) {
// As long as the user has access to delete one entity allow access to the
// delete form. Access will be checked again in
// Drupal\Core\Entity\Form\DeleteMultipleForm::submit() in case it has
// changed in the meantime.
if ($entity->access('delete', $account)) {
return AccessResult::allowed();
}
}
return AccessResult::forbidden();
}
}
@@ -26,7 +26,7 @@ class EntityRevisionRouteAccessChecker implements AccessInterface {
*
* @var array
*/
protected $accessCache = array();
protected $accessCache = [];
/**
* The currently active route match object.
@@ -13,8 +13,8 @@ use Drupal\Core\Entity\EntityTypeInterface;
* Provided permissions:
* - administer $entity_type
* - access $entity_type overview
* - view ($bundle) $entity_type
* - view own unpublished $entity_type
* - view ($bundle) $entity_type
* - update (own|any) ($bundle) $entity_type
* - delete (own|any) ($bundle) $entity_type
* - create $bundle $entity_type
@@ -2,322 +2,13 @@
namespace Drupal\entity\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\Form\DeleteMultipleForm as CoreDeleteMultipleForm;
@trigger_error('\Drupal\entity\Form\DeleteMultipleForm has been deprecated in favor of \Drupal\Core\Entity\Form\DeleteMultipleForm. Use that instead.');
/**
* Provides an entities deletion confirmation form.
*
* @deprecated Use \Drupal\Core\Entity\Form\DeleteMultipleForm instead.
*/
class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface {
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The tempstore.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The entity type ID.
*
* @var string
*/
protected $entityTypeId;
/**
* The selection, in the entity_id => langcodes format.
*
* @var array
*/
protected $selection = [];
/**
* The entity type definition.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* Constructs a new DeleteMultiple object.
*
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory, MessengerInterface $messenger) {
$this->currentUser = $current_user;
$this->entityTypeManager = $entity_type_manager;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('current_user'),
$container->get('entity_type.manager'),
$container->get('tempstore.private'),
$container->get('messenger')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'entity_delete_multiple_confirm_form';
}
/**
* {@inheritdoc}
*/
public function getFormId() {
// Get entity type ID from the route because ::buildForm has not yet been
// called.
$entity_type_id = $this->getRouteMatch()->getParameter('entity_type_id');
return $entity_type_id . '_delete_multiple_confirm_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this @item?', 'Are you sure you want to delete these @items?', [
'@item' => $this->entityType->getSingularLabel(),
'@items' => $this->entityType->getPluralLabel(),
]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
if ($this->entityType->hasLinkTemplate('collection')) {
return new Url('entity.' . $this->entityTypeId . '.collection');
}
else {
return new Url('<front>');
}
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) {
$this->entityTypeId = $entity_type_id;
$this->entityType = $this->entityTypeManager->getDefinition($this->entityTypeId);
$this->selection = $this->tempStore->get($this->currentUser->id() . ':' . $entity_type_id);
if (empty($this->entityTypeId) || empty($this->selection)) {
return new RedirectResponse($this->getCancelUrl()
->setAbsolute()
->toString());
}
$items = [];
$entities = $this->entityTypeManager->getStorage($entity_type_id)->loadMultiple(array_keys($this->selection));
foreach ($this->selection as $id => $selected_langcodes) {
$entity = $entities[$id];
foreach ($selected_langcodes as $langcode) {
$key = $id . ':' . $langcode;
if ($entity instanceof TranslatableInterface) {
$entity = $entity->getTranslation($langcode);
$default_key = $id . ':' . $entity->getUntranslated()->language()->getId();
// Build a nested list of translations that will be deleted if the
// entity has multiple translations.
$entity_languages = $entity->getTranslationLanguages();
if (count($entity_languages) > 1 && $entity->isDefaultTranslation()) {
$names = [];
foreach ($entity_languages as $translation_langcode => $language) {
$names[] = $language->getName();
unset($items[$id . ':' . $translation_langcode]);
}
$items[$default_key] = [
'label' => [
'#markup' => $this->t('@label (Original translation) - <em>The following @entity_type translations will be deleted:</em>',
[
'@label' => $entity->label(),
'@entity_type' => $this->entityType->getSingularLabel(),
]),
],
'deleted_translations' => [
'#theme' => 'item_list',
'#items' => $names,
],
];
}
elseif (!isset($items[$default_key])) {
$items[$key] = $entity->label();
}
}
elseif (!isset($items[$key])) {
$items[$key] = $entity->label();
}
}
}
$form['entities'] = [
'#theme' => 'item_list',
'#items' => $items,
];
$form = parent::buildForm($form, $form_state);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$total_count = 0;
$delete_entities = [];
$delete_translations = [];
$inaccessible_entities = [];
$storage = $this->entityTypeManager->getStorage($this->entityTypeId);
$entities = $storage->loadMultiple(array_keys($this->selection));
foreach ($this->selection as $id => $selected_langcodes) {
$entity = $entities[$id];
if (!$entity->access('delete', $this->currentUser)) {
$inaccessible_entities[] = $entity;
continue;
}
foreach ($selected_langcodes as $langcode) {
if ($entity instanceof TranslatableInterface) {
$entity = $entity->getTranslation($langcode);
// If the entity is the default translation then deleting it will
// delete all the translations.
if ($entity->isDefaultTranslation()) {
$delete_entities[$id] = $entity;
// If there are translations already marked for deletion then remove
// them as they will be deleted anyway.
unset($delete_translations[$id]);
// Update the total count. Since a single delete will delete all
// translations, we need to add the number of translations to the
// count.
$total_count += count($entity->getTranslationLanguages());
}
// Add the translation to the list of translations to be deleted
// unless the default translation is being deleted.
elseif (!isset($delete_entities[$id])) {
$delete_translations[$id][] = $entity;
}
}
elseif (!isset($delete_entities[$id])) {
$delete_entities[$id] = $entity;
$total_count++;
}
}
}
if ($delete_entities) {
$storage->delete($delete_entities);
foreach ($delete_entities as $entity) {
$this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label has been deleted.', [
'@entity-type' => $entity->getEntityType()->getLowercaseLabel(),
'%label' => $entity->label(),
]);
}
}
if ($delete_translations) {
/** @var \Drupal\Core\Entity\TranslatableInterface[][] $delete_translations */
foreach ($delete_translations as $id => $translations) {
$entity = $entities[$id]->getUntranslated();
foreach ($translations as $translation) {
$entity->removeTranslation($translation->language()->getId());
}
$entity->save();
foreach ($translations as $translation) {
$this->logger($entity->getEntityType()->getProvider())->notice('The @entity-type %label @language translation has been deleted.', [
'@entity-type' => $entity->getEntityType()->getLowercaseLabel(),
'%label' => $entity->label(),
'@language' => $translation->language()->getName(),
]);
}
$total_count += count($translations);
}
}
if ($total_count) {
$this->messenger->addStatus($this->getDeletedMessage($total_count));
}
if ($inaccessible_entities) {
$this->messenger->addWarning($this->getInaccessibleMessage(count($inaccessible_entities)));
}
$this->tempStore->delete($this->currentUser->id());
$form_state->setRedirectUrl($this->getCancelUrl());
}
/**
* Returns the message to show the user after an item was deleted.
*
* @param int $count
* Count of deleted translations.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The item deleted message.
*/
protected function getDeletedMessage($count) {
return $this->formatPlural($count, 'Deleted @count item.', 'Deleted @count items.');
}
/**
* Returns the message to show the user when an item has not been deleted.
*
* @param int $count
* Count of deleted translations.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The item inaccessible message.
*/
protected function getInaccessibleMessage($count) {
return $this->formatPlural($count, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions.");
}
}
class DeleteMultipleForm extends CoreDeleteMultipleForm {}
@@ -119,10 +119,17 @@ class RevisionRevertForm extends ConfirmFormBase {
$original_revision_timestamp = $this->revision->getRevisionCreationTime();
$this->revision->setRevisionLogMessage($this->t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]));
drupal_set_message(t('@type %title has been reverted to the revision from %revision-date.', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
$this->messenger()->addStatus(t('@type %title has been reverted to the revision from %revision-date.', [
'@type' => $this->getBundleLabel($this->revision),
'%title' => $this->revision->label(),
'%revision-date' => $this->dateFormatter->format($original_revision_timestamp),
]));
}
else {
drupal_set_message(t('@type %title has been reverted', ['@type' => $this->getBundleLabel($this->revision), '%title' => $this->revision->label()]));
$this->messenger()->addStatus(t('@type %title has been reverted', [
'@type' => $this->getBundleLabel($this->revision),
'%title' => $this->revision->label(),
]));
}
$this->revision->save();
@@ -11,15 +11,14 @@ use Drupal\Core\Form\FormStateInterface;
/**
* Extends the base entity form with revision support in the UI.
*
* @deprecated in favor of \Drupal\Core\Entity\ContentEntityForm. Use that
* instead.
* @deprecated Use \Drupal\Core\Entity\ContentEntityForm instead.
*/
class RevisionableContentEntityForm extends ContentEntityForm {
/**
* The entity being used by this form.
*
* @var \Drupal\Core\Entity\EntityInterface|\Drupal\Core\Entity\RevisionableInterface|\Drupal\entity\Revision\EntityRevisionLogInterface
* @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
*/
protected $entity;
@@ -49,7 +48,8 @@ class RevisionableContentEntityForm extends ContentEntityForm {
* The bundle entity, or NULL if there is none.
*/
protected function getBundleEntity() {
if ($bundle_key = $this->entity->getEntityType()->getKey('bundle')) {
if ($this->entity->getEntityType()->getBundleEntityType()) {
$bundle_key = $this->entity->getEntityType()->getKey('bundle');
return $this->entity->{$bundle_key}->referencedEntities()[0];
}
return NULL;
@@ -30,6 +30,12 @@ class EntityCollectionLocalActionProvider implements EntityLocalActionProviderIn
/* @see \Drupal\entity\Menu\EntityAddLocalAction::getTitle() */
'title' => 'Add ' . $entity_type->getSingularLabel(),
'route_name' => $route_name,
'options' => [
// Redirect back to the collection after form submission.
'query' => [
'destination' => $entity_type->getLinkTemplate('collection'),
],
],
'appears_on' => ["entity.$entity_type_id.collection"],
'class' => EntityAddLocalAction::class,
];
@@ -2,96 +2,19 @@
namespace Drupal\entity\Plugin\Action;
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Action\Plugin\Action\DeleteAction as CoreDeleteAction;
@trigger_error('\Drupal\entity\Plugin\Action\DeleteAction has been deprecated in favor of \Drupal\Core\Action\Plugin\Action\DeleteAction. Use that instead.');
/**
* Redirects to an entity deletion form.
*
* @deprecated Use "entity:delete_action" instead.
*
* @Action(
* id = "entity_delete_action",
* label = @Translation("Delete entity"),
* deriver = "Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver",
* )
*/
class DeleteAction extends ActionBase implements ContainerFactoryPluginInterface {
/**
* The tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected $tempStore;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new DeleteAction object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
* The tempstore factory.
* @param \Drupal\Core\Session\AccountInterface $current_user
* Current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) {
$this->currentUser = $current_user;
$this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm');
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('tempstore.private'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function executeMultiple(array $entities) {
/** @var \Drupal\Core\Entity\EntityInterface[] $entities */
$selection = [];
foreach ($entities as $entity) {
$langcode = $entity->language()->getId();
$selection[$entity->id()][$langcode] = $langcode;
}
$this->tempStore->set($this->currentUser->id() . ':' . $this->getPluginDefinition()['type'], $selection);
}
/**
* {@inheritdoc}
*/
public function execute($object = NULL) {
$this->executeMultiple([$object]);
}
/**
* {@inheritdoc}
*/
public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
return $object->access('delete', $account, $return_as_object);
}
}
class DeleteAction extends CoreDeleteAction {}
@@ -4,13 +4,14 @@ namespace Drupal\entity\Plugin\Action\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a delete action for each content entity type.
*
* @deprecated
*/
class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterface {
@@ -46,7 +47,7 @@ class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterfa
$definitions = [];
foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) {
$definition = $base_plugin_definition;
$definition['label'] = t('Delete @entity_type', ['@entity_type' => $entity_type->getSingularLabel()]);
$definition['label'] = t('Delete @entity_type (Deprecated)', ['@entity_type' => $entity_type->getSingularLabel()]);
$definition['type'] = $entity_type_id;
$definition['confirm_form_route_name'] = 'entity.' . $entity_type_id . '.delete_multiple_form';
$definitions[$entity_type_id] = $definition;
@@ -69,7 +70,10 @@ class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterfa
protected function getParticipatingEntityTypes() {
$entity_types = $this->entityTypeManager->getDefinitions();
$entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form');
// Core requires a "delete-multiple-confirm" form to be declared as well,
// if it's missing, it's safe to assume that the entity type is still
// relying on previous Entity API contrib behavior.
return $entity_type->hasLinkTemplate('delete-multiple-form') && !$entity_type->hasHandlerClass('form', 'delete-multiple-confirm');
});
return $entity_types;
@@ -0,0 +1,113 @@
<?php
namespace Drupal\entity\QueryAccess;
/**
* Represents a single query access condition.
*/
final class Condition {
/**
* The supported operators.
*
* @var string[]
*/
protected static $supportedOperators = [
'=', '<>', '<', '<=', '>', '>=', 'BETWEEN', 'NOT BETWEEN',
'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL',
];
/**
* The field.
*
* @var string
*/
protected $field;
/**
* The value.
*
* @var mixed
*/
protected $value;
/**
* The operator.
*
* @var string
*/
protected $operator;
/**
* Constructs a new Condition object.
*
* @param string $field
* The field, with an optional column name. E.g: 'uid', 'address.locality'.
* @param mixed $value
* The value.
* @param string $operator
* The operator.
* Possible values: =, <>, <, <=, >, >=, BETWEEN, NOT BETWEEN,
* IN, NOT IN, IS NULL, IS NOT NULL.
*/
public function __construct($field, $value, $operator = NULL) {
// Provide a default based on the data type of the value.
if (!isset($operator)) {
$operator = is_array($value) ? 'IN' : '=';
}
// Validate the selected operator.
if (!in_array($operator, self::$supportedOperators)) {
throw new \InvalidArgumentException(sprintf('Unrecognized operator "%s".', $operator));
}
$this->field = $field;
$this->value = $value;
$this->operator = $operator;
}
/**
* {@inheritdoc}
*/
public function getField() {
return $this->field;
}
/**
* {@inheritdoc}
*/
public function getValue() {
return $this->value;
}
/**
* {@inheritdoc}
*/
public function getOperator() {
return $this->operator;
}
/**
* Gets the string representation of the condition.
*
* Used for debugging purposes.
*
* @return string
* The string representation of the condition.
*/
public function __toString() {
if (in_array($this->operator, ['IS NULL', 'IS NOT NULL'])) {
return "{$this->field} {$this->operator}";
}
else {
if (is_array($this->value)) {
$value = "['" . implode("', '", $this->value) . "']";
}
else {
$value = "'" . $this->value . "'";
}
return "{$this->field} {$this->operator} $value";
}
}
}
@@ -0,0 +1,227 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
/**
* Represents a group of query access conditions.
*
* Used by query access handlers for filtering lists of entities based on
* granted permissions.
*
* Examples:
* @code
* // Filter by node type and uid.
* $condition_group = new ConditionGroup();
* $condition_group->addCondition('type', ['article', 'page']);
* $condition_group->addCondition('uid', '1');
*
* // Filter by node type or status.
* $condition_group = new ConditionGroup('OR');
* $condition_group->addCondition('type', ['article', 'page']);
* $condition_group->addCondition('status', '1', '<>');
*
* // Nested condition groups: node type AND (uid OR status).
* $condition_group = new ConditionGroup();
* $condition_group->addCondition('type', ['article', 'page']);
* $condition_group->addCondition((new ConditionGroup('OR'))
* ->addCondition('uid', 1)
* ->addCondition('status', '1')
* );
* @endcode
*/
final class ConditionGroup implements \Countable, RefinableCacheableDependencyInterface {
use RefinableCacheableDependencyTrait;
/**
* The conjunction.
*
* @var string
*/
protected $conjunction;
/**
* The conditions.
*
* @var \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[]
*/
protected $conditions = [];
/**
* Whether the condition group is always FALSE.
*
* @var bool
*/
protected $alwaysFalse = FALSE;
/**
* Constructs a new ConditionGroup object.
*
* @param string $conjunction
* The conjunction.
*/
public function __construct($conjunction = 'AND') {
$this->conjunction = $conjunction;
}
/**
* Gets the conjunction.
*
* @return string
* The conjunction. Possible values: AND, OR.
*/
public function getConjunction() {
return $this->conjunction;
}
/**
* Gets all conditions and nested condition groups.
*
* @return \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[]
* The conditions, where each one is either a Condition or a nested
* ConditionGroup. Returned by reference, to allow callers to replace
* or remove conditions.
*/
public function &getConditions() {
return $this->conditions;
}
/**
* Adds a condition.
*
* @param string|\Drupal\entity\QueryAccess\ConditionGroup $field
* Either a condition group (for nested AND/OR conditions), or a
* field name with an optional column name. E.g: 'uid', 'address.locality'.
* @param mixed $value
* The value.
* @param string $operator
* The operator.
* Possible values: =, <>, <, <=, >, >=, BETWEEN, NOT BETWEEN,
* IN, NOT IN, IS NULL, IS NOT NULL.
*
* @return $this
*/
public function addCondition($field, $value = NULL, $operator = NULL) {
if ($field instanceof ConditionGroup) {
if ($field->count() === 1) {
// The condition group only has a single condition, merge it.
$this->conditions[] = reset($field->getConditions());
$this->addCacheTags($field->getCacheTags());
$this->addCacheContexts($field->getCacheContexts());
$this->mergeCacheMaxAge($field->getCacheMaxAge());
}
elseif ($field->count() > 1) {
$this->conditions[] = $field;
}
}
else {
$this->conditions[] = new Condition($field, $value, $operator);
}
return $this;
}
/**
* Gets whether the condition group is always FALSE.
*
* Used when the user doesn't have access to any entities, to ensure that a
* query returns no results.
*
* @return bool
* Whether the condition group is always FALSE.
*/
public function isAlwaysFalse() {
return $this->alwaysFalse;
}
/**
* Sets whether the condition group should always be FALSE.
*
* @param bool $always_false
* Whether the condition group should always be FALSE.
*
* @return $this
*/
public function alwaysFalse($always_false = TRUE) {
$this->alwaysFalse = $always_false;
return $this;
}
/**
* Clones the contained conditions when the condition group is cloned.
*/
public function __clone() {
foreach ($this->conditions as $i => $condition) {
$this->conditions[$i] = clone $condition;
}
}
/**
* Gets the string representation of the condition group.
*
* @return string
* The string representation of the condition group.
*/
public function __toString() {
// Special case for a single, nested condition group:
if (count($this->conditions) == 1) {
return (string) reset($this->conditions);
}
$lines = [];
foreach ($this->conditions as $condition) {
$lines[] = str_replace("\n", "\n ", (string) $condition);
}
return $lines ? "(\n " . implode("\n {$this->conjunction}\n ", $lines) . "\n)" : '';
}
/**
* {@inheritdoc}
*/
public function count() {
return count($this->conditions);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$tags = $this->cacheTags;
foreach ($this->conditions as $condition) {
if ($condition instanceof ConditionGroup) {
$tags = array_merge($tags, $condition->getCacheTags());
}
}
return Cache::mergeTags($tags, []);
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$cache_contexts = $this->cacheContexts;
foreach ($this->conditions as $condition) {
if ($condition instanceof ConditionGroup) {
$cache_contexts = array_merge($cache_contexts, $condition->getCacheContexts());
}
}
return Cache::mergeContexts($cache_contexts);
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$max_age = $this->cacheMaxAge;
foreach ($this->conditions as $condition) {
if ($condition instanceof ConditionGroup) {
$max_age = Cache::mergeMaxAges($max_age, $condition->getCacheMaxAge());
}
}
return $max_age;
}
}
@@ -0,0 +1,181 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\Sql\Tables;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines a class for altering entity queries.
*
* EntityQuery doesn't have an alter hook, forcing this class to operate
* on the underlying SQL query, duplicating the EntityQuery condition logic.
*
* @internal
*/
class EntityQueryAlter implements ContainerInjectionInterface {
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new EntityQueryAlter object.
*
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, RequestStack $request_stack) {
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('request_stack')
);
}
/**
* Alters the select query for the given entity type.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The select query.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*/
public function alter(SelectInterface $query, EntityTypeInterface $entity_type) {
if (!$entity_type->hasHandlerClass('query_access')) {
return;
}
$entity_type_id = $entity_type->id();
$storage = $this->entityTypeManager->getStorage($entity_type_id);
if (!$storage instanceof SqlContentEntityStorage) {
return;
}
/** @var \Drupal\entity\QueryAccess\QueryAccessHandlerInterface $query_access */
$query_access = $this->entityTypeManager->getHandler($entity_type_id, 'query_access');
$conditions = $query_access->getConditions('view');
if ($conditions->isAlwaysFalse()) {
$query->where('1 = 0');
}
elseif (count($conditions)) {
$sql_conditions = $this->mapConditions($conditions, $query);
$query->condition($sql_conditions);
}
$this->applyCacheability(CacheableMetadata::createFromObject($conditions));
}
/**
* Maps an entity type's access conditions to SQL conditions.
*
* @param \Drupal\entity\QueryAccess\ConditionGroup $conditions
* The access conditions.
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The SQL query.
* @param bool $nested_inside_or
* Whether the access conditions are nested inside an OR condition.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The SQL conditions.
*/
protected function mapConditions(ConditionGroup $conditions, SelectInterface $query, $nested_inside_or = FALSE) {
$sql_condition = $query->conditionGroupFactory($conditions->getConjunction());
$tables = new Tables($query);
$nested_inside_or = $nested_inside_or || $conditions->getConjunction() == 'OR';
foreach ($conditions->getConditions() as $condition) {
if ($condition instanceof ConditionGroup) {
$nested_sql_conditions = $this->mapConditions($condition, $query, $nested_inside_or);
$sql_condition->condition($nested_sql_conditions);
}
else {
// Access conditions don't specify a langcode.
$langcode = NULL;
$type = $nested_inside_or || $condition->getOperator() === 'IS NULL' ? 'LEFT' : 'INNER';
$sql_field = $tables->addField($condition->getField(), $type, $langcode);
$value = $condition->getValue();
$operator = $condition->getOperator();
// Using LIKE/NOT LIKE ensures a case insensitive comparison.
// @see \Drupal\Core\Entity\Query\Sql\Condition::translateCondition().
$case_sensitive = $tables->isFieldCaseSensitive($condition->getField());
$operator_map = [
'=' => 'LIKE',
'<>' => 'NOT LIKE',
];
if (!$case_sensitive && isset($operator_map[$operator])) {
$operator = $operator_map[$operator];
$value = $query->escapeLike($value);
}
$sql_condition->condition($sql_field, $value, $operator);
}
}
return $sql_condition;
}
/**
* Applies the cacheablity metadata to the current request.
*
* @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
* The cacheability metadata.
*/
protected function applyCacheability(CacheableMetadata $cacheable_metadata) {
$request = $this->requestStack->getCurrentRequest();
if ($request->isMethodCacheable() && $this->renderer->hasRenderContext()) {
$build = [];
$cacheable_metadata->applyTo($build);
$this->renderer->render($build);
}
}
}
@@ -0,0 +1,92 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Defines the query access event.
*
* Allows modules to modify access conditions before they're applied to a query.
*
* The event ID is dynamic: entity.query_access.$entity_type_id
*/
class QueryAccessEvent extends Event {
/**
* The conditions.
*
* @var \Drupal\entity\QueryAccess\ConditionGroup
*/
protected $conditions;
/**
* The operation.
*
* @var string
*/
protected $operation;
/**
* The user for which to restrict access.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* Constructs a new QueryAccessEvent.
*
* @param \Drupal\entity\QueryAccess\ConditionGroup $conditions
* The conditions.
* @param string $operation
* The operation. Usually one of "view", "update" or "delete".
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to restrict access.
*/
public function __construct(ConditionGroup $conditions, $operation, AccountInterface $account) {
$this->conditions = $conditions;
$this->operation = $operation;
$this->account = $account;
}
/**
* Gets the conditions.
*
* If $conditions->isAlwaysFalse() is TRUE, the user doesn't have access to
* any entities, and the query is expected to return no results.
* This can be reversed by calling $conditions->alwaysFalse(FALSE).
*
* If $conditions->isAlwaysFalse() is FALSE, and the condition group is
* empty (count is 0), the user has full access, and the query doesn't
* need to be restricted.
*
* @return \Drupal\entity\QueryAccess\ConditionGroup
* The conditions.
*/
public function getConditions() {
return $this->conditions;
}
/**
* Gets the operation.
*
* @return string
* The operation. Usually one of "view", "update" or "delete".
*/
public function getOperation() {
return $this->operation;
}
/**
* Gets the user for which to restrict access.
*
* @return \Drupal\Core\Session\AccountInterface
* The user.
*/
public function getAccount() {
return $this->account;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Session\AccountInterface;
/**
* Controls query access based on the generic entity permissions.
*
* @see \Drupal\entity\EntityAccessControlHandler
* @see \Drupal\entity\EntityPermissionProvider
*/
class QueryAccessHandler extends QueryAccessHandlerBase {
/**
* {@inheritdoc}
*/
protected function buildEntityOwnerConditions($operation, AccountInterface $account) {
if ($operation == 'view') {
// EntityPermissionProvider doesn't provide own/any view permissions.
return $this->buildEntityConditions($operation, $account);
}
return parent::buildEntityOwnerConditions($operation, $account);
}
}
@@ -0,0 +1,268 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Provides common logic for query access handlers.
*
* @see \Drupal\entity\QueryAccess\QueryAccessHandler
* @see \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
*/
abstract class QueryAccessHandlerBase implements EntityHandlerInterface, QueryAccessHandlerInterface {
/**
* The entity type.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfo;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new QueryAccessHandlerBase object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
* The entity type bundle info.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(EntityTypeInterface $entity_type, EntityTypeBundleInfoInterface $bundle_info, EventDispatcherInterface $event_dispatcher, AccountInterface $current_user) {
$this->entityType = $entity_type;
$this->bundleInfo = $bundle_info;
$this->eventDispatcher = $event_dispatcher;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.bundle.info'),
$container->get('event_dispatcher'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function getConditions($operation, AccountInterface $account = NULL) {
$account = $account ?: $this->currentUser;
$entity_type_id = $this->entityType->id();
$conditions = $this->buildConditions($operation, $account);
// Allow other modules to modify the conditions before they are used.
$event = new QueryAccessEvent($conditions, $operation, $account);
$this->eventDispatcher->dispatch("entity.query_access.{$entity_type_id}", $event);
return $conditions;
}
/**
* Builds the conditions for the given operation and user.
*
* @param string $operation
* The access operation. Usually one of "view", "update" or "delete".
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to restrict access.
*
* @return \Drupal\entity\QueryAccess\ConditionGroup
* The conditions.
*/
public function buildConditions($operation, AccountInterface $account) {
$entity_type_id = $this->entityType->id();
$has_owner = $this->entityType->entityClassImplements(EntityOwnerInterface::class);
if ($account->hasPermission("administer {$entity_type_id}")) {
// The user has full access to all operations, no conditions needed.
$conditions = new ConditionGroup('OR');
$conditions->addCacheContexts(['user.permissions']);
return $conditions;
}
if ($has_owner) {
$entity_conditions = $this->buildEntityOwnerConditions($operation, $account);
}
else {
$entity_conditions = $this->buildEntityConditions($operation, $account);
}
$conditions = NULL;
if ($operation == 'view' && $this->entityType->entityClassImplements(EntityPublishedInterface::class)) {
$uid_key = $this->entityType->getKey('uid');
$published_key = $this->entityType->getKey('published');
$published_conditions = NULL;
$unpublished_conditions = NULL;
if ($entity_conditions) {
// Restrict the existing conditions to published entities only.
$published_conditions = new ConditionGroup('AND');
$published_conditions->addCacheContexts(['user.permissions']);
$published_conditions->addCondition($entity_conditions);
$published_conditions->addCondition($published_key, '1');
}
if ($has_owner && $account->hasPermission("view own unpublished $entity_type_id")) {
$unpublished_conditions = new ConditionGroup('AND');
$unpublished_conditions->addCacheContexts(['user']);
$unpublished_conditions->addCondition($uid_key, $account->id());
$unpublished_conditions->addCondition($published_key, '0');
}
if ($published_conditions && $unpublished_conditions) {
$conditions = new ConditionGroup('OR');
$conditions->addCondition($published_conditions);
$conditions->addCondition($unpublished_conditions);
}
elseif ($published_conditions) {
$conditions = $published_conditions;
}
elseif ($unpublished_conditions) {
$conditions = $unpublished_conditions;
}
}
else {
$conditions = $entity_conditions;
}
if (!$conditions) {
// The user doesn't have access to any entities.
// Falsify the query to ensure no results are returned.
$conditions = new ConditionGroup('OR');
$conditions->addCacheContexts(['user.permissions']);
$conditions->alwaysFalse();
}
return $conditions;
}
/**
* Builds the conditions for entities that have an owner.
*
* @param string $operation
* The access operation. Usually one of "view", "update" or "delete".
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to restrict access.
*
* @return \Drupal\entity\QueryAccess\ConditionGroup|null
* The conditions, or NULL if the user doesn't have access to any entity.
*/
protected function buildEntityOwnerConditions($operation, AccountInterface $account) {
$entity_type_id = $this->entityType->id();
$uid_key = $this->entityType->getKey('uid');
$bundle_key = $this->entityType->getKey('bundle');
$conditions = new ConditionGroup('OR');
$conditions->addCacheContexts(['user.permissions']);
// Any $entity_type permission.
if ($account->hasPermission("$operation any $entity_type_id")) {
// The user has full access, no conditions needed.
return $conditions;
}
// Own $entity_type permission.
if ($account->hasPermission("$operation own $entity_type_id")) {
$conditions->addCacheContexts(['user']);
$conditions->addCondition($uid_key, $account->id());
}
$bundles = array_keys($this->bundleInfo->getBundleInfo($entity_type_id));
$bundles_with_any_permission = [];
$bundles_with_own_permission = [];
foreach ($bundles as $bundle) {
if ($account->hasPermission("$operation any $bundle $entity_type_id")) {
$bundles_with_any_permission[] = $bundle;
}
if ($account->hasPermission("$operation own $bundle $entity_type_id")) {
$bundles_with_own_permission[] = $bundle;
}
}
// Any $bundle permission.
if ($bundles_with_any_permission) {
$conditions->addCondition($bundle_key, $bundles_with_any_permission);
}
// Own $bundle permission.
if ($bundles_with_own_permission) {
$conditions->addCacheContexts(['user']);
$conditions->addCondition((new ConditionGroup('AND'))
->addCondition($uid_key, $account->id())
->addCondition($bundle_key, $bundles_with_own_permission)
);
}
return $conditions->count() ? $conditions : NULL;
}
/**
* Builds the conditions for entities that do not have an owner.
*
* @param string $operation
* The access operation. Usually one of "view", "update" or "delete".
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to restrict access.
*
* @return \Drupal\entity\QueryAccess\ConditionGroup|null
* The conditions, or NULL if the user doesn't have access to any entity.
*/
protected function buildEntityConditions($operation, AccountInterface $account) {
$entity_type_id = $this->entityType->id();
$bundle_key = $this->entityType->getKey('bundle');
$conditions = new ConditionGroup('OR');
$conditions->addCacheContexts(['user.permissions']);
// The $entity_type permission.
if ($account->hasPermission("$operation $entity_type_id")) {
// The user has full access, no conditions needed.
return $conditions;
}
$bundles = array_keys($this->bundleInfo->getBundleInfo($entity_type_id));
$bundles_with_any_permission = [];
foreach ($bundles as $bundle) {
if ($account->hasPermission("$operation $bundle $entity_type_id")) {
$bundles_with_any_permission[] = $bundle;
}
}
// The $bundle permission.
if ($bundles_with_any_permission) {
$conditions->addCondition($bundle_key, $bundles_with_any_permission);
}
return $conditions->count() ? $conditions : NULL;
}
}
@@ -0,0 +1,41 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Session\AccountInterface;
/**
* Query access handlers control access to entities in queries.
*
* An entity defines a query access handler in its annotation:
* @code
* query_access = "\Drupal\entity\QueryAccess\QueryAccessHandler"
* @code
* The handler builds a set of conditions which are then applied to a query
* to filter it. For example, if the user #22 only has access to view
* their own entities, a uid = '22' condition will be built and applied.
*
* The following query types are supported:
* - Entity queries with the $entity_type_id . '_access' tag.
* - Views queries.
*/
interface QueryAccessHandlerInterface {
/**
* Gets the conditions for the given operation and user.
*
* The "entity.query_access.$entity_type_id" event is fired to allow
* modules to alter the conditions.
*
* @param string $operation
* The access operation. Usually one of "view", "update" or "delete".
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to restrict access, or NULL
* to assume the current user. Defaults to NULL.
*
* @return \Drupal\entity\QueryAccess\ConditionGroup
* The conditions.
*/
public function getConditions($operation, AccountInterface $account = NULL);
}
@@ -0,0 +1,11 @@
<?php
namespace Drupal\entity\QueryAccess;
/**
* Controls query access based on the uncacheable entity permissions.
*
* @see \Drupal\entity\UncacheableEntityAccessControlHandler
* @see \Drupal\entity\UncacheableEntityPermissionProvider
*/
class UncacheableQueryAccessHandler extends QueryAccessHandlerBase {}
@@ -0,0 +1,251 @@
<?php
namespace Drupal\entity\QueryAccess;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\Condition as SqlCondition;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\DefaultTableMapping;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Render\RendererInterface;
use Drupal\views\Plugin\views\query\Sql;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines a class for altering views queries.
*
* @internal
*/
class ViewsQueryAlter implements ContainerInjectionInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new ViewsQueryAlter object.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(Connection $connection, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer, RequestStack $request_stack) {
$this->connection = $connection;
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_type_manager;
$this->renderer = $renderer;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('renderer'),
$container->get('request_stack')
);
}
/**
* Alters the given views query.
*
* @param \Drupal\views\Plugin\views\query\Sql $query
* The views query.
* @param \Drupal\views\ViewExecutable $view
* The view.
*/
public function alter(Sql $query, ViewExecutable $view) {
$table_info = $query->getEntityTableInfo();
$base_table = reset($table_info);
if (empty($base_table['entity_type']) || $base_table['relationship_id'] != 'none') {
return;
}
$entity_type_id = $base_table['entity_type'];
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->hasHandlerClass('query_access')) {
return;
}
$storage = $this->entityTypeManager->getStorage($entity_type_id);
if (!$storage instanceof SqlContentEntityStorage) {
return;
}
/** @var \Drupal\entity\QueryAccess\QueryAccessHandlerInterface $query_access */
$query_access = $this->entityTypeManager->getHandler($entity_type_id, 'query_access');
$conditions = $query_access->getConditions('view');
if ($conditions->isAlwaysFalse()) {
$query->addWhereExpression(0, '1 = 0');
}
elseif (count($conditions)) {
// Store the data table, in case mapConditions() needs to join it in.
$base_table['data_table'] = $entity_type->getDataTable();
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $storage->getTableMapping();
$sql_conditions = $this->mapConditions($conditions, $query, $base_table, $field_storage_definitions, $table_mapping);
$query->addWhere(0, $sql_conditions);
}
$this->applyCacheability(CacheableMetadata::createFromObject($conditions));
}
/**
* Maps an entity type's access conditions to views SQL conditions.
*
* @param \Drupal\entity\QueryAccess\ConditionGroup $conditions
* The access conditions.
* @param \Drupal\views\Plugin\views\query\Sql $query
* The views query.
* @param array $base_table
* The base table information.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
* The field storage definitions.
* @param \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping
* The table mapping.
*
* @return \Drupal\Core\Database\Query\ConditionInterface
* The SQL conditions.
*/
protected function mapConditions(ConditionGroup $conditions, Sql $query, array $base_table, array $field_storage_definitions, DefaultTableMapping $table_mapping) {
$sql_condition = new SqlCondition($conditions->getConjunction());
foreach ($conditions->getConditions() as $condition) {
if ($condition instanceof ConditionGroup) {
$nested_sql_conditions = $this->mapConditions($condition, $query, $base_table, $field_storage_definitions, $table_mapping);
$sql_condition->condition($nested_sql_conditions);
}
else {
$field = $condition->getField();
$property_name = NULL;
if (strpos($field, '.') !== FALSE) {
list($field, $property_name) = explode('.', $field);
}
// Skip unknown fields.
if (!isset($field_storage_definitions[$field])) {
continue;
}
$field_storage_definition = $field_storage_definitions[$field];
if (!$property_name) {
$property_name = $field_storage_definition->getMainPropertyName();
}
$column = $table_mapping->getFieldColumnName($field_storage_definition, $property_name);
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definitions[$field])) {
if ($base_table['revision']) {
$dedicated_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
}
else {
$dedicated_table = $table_mapping->getDedicatedDataTableName($field_storage_definition);
}
// Views defaults to LEFT JOIN. For simplicity, we don't try to
// use an INNER JOIN when it's safe to do so (AND conjunctions).
$alias = $query->ensureTable($dedicated_table);
}
elseif ($base_table['revision'] && !$field_storage_definition->isRevisionable()) {
// Workaround for #2652652, which causes $query->ensureTable()
// to not work in this case, due to a missing relationship.
if ($data_table = $query->getTableInfo($base_table['data_table'])) {
$alias = $data_table['alias'];
}
else {
$configuration = [
'type' => 'INNER',
'table' => $base_table['data_table'],
'field' => 'id',
'left_table' => $base_table['alias'],
'left_field' => 'id',
];
/** @var \Drupal\Views\Plugin\views\join\JoinPluginBase $join */
$join = Views::pluginManager('join')->createInstance('standard', $configuration);
$alias = $query->addRelationship($base_table['data_table'], $join, $data_table);
}
}
else {
$alias = $base_table['alias'];
}
$value = $condition->getValue();
$operator = $condition->getOperator();
// Using LIKE/NOT LIKE ensures a case insensitive comparison.
// @see \Drupal\Core\Entity\Query\Sql\Condition::translateCondition().
$property_definitions = $field_storage_definition->getPropertyDefinitions();
$case_sensitive = $property_definitions[$property_name]->getSetting('case_sensitive');
$operator_map = [
'=' => 'LIKE',
'<>' => 'NOT LIKE',
];
if (!$case_sensitive && isset($operator_map[$operator])) {
$operator = $operator_map[$operator];
$value = $this->connection->escapeLike($value);
}
$sql_condition->condition("$alias.$column", $value, $operator);
}
}
return $sql_condition;
}
/**
* Applies the cacheablity metadata to the current request.
*
* @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
* The cacheability metadata.
*/
protected function applyCacheability(CacheableMetadata $cacheable_metadata) {
$request = $this->requestStack->getCurrentRequest();
if ($request->isMethodCacheable() && $this->renderer->hasRenderContext()) {
$build = [];
$cacheable_metadata->applyTo($build);
$this->renderer->render($build);
}
}
}
@@ -9,6 +9,10 @@ use Symfony\Component\Routing\RouteCollection;
/**
* Provides the HTML route for deleting multiple entities.
*
* @deprecated Since Drupal 8.6.x the core DefaultHtmlRouteProvider provides
* the route for any entity type with a "delete-multiple-form" link template
* and a "delete-multiple-confirm" form.
*/
class DeleteMultipleRouteProvider implements EntityRouteProviderInterface {
@@ -34,7 +38,10 @@ class DeleteMultipleRouteProvider implements EntityRouteProviderInterface {
* The generated route, if available.
*/
protected function deleteMultipleFormRoute(EntityTypeInterface $entity_type) {
if ($entity_type->hasLinkTemplate('delete-multiple-form')) {
// Core requires a "delete-multiple-confirm" form to be declared as well,
// if it's missing, it's safe to assume that the entity type is still
// relying on previous Entity API contrib behavior.
if ($entity_type->hasLinkTemplate('delete-multiple-form') && !$entity_type->hasHandlerClass('form', 'delete-multiple-confirm')) {
$route = new Route($entity_type->getLinkTemplate('delete-multiple-form'));
$route->setDefault('_form', '\Drupal\entity\Form\DeleteMultipleForm');
$route->setDefault('entity_type_id', $entity_type->id());
@@ -14,9 +14,8 @@ use Drupal\user\EntityOwnerInterface;
* Provided permissions:
* - administer $entity_type
* - access $entity_type overview
* - view any ($bundle) $entity_type
* - view own ($bundle) $entity_type
* - view own unpublished $entity_type
* - view (own|any) ($bundle) $entity_type
* - update (own|any) ($bundle) $entity_type
* - delete (own|any) ($bundle) $entity_type
* - create $bundle $entity_type
@@ -69,8 +68,8 @@ class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase {
];
}
else {
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
$permissions["view {$entity_type_id}"] = [
'title' => $this->t('View @type', [
'@type' => $plural_label,
]),
];
@@ -95,18 +94,25 @@ class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase {
$has_owner = $entity_type->entityClassImplements(EntityOwnerInterface::class);
$plural_label = $entity_type->getPluralLabel();
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $plural_label,
]),
];
if ($has_owner) {
$permissions["view any {$entity_type_id}"] = [
'title' => $this->t('View any @type', [
'@type' => $plural_label,
]),
];
$permissions["view own {$entity_type_id}"] = [
'title' => $this->t('View own @type', [
'@type' => $plural_label,
]),
];
}
else {
$permissions["view {$entity_type_id}"] = [
'title' => $this->t('View @type', [
'@type' => $plural_label,
]),
];
}
foreach ($bundles as $bundle_name => $bundle_info) {
if ($has_owner) {
@@ -124,8 +130,8 @@ class UncacheableEntityPermissionProvider extends EntityPermissionProviderBase {
];
}
else {
$permissions["view any {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: View any @type', [
$permissions["view {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('@bundle: View @type', [
'@bundle' => $bundle_info['label'],
'@type' => $plural_label,
]),