Newer
Older
Angie Byron
committed
<?php
namespace Drupal\Core\Entity;
catch
committed
use Drupal\Core\Access\AccessResult;
Alex Pott
committed
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Session\AccountInterface;
Angie Byron
committed
/**
* Defines a default implementation for entity access control handler.
Angie Byron
committed
*/
Alex Pott
committed
class EntityAccessControlHandler extends EntityHandlerBase implements EntityAccessControlHandlerInterface {
Angie Byron
committed
Dries Buytaert
committed
/**
* Stores calculated access check results.
Dries Buytaert
committed
*
* @var array
*/
protected $accessCache = array();
/**
* The entity type ID of the access control handler instance.
*
* @var string
*/
protected $entityTypeId;
* Information about the entity type.
Alex Pott
committed
* @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.
Alex Pott
committed
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
*/
Alex Pott
committed
public function __construct(EntityTypeInterface $entity_type) {
$this->entityTypeId = $entity_type->id();
$this->entityType = $entity_type;
}
Angie Byron
committed
/**
Alex Pott
committed
* {@inheritdoc}
Angie Byron
committed
*/
Alex Bronstein
committed
public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
$account = $this->prepareUser($account);
Alex Bronstein
committed
$langcode = $entity->language()->getId();
Dries Buytaert
committed
if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
$operation = 'view';
}
catch
committed
if (($return = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
Alex Pott
committed
// Cache hit, no work necessary.
catch
committed
return $return_as_object ? $return : $return->isAllowed();
Dries Buytaert
committed
}
Alex Pott
committed
// Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
// take precedence over overridden implementations of
// EntityAccessControlHandler::checkAccess(). Entities that have checks that
Alex Pott
committed
// need to be done before the hook is invoked should do so by overriding
// this method.
Angie Byron
committed
Alex Pott
committed
// 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.
Alex Pott
committed
$access = array_merge(
Alex Bronstein
committed
$this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
$this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
Alex Pott
committed
);
Dries Buytaert
committed
catch
committed
$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()) {
Alex Bronstein
committed
$return = $return->orIf($this->checkAccess($entity, $operation, $account));
catch
committed
$result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
return $return_as_object ? $result : $result->isAllowed();
}
/**
* 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.
*
catch
committed
* @param \Drupal\Core\Access\AccessResultInterface[] $access
* An array of access results of the fired access hook.
*
catch
committed
* @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) {
catch
committed
// No results means no opinion.
if (empty($access)) {
Alex Pott
committed
return AccessResult::neutral();
Alex Pott
committed
}
catch
committed
/** @var \Drupal\Core\Access\AccessResultInterface $result */
$result = array_shift($access);
foreach ($access as $other) {
$result = $result->orIf($other);
Alex Pott
committed
}
catch
committed
return $result;
Dries Buytaert
committed
}
/**
Alex Pott
committed
* Performs access checks.
*
* This method is supposed to be overwritten by extending classes that
* do their own custom access checking.
Dries Buytaert
committed
*
* @param \Drupal\Core\Entity\EntityInterface $entity
Alex Pott
committed
* The entity for which to check access.
Dries Buytaert
committed
* @param string $operation
* The entity operation. Usually one of 'view', 'view label', 'update' or
* 'delete'.
* @param \Drupal\Core\Session\AccountInterface $account
Alex Pott
committed
* The user for which to check access.
Dries Buytaert
committed
*
catch
committed
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
Dries Buytaert
committed
*/
Alex Bronstein
committed
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation == 'delete' && $entity->isNew()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
if ($admin_permission = $this->entityType->getAdminPermission()) {
catch
committed
return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
}
else {
catch
committed
// No opinion.
Alex Pott
committed
return AccessResult::neutral();
Dries Buytaert
committed
}
/**
* 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.
Dries Buytaert
committed
* @param string $operation
* The entity operation. Usually one of 'view', 'update', 'create' or
Dries Buytaert
committed
* 'delete'.
* @param string $langcode
Alex Pott
committed
* The language code for which to check access.
* @param \Drupal\Core\Session\AccountInterface $account
Alex Pott
committed
* The user for which to check access.
Dries Buytaert
committed
*
catch
committed
* @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.
Dries Buytaert
committed
*/
protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
Dries Buytaert
committed
// 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];
Dries Buytaert
committed
}
}
/**
* Statically caches whether the given user has access.
*
catch
committed
* @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.
Dries Buytaert
committed
* @param string $operation
* The entity operation. Usually one of 'view', 'update', 'create' or
Dries Buytaert
committed
* 'delete'.
* @param string $langcode
Alex Pott
committed
* The language code for which to check access.
* @param \Drupal\Core\Session\AccountInterface $account
Alex Pott
committed
* The user for which to check access.
Dries Buytaert
committed
*
catch
committed
* @return \Drupal\Core\Access\AccessResultInterface
* Whether the user has access, plus cacheability metadata.
Dries Buytaert
committed
*/
protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {
Dries Buytaert
committed
// Save the given value in the static cache and directly return it.
catch
committed
return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access;
Dries Buytaert
committed
}
/**
Alex Pott
committed
* {@inheritdoc}
Dries Buytaert
committed
*/
public function resetCache() {
$this->accessCache = array();
Angie Byron
committed
}
/**
* {@inheritdoc}
*/
catch
committed
public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = array(), $return_as_object = FALSE) {
$account = $this->prepareUser($account);
$context += array(
'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.
catch
committed
return $return_as_object ? $access : $access->isAllowed();
}
Alex Pott
committed
// 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.
Alex Pott
committed
$access = array_merge(
Alex Pott
committed
$this->moduleHandler()->invokeAll('entity_create_access', array($account, $context, $entity_bundle)),
$this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', array($account, $context, $entity_bundle))
Alex Pott
committed
);
catch
committed
$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()) {
Alex Pott
committed
$return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle));
}
catch
committed
$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.
*
catch
committed
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
if ($admin_permission = $this->entityType->getAdminPermission()) {
catch
committed
return AccessResult::allowedIfHasPermission($account, $admin_permission);
}
else {
catch
committed
// No opinion.
Alex Pott
committed
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;
}
Alex Pott
committed
/**
* {@inheritdoc}
*/
catch
committed
public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
Alex Pott
committed
$account = $this->prepareUser($account);
// Get the default access restriction that lives within this field.
catch
committed
$default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
Alex Pott
committed
Alex Pott
committed
// Get the default access restriction as specified by the access control
// handler.
Alex Pott
committed
$entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);
// Combine default access, denying access wins.
catch
committed
$default = $default->andIf($entity_default);
Alex Pott
committed
Alex Pott
committed
// Invoke hook and collect grants/denies for field access from other
// modules. Our default access flag is masked under the ':default' key.
$grants = array(':default' => $default);
$hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
Alex Pott
committed
foreach ($hook_implementations as $module) {
$grants = array_merge($grants, array($module => $this->moduleHandler()->invoke($module, 'entity_field_access', array($operation, $field_definition, $account, $items))));
Alex Pott
committed
}
// Also allow modules to alter the returned grants/denies.
$context = array(
'operation' => $operation,
'field_definition' => $field_definition,
'items' => $items,
'account' => $account,
);
$this->moduleHandler()->alter('entity_field_access', $grants, $context);
Alex Pott
committed
catch
committed
$result = $this->processAccessHookResults($grants);
return $return_as_object ? $result : $result->isAllowed();
Alex Pott
committed
}
Alex Pott
committed
/**
Alex Pott
committed
* Default field access as determined by this access control handler.
Alex Pott
committed
*
* @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.
Alex Pott
committed
*/
protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
catch
committed
return AccessResult::allowed();
Alex Pott
committed
}