3 namespace Drupal\Core\Entity;
5 use Drupal\Core\Access\AccessResult;
6 use Drupal\Core\Field\FieldItemListInterface;
7 use Drupal\Core\Field\FieldDefinitionInterface;
8 use Drupal\Core\Language\LanguageInterface;
9 use Drupal\Core\Session\AccountInterface;
12 * Defines a default implementation for entity access control handler.
14 class EntityAccessControlHandler extends EntityHandlerBase implements EntityAccessControlHandlerInterface {
17 * Stores calculated access check results.
21 protected $accessCache = [];
24 * The entity type ID of the access control handler instance.
28 protected $entityTypeId;
31 * Information about the entity type.
33 * @var \Drupal\Core\Entity\EntityTypeInterface
35 protected $entityType;
38 * Allows to grant access to just the labels.
40 * By default, the "view label" operation falls back to "view". Set this to
41 * TRUE to allow returning different access when just listing entity labels.
45 protected $viewLabelOperation = FALSE;
48 * Constructs an access control handler instance.
50 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
51 * The entity type definition.
53 public function __construct(EntityTypeInterface $entity_type) {
54 $this->entityTypeId = $entity_type->id();
55 $this->entityType = $entity_type;
61 public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
62 $account = $this->prepareUser($account);
63 $langcode = $entity->language()->getId();
65 if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
69 if (($return = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
70 // Cache hit, no work necessary.
71 return $return_as_object ? $return : $return->isAllowed();
74 // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
75 // take precedence over overridden implementations of
76 // EntityAccessControlHandler::checkAccess(). Entities that have checks that
77 // need to be done before the hook is invoked should do so by overriding
80 // We grant access to the entity if both of these conditions are met:
81 // - No modules say to deny access.
82 // - At least one module says to grant access.
83 $access = array_merge(
84 $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
85 $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
88 $return = $this->processAccessHookResults($access);
90 // Also execute the default access check except when the access result is
91 // already forbidden, as in that case, it can not be anything else.
92 if (!$return->isForbidden()) {
93 $return = $return->orIf($this->checkAccess($entity, $operation, $account));
95 $result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
96 return $return_as_object ? $result : $result->isAllowed();
100 * We grant access to the entity if both of these conditions are met:
101 * - No modules say to deny access.
102 * - At least one module says to grant access.
104 * @param \Drupal\Core\Access\AccessResultInterface[] $access
105 * An array of access results of the fired access hook.
107 * @return \Drupal\Core\Access\AccessResultInterface
108 * The combined result of the various access checks' results. All their
109 * cacheability metadata is merged as well.
111 * @see \Drupal\Core\Access\AccessResultInterface::orIf()
113 protected function processAccessHookResults(array $access) {
114 // No results means no opinion.
115 if (empty($access)) {
116 return AccessResult::neutral();
119 /** @var \Drupal\Core\Access\AccessResultInterface $result */
120 $result = array_shift($access);
121 foreach ($access as $other) {
122 $result = $result->orIf($other);
128 * Performs access checks.
130 * This method is supposed to be overwritten by extending classes that
131 * do their own custom access checking.
133 * @param \Drupal\Core\Entity\EntityInterface $entity
134 * The entity for which to check access.
135 * @param string $operation
136 * The entity operation. Usually one of 'view', 'view label', 'update' or
138 * @param \Drupal\Core\Session\AccountInterface $account
139 * The user for which to check access.
141 * @return \Drupal\Core\Access\AccessResultInterface
144 protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
145 if ($operation == 'delete' && $entity->isNew()) {
146 return AccessResult::forbidden()->addCacheableDependency($entity);
148 if ($admin_permission = $this->entityType->getAdminPermission()) {
149 return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
153 return AccessResult::neutral();
158 * Tries to retrieve a previously cached access value from the static cache.
161 * Unique string identifier for the entity/operation, for example the
162 * entity UUID or a custom string.
163 * @param string $operation
164 * The entity operation. Usually one of 'view', 'update', 'create' or
166 * @param string $langcode
167 * The language code for which to check access.
168 * @param \Drupal\Core\Session\AccountInterface $account
169 * The user for which to check access.
171 * @return \Drupal\Core\Access\AccessResultInterface|null
172 * The cached AccessResult, or NULL if there is no record for the given
173 * user, operation, langcode and entity in the cache.
175 protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
176 // Return from cache if a value has been set for it previously.
177 if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
178 return $this->accessCache[$account->id()][$cid][$langcode][$operation];
183 * Statically caches whether the given user has access.
185 * @param \Drupal\Core\Access\AccessResultInterface $access
188 * Unique string identifier for the entity/operation, for example the
189 * entity UUID or a custom string.
190 * @param string $operation
191 * The entity operation. Usually one of 'view', 'update', 'create' or
193 * @param string $langcode
194 * The language code for which to check access.
195 * @param \Drupal\Core\Session\AccountInterface $account
196 * The user for which to check access.
198 * @return \Drupal\Core\Access\AccessResultInterface
199 * Whether the user has access, plus cacheability metadata.
201 protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {
202 // Save the given value in the static cache and directly return it.
203 return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access;
209 public function resetCache() {
210 $this->accessCache = [];
216 public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
217 $account = $this->prepareUser($account);
219 'entity_type_id' => $this->entityTypeId,
220 'langcode' => LanguageInterface::LANGCODE_DEFAULT,
223 $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
224 if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {
225 // Cache hit, no work necessary.
226 return $return_as_object ? $access : $access->isAllowed();
229 // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
230 // Hook results take precedence over overridden implementations of
231 // EntityAccessControlHandler::checkCreateAccess(). Entities that have
232 // checks that need to be done before the hook is invoked should do so by
233 // overriding this method.
235 // We grant access to the entity if both of these conditions are met:
236 // - No modules say to deny access.
237 // - At least one module says to grant access.
238 $access = array_merge(
239 $this->moduleHandler()->invokeAll('entity_create_access', [$account, $context, $entity_bundle]),
240 $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', [$account, $context, $entity_bundle])
243 $return = $this->processAccessHookResults($access);
245 // Also execute the default access check except when the access result is
246 // already forbidden, as in that case, it can not be anything else.
247 if (!$return->isForbidden()) {
248 $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle));
250 $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account);
251 return $return_as_object ? $result : $result->isAllowed();
255 * Performs create access checks.
257 * This method is supposed to be overwritten by extending classes that
258 * do their own custom access checking.
260 * @param \Drupal\Core\Session\AccountInterface $account
261 * The user for which to check access.
262 * @param array $context
263 * An array of key-value pairs to pass additional context when needed.
264 * @param string|null $entity_bundle
265 * (optional) The bundle of the entity. Required if the entity supports
266 * bundles, defaults to NULL otherwise.
268 * @return \Drupal\Core\Access\AccessResultInterface
271 protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
272 if ($admin_permission = $this->entityType->getAdminPermission()) {
273 return AccessResult::allowedIfHasPermission($account, $admin_permission);
277 return AccessResult::neutral();
282 * Loads the current account object, if it does not exist yet.
284 * @param \Drupal\Core\Session\AccountInterface $account
285 * The account interface instance.
287 * @return \Drupal\Core\Session\AccountInterface
288 * Returns the current account object.
290 protected function prepareUser(AccountInterface $account = NULL) {
292 $account = \Drupal::currentUser();
300 public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
301 $account = $this->prepareUser($account);
303 // Get the default access restriction that lives within this field.
304 $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
306 // Explicitly disallow changing the entity ID and entity UUID.
307 if ($operation === 'edit') {
308 if ($field_definition->getName() === $this->entityType->getKey('id')) {
309 return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed') : FALSE;
311 elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) {
312 // UUIDs can be set when creating an entity.
313 if ($items && ($entity = $items->getEntity()) && !$entity->isNew()) {
314 return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed')->addCacheableDependency($entity) : FALSE;
319 // Get the default access restriction as specified by the access control
321 $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);
323 // Combine default access, denying access wins.
324 $default = $default->andIf($entity_default);
326 // Invoke hook and collect grants/denies for field access from other
327 // modules. Our default access flag is masked under the ':default' key.
328 $grants = [':default' => $default];
329 $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
330 foreach ($hook_implementations as $module) {
331 $grants = array_merge($grants, [$module => $this->moduleHandler()->invoke($module, 'entity_field_access', [$operation, $field_definition, $account, $items])]);
334 // Also allow modules to alter the returned grants/denies.
336 'operation' => $operation,
337 'field_definition' => $field_definition,
339 'account' => $account,
341 $this->moduleHandler()->alter('entity_field_access', $grants, $context);
343 $result = $this->processAccessHookResults($grants);
344 return $return_as_object ? $result : $result->isAllowed();
348 * Default field access as determined by this access control handler.
350 * @param string $operation
351 * The operation access should be checked for.
352 * Usually one of "view" or "edit".
353 * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
354 * The field definition.
355 * @param \Drupal\Core\Session\AccountInterface $account
356 * The user session for which to check access.
357 * @param \Drupal\Core\Field\FieldItemListInterface $items
358 * (optional) The field values for which to check access, or NULL if access
359 * is checked for the field definition, without any specific value
360 * available. Defaults to NULL.
362 * @return \Drupal\Core\Access\AccessResultInterface
365 protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
366 return AccessResult::allowed();