Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / lib / Drupal / Core / Entity / EntityAccessControlHandler.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
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;
10
11 /**
12  * Defines a default implementation for entity access control handler.
13  */
14 class EntityAccessControlHandler extends EntityHandlerBase implements EntityAccessControlHandlerInterface {
15
16   /**
17    * Stores calculated access check results.
18    *
19    * @var array
20    */
21   protected $accessCache = [];
22
23   /**
24    * The entity type ID of the access control handler instance.
25    *
26    * @var string
27    */
28   protected $entityTypeId;
29
30   /**
31    * Information about the entity type.
32    *
33    * @var \Drupal\Core\Entity\EntityTypeInterface
34    */
35   protected $entityType;
36
37   /**
38    * Allows to grant access to just the labels.
39    *
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.
42    *
43    * @var bool
44    */
45   protected $viewLabelOperation = FALSE;
46
47   /**
48    * Constructs an access control handler instance.
49    *
50    * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
51    *   The entity type definition.
52    */
53   public function __construct(EntityTypeInterface $entity_type) {
54     $this->entityTypeId = $entity_type->id();
55     $this->entityType = $entity_type;
56   }
57
58   /**
59    * {@inheritdoc}
60    */
61   public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
62     $account = $this->prepareUser($account);
63     $langcode = $entity->language()->getId();
64
65     if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
66       $operation = 'view';
67     }
68
69     // If an entity does not have a UUID, either from not being set or from not
70     // having them, use the 'entity type:ID' pattern as the cache $cid.
71     $cid = $entity->uuid() ?: $entity->getEntityTypeId() . ':' . $entity->id();
72
73     // If the entity is revisionable, then append the revision ID to allow
74     // individual revisions to have specific access control and be cached
75     // separately.
76     if ($entity instanceof RevisionableInterface) {
77       /** @var $entity \Drupal\Core\Entity\RevisionableInterface */
78       $cid .= ':' . $entity->getRevisionId();
79     }
80
81     if (($return = $this->getCache($cid, $operation, $langcode, $account)) !== NULL) {
82       // Cache hit, no work necessary.
83       return $return_as_object ? $return : $return->isAllowed();
84     }
85
86     // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
87     // take precedence over overridden implementations of
88     // EntityAccessControlHandler::checkAccess(). Entities that have checks that
89     // need to be done before the hook is invoked should do so by overriding
90     // this method.
91
92     // We grant access to the entity if both of these conditions are met:
93     // - No modules say to deny access.
94     // - At least one module says to grant access.
95     $access = array_merge(
96       $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
97       $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
98     );
99
100     $return = $this->processAccessHookResults($access);
101
102     // Also execute the default access check except when the access result is
103     // already forbidden, as in that case, it can not be anything else.
104     if (!$return->isForbidden()) {
105       $return = $return->orIf($this->checkAccess($entity, $operation, $account));
106     }
107     $result = $this->setCache($return, $cid, $operation, $langcode, $account);
108     return $return_as_object ? $result : $result->isAllowed();
109   }
110
111   /**
112    * We grant access to the entity if both of these conditions are met:
113    * - No modules say to deny access.
114    * - At least one module says to grant access.
115    *
116    * @param \Drupal\Core\Access\AccessResultInterface[] $access
117    *   An array of access results of the fired access hook.
118    *
119    * @return \Drupal\Core\Access\AccessResultInterface
120    *   The combined result of the various access checks' results. All their
121    *   cacheability metadata is merged as well.
122    *
123    * @see \Drupal\Core\Access\AccessResultInterface::orIf()
124    */
125   protected function processAccessHookResults(array $access) {
126     // No results means no opinion.
127     if (empty($access)) {
128       return AccessResult::neutral();
129     }
130
131     /** @var \Drupal\Core\Access\AccessResultInterface $result */
132     $result = array_shift($access);
133     foreach ($access as $other) {
134       $result = $result->orIf($other);
135     }
136     return $result;
137   }
138
139   /**
140    * Performs access checks.
141    *
142    * This method is supposed to be overwritten by extending classes that
143    * do their own custom access checking.
144    *
145    * @param \Drupal\Core\Entity\EntityInterface $entity
146    *   The entity for which to check access.
147    * @param string $operation
148    *   The entity operation. Usually one of 'view', 'view label', 'update' or
149    *   'delete'.
150    * @param \Drupal\Core\Session\AccountInterface $account
151    *   The user for which to check access.
152    *
153    * @return \Drupal\Core\Access\AccessResultInterface
154    *   The access result.
155    */
156   protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
157     if ($operation == 'delete' && $entity->isNew()) {
158       return AccessResult::forbidden()->addCacheableDependency($entity);
159     }
160     if ($admin_permission = $this->entityType->getAdminPermission()) {
161       return AccessResult::allowedIfHasPermission($account, $admin_permission);
162     }
163     else {
164       // No opinion.
165       return AccessResult::neutral();
166     }
167   }
168
169   /**
170    * Tries to retrieve a previously cached access value from the static cache.
171    *
172    * @param string $cid
173    *   Unique string identifier for the entity/operation, for example the
174    *   entity UUID or a custom string.
175    * @param string $operation
176    *   The entity operation. Usually one of 'view', 'update', 'create' or
177    *   'delete'.
178    * @param string $langcode
179    *   The language code for which to check access.
180    * @param \Drupal\Core\Session\AccountInterface $account
181    *   The user for which to check access.
182    *
183    * @return \Drupal\Core\Access\AccessResultInterface|null
184    *   The cached AccessResult, or NULL if there is no record for the given
185    *   user, operation, langcode and entity in the cache.
186    */
187   protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
188     // Return from cache if a value has been set for it previously.
189     if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
190       return $this->accessCache[$account->id()][$cid][$langcode][$operation];
191     }
192   }
193
194   /**
195    * Statically caches whether the given user has access.
196    *
197    * @param \Drupal\Core\Access\AccessResultInterface $access
198    *   The access result.
199    * @param string $cid
200    *   Unique string identifier for the entity/operation, for example the
201    *   entity UUID or a custom string.
202    * @param string $operation
203    *   The entity operation. Usually one of 'view', 'update', 'create' or
204    *   'delete'.
205    * @param string $langcode
206    *   The language code for which to check access.
207    * @param \Drupal\Core\Session\AccountInterface $account
208    *   The user for which to check access.
209    *
210    * @return \Drupal\Core\Access\AccessResultInterface
211    *   Whether the user has access, plus cacheability metadata.
212    */
213   protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) {
214     // Save the given value in the static cache and directly return it.
215     return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access;
216   }
217
218   /**
219    * {@inheritdoc}
220    */
221   public function resetCache() {
222     $this->accessCache = [];
223   }
224
225   /**
226    * {@inheritdoc}
227    */
228   public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
229     $account = $this->prepareUser($account);
230     $context += [
231       'entity_type_id' => $this->entityTypeId,
232       'langcode' => LanguageInterface::LANGCODE_DEFAULT,
233     ];
234
235     $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
236     if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {
237       // Cache hit, no work necessary.
238       return $return_as_object ? $access : $access->isAllowed();
239     }
240
241     // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
242     // Hook results take precedence over overridden implementations of
243     // EntityAccessControlHandler::checkCreateAccess(). Entities that have
244     // checks that need to be done before the hook is invoked should do so by
245     // overriding this method.
246
247     // We grant access to the entity if both of these conditions are met:
248     // - No modules say to deny access.
249     // - At least one module says to grant access.
250     $access = array_merge(
251       $this->moduleHandler()->invokeAll('entity_create_access', [$account, $context, $entity_bundle]),
252       $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', [$account, $context, $entity_bundle])
253     );
254
255     $return = $this->processAccessHookResults($access);
256
257     // Also execute the default access check except when the access result is
258     // already forbidden, as in that case, it can not be anything else.
259     if (!$return->isForbidden()) {
260       $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle));
261     }
262     $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account);
263     return $return_as_object ? $result : $result->isAllowed();
264   }
265
266   /**
267    * Performs create access checks.
268    *
269    * This method is supposed to be overwritten by extending classes that
270    * do their own custom access checking.
271    *
272    * @param \Drupal\Core\Session\AccountInterface $account
273    *   The user for which to check access.
274    * @param array $context
275    *   An array of key-value pairs to pass additional context when needed.
276    * @param string|null $entity_bundle
277    *   (optional) The bundle of the entity. Required if the entity supports
278    *   bundles, defaults to NULL otherwise.
279    *
280    * @return \Drupal\Core\Access\AccessResultInterface
281    *   The access result.
282    */
283   protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
284     if ($admin_permission = $this->entityType->getAdminPermission()) {
285       return AccessResult::allowedIfHasPermission($account, $admin_permission);
286     }
287     else {
288       // No opinion.
289       return AccessResult::neutral();
290     }
291   }
292
293   /**
294    * Loads the current account object, if it does not exist yet.
295    *
296    * @param \Drupal\Core\Session\AccountInterface $account
297    *   The account interface instance.
298    *
299    * @return \Drupal\Core\Session\AccountInterface
300    *   Returns the current account object.
301    */
302   protected function prepareUser(AccountInterface $account = NULL) {
303     if (!$account) {
304       $account = \Drupal::currentUser();
305     }
306     return $account;
307   }
308
309   /**
310    * {@inheritdoc}
311    */
312   public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
313     $account = $this->prepareUser($account);
314
315     // Get the default access restriction that lives within this field.
316     $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
317
318     // Explicitly disallow changing the entity ID and entity UUID.
319     $entity = $items ? $items->getEntity() : NULL;
320     if ($operation === 'edit' && $entity) {
321       if ($field_definition->getName() === $this->entityType->getKey('id')) {
322         // String IDs can be set when creating the entity.
323         if (!($entity->isNew() && $field_definition->getType() === 'string')) {
324           return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE;
325         }
326       }
327       elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) {
328         // UUIDs can be set when creating an entity.
329         if (!$entity->isNew()) {
330           return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE;
331         }
332       }
333     }
334
335     // Get the default access restriction as specified by the access control
336     // handler.
337     $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);
338
339     // Combine default access, denying access wins.
340     $default = $default->andIf($entity_default);
341
342     // Invoke hook and collect grants/denies for field access from other
343     // modules. Our default access flag is masked under the ':default' key.
344     $grants = [':default' => $default];
345     $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
346     foreach ($hook_implementations as $module) {
347       $grants = array_merge($grants, [$module => $this->moduleHandler()->invoke($module, 'entity_field_access', [$operation, $field_definition, $account, $items])]);
348     }
349
350     // Also allow modules to alter the returned grants/denies.
351     $context = [
352       'operation' => $operation,
353       'field_definition' => $field_definition,
354       'items' => $items,
355       'account' => $account,
356     ];
357     $this->moduleHandler()->alter('entity_field_access', $grants, $context);
358
359     $result = $this->processAccessHookResults($grants);
360     return $return_as_object ? $result : $result->isAllowed();
361   }
362
363   /**
364    * Default field access as determined by this access control handler.
365    *
366    * @param string $operation
367    *   The operation access should be checked for.
368    *   Usually one of "view" or "edit".
369    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
370    *   The field definition.
371    * @param \Drupal\Core\Session\AccountInterface $account
372    *   The user session for which to check access.
373    * @param \Drupal\Core\Field\FieldItemListInterface $items
374    *   (optional) The field values for which to check access, or NULL if access
375    *   is checked for the field definition, without any specific value
376    *   available. Defaults to NULL.
377    *
378    * @return \Drupal\Core\Access\AccessResultInterface
379    *   The access result.
380    */
381   protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
382     return AccessResult::allowed();
383   }
384
385 }