annotate core/lib/Drupal/Core/Entity/EntityAccessControlHandler.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
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 }