3b16d1cddbc30021f65a7e433f4132a13bfe899b
[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 (($return = $this->getCache($entity->uuid(), $operation, $langcode, $account)) !== NULL) {
70       // Cache hit, no work necessary.
71       return $return_as_object ? $return : $return->isAllowed();
72     }
73
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
78     // this method.
79
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])
86     );
87
88     $return = $this->processAccessHookResults($access);
89
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));
94     }
95     $result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
96     return $return_as_object ? $result : $result->isAllowed();
97   }
98
99   /**
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.
103    *
104    * @param \Drupal\Core\Access\AccessResultInterface[] $access
105    *   An array of access results of the fired access hook.
106    *
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.
110    *
111    * @see \Drupal\Core\Access\AccessResultInterface::orIf()
112    */
113   protected function processAccessHookResults(array $access) {
114     // No results means no opinion.
115     if (empty($access)) {
116       return AccessResult::neutral();
117     }
118
119     /** @var \Drupal\Core\Access\AccessResultInterface $result */
120     $result = array_shift($access);
121     foreach ($access as $other) {
122       $result = $result->orIf($other);
123     }
124     return $result;
125   }
126
127   /**
128    * Performs access checks.
129    *
130    * This method is supposed to be overwritten by extending classes that
131    * do their own custom access checking.
132    *
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
137    *   'delete'.
138    * @param \Drupal\Core\Session\AccountInterface $account
139    *   The user for which to check access.
140    *
141    * @return \Drupal\Core\Access\AccessResultInterface
142    *   The access result.
143    */
144   protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
145     if ($operation == 'delete' && $entity->isNew()) {
146       return AccessResult::forbidden()->addCacheableDependency($entity);
147     }
148     if ($admin_permission = $this->entityType->getAdminPermission()) {
149       return AccessResult::allowedIfHasPermission($account, $this->entityType->getAdminPermission());
150     }
151     else {
152       // No opinion.
153       return AccessResult::neutral();
154     }
155   }
156
157   /**
158    * Tries to retrieve a previously cached access value from the static cache.
159    *
160    * @param string $cid
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
165    *   'delete'.
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.
170    *
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.
174    */
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];
179     }
180   }
181
182   /**
183    * Statically caches whether the given user has access.
184    *
185    * @param \Drupal\Core\Access\AccessResultInterface $access
186    *   The access result.
187    * @param string $cid
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
192    *   'delete'.
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.
197    *
198    * @return \Drupal\Core\Access\AccessResultInterface
199    *   Whether the user has access, plus cacheability metadata.
200    */
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;
204   }
205
206   /**
207    * {@inheritdoc}
208    */
209   public function resetCache() {
210     $this->accessCache = [];
211   }
212
213   /**
214    * {@inheritdoc}
215    */
216   public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) {
217     $account = $this->prepareUser($account);
218     $context += [
219       'entity_type_id' => $this->entityTypeId,
220       'langcode' => LanguageInterface::LANGCODE_DEFAULT,
221     ];
222
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();
227     }
228
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.
234
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])
241     );
242
243     $return = $this->processAccessHookResults($access);
244
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));
249     }
250     $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account);
251     return $return_as_object ? $result : $result->isAllowed();
252   }
253
254   /**
255    * Performs create access checks.
256    *
257    * This method is supposed to be overwritten by extending classes that
258    * do their own custom access checking.
259    *
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.
267    *
268    * @return \Drupal\Core\Access\AccessResultInterface
269    *   The access result.
270    */
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);
274     }
275     else {
276       // No opinion.
277       return AccessResult::neutral();
278     }
279   }
280
281   /**
282    * Loads the current account object, if it does not exist yet.
283    *
284    * @param \Drupal\Core\Session\AccountInterface $account
285    *   The account interface instance.
286    *
287    * @return \Drupal\Core\Session\AccountInterface
288    *   Returns the current account object.
289    */
290   protected function prepareUser(AccountInterface $account = NULL) {
291     if (!$account) {
292       $account = \Drupal::currentUser();
293     }
294     return $account;
295   }
296
297   /**
298    * {@inheritdoc}
299    */
300   public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
301     $account = $this->prepareUser($account);
302
303     // Get the default access restriction that lives within this field.
304     $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
305
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;
310       }
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;
315         }
316       }
317     }
318
319     // Get the default access restriction as specified by the access control
320     // handler.
321     $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);
322
323     // Combine default access, denying access wins.
324     $default = $default->andIf($entity_default);
325
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])]);
332     }
333
334     // Also allow modules to alter the returned grants/denies.
335     $context = [
336       'operation' => $operation,
337       'field_definition' => $field_definition,
338       'items' => $items,
339       'account' => $account,
340     ];
341     $this->moduleHandler()->alter('entity_field_access', $grants, $context);
342
343     $result = $this->processAccessHookResults($grants);
344     return $return_as_object ? $result : $result->isAllowed();
345   }
346
347   /**
348    * Default field access as determined by this access control handler.
349    *
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.
361    *
362    * @return \Drupal\Core\Access\AccessResultInterface
363    *   The access result.
364    */
365   protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
366     return AccessResult::allowed();
367   }
368
369 }