entityTypeId = $entity_type->id(); $this->entityType = $entity_type; } /** * {@inheritdoc} */ public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) { $account = $this->prepareUser($account); $langcode = $entity->language()->getId(); if ($operation === 'view label' && $this->viewLabelOperation == FALSE) { $operation = 'view'; } // If an entity does not have a UUID, either from not being set or from not // having them, use the 'entity type:ID' pattern as the cache $cid. $cid = $entity->uuid() ?: $entity->getEntityTypeId() . ':' . $entity->id(); // If the entity is revisionable, then append the revision ID to allow // individual revisions to have specific access control and be cached // separately. if ($entity instanceof RevisionableInterface) { /** @var \Drupal\Core\Entity\RevisionableInterface $entity */ $cid .= ':' . $entity->getRevisionId(); // It is not possible to delete or revert the default revision. if ($entity->isDefaultRevision() && ($operation === 'revert' || $operation === 'delete revision')) { return $return_as_object ? AccessResult::forbidden() : FALSE; } } if (($return = $this->getCache($cid, $operation, $langcode, $account)) !== NULL) { // Cache hit, no work necessary. return $return_as_object ? $return : $return->isAllowed(); } // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results // take precedence over overridden implementations of // EntityAccessControlHandler::checkAccess(). Entities that have checks that // need to be done before the hook is invoked should do so by overriding // this method. // 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. $access = array_merge( $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]), $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account]) ); $return = $this->processAccessHookResults($access); // 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()) { $return = $return->orIf($this->checkAccess($entity, $operation, $account)); } $result = $this->setCache($return, $cid, $operation, $langcode, $account); return $return_as_object ? $result : $result->isAllowed(); } /** * Determines entity access. * * 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. * * @param \Drupal\Core\Access\AccessResultInterface[] $access * An array of access results of the fired access hook. * * @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() */ protected function processAccessHookResults(array $access) { // No results means no opinion. if (empty($access)) { return AccessResult::neutral(); } /** @var \Drupal\Core\Access\AccessResultInterface $result */ $result = array_shift($access); foreach ($access as $other) { $result = $result->orIf($other); } return $result; } /** * Performs access checks. * * This method is supposed to be overwritten by extending classes that * do their own custom access checking. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity for which to check access. * @param string $operation * The entity operation. Usually one of 'view', 'view label', 'update' or * 'delete'. * @param \Drupal\Core\Session\AccountInterface $account * The user for which to check access. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { if ($operation == 'delete' && $entity->isNew()) { return AccessResult::forbidden()->addCacheableDependency($entity); } if ($admin_permission = $this->entityType->getAdminPermission()) { return AccessResult::allowedIfHasPermission($account, $admin_permission); } else { // No opinion. return AccessResult::neutral(); } } /** * Tries to retrieve a previously cached access value from the static cache. * * @param string $cid * Unique string identifier for the entity/operation, for example the * entity UUID or a custom string. * @param string $operation * The entity operation. Usually one of 'view', 'update', 'create' or * 'delete'. * @param string $langcode * The language code for which to check access. * @param \Drupal\Core\Session\AccountInterface $account * The user for which to check access. * * @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. */ protected function getCache($cid, $operation, $langcode, AccountInterface $account) { // Return from cache if a value has been set for it previously. if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) { return $this->accessCache[$account->id()][$cid][$langcode][$operation]; } } /** * Statically caches whether the given user has access. * * @param \Drupal\Core\Access\AccessResultInterface $access * The access result. * @param string $cid * Unique string identifier for the entity/operation, for example the * entity UUID or a custom string. * @param string $operation * The entity operation. Usually one of 'view', 'update', 'create' or * 'delete'. * @param string $langcode * The language code for which to check access. * @param \Drupal\Core\Session\AccountInterface $account * The user for which to check access. * * @return \Drupal\Core\Access\AccessResultInterface * Whether the user has access, plus cacheability metadata. */ protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) { // Save the given value in the static cache and directly return it. return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access; } /** * {@inheritdoc} */ public function resetCache() { $this->accessCache = []; } /** * {@inheritdoc} */ public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { $account = $this->prepareUser($account); $context += [ 'entity_type_id' => $this->entityTypeId, 'langcode' => LanguageInterface::LANGCODE_DEFAULT, ]; $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create'; if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) { // Cache hit, no work necessary. return $return_as_object ? $access : $access->isAllowed(); } // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access(). // Hook results take precedence over overridden implementations of // EntityAccessControlHandler::checkCreateAccess(). Entities that have // checks that need to be done before the hook is invoked should do so by // overriding this method. // 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. $access = array_merge( $this->moduleHandler()->invokeAll('entity_create_access', [$account, $context, $entity_bundle]), $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', [$account, $context, $entity_bundle]) ); $return = $this->processAccessHookResults($access); // 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()) { $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle)); } $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account); return $return_as_object ? $result : $result->isAllowed(); } /** * 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. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { if ($admin_permission = $this->entityType->getAdminPermission()) { return AccessResult::allowedIfHasPermission($account, $admin_permission); } else { // No opinion. return AccessResult::neutral(); } } /** * 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) { $account = \Drupal::currentUser(); } return $account; } /** * {@inheritdoc} */ public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) { $account = $this->prepareUser($account); // Get the default access restriction that lives within this field. $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed(); // Explicitly disallow changing the entity ID and entity UUID. $entity = $items ? $items->getEntity() : NULL; if ($operation === 'edit' && $entity) { if ($field_definition->getName() === $this->entityType->getKey('id')) { // String IDs can be set when creating the entity. if (!($entity->isNew() && $field_definition->getType() === 'string')) { return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE; } } elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) { // UUIDs can be set when creating an entity. if (!$entity->isNew()) { return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE; } } } // Get the default access restriction as specified by the access control // handler. $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items); // Combine default access, denying access wins. $default = $default->andIf($entity_default); // Invoke hook and collect grants/denies for field access from other // modules. $grants = []; $this->moduleHandler()->invokeAllWith( 'entity_field_access', function (callable $hook, string $module) use ($operation, $field_definition, $account, $items, &$grants) { $grants[] = [$module => $hook($operation, $field_definition, $account, $items)]; } ); // Our default access flag is masked under the ':default' key. $grants = array_merge([':default' => $default], ...$grants); // Also allow modules to alter the returned grants/denies. $context = [ 'operation' => $operation, 'field_definition' => $field_definition, 'items' => $items, 'account' => $account, ]; $this->moduleHandler()->alter('entity_field_access', $grants, $context); $result = $this->processAccessHookResults($grants); return $return_as_object ? $result : $result->isAllowed(); } /** * Default field access as determined by this access control handler. * * @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. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { if (!$items instanceof FieldItemListInterface || $operation !== 'view') { return AccessResult::allowed(); } $entity = $items->getEntity(); $isRevisionLogField = $this->entityType instanceof ContentEntityTypeInterface && $field_definition->getName() === $this->entityType->getRevisionMetadataKey('revision_log_message'); if ($entity && $isRevisionLogField) { // The revision log should only be visible to those who can view the // revisions OR edit the entity. return $entity->access('view revision', $account, TRUE) ->orIf($entity->access('update', $account, TRUE)); } return AccessResult::allowed(); } }