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