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,11 +1,13 @@
Entity API module
-----------------
This module contains improvements and extensions to the Drupal 8 Entity system.
The goal is to bring useful improvements to upcoming Drupal 8 minor releases
(e.g. 8.1, 8.2, ..) while maintaining backwards compatibility in future Entity
API module versions (based on the improvements which went into core).
@todo: Explain version compatibility pattern.
@todo: Add overview of all items and maintainers.
Provides improvements and extensions to the Drupal 8 Entity system.
Acts as a staging ground for Drupal core, with each core minor release (8.5, 8.6, 8.7)
receiving a portion of this module's functionality.
Current functionality:
- Local action providers (core issue: #2976861)
- Permission providers (core issue: #2809177)
- Query access API (Change record: https://www.drupal.org/node/3002038, core issue: #777578)
- Bundle plugin API (plugin-based entity bundles, currently not proposed for core inclusion)
- A generic UI for revisions (WIP, see #2625122)
@@ -5,6 +5,6 @@
"homepage": "http://drupal.org/project/entity",
"license": "GPL-2.0+",
"require": {
"drupal/core": "~8.5"
"drupal/core": "^8.6"
}
}
@@ -3,10 +3,10 @@ description: Provides expanded entity APIs, which will be moved to Drupal core o
type: module
# core: 8.x
dependencies:
- drupal:system (>=8.5.0)
- drupal:system (>=8.6.0)
# Information added by Drupal.org packaging script on 2018-06-08
version: '8.x-1.0-beta4'
# Information added by Drupal.org packaging script on 2018-10-11
version: '8.x-1.0-rc1'
core: '8.x'
project: 'entity'
datestamp: 1528452194
datestamp: 1539272605
@@ -5,8 +5,14 @@
* Provides expanded entity APIs.
*/
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\entity\BundlePlugin\BundlePluginHandler;
use Drupal\entity\QueryAccess\EntityQueryAlter;
use Drupal\entity\QueryAccess\ViewsQueryAlter;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\Plugin\views\query\Sql;
use Drupal\views\ViewExecutable;
/**
* Gets the entity types which use bundle plugins.
@@ -86,3 +92,29 @@ function entity_module_preuninstall($module) {
\Drupal::service('entity.bundle_plugin_installer')->uninstallBundles($entity_type, [$module]);
}
}
/**
* Implements hook_query_TAG_alter().
*/
function entity_query_entity_query_alter(SelectInterface $query) {
$entity_type_id = $query->getMetaData('entity_type');
if ($query->hasTag($entity_type_id . '_access')) {
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type = $entity_type_manager->getDefinition($entity_type_id);
\Drupal::service('class_resolver')
->getInstanceFromDefinition(EntityQueryAlter::class)
->alter($query, $entity_type);
}
}
/**
* Implements hook_views_query_alter().
*/
function entity_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
if ($query instanceof Sql) {
\Drupal::service('class_resolver')
->getInstanceFromDefinition(ViewsQueryAlter::class)
->alter($query, $view);
}
}
@@ -1,10 +1,4 @@
services:
access_check.entity_delete_multiple:
class: Drupal\entity\Access\EntityDeleteMultipleAccessCheck
arguments: ['@entity_type.manager', '@tempstore.private', '@request_stack']
tags:
- { name: access_check, applies_to: _entity_delete_multiple_access }
access_checker.entity_revision:
class: \Drupal\entity\Access\EntityRevisionRouteAccessChecker
arguments: ['@entity_type.manager', '@current_route_match']
@@ -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,
]),
@@ -6,8 +6,8 @@ package: Testing
dependencies:
- entity
# Information added by Drupal.org packaging script on 2018-06-08
version: '8.x-1.0-beta4'
# Information added by Drupal.org packaging script on 2018-10-11
version: '8.x-1.0-rc1'
core: '8.x'
project: 'entity'
datestamp: 1528452194
datestamp: 1539272605
@@ -6,8 +6,8 @@ package: Testing
dependencies:
- entity
# Information added by Drupal.org packaging script on 2018-06-08
version: '8.x-1.0-beta4'
# Information added by Drupal.org packaging script on 2018-10-11
version: '8.x-1.0-rc1'
core: '8.x'
project: 'entity'
datestamp: 1528452194
datestamp: 1539272605
@@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
config:
- field.storage.entity_test_enhanced.assigned
id: entity_test_enhanced.first.assigned
field_name: assigned
entity_type: entity_test_enhanced
bundle: first
label: Assigned
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string
@@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
config:
- field.storage.entity_test_enhanced.assigned
id: entity_test_enhanced.second.assigned
field_name: assigned
entity_type: entity_test_enhanced
bundle: second
label: Assigned
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings: { }
field_type: string
@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- entity_module_test
id: entity_test_enhanced.assigned
field_name: assigned
entity_type: entity_test_enhanced
type: string
settings:
max_length: 255
is_ascii: false
case_sensitive: true
module: core
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
@@ -0,0 +1,170 @@
langcode: en
status: true
dependencies:
module:
- entity_module_test
id: entity_test_enhanced
label: 'Enhanced entities'
module: views
description: ''
tag: ''
base_table: entity_test_enhanced_field_data
base_field: id
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
table: entity_test_enhanced_field_data
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters: { }
sorts:
id:
id: id
table: entity_test_enhanced_field_data
field: id
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: entity_test_enhanced
entity_field: id
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
@@ -0,0 +1,170 @@
langcode: en
status: true
dependencies:
module:
- entity_module_test
id: entity_test_enhanced_revisions
label: 'Enhanced entities (Revisions)'
module: views
description: ''
tag: ''
base_table: entity_test_enhanced_field_revision
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
table: entity_test_enhanced_field_revision
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters: { }
sorts:
vid:
id: vid
table: entity_test_enhanced_field_revision
field: vid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: entity_test_enhanced
entity_field: vid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
@@ -0,0 +1,170 @@
langcode: en
status: true
dependencies:
module:
- entity_module_test
id: entity_test_enhanced_with_owner
label: 'Enhanced entities with an owner'
module: views
description: ''
tag: ''
base_table: entity_test_enhanced_with_owner_field_data
base_field: id
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
table: entity_test_enhanced_with_owner_field_data
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters: { }
sorts:
id:
id: id
table: entity_test_enhanced_with_owner_field_data
field: id
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: entity_test_enhanced_with_owner
entity_field: id
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
@@ -0,0 +1,170 @@
langcode: en
status: true
dependencies:
module:
- entity_module_test
id: entity_test_enhanced_with_owner_revisions
label: 'Enhanced entities with an owner (Revisions)'
module: views
description: ''
tag: ''
base_table: entity_test_enhanced_with_owner_field_revision
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: none
options: { }
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
name:
table: entity_test_enhanced_with_owner_field_revision
field: name
id: name
entity_type: null
entity_field: name
plugin_id: field
relationship: none
group_type: group
admin_label: ''
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: string
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
filters: { }
sorts:
vid:
id: vid
table: entity_test_enhanced_with_owner_field_revision
field: vid
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: entity_test_enhanced_with_owner
entity_field: vid
plugin_id: standard
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
tags: { }
@@ -1,16 +0,0 @@
entity_module_test.entity_test_enhanced_bundle.*:
type: config_entity
label: 'Entity test with enhancements - Bundle'
mapping:
id:
type: string
label: 'Type'
label:
type: label
label: 'Label'
description:
type: text
label: 'Description'
new_revision:
type: boolean
label: 'New revision'
@@ -2,9 +2,11 @@ name: Entity test
type: module
package: Testing
# core: 8.x
dependencies:
- field
# Information added by Drupal.org packaging script on 2018-06-08
version: '8.x-1.0-beta4'
# Information added by Drupal.org packaging script on 2018-10-11
version: '8.x-1.0-rc1'
core: '8.x'
project: 'entity'
datestamp: 1528452194
datestamp: 1539272605
@@ -0,0 +1,15 @@
<?php
/**
* Implements hook_entity_bundle_info().
*/
function entity_module_test_entity_bundle_info() {
$bundles['entity_test_enhanced']['default']['label'] = t('Default');
$bundles['entity_test_enhanced']['first']['label'] = t('First');
$bundles['entity_test_enhanced']['second']['label'] = t('Second');
$bundles['entity_test_enhanced_with_owner']['default']['label'] = t('Default');
$bundles['entity_test_enhanced_with_owner']['first']['label'] = t('First');
$bundles['entity_test_enhanced_with_owner']['second']['label'] = t('Second');
return $bundles;
}
@@ -0,0 +1,5 @@
services:
entity_module_test.query_access_subscriber:
class: Drupal\entity_module_test\EventSubscriber\QueryAccessSubscriber
tags:
- { name: event_subscriber }
@@ -2,6 +2,8 @@
namespace Drupal\entity_module_test\Entity;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity\Revision\RevisionableContentEntityBase;
@@ -21,7 +23,8 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
* ),
* handlers = {
* "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "access" = "\Drupal\Core\Entity\EntityAccessControlHandler",
* "access" = "\Drupal\entity\EntityAccessControlHandler",
* "query_access" = "\Drupal\entity\QueryAccess\QueryAccessHandler",
* "permission_provider" = "\Drupal\entity\EntityPermissionProvider",
* "form" = {
* "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
@@ -37,6 +40,7 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
* "collection" = "\Drupal\entity\Menu\EntityCollectionLocalActionProvider",
* },
* "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
* "views_data" = "\Drupal\views\EntityViewsData",
* },
* base_table = "entity_test_enhanced",
* data_table = "entity_test_enhanced_field_data",
@@ -51,6 +55,8 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
* "bundle" = "type",
* "revision" = "vid",
* "langcode" = "langcode",
* "label" = "name",
* "published" = "status",
* },
* links = {
* "add-page" = "/entity_test_enhanced/add",
@@ -63,16 +69,18 @@ use Drupal\entity\Revision\RevisionableContentEntityBase;
* "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert",
* "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions",
* },
* bundle_entity_type = "entity_test_enhanced_bundle",
* )
*/
class EnhancedEntity extends RevisionableContentEntityBase {
class EnhancedEntity extends RevisionableContentEntityBase implements EntityPublishedInterface {
use EntityPublishedTrait;
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel('Name')
@@ -1,84 +0,0 @@
<?php
namespace Drupal\entity_module_test\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\EntityDescriptionInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides bundles for the test entity.
*
* @ConfigEntityType(
* id = "entity_test_enhanced_bundle",
* label = @Translation("Entity test with enhancments - Bundle"),
* admin_permission = "administer entity_test_enhanced",
* config_prefix = "entity_test_enhanced_bundle",
* bundle_of = "entity_test_enhanced",
* handlers = {
* "access" = "\Drupal\entity\BundleEntityAccessControlHandler",
* },
* entity_keys = {
* "id" = "id",
* "label" = "label"
* },
* config_export = {
* "id",
* "label",
* "description"
* },
* )
*/
class EnhancedEntityBundle extends ConfigEntityBundleBase implements EntityDescriptionInterface, RevisionableEntityBundleInterface {
/**
* The bundle ID.
*
* @var string
*/
protected $id;
/**
* The bundle label.
*
* @var string
*/
protected $label;
/**
* The bundle description.
*
* @var string
*/
protected $description;
/**
* Should new entities of this bundle have a new revision by default.
*
* @var bool
*/
protected $new_revision = FALSE;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description;
}
/**
* {@inheritdoc}
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->new_revision;
}
}
@@ -0,0 +1,145 @@
<?php
namespace Drupal\entity_module_test\Entity;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\EntityPublishedTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\entity\Revision\RevisionableContentEntityBase;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\UserInterface;
/**
* Provides a test entity which uses all the capabilities of entity module.
*
* @ContentEntityType(
* id = "entity_test_enhanced_with_owner",
* label = @Translation("Enhanced entity with owner"),
* label_collection = @Translation("Enhanced entities with owner"),
* label_singular = @Translation("enhanced entity with owner"),
* label_plural = @Translation("enhanced entities with owner"),
* label_count = @PluralTranslation(
* singular = "@count enhanced entity with owner",
* plural = "@count enhanced entities with owner",
* ),
* handlers = {
* "storage" = "\Drupal\Core\Entity\Sql\SqlContentEntityStorage",
* "access" = "\Drupal\entity\UncacheableEntityAccessControlHandler",
* "query_access" = "\Drupal\entity\QueryAccess\UncacheableQueryAccessHandler",
* "permission_provider" = "\Drupal\entity\UncacheableEntityPermissionProvider",
* "form" = {
* "add" = "\Drupal\entity\Form\RevisionableContentEntityForm",
* "edit" = "\Drupal\entity\Form\RevisionableContentEntityForm",
* "delete" = "\Drupal\Core\Entity\EntityDeleteForm",
* },
* "route_provider" = {
* "html" = "\Drupal\entity\Routing\DefaultHtmlRouteProvider",
* "revision" = "\Drupal\entity\Routing\RevisionRouteProvider",
* "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider",
* },
* "local_action_provider" = {
* "collection" = "\Drupal\entity\Menu\EntityCollectionLocalActionProvider",
* },
* "list_builder" = "\Drupal\Core\Entity\EntityListBuilder",
* "views_data" = "\Drupal\views\EntityViewsData",
* },
* base_table = "entity_test_enhanced_with_owner",
* data_table = "entity_test_enhanced_with_owner_field_data",
* revision_table = "entity_test_enhanced_with_owner_revision",
* revision_data_table = "entity_test_enhanced_with_owner_field_revision",
* translatable = TRUE,
* revisionable = TRUE,
* admin_permission = "administer entity_test_enhanced_with_owner",
* permission_granularity = "bundle",
* entity_keys = {
* "id" = "id",
* "bundle" = "type",
* "revision" = "vid",
* "langcode" = "langcode",
* "label" = "name",
* "uid" = "user_id",
* "published" = "status",
* },
* links = {
* "add-page" = "/entity_test_enhanced_with_owner/add",
* "add-form" = "/entity_test_enhanced_with_owner/add/{type}",
* "edit-form" = "/entity_test_enhanced_with_owner/{entity_test_enhanced_with_owner}/edit",
* "canonical" = "/entity_test_enhanced_with_owner/{entity_test_enhanced_with_owner}",
* "collection" = "/entity_test_enhanced_with_owner",
* },
* )
*/
class EnhancedEntityWithOwner extends RevisionableContentEntityBase implements EntityOwnerInterface, EntityPublishedInterface {
use EntityPublishedTrait;
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('user_id')->entity;
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('user_id')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
$this->set('user_id', $uid);
return $this;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
$this->set('user_id', $account->id());
return $this;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields += static::publishedBaseFieldDefinitions($entity_type);
$fields['name'] = BaseFieldDefinition::create('string')
->setLabel('Name')
->setRevisionable(TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
]);
$fields['user_id'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('User ID'))
->setDescription(t('The ID of the associated user.'))
->setSetting('target_type', 'user')
->setSetting('handler', 'default')
// Default EntityTest entities to have the root user as the owner, to
// simplify testing.
->setDefaultValue([0 => ['target_id' => 1]])
->setTranslatable(TRUE)
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => -1,
'settings' => [
'match_operator' => 'CONTAINS',
'size' => '60',
'placeholder' => '',
],
]);
return $fields;
}
}
@@ -0,0 +1,58 @@
<?php
namespace Drupal\entity_module_test\EventSubscriber;
use Drupal\entity\QueryAccess\ConditionGroup;
use Drupal\entity\QueryAccess\QueryAccessEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class QueryAccessSubscriber implements EventSubscriberInterface {
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [
'entity.query_access.entity_test_enhanced' => 'onQueryAccess',
];
}
/**
* Modifies the access conditions based on the current user.
*
* This is just a convenient example for testing. A real subscriber would
* ignore the account and extend the conditions to cover additional factors,
* such as a custom entity field.
*
* @param \Drupal\entity\QueryAccess\QueryAccessEvent $event
* The event.
*/
public function onQueryAccess(QueryAccessEvent $event) {
$conditions = $event->getConditions();
$email = $event->getAccount()->getEmail();
if ($email == 'user1@example.com') {
// This user should not have access to any entities.
$conditions->alwaysFalse();
}
elseif ($email == 'user2@example.com') {
// This user should have access to entities with the IDs 1, 2, and 3.
// The query access handler might have already set ->alwaysFalse()
// due to the user not having any other access, so we make sure
// to undo it with $conditions->alwaysFalse(TRUE).
$conditions->alwaysFalse(FALSE);
$conditions->addCondition('id', ['1', '2', '3']);
}
elseif ($email == 'user3@example.com') {
// This user should only have access to entities assigned to "marketing",
// or unassigned entities.
$conditions->alwaysFalse(FALSE);
$conditions->addCondition((new ConditionGroup('OR'))
->addCondition('assigned', NULL, 'IS NULL')
// Confirm that explicitly specifying the property name works.
->addCondition('assigned.value', 'marketing')
);
}
}
}
@@ -3,7 +3,6 @@
namespace Drupal\Tests\entity\Functional;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\Tests\BrowserTestBase;
@@ -33,11 +32,6 @@ class CollectionRouteAccessTest extends BrowserTestBase {
protected function setUp() {
parent::setUp();
EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
])->save();
$this->placeBlock('local_tasks_block');
$this->placeBlock('system_breadcrumb_block');
}
@@ -3,7 +3,6 @@
namespace Drupal\Tests\entity\Functional;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\Tests\BrowserTestBase;
/**
@@ -35,10 +34,6 @@ class DeleteMultipleFormTest extends BrowserTestBase {
protected function setUp() {
parent::setUp();
EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
])->save();
$this->account = $this->drupalCreateUser(['administer entity_test_enhanced']);
$this->drupalLogin($this->account);
}
@@ -37,7 +37,7 @@ class EntityLocalActionTest extends BrowserTestBase {
*/
public function testCollectionLocalAction() {
$this->drupalGet('/entity_test_enhanced');
$this->assertSession()->linkByHrefExists('/entity_test_enhanced/add');
$this->assertSession()->linkByHrefExists('/entity_test_enhanced/add?destination=/entity_test_enhanced');
$this->assertSession()->linkExists('Add enhanced entity');
}
@@ -3,7 +3,6 @@
namespace Drupal\Tests\entity\Functional;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\Tests\block\Traits\BlockCreationTrait;
use Drupal\Tests\BrowserTestBase;
@@ -40,11 +39,6 @@ class RevisionRouteAccessTest extends BrowserTestBase {
protected function setUp() {
parent::setUp();
EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
])->save();
$this->placeBlock('local_tasks_block');
$this->placeBlock('system_breadcrumb_block');
@@ -1,56 +0,0 @@
<?php
namespace Drupal\Tests\entity\Kernel;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests the bundle entity access control handler.
*
* @group entity
*/
class BundleEntityAccessControlHandlerTest extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced');
}
/**
* Tests the "view label" access checking.
*/
public function testAccess() {
$bundle = EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
]);
$bundle->save();
// The default user has no permissions related to entity_test_enhanced.
$this->assertFalse($bundle->access('view label'));
$permissions = [
'administer entity_test_enhanced',
'view entity_test_enhanced',
'view default entity_test_enhanced',
];
foreach ($permissions as $permission) {
$account = $this->createUser([], [$permission]);
$this->assertTrue($bundle->access('view label', $account));
}
}
}
@@ -4,7 +4,6 @@ namespace Drupal\Tests\entity\Kernel;
use Drupal\entity\Plugin\Action\DeleteAction;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\system\Entity\Action;
use Drupal\user\Entity\User;
use Drupal\KernelTests\KernelTestBase;
@@ -40,12 +39,6 @@ class DeleteActionTest extends KernelTestBase {
$this->installEntitySchema('entity_test_enhanced');
$this->installSchema('system', ['key_value_expire', 'sequences']);
$bundle = EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
]);
$bundle->save();
$this->user = User::create([
'name' => 'username',
'status' => 1,
@@ -0,0 +1,106 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity\QueryAccess\Condition;
use Drupal\entity\QueryAccess\ConditionGroup;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the condition group class.
*
* ConditionGroup uses \Drupal\Core\Cache\Cache internally, which makes it
* impossible to use a unit test (due to Cache accessing the global container).
*
* @coversDefaultClass \Drupal\entity\QueryAccess\ConditionGroup
* @group entity
*/
class ConditionGroupTest extends KernelTestBase {
/**
* ::covers getConjunction
* ::covers addCondition
* ::covers getConditions
* ::covers count.
*/
public function testGetters() {
$condition_group = new ConditionGroup();
$condition_group->addCondition('uid', '2');
$this->assertEquals('AND', $condition_group->getConjunction());
$expected_conditions = [
new Condition('uid', '2'),
];
$this->assertEquals($expected_conditions, $condition_group->getConditions());
$this->assertEquals(1, $condition_group->count());
$this->assertEquals("uid = '2'", $condition_group->__toString());
$condition_group = new ConditionGroup('OR');
$condition_group->addCondition('type', ['article', 'page']);
$condition_group->addCondition('status', '1', '<>');
$this->assertEquals('OR', $condition_group->getConjunction());
$expected_conditions = [
new Condition('type', ['article', 'page']),
new Condition('status', '1', '<>'),
];
$expected_lines = [
"(",
" type IN ['article', 'page']",
" OR",
" status <> '1'",
")",
];
$this->assertEquals($expected_conditions, $condition_group->getConditions());
$this->assertEquals(2, $condition_group->count());
$this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString());
// Nested condition group with a single condition.
$condition_group = new ConditionGroup();
$condition_group->addCondition('type', ['article', 'page']);
$condition_group->addCondition((new ConditionGroup('AND'))
->addCondition('status', '1')
);
$expected_conditions = [
new Condition('type', ['article', 'page']),
new Condition('status', '1'),
];
$expected_lines = [
"(",
" type IN ['article', 'page']",
" AND",
" status = '1'",
")",
];
$this->assertEquals($expected_conditions, $condition_group->getConditions());
$this->assertEquals('AND', $condition_group->getConjunction());
$this->assertEquals(2, $condition_group->count());
$this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString());
// Nested condition group with multiple conditions.
$condition_group = new ConditionGroup();
$condition_group->addCondition('type', ['article', 'page']);
$nested_condition_group = new ConditionGroup('OR');
$nested_condition_group->addCondition('uid', '1');
$nested_condition_group->addCondition('status', '1');
$condition_group->addCondition($nested_condition_group);
$expected_conditions = [
new Condition('type', ['article', 'page']),
$nested_condition_group,
];
$expected_lines = [
"(",
" type IN ['article', 'page']",
" AND",
" (",
" uid = '1'",
" OR",
" status = '1'",
" )",
")",
];
$this->assertEquals($expected_conditions, $condition_group->getConditions());
$this->assertEquals('AND', $condition_group->getConjunction());
$this->assertEquals(2, $condition_group->count());
$this->assertEquals(implode("\n", $expected_lines), $condition_group->__toString());
}
}
@@ -0,0 +1,62 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity\QueryAccess\QueryAccessHandler;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests the query access event.
*
* @group entity
*/
class QueryAccessEventTest extends EntityKernelTestBase {
/**
* The query access handler.
*
* @var \Drupal\entity\QueryAccess\QueryAccessHandler
*/
protected $handler;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced');
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type = $entity_type_manager->getDefinition('entity_test_enhanced');
$this->handler = QueryAccessHandler::createInstance($this->container, $entity_type);
}
/**
* Tests the event.
*/
public function testEvent() {
// By default, the first user should have full access, and the second
// user should have no access. The QueryAccessSubscriber flips that.
$first_user = $this->createUser(['mail' => 'user1@example.com'], ['administer entity_test_enhanced']);
$second_user = $this->createUser(['mail' => 'user2@example.com']);
$conditions = $this->handler->getConditions('view', $first_user);
$this->assertTrue($conditions->isAlwaysFalse());
$conditions = $this->handler->getConditions('view', $second_user);
$this->assertFalse($conditions->isAlwaysFalse());
}
}
@@ -0,0 +1,137 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity\QueryAccess\Condition;
use Drupal\entity\QueryAccess\QueryAccessHandler;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests the query access handler.
*
* Uses the "entity_test_enhanced" entity type, which has no owner.
* UncacheableQueryAccessHandlerTest uses the "entity_test_enhanced_with_owner"
* entity type, which has an owner. This ensures both sides (owner and
* no owner) are covered.
*
* @coversDefaultClass \Drupal\entity\QueryAccess\QueryAccessHandler
* @group entity
*/
class QueryAccessHandlerTest extends EntityKernelTestBase {
/**
* The query access handler.
*
* @var \Drupal\entity\QueryAccess\QueryAccessHandler
*/
protected $handler;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced');
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type = $entity_type_manager->getDefinition('entity_test_enhanced');
$this->handler = QueryAccessHandler::createInstance($this->container, $entity_type);
}
/**
* @covers ::getConditions
*/
public function testNoAccess() {
foreach (['view', 'update', 'delete'] as $operation) {
$user = $this->createUser([], ['access content']);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertTrue($conditions->isAlwaysFalse());
}
}
/**
* @covers ::getConditions
*/
public function testAdmin() {
foreach (['view', 'update', 'delete'] as $operation) {
$user = $this->createUser([], ['administer entity_test_enhanced']);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
}
/**
* @covers ::getConditions
*/
public function testView() {
// Entity type permission.
$user = $this->createUser([], ['view entity_test_enhanced']);
$conditions = $this->handler->getConditions('view', $user);
$expected_conditions = [
new Condition('status', '1'),
];
$this->assertEquals(1, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Bundle permission.
$user = $this->createUser([], ['view first entity_test_enhanced']);
$conditions = $this->handler->getConditions('view', $user);
$expected_conditions = [
new Condition('type', ['first']),
new Condition('status', '1'),
];
$this->assertEquals('AND', $conditions->getConjunction());
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
/**
* @covers ::getConditions
*/
public function testUpdateDelete() {
foreach (['update', 'delete'] as $operation) {
// Entity type permission.
$user = $this->createUser([], ["$operation entity_test_enhanced"]);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Bundle permission.
$user = $this->createUser([], [
"$operation first entity_test_enhanced",
"$operation second entity_test_enhanced",
]);
$conditions = $this->handler->getConditions($operation, $user);
$expected_conditions = [
new Condition('type', ['first', 'second']),
];
$this->assertEquals('OR', $conditions->getConjunction());
$this->assertEquals(1, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
}
}
@@ -0,0 +1,343 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\views\Tests\ViewResultAssertionTrait;
use Drupal\views\Views;
/**
* Test query access filtering for EntityQuery and Views.
*
* @group entity
*
* @see \Drupal\entity\QueryAccess\QueryAccessHandler
* @see \Drupal\entity\QueryAccess\EntityQueryAlter
* @see \Drupal\entity\QueryAccess\ViewsQueryAlter
*/
class QueryAccessTest extends EntityKernelTestBase {
use ViewResultAssertionTrait;
/**
* The test entities.
*
* @var \Drupal\Core\Entity\ContentEntityInterface[]
*/
protected $entities;
/**
* The entity_test_enhanced storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
'user',
'views',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced');
$this->installConfig(['entity_module_test']);
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
$first_entity = EnhancedEntity::create([
'type' => 'first',
'label' => 'First',
'status' => 1,
]);
$first_entity->save();
$first_entity->set('name', 'First!');
$first_entity->set('status', 0);
$first_entity->setNewRevision(TRUE);
$first_entity->save();
$second_entity = EnhancedEntity::create([
'type' => 'first',
'label' => 'Second',
'status' => 0,
]);
$second_entity->save();
$second_entity->set('name', 'Second!');
$second_entity->set('status', 1);
$second_entity->setNewRevision(TRUE);
$second_entity->save();
$third_entity = EnhancedEntity::create([
'type' => 'second',
'label' => 'Third',
'status' => 1,
]);
$third_entity->save();
$third_entity->set('name', 'Third!');
$third_entity->setNewRevision(TRUE);
$third_entity->save();
$this->entities = [$first_entity, $second_entity, $third_entity];
$this->storage = \Drupal::entityTypeManager()->getStorage('entity_test_enhanced');
}
/**
* Tests EntityQuery filtering.
*/
public function testEntityQuery() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced']);
\Drupal::currentUser()->setAccount($admin_user);
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[0]->id(),
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->execute();
$this->assertEmpty($result);
// View (published-only).
$user = $this->createUser([], ['view entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
// View $bundle (published-only).
$user = $this->createUser([], ['view first entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[1]->id(),
], array_values($result));
}
/**
* Tests EntityQuery filtering when all revisions are queried.
*/
public function testEntityQueryWithRevisions() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced']);
\Drupal::currentUser()->setAccount($admin_user);
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'2' => $this->entities[0]->id(),
'3' => $this->entities[1]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->execute();
$this->assertEmpty($result);
// View (published-only).
$user = $this->createUser([], ['view entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
// View $bundle (published-only).
$user = $this->createUser([], ['view first entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'4' => $this->entities[1]->id(),
], $result);
}
/**
* Tests Views filtering.
*/
public function testViews() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced']);
\Drupal::currentUser()->setAccount($admin_user);
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[0]->id()],
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, []);
// View (published-only).
$user = $this->createUser([], ['view entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
// View $bundle (published-only).
$user = $this->createUser([], ['view first entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[1]->id()],
], ['id' => 'id']);
}
/**
* Tests Views filtering when all revisions are queried.
*/
public function testViewsWithRevisions() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced']);
\Drupal::currentUser()->setAccount($admin_user);
$view = Views::getView('entity_test_enhanced_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '2', 'id' => $this->entities[0]->id()],
['vid' => '3', 'id' => $this->entities[1]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, []);
// View (published-only).
$user = $this->createUser([], ['view entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
// View $bundle (published-only).
$user = $this->createUser([], ['view first entity_test_enhanced']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
], ['vid' => 'vid']);
}
/**
* Tests filtering based on a configurable field.
*
* QueryAccessSubscriber adds a condition that ensures that the field value
* is either empty or matches "marketing".
*
* @see \Drupal\entity_module_test\EventSubscriber\QueryAccessSubscriber
*/
public function testConfigurableField() {
$this->entities[0]->set('assigned', 'marketing');
$this->entities[0]->save();
// The field is case sensitive, so the third entity should be ignored.
$this->entities[2]->set('assigned', 'MarKeTing');
$this->entities[2]->save();
$user = $this->createUser([
'mail' => 'user3@example.com',
], ['access content']);
\Drupal::currentUser()->setAccount($user);
// EntityQuery.
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[0]->id(),
$this->entities[1]->id(),
], array_values($result));
// EntityQuery with revisions.
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'2' => $this->entities[0]->id(),
'3' => $this->entities[1]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
], $result);
// View.
$view = Views::getView('entity_test_enhanced');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[0]->id()],
['id' => $this->entities[1]->id()],
], ['id' => 'id']);
// View with revisions.
$view = Views::getView('entity_test_enhanced_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '2', 'id' => $this->entities[0]->id()],
['vid' => '3', 'id' => $this->entities[1]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
}
}
@@ -0,0 +1,203 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity\QueryAccess\Condition;
use Drupal\entity\QueryAccess\ConditionGroup;
use Drupal\entity\QueryAccess\UncacheableQueryAccessHandler;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
/**
* Tests the uncacheable query access handler.
*
* Uses the "entity_test_enhanced_with_owner" entity type, which has an owner.
* QueryAccessHandlerTest uses the "entity_test_enhanced" entity type, which
* has no owner. This ensures both sides (owner and no owner) are covered.
*
* @coversDefaultClass \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
* @group entity
*/
class UncacheableQueryAccessHandlerTest extends EntityKernelTestBase {
/**
* The query access handler.
*
* @var \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
*/
protected $handler;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced_with_owner');
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type = $entity_type_manager->getDefinition('entity_test_enhanced_with_owner');
$this->handler = UncacheableQueryAccessHandler::createInstance($this->container, $entity_type);
}
/**
* @covers ::getConditions
*/
public function testNoAccess() {
foreach (['view', 'update', 'delete'] as $operation) {
$user = $this->createUser([], ['access content']);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertTrue($conditions->isAlwaysFalse());
}
}
/**
* @covers ::getConditions
*/
public function testAdmin() {
foreach (['view', 'update', 'delete'] as $operation) {
$user = $this->createUser([], ['administer entity_test_enhanced_with_owner']);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
}
/**
* @covers ::getConditions
*/
public function testView() {
// Any permission.
$user = $this->createUser([], ['view any entity_test_enhanced_with_owner']);
$conditions = $this->handler->getConditions('view', $user);
$expected_conditions = [
new Condition('status', '1'),
];
$this->assertEquals(1, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Own permission.
$user = $this->createUser([], ['view own entity_test_enhanced_with_owner']);
$conditions = $this->handler->getConditions('view', $user);
$expected_conditions = [
new Condition('user_id', $user->id()),
new Condition('status', '1'),
];
$this->assertEquals('AND', $conditions->getConjunction());
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Any permission for the first bundle, own permission for the second.
$user = $this->createUser([], [
'view any first entity_test_enhanced_with_owner',
'view own second entity_test_enhanced_with_owner',
]);
$conditions = $this->handler->getConditions('view', $user);
$expected_conditions = [
(new ConditionGroup('OR'))
->addCacheContexts(['user', 'user.permissions'])
->addCondition('type', ['first'])
->addCondition((new ConditionGroup('AND'))
->addCondition('user_id', $user->id())
->addCondition('type', ['second'])
),
new Condition('status', '1'),
];
$this->assertEquals('AND', $conditions->getConjunction());
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// View own unpublished permission.
$user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']);
$conditions = $this->handler->buildConditions('view', $user);
$expected_conditions = [
new Condition('user_id', $user->id()),
new Condition('status', '0'),
];
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Both view any and view own unpublished permissions.
$user = $this->createUser([], [
'view any entity_test_enhanced_with_owner',
'view own unpublished entity_test_enhanced_with_owner',
]);
$conditions = $this->handler->buildConditions('view', $user);
$expected_conditions = [
new Condition('status', '1'),
(new ConditionGroup('AND'))
->addCondition('user_id', $user->id())
->addCondition('status', '0')
->addCacheContexts(['user']),
];
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
/**
* @covers ::getConditions
*/
public function testUpdateDelete() {
foreach (['update', 'delete'] as $operation) {
// Any permission.
$user = $this->createUser([], ["$operation any entity_test_enhanced_with_owner"]);
$conditions = $this->handler->getConditions($operation, $user);
$this->assertEquals(0, $conditions->count());
$this->assertEquals(['user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Own permission.
$user = $this->createUser([], ["$operation own entity_test_enhanced_with_owner"]);
$conditions = $this->handler->getConditions($operation, $user);
$expected_conditions = [
new Condition('user_id', $user->id()),
];
$this->assertEquals(1, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
// Any permission for the first bundle, own permission for the second.
$user = $this->createUser([], [
"$operation any first entity_test_enhanced_with_owner",
"$operation own second entity_test_enhanced_with_owner",
]);
$conditions = $this->handler->getConditions($operation, $user);
$expected_conditions = [
new Condition('type', ['first']),
(new ConditionGroup('AND'))
->addCondition('user_id', $user->id())
->addCondition('type', ['second']),
];
$this->assertEquals('OR', $conditions->getConjunction());
$this->assertEquals(2, $conditions->count());
$this->assertEquals($expected_conditions, $conditions->getConditions());
$this->assertEquals(['user', 'user.permissions'], $conditions->getCacheContexts());
$this->assertFalse($conditions->isAlwaysFalse());
}
}
}
@@ -0,0 +1,511 @@
<?php
namespace Drupal\Tests\entity\Kernel\QueryAccess;
use Drupal\entity_module_test\Entity\EnhancedEntityWithOwner;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\views\Tests\ViewResultAssertionTrait;
use Drupal\views\Views;
/**
* Test uncacheable query access filtering for EntityQuery and Views.
*
* @group entity
*
* @see \Drupal\entity\QueryAccess\UncacheableQueryAccessHandler
* @see \Drupal\entity\QueryAccess\EntityQueryAlter
* @see \Drupal\entity\QueryAccess\ViewsQueryAlter
*/
class UncacheableQueryAccessTest extends EntityKernelTestBase {
use ViewResultAssertionTrait;
/**
* The test entities.
*
* @var \Drupal\Core\Entity\ContentEntityInterface[]
*/
protected $entities;
/**
* The entity_test_enhanced storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $storage;
/**
* {@inheritdoc}
*/
public static $modules = [
'entity',
'entity_module_test',
'user',
'views',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('entity_test_enhanced_with_owner');
$this->installConfig(['entity_module_test']);
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
$first_entity = EnhancedEntityWithOwner::create([
'type' => 'first',
'name' => 'First',
'status' => 1,
]);
$first_entity->save();
$first_entity->set('name', 'First!');
$first_entity->set('status', 0);
$first_entity->setNewRevision(TRUE);
$first_entity->save();
$second_entity = EnhancedEntityWithOwner::create([
'type' => 'first',
'name' => 'Second',
'status' => 0,
]);
$second_entity->save();
$second_entity->set('name', 'Second!');
$second_entity->set('status', 1);
$second_entity->setNewRevision(TRUE);
$second_entity->save();
$third_entity = EnhancedEntityWithOwner::create([
'type' => 'second',
'name' => 'Third',
'status' => 1,
]);
$third_entity->save();
$third_entity->set('name', 'Third!');
$third_entity->setNewRevision(TRUE);
$third_entity->save();
$this->entities = [$first_entity, $second_entity, $third_entity];
$this->storage = \Drupal::entityTypeManager()->getStorage('entity_test_enhanced_with_owner');
}
/**
* Tests EntityQuery filtering.
*/
public function testEntityQuery() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($admin_user);
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[0]->id(),
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->execute();
$this->assertEmpty($result);
// View own (published-only).
$user = $this->createUser([], ['view own entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[1]->id(),
], array_values($result));
// View any (published-only).
$user = $this->createUser([], ['view any entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
// View own unpublished.
$user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[0]->id(),
], array_values($result));
// View own unpublished + view any (published-only).
$user = $this->createUser([], [
'view own unpublished entity_test_enhanced_with_owner',
'view any entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[0]->id(),
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
// View own $first_bundle + View any $second_bundle.
$user = $this->createUser([], [
'view own first entity_test_enhanced_with_owner',
'view any second entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->sort('id')->execute();
$this->assertEquals([
$this->entities[1]->id(),
$this->entities[2]->id(),
], array_values($result));
}
/**
* Tests EntityQuery filtering when all revisions are queried.
*/
public function testEntityQueryWithRevisions() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($admin_user);
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'2' => $this->entities[0]->id(),
'3' => $this->entities[1]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->allRevisions()->execute();
$this->assertEmpty($result);
// View own (published-only).
$user = $this->createUser([], ['view own entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
// The user_id field is not revisionable, which means that updating it
// will modify both revisions for each entity.
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'4' => $this->entities[1]->id(),
], $result);
// View any (published-only).
$user = $this->createUser([], ['view any entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
// View own unpublished.
$user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'2' => $this->entities[0]->id(),
'3' => $this->entities[1]->id(),
], $result);
// View own unpublished + view any (published-only).
$user = $this->createUser([], [
'view own unpublished entity_test_enhanced_with_owner',
'view any entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'1' => $this->entities[0]->id(),
'2' => $this->entities[0]->id(),
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
// View own $first_bundle + View any $second_bundle.
$user = $this->createUser([], [
'view own first entity_test_enhanced_with_owner',
'view any second entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$result = $this->storage->getQuery()->allRevisions()->sort('id')->execute();
$this->assertEquals([
'4' => $this->entities[1]->id(),
'5' => $this->entities[2]->id(),
'6' => $this->entities[2]->id(),
], $result);
}
/**
* Tests Views filtering.
*/
public function testViews() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($admin_user);
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[0]->id()],
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, []);
// View own (published-only).
$user = $this->createUser([], ['view own entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[1]->id()],
], ['id' => 'id']);
// View any (published-only).
$user = $this->createUser([], ['view any entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
// View own unpublished.
$user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[0]->id()],
], ['id' => 'id']);
// View own unpublished + view any (published-only).
$user = $this->createUser([], [
'view own unpublished entity_test_enhanced_with_owner',
'view any entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[0]->id()],
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
// View own $first_bundle + View any $second_bundle.
$user = $this->createUser([], [
'view own first entity_test_enhanced_with_owner',
'view any second entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner');
$view->execute();
$this->assertIdenticalResultset($view, [
['id' => $this->entities[1]->id()],
['id' => $this->entities[2]->id()],
], ['id' => 'id']);
}
/**
* Tests Views filtering when all revisions are queried.
*/
public function testViewsWithRevisions() {
// Admin permission, full access.
$admin_user = $this->createUser([], ['administer entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($admin_user);
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '2', 'id' => $this->entities[0]->id()],
['vid' => '3', 'id' => $this->entities[1]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
// No view permissions, no access.
$user = $this->createUser([], ['access content']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, []);
// View own (published-only).
$user = $this->createUser([], ['view own entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
], ['vid' => 'vid']);
// View any (published-only).
$user = $this->createUser([], ['view any entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
// View own unpublished.
$user = $this->createUser([], ['view own unpublished entity_test_enhanced_with_owner']);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '2', 'id' => $this->entities[0]->id()],
['vid' => '3', 'id' => $this->entities[1]->id()],
], ['vid' => 'vid']);
// View own unpublished + view any (published-only).
$user = $this->createUser([], [
'view own unpublished entity_test_enhanced_with_owner',
'view any entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[0]->set('user_id', $user->id());
$this->entities[0]->save();
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '1', 'id' => $this->entities[0]->id()],
['vid' => '2', 'id' => $this->entities[0]->id()],
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
// View own $first_bundle + View any $second_bundle.
$user = $this->createUser([], [
'view own first entity_test_enhanced_with_owner',
'view any second entity_test_enhanced_with_owner',
]);
\Drupal::currentUser()->setAccount($user);
$this->entities[1]->set('user_id', $user->id());
$this->entities[1]->save();
$view = Views::getView('entity_test_enhanced_with_owner_revisions');
$view->execute();
$this->assertIdenticalResultset($view, [
['vid' => '4', 'id' => $this->entities[1]->id()],
['vid' => '5', 'id' => $this->entities[2]->id()],
['vid' => '6', 'id' => $this->entities[2]->id()],
], ['vid' => 'vid']);
}
}
@@ -3,7 +3,6 @@
namespace Drupal\Tests\entity\Kernel;
use Drupal\entity_module_test\Entity\EnhancedEntity;
use Drupal\entity_module_test\Entity\EnhancedEntityBundle;
use Drupal\KernelTests\KernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
@@ -30,12 +29,6 @@ class RevisionBasicUITest extends KernelTestBase {
$this->installSchema('system', 'router');
$this->installConfig(['system']);
$bundle = EnhancedEntityBundle::create([
'id' => 'default',
'label' => 'Default',
]);
$bundle->save();
\Drupal::service('router.builder')->rebuild();
}
@@ -0,0 +1,115 @@
<?php
namespace Drupal\Tests\entity\Unit;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity\BundleEntityAccessControlHandler;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\entity\BundleEntityAccessControlHandler
* @group entity
*/
class BundleEntityAccessControlHandlerTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$module_handler->invokeAll(Argument::any(), Argument::any())->willReturn([]);
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE);
$container = new ContainerBuilder();
$container->set('module_handler', $module_handler->reveal());
$container->set('cache_contexts_manager', $cache_contexts_manager->reveal());
\Drupal::setContainer($container);
}
/**
* @covers ::checkAccess
*
* @dataProvider accessProvider
*/
public function testAccess(EntityInterface $entity, $operation, $account, $allowed) {
$handler = new BundleEntityAccessControlHandler($entity->getEntityType());
$handler->setStringTranslation($this->getStringTranslationStub());
$result = $handler->access($entity, $operation, $account);
$this->assertEquals($allowed, $result);
}
/**
* Data provider for testAccess().
*
* @return array
* A list of testAccess method arguments.
*/
public function accessProvider() {
$entity_type = $this->prophesize(ContentEntityTypeInterface::class);
$entity_type->id()->willReturn('green_entity_bundle');
$entity_type->getBundleOf()->willReturn('green_entity');
$entity_type->getAdminPermission()->willReturn('administer green_entity');
$entity_type = $entity_type->reveal();
$entity = $this->prophesize(ConfigEntityInterface::class);
$entity->getEntityType()->willReturn($entity_type);
$entity->getEntityTypeId()->willReturn('green_entity_bundle');
$entity->id()->willReturn('default');
$entity->uuid()->willReturn('fake uuid');
$entity->language()->willReturn(new Language(['id' => LanguageInterface::LANGCODE_NOT_SPECIFIED]));
// User with no access.
$user = $this->buildMockUser(1, 'access content');
$data[] = [$entity->reveal(), 'view label', $user->reveal(), FALSE];
// Permissions which grant "view label" access.
$permissions = [
'administer green_entity',
'view green_entity',
'view default green_entity',
'view own green_entity',
'view any green_entity',
'view own default green_entity',
'view any default green_entity',
];
foreach ($permissions as $index => $permission) {
$user = $this->buildMockUser(10 + $index, $permission);
$data[] = [$entity->reveal(), 'view label', $user->reveal(), TRUE];
}
return $data;
}
/**
* Builds a mock user.
*
* @param int $uid
* The user ID.
* @param string $permission
* The permission to grant.
*
* @return \Prophecy\Prophecy\ObjectProphecy
* The user mock.
*/
protected function buildMockUser($uid, $permission) {
$account = $this->prophesize(AccountInterface::class);
$account->id()->willReturn($uid);
$account->hasPermission($permission)->willReturn(TRUE);
$account->hasPermission(Argument::any())->willReturn(FALSE);
return $account;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Drupal\Tests\entity\Unit\QueryAccess;
use Drupal\entity\QueryAccess\Condition;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\entity\QueryAccess\Condition
* @group entity
*/
class ConditionTest extends UnitTestCase {
/**
* ::covers __construct.
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Unrecognized operator "INVALID".
*/
public function testInvalidOperator() {
$condition = new Condition('uid', '1', 'INVALID');
}
/**
* ::covers getField
* ::covers getValue
* ::covers getOperator
* ::covers __toString.
*/
public function testGetters() {
$condition = new Condition('uid', '2');
$this->assertEquals('uid', $condition->getField());
$this->assertEquals('2', $condition->getValue());
$this->assertEquals('=', $condition->getOperator());
$this->assertEquals("uid = '2'", $condition->__toString());
$condition = new Condition('type', ['article', 'page']);
$this->assertEquals('type', $condition->getField());
$this->assertEquals(['article', 'page'], $condition->getValue());
$this->assertEquals('IN', $condition->getOperator());
$this->assertEquals("type IN ['article', 'page']", $condition->__toString());
$condition = new Condition('title', NULL, 'IS NULL');
$this->assertEquals('title', $condition->getField());
$this->assertEquals(NULL, $condition->getValue());
$this->assertEquals('IS NULL', $condition->getOperator());
$this->assertEquals("title IS NULL", $condition->__toString());
}
}
@@ -81,7 +81,7 @@ class UncacheableEntityPermissionProviderTest extends UnitTestCase {
'create green_entity' => 'Create green entities',
'update green_entity' => 'Update green entities',
'delete green_entity' => 'Delete green entities',
'view any green_entity' => 'View any green entities',
'view green_entity' => 'View green entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];
@@ -127,9 +127,9 @@ class UncacheableEntityPermissionProviderTest extends UnitTestCase {
'create second white_entity' => 'Second: Create white entities',
'update second white_entity' => 'Second: Update white entities',
'delete second white_entity' => 'Second: Delete white entities',
'view any white_entity' => 'View any white entities',
'view any first white_entity' => 'First: View any white entities',
'view any second white_entity' => 'Second: View any white entities',
'view white_entity' => 'View white entities',
'view first white_entity' => 'First: View white entities',
'view second white_entity' => 'Second: View white entities',
];
$data[] = [$entity_type->reveal(), $expected_permissions];