Same name and namespace in other branches
  1. 8.9.x core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php \Drupal\Core\Entity\EntityAccessControlHandler
  2. 9 core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php \Drupal\Core\Entity\EntityAccessControlHandler

Defines a default implementation for entity access control handler.

Hierarchy

Expanded class hierarchy of EntityAccessControlHandler

35 files declare their use of EntityAccessControlHandler
BaseFieldOverrideAccessControlHandler.php in core/lib/Drupal/Core/Field/BaseFieldOverrideAccessControlHandler.php
BlockAccessControlHandler.php in core/modules/block/src/BlockAccessControlHandler.php
BlockTypeAccessControlHandler.php in core/modules/block_content/src/BlockTypeAccessControlHandler.php
CommentAccessControlHandler.php in core/modules/comment/src/CommentAccessControlHandler.php
ConfigTestAccessControlHandler.php in core/modules/config/tests/config_test/src/ConfigTestAccessControlHandler.php

... See full list

File

core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php, line 14

Namespace

Drupal\Core\Entity
View source
class EntityAccessControlHandler extends EntityHandlerBase implements EntityAccessControlHandlerInterface {

  /**
   * Stores calculated access check results.
   *
   * @var array
   */
  protected $accessCache = [];

  /**
   * The entity type ID of the access control handler instance.
   *
   * @var string
   */
  protected $entityTypeId;

  /**
   * Information about the entity type.
   *
   * @var \Drupal\Core\Entity\EntityTypeInterface
   */
  protected $entityType;

  /**
   * Allows to grant access to just the labels.
   *
   * By default, the "view label" operation falls back to "view". Set this to
   * TRUE to allow returning different access when just listing entity labels.
   *
   * @var bool
   */
  protected $viewLabelOperation = FALSE;

  /**
   * Constructs an access control handler instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
   */
  public function __construct(EntityTypeInterface $entity_type) {
    $this->entityTypeId = $entity_type
      ->id();
    $this->entityType = $entity_type;
  }

  /**
   * {@inheritdoc}
   */
  public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
    $account = $this
      ->prepareUser($account);
    $langcode = $entity
      ->language()
      ->getId();
    if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
      $operation = 'view';
    }

    // If an entity does not have a UUID, either from not being set or from not
    // having them, use the 'entity type:ID' pattern as the cache $cid.
    $cid = $entity
      ->uuid() ?: $entity
      ->getEntityTypeId() . ':' . $entity
      ->id();

    // If the entity is revisionable, then append the revision ID to allow
    // individual revisions to have specific access control and be cached
    // separately.
    if ($entity instanceof RevisionableInterface) {

      /** @var \Drupal\Core\Entity\RevisionableInterface $entity */
      $cid .= ':' . $entity
        ->getRevisionId();

      // It is not possible to delete or revert the default revision.
      if ($entity
        ->isDefaultRevision() && ($operation === 'revert' || $operation === 'delete revision')) {
        return $return_as_object ? AccessResult::forbidden() : FALSE;
      }
    }
    if (($return = $this
      ->getCache($cid, $operation, $langcode, $account)) !== NULL) {

      // Cache hit, no work necessary.
      return $return_as_object ? $return : $return
        ->isAllowed();
    }

    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
    // take precedence over overridden implementations of
    // EntityAccessControlHandler::checkAccess(). Entities that have checks that
    // need to be done before the hook is invoked should do so by overriding
    // this method.
    // We grant access to the entity if both of these conditions are met:
    // - No modules say to deny access.
    // - At least one module says to grant access.
    $access = array_merge($this
      ->moduleHandler()
      ->invokeAll('entity_access', [
      $entity,
      $operation,
      $account,
    ]), $this
      ->moduleHandler()
      ->invokeAll($entity
      ->getEntityTypeId() . '_access', [
      $entity,
      $operation,
      $account,
    ]));
    $return = $this
      ->processAccessHookResults($access);

    // Also execute the default access check except when the access result is
    // already forbidden, as in that case, it can not be anything else.
    if (!$return
      ->isForbidden()) {
      $return = $return
        ->orIf($this
        ->checkAccess($entity, $operation, $account));
    }
    $result = $this
      ->setCache($return, $cid, $operation, $langcode, $account);
    return $return_as_object ? $result : $result
      ->isAllowed();
  }

  /**
   * Determines entity access.
   *
   * We grant access to the entity if both of these conditions are met:
   * - No modules say to deny access.
   * - At least one module says to grant access.
   *
   * @param \Drupal\Core\Access\AccessResultInterface[] $access
   *   An array of access results of the fired access hook.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The combined result of the various access checks' results. All their
   *   cacheability metadata is merged as well.
   *
   * @see \Drupal\Core\Access\AccessResultInterface::orIf()
   */
  protected function processAccessHookResults(array $access) {

    // No results means no opinion.
    if (empty($access)) {
      return AccessResult::neutral();
    }

    /** @var \Drupal\Core\Access\AccessResultInterface $result */
    $result = array_shift($access);
    foreach ($access as $other) {
      $result = $result
        ->orIf($other);
    }
    return $result;
  }

  /**
   * Performs access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * do their own custom access checking.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity for which to check access.
   * @param string $operation
   *   The entity operation. Usually one of 'view', 'view label', 'update' or
   *   'delete'.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for which to check access.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    if ($operation == 'delete' && $entity
      ->isNew()) {
      return AccessResult::forbidden()
        ->addCacheableDependency($entity);
    }
    if ($admin_permission = $this->entityType
      ->getAdminPermission()) {
      return AccessResult::allowedIfHasPermission($account, $admin_permission);
    }
    else {

      // No opinion.
      return AccessResult::neutral();
    }
  }

  /**
   * Tries to retrieve a previously cached access value from the static cache.
   *
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
   * @param string $operation
   *   The entity operation. Usually one of 'view', 'update', 'create' or
   *   'delete'.
   * @param string $langcode
   *   The language code for which to check access.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for which to check access.
   *
   * @return \Drupal\Core\Access\AccessResultInterface|null
   *   The cached AccessResult, or NULL if there is no record for the given
   *   user, operation, langcode and entity in the cache.
   */
  protected function getCache($cid, $operation, $langcode, AccountInterface $account) {

    // Return from cache if a value has been set for it previously.
    if (isset($this->accessCache[$account
      ->id()][$cid][$langcode][$operation])) {
      return $this->accessCache[$account
        ->id()][$cid][$langcode][$operation];
    }
  }

  /**
   * Statically caches whether the given user has access.
   *
   * @param \Drupal\Core\Access\AccessResultInterface $access
   *   The access result.
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
   * @param string $operation
   *   The entity operation. Usually one of 'view', 'update', 'create' or
   *   'delete'.
   * @param string $langcode
   *   The language code for which to check access.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for which to check access.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   Whether the user has access, plus cacheability metadata.
   */
  protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {

    // Save the given value in the static cache and directly return it.
    return $this->accessCache[$account
      ->id()][$cid][$langcode][$operation] = $access;
  }

  /**
   * {@inheritdoc}
   */
  public function resetCache() {
    $this->accessCache = [];
  }

  /**
   * {@inheritdoc}
   */
  public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
    $account = $this
      ->prepareUser($account);
    $context += [
      'entity_type_id' => $this->entityTypeId,
      'langcode' => LanguageInterface::LANGCODE_DEFAULT,
    ];
    $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
    if (($access = $this
      ->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {

      // Cache hit, no work necessary.
      return $return_as_object ? $access : $access
        ->isAllowed();
    }

    // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
    // Hook results take precedence over overridden implementations of
    // EntityAccessControlHandler::checkCreateAccess(). Entities that have
    // checks that need to be done before the hook is invoked should do so by
    // overriding this method.
    // We grant access to the entity if both of these conditions are met:
    // - No modules say to deny access.
    // - At least one module says to grant access.
    $access = array_merge($this
      ->moduleHandler()
      ->invokeAll('entity_create_access', [
      $account,
      $context,
      $entity_bundle,
    ]), $this
      ->moduleHandler()
      ->invokeAll($this->entityTypeId . '_create_access', [
      $account,
      $context,
      $entity_bundle,
    ]));
    $return = $this
      ->processAccessHookResults($access);

    // Also execute the default access check except when the access result is
    // already forbidden, as in that case, it can not be anything else.
    if (!$return
      ->isForbidden()) {
      $return = $return
        ->orIf($this
        ->checkCreateAccess($account, $context, $entity_bundle));
    }
    $result = $this
      ->setCache($return, $cid, 'create', $context['langcode'], $account);
    return $return_as_object ? $result : $result
      ->isAllowed();
  }

  /**
   * Performs create access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * do their own custom access checking.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for which to check access.
   * @param array $context
   *   An array of key-value pairs to pass additional context when needed.
   * @param string|null $entity_bundle
   *   (optional) The bundle of the entity. Required if the entity supports
   *   bundles, defaults to NULL otherwise.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
    if ($admin_permission = $this->entityType
      ->getAdminPermission()) {
      return AccessResult::allowedIfHasPermission($account, $admin_permission);
    }
    else {

      // No opinion.
      return AccessResult::neutral();
    }
  }

  /**
   * Loads the current account object, if it does not exist yet.
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account interface instance.
   *
   * @return \Drupal\Core\Session\AccountInterface
   *   Returns the current account object.
   */
  protected function prepareUser(AccountInterface $account = NULL) {
    if (!$account) {
      $account = \Drupal::currentUser();
    }
    return $account;
  }

  /**
   * {@inheritdoc}
   */
  public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
    $account = $this
      ->prepareUser($account);

    // Get the default access restriction that lives within this field.
    $default = $items ? $items
      ->defaultAccess($operation, $account) : AccessResult::allowed();

    // Explicitly disallow changing the entity ID and entity UUID.
    $entity = $items ? $items
      ->getEntity() : NULL;
    if ($operation === 'edit' && $entity) {
      if ($field_definition
        ->getName() === $this->entityType
        ->getKey('id')) {

        // String IDs can be set when creating the entity.
        if (!($entity
          ->isNew() && $field_definition
          ->getType() === 'string')) {
          return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')
            ->addCacheableDependency($entity) : FALSE;
        }
      }
      elseif ($field_definition
        ->getName() === $this->entityType
        ->getKey('uuid')) {

        // UUIDs can be set when creating an entity.
        if (!$entity
          ->isNew()) {
          return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')
            ->addCacheableDependency($entity) : FALSE;
        }
      }
    }

    // Get the default access restriction as specified by the access control
    // handler.
    $entity_default = $this
      ->checkFieldAccess($operation, $field_definition, $account, $items);

    // Combine default access, denying access wins.
    $default = $default
      ->andIf($entity_default);

    // Invoke hook and collect grants/denies for field access from other
    // modules.
    $grants = [];
    $this
      ->moduleHandler()
      ->invokeAllWith('entity_field_access', function (callable $hook, string $module) use ($operation, $field_definition, $account, $items, &$grants) {
      $grants[] = [
        $module => $hook($operation, $field_definition, $account, $items),
      ];
    });

    // Our default access flag is masked under the ':default' key.
    $grants = array_merge([
      ':default' => $default,
    ], ...$grants);

    // Also allow modules to alter the returned grants/denies.
    $context = [
      'operation' => $operation,
      'field_definition' => $field_definition,
      'items' => $items,
      'account' => $account,
    ];
    $this
      ->moduleHandler()
      ->alter('entity_field_access', $grants, $context);
    $result = $this
      ->processAccessHookResults($grants);
    return $return_as_object ? $result : $result
      ->isAllowed();
  }

  /**
   * Default field access as determined by this access control handler.
   *
   * @param string $operation
   *   The operation access should be checked for.
   *   Usually one of "view" or "edit".
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   The field definition.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user session for which to check access.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   (optional) The field values for which to check access, or NULL if access
   *   is checked for the field definition, without any specific value
   *   available. Defaults to NULL.
   *
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
   */
  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
    if (!$items instanceof FieldItemListInterface || $operation !== 'view') {
      return AccessResult::allowed();
    }
    $entity = $items
      ->getEntity();
    $isRevisionLogField = $this->entityType instanceof ContentEntityTypeInterface && $field_definition
      ->getName() === $this->entityType
      ->getRevisionMetadataKey('revision_log_message');
    if ($entity && $isRevisionLogField) {

      // The revision log should only be visible to those who can view the
      // revisions OR edit the entity.
      return $entity
        ->access('view revision', $account, TRUE)
        ->orIf($entity
        ->access('update', $account, TRUE));
    }
    return AccessResult::allowed();
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
EntityAccessControlHandler::$accessCache protected property Stores calculated access check results.
EntityAccessControlHandler::$entityType protected property Information about the entity type.
EntityAccessControlHandler::$entityTypeId protected property The entity type ID of the access control handler instance.
EntityAccessControlHandler::$viewLabelOperation protected property Allows to grant access to just the labels. 6
EntityAccessControlHandler::access public function Checks access to an operation on a given entity or entity translation. Overrides EntityAccessControlHandlerInterface::access 1
EntityAccessControlHandler::checkAccess protected function Performs access checks. 33
EntityAccessControlHandler::checkCreateAccess protected function Performs create access checks. 12
EntityAccessControlHandler::checkFieldAccess protected function Default field access as determined by this access control handler. 4
EntityAccessControlHandler::createAccess public function Checks access to create an entity. Overrides EntityAccessControlHandlerInterface::createAccess 1
EntityAccessControlHandler::fieldAccess public function Checks access to an operation on a given entity field. Overrides EntityAccessControlHandlerInterface::fieldAccess
EntityAccessControlHandler::getCache protected function Tries to retrieve a previously cached access value from the static cache.
EntityAccessControlHandler::prepareUser protected function Loads the current account object, if it does not exist yet.
EntityAccessControlHandler::processAccessHookResults protected function Determines entity access.
EntityAccessControlHandler::resetCache public function Clears all cached access checks. Overrides EntityAccessControlHandlerInterface::resetCache
EntityAccessControlHandler::setCache protected function Statically caches whether the given user has access.
EntityAccessControlHandler::__construct public function Constructs an access control handler instance. 5
EntityHandlerBase::$moduleHandler protected property The module handler to invoke hooks on. 5
EntityHandlerBase::moduleHandler protected function Gets the module handler. 5
EntityHandlerBase::setModuleHandler public function Sets the module handler for this handler.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 1
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.