EntityAccessControlHandler.php 13.6 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Entity;

5
use Drupal\Core\Access\AccessResult;
6 7
use Drupal\Core\Field\FieldItemListInterface;
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 20 21 22
   *
   * @var array
   */
  protected $accessCache = array();

23
  /**
24
   * The entity type ID of the access control handler instance.
25 26 27
   *
   * @var string
   */
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 39 40 41 42 43 44 45 46
  /**
   * 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;

47
  /**
48
   * Constructs an access control handler instance.
49
   *
50 51
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
52
   */
53 54 55
  public function __construct(EntityTypeInterface $entity_type) {
    $this->entityTypeId = $entity_type->id();
    $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 66 67 68
    if ($operation === 'view label' && $this->viewLabelOperation == FALSE) {
      $operation = 'view';
    }

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 75
    // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results
    // take precedence over overridden implementations of
76
    // EntityAccessControlHandler::checkAccess(). Entities that have checks that
77 78
    // need to be done before the hook is invoked should do so by overriding
    // this method.
79

80 81 82
    // 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.
83
    $access = array_merge(
84 85
      $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]),
      $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account])
86
    );
87

88
    $return = $this->processAccessHookResults($access);
89 90 91 92

    // 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()) {
93
      $return = $return->orIf($this->checkAccess($entity, $operation, $account));
94
    }
95 96
    $result = $this->setCache($return, $entity->uuid(), $operation, $langcode, $account);
    return $return_as_object ? $result : $result->isAllowed();
97 98 99 100 101 102 103
  }

  /**
   * 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.
   *
104
   * @param \Drupal\Core\Access\AccessResultInterface[] $access
105 106
   *   An array of access results of the fired access hook.
   *
107 108 109 110 111
   * @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()
112 113
   */
  protected function processAccessHookResults(array $access) {
114 115
    // No results means no opinion.
    if (empty($access)) {
116
      return AccessResult::neutral();
117
    }
118 119 120 121 122

    /** @var \Drupal\Core\Access\AccessResultInterface $result */
    $result = array_shift($access);
    foreach ($access as $other) {
      $result = $result->orIf($other);
123
    }
124
    return $result;
125 126 127
  }

  /**
128 129 130 131
   * Performs access checks.
   *
   * This method is supposed to be overwritten by extending classes that
   * 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 137
   *   The entity operation. Usually one of 'view', 'view label', 'update' or
   *   'delete'.
138
   * @param \Drupal\Core\Session\AccountInterface $account
139
   *   The user for which to check access.
140
   *
141 142
   * @return \Drupal\Core\Access\AccessResultInterface
   *   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 159
  }

  /**
   * Tries to retrieve a previously cached access value from the static cache.
   *
160 161 162
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
163
   * @param string $operation
164
   *   The entity operation. Usually one of 'view', 'update', 'create' or
165 166
   *   'delete'.
   * @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 172 173
   * @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.
174
   */
175
  protected function getCache($cid, $operation, $langcode, AccountInterface $account) {
176
    // Return from cache if a value has been set for it previously.
177 178
    if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) {
      return $this->accessCache[$account->id()][$cid][$langcode][$operation];
179 180 181 182 183 184
    }
  }

  /**
   * Statically caches whether the given user has access.
   *
185 186
   * @param \Drupal\Core\Access\AccessResultInterface $access
   *   The access result.
187 188 189
   * @param string $cid
   *   Unique string identifier for the entity/operation, for example the
   *   entity UUID or a custom string.
190
   * @param string $operation
191
   *   The entity operation. Usually one of 'view', 'update', 'create' or
192 193
   *   'delete'.
   * @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 199
   * @return \Drupal\Core\Access\AccessResultInterface
   *   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 210
   */
  public function resetCache() {
    $this->accessCache = array();
211 212
  }

213 214 215
  /**
   * {@inheritdoc}
   */
216
  public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = array(), $return_as_object = FALSE) {
217 218
    $account = $this->prepareUser($account);
    $context += array(
219
      'entity_type_id' => $this->entityTypeId,
220
      'langcode' => LanguageInterface::LANGCODE_DEFAULT,
221 222 223 224 225
    );

    $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create';
    if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) {
      // Cache hit, no work necessary.
226
      return $return_as_object ? $access : $access->isAllowed();
227 228
    }

229 230
    // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access().
    // Hook results take precedence over overridden implementations of
231 232 233
    // EntityAccessControlHandler::checkCreateAccess(). Entities that have
    // checks that need to be done before the hook is invoked should do so by
    // overriding this method.
234 235 236 237

    // 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.
238
    $access = array_merge(
239 240
      $this->moduleHandler()->invokeAll('entity_create_access', array($account, $context, $entity_bundle)),
      $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', array($account, $context, $entity_bundle))
241
    );
242

243
    $return = $this->processAccessHookResults($access);
244 245 246 247

    // 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()) {
248
      $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle));
249
    }
250 251
    $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account);
    return $return_as_object ? $result : $result->isAllowed();
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  }

  /**
   * 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.
   *
268 269
   * @return \Drupal\Core\Access\AccessResultInterface
   *   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 283 284 285 286 287 288 289 290 291
  }

  /**
   * 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) {
292
      $account = \Drupal::currentUser();
293 294 295 296
    }
    return $account;
  }

297 298 299
  /**
   * {@inheritdoc}
   */
300
  public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) {
301 302 303
    $account = $this->prepareUser($account);

    // Get the default access restriction that lives within this field.
304
    $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed();
305

306 307 308 309 310 311 312 313 314 315 316 317 318
    // Explicitly disallow changing the entity ID and entity UUID.
    if ($operation === 'edit') {
      if ($field_definition->getName() === $this->entityType->getKey('id')) {
        return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed') : FALSE;
      }
      elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) {
        // UUIDs can be set when creating an entity.
        if ($items && ($entity = $items->getEntity()) && !$entity->isNew()) {
          return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed')->addCacheableDependency($entity) : FALSE;
        }
      }
    }

319 320
    // Get the default access restriction as specified by the access control
    // handler.
321 322 323
    $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items);

    // Combine default access, denying access wins.
324
    $default = $default->andIf($entity_default);
325

326 327 328
    // 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);
329
    $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access');
330
    foreach ($hook_implementations as $module) {
331
      $grants = array_merge($grants, array($module => $this->moduleHandler()->invoke($module, 'entity_field_access', array($operation, $field_definition, $account, $items))));
332 333 334 335 336 337 338 339 340
    }

    // Also allow modules to alter the returned grants/denies.
    $context = array(
      'operation' => $operation,
      'field_definition' => $field_definition,
      'items' => $items,
      'account' => $account,
    );
341
    $this->moduleHandler()->alter('entity_field_access', $grants, $context);
342

343 344
    $result = $this->processAccessHookResults($grants);
    return $return_as_object ? $result : $result->isAllowed();
345 346
  }

347
  /**
348
   * Default field access as determined by this access control handler.
349 350 351 352 353 354 355 356 357 358 359 360 361
   *
   * @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.
   *
362 363
   * @return \Drupal\Core\Access\AccessResultInterface
   *   The access result.
364 365
   */
  protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
366
    return AccessResult::allowed();
367 368
  }

369
}