Chris@0: entityTypeId = $entity_type->id(); Chris@0: $this->entityType = $entity_type; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function access(EntityInterface $entity, $operation, AccountInterface $account = NULL, $return_as_object = FALSE) { Chris@0: $account = $this->prepareUser($account); Chris@0: $langcode = $entity->language()->getId(); Chris@0: Chris@0: if ($operation === 'view label' && $this->viewLabelOperation == FALSE) { Chris@0: $operation = 'view'; Chris@0: } Chris@0: Chris@0: // If an entity does not have a UUID, either from not being set or from not Chris@0: // having them, use the 'entity type:ID' pattern as the cache $cid. Chris@0: $cid = $entity->uuid() ?: $entity->getEntityTypeId() . ':' . $entity->id(); Chris@0: Chris@0: // If the entity is revisionable, then append the revision ID to allow Chris@0: // individual revisions to have specific access control and be cached Chris@0: // separately. Chris@0: if ($entity instanceof RevisionableInterface) { Chris@0: /** @var $entity \Drupal\Core\Entity\RevisionableInterface */ Chris@0: $cid .= ':' . $entity->getRevisionId(); Chris@0: } Chris@0: Chris@0: if (($return = $this->getCache($cid, $operation, $langcode, $account)) !== NULL) { Chris@0: // Cache hit, no work necessary. Chris@0: return $return_as_object ? $return : $return->isAllowed(); Chris@0: } Chris@0: Chris@0: // Invoke hook_entity_access() and hook_ENTITY_TYPE_access(). Hook results Chris@0: // take precedence over overridden implementations of Chris@0: // EntityAccessControlHandler::checkAccess(). Entities that have checks that Chris@0: // need to be done before the hook is invoked should do so by overriding Chris@0: // this method. Chris@0: Chris@0: // We grant access to the entity if both of these conditions are met: Chris@0: // - No modules say to deny access. Chris@0: // - At least one module says to grant access. Chris@0: $access = array_merge( Chris@0: $this->moduleHandler()->invokeAll('entity_access', [$entity, $operation, $account]), Chris@0: $this->moduleHandler()->invokeAll($entity->getEntityTypeId() . '_access', [$entity, $operation, $account]) Chris@0: ); Chris@0: Chris@0: $return = $this->processAccessHookResults($access); Chris@0: Chris@0: // Also execute the default access check except when the access result is Chris@0: // already forbidden, as in that case, it can not be anything else. Chris@0: if (!$return->isForbidden()) { Chris@0: $return = $return->orIf($this->checkAccess($entity, $operation, $account)); Chris@0: } Chris@0: $result = $this->setCache($return, $cid, $operation, $langcode, $account); Chris@0: return $return_as_object ? $result : $result->isAllowed(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * We grant access to the entity if both of these conditions are met: Chris@0: * - No modules say to deny access. Chris@0: * - At least one module says to grant access. Chris@0: * Chris@0: * @param \Drupal\Core\Access\AccessResultInterface[] $access Chris@0: * An array of access results of the fired access hook. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface Chris@0: * The combined result of the various access checks' results. All their Chris@0: * cacheability metadata is merged as well. Chris@0: * Chris@0: * @see \Drupal\Core\Access\AccessResultInterface::orIf() Chris@0: */ Chris@0: protected function processAccessHookResults(array $access) { Chris@0: // No results means no opinion. Chris@0: if (empty($access)) { Chris@0: return AccessResult::neutral(); Chris@0: } Chris@0: Chris@0: /** @var \Drupal\Core\Access\AccessResultInterface $result */ Chris@0: $result = array_shift($access); Chris@0: foreach ($access as $other) { Chris@0: $result = $result->orIf($other); Chris@0: } Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs access checks. Chris@0: * Chris@0: * This method is supposed to be overwritten by extending classes that Chris@0: * do their own custom access checking. Chris@0: * Chris@0: * @param \Drupal\Core\Entity\EntityInterface $entity Chris@0: * The entity for which to check access. Chris@0: * @param string $operation Chris@0: * The entity operation. Usually one of 'view', 'view label', 'update' or Chris@0: * 'delete'. Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The user for which to check access. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface Chris@0: * The access result. Chris@0: */ Chris@0: protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { Chris@0: if ($operation == 'delete' && $entity->isNew()) { Chris@0: return AccessResult::forbidden()->addCacheableDependency($entity); Chris@0: } Chris@0: if ($admin_permission = $this->entityType->getAdminPermission()) { Chris@14: return AccessResult::allowedIfHasPermission($account, $admin_permission); Chris@0: } Chris@0: else { Chris@0: // No opinion. Chris@0: return AccessResult::neutral(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tries to retrieve a previously cached access value from the static cache. Chris@0: * Chris@0: * @param string $cid Chris@0: * Unique string identifier for the entity/operation, for example the Chris@0: * entity UUID or a custom string. Chris@0: * @param string $operation Chris@0: * The entity operation. Usually one of 'view', 'update', 'create' or Chris@0: * 'delete'. Chris@0: * @param string $langcode Chris@0: * The language code for which to check access. Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The user for which to check access. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface|null Chris@0: * The cached AccessResult, or NULL if there is no record for the given Chris@0: * user, operation, langcode and entity in the cache. Chris@0: */ Chris@0: protected function getCache($cid, $operation, $langcode, AccountInterface $account) { Chris@0: // Return from cache if a value has been set for it previously. Chris@0: if (isset($this->accessCache[$account->id()][$cid][$langcode][$operation])) { Chris@0: return $this->accessCache[$account->id()][$cid][$langcode][$operation]; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Statically caches whether the given user has access. Chris@0: * Chris@0: * @param \Drupal\Core\Access\AccessResultInterface $access Chris@0: * The access result. Chris@0: * @param string $cid Chris@0: * Unique string identifier for the entity/operation, for example the Chris@0: * entity UUID or a custom string. Chris@0: * @param string $operation Chris@0: * The entity operation. Usually one of 'view', 'update', 'create' or Chris@0: * 'delete'. Chris@0: * @param string $langcode Chris@0: * The language code for which to check access. Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The user for which to check access. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface Chris@0: * Whether the user has access, plus cacheability metadata. Chris@0: */ Chris@0: protected function setCache($access, $cid, $operation, $langcode, AccountInterface $account) { Chris@0: // Save the given value in the static cache and directly return it. Chris@0: return $this->accessCache[$account->id()][$cid][$langcode][$operation] = $access; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function resetCache() { Chris@0: $this->accessCache = []; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { Chris@0: $account = $this->prepareUser($account); Chris@0: $context += [ Chris@0: 'entity_type_id' => $this->entityTypeId, Chris@0: 'langcode' => LanguageInterface::LANGCODE_DEFAULT, Chris@0: ]; Chris@0: Chris@0: $cid = $entity_bundle ? 'create:' . $entity_bundle : 'create'; Chris@0: if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) { Chris@0: // Cache hit, no work necessary. Chris@0: return $return_as_object ? $access : $access->isAllowed(); Chris@0: } Chris@0: Chris@0: // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access(). Chris@0: // Hook results take precedence over overridden implementations of Chris@0: // EntityAccessControlHandler::checkCreateAccess(). Entities that have Chris@0: // checks that need to be done before the hook is invoked should do so by Chris@0: // overriding this method. Chris@0: Chris@0: // We grant access to the entity if both of these conditions are met: Chris@0: // - No modules say to deny access. Chris@0: // - At least one module says to grant access. Chris@0: $access = array_merge( Chris@0: $this->moduleHandler()->invokeAll('entity_create_access', [$account, $context, $entity_bundle]), Chris@0: $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', [$account, $context, $entity_bundle]) Chris@0: ); Chris@0: Chris@0: $return = $this->processAccessHookResults($access); Chris@0: Chris@0: // Also execute the default access check except when the access result is Chris@0: // already forbidden, as in that case, it can not be anything else. Chris@0: if (!$return->isForbidden()) { Chris@0: $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle)); Chris@0: } Chris@0: $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account); Chris@0: return $return_as_object ? $result : $result->isAllowed(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs create access checks. Chris@0: * Chris@0: * This method is supposed to be overwritten by extending classes that Chris@0: * do their own custom access checking. Chris@0: * Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The user for which to check access. Chris@0: * @param array $context Chris@0: * An array of key-value pairs to pass additional context when needed. Chris@0: * @param string|null $entity_bundle Chris@0: * (optional) The bundle of the entity. Required if the entity supports Chris@0: * bundles, defaults to NULL otherwise. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface Chris@0: * The access result. Chris@0: */ Chris@0: protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { Chris@0: if ($admin_permission = $this->entityType->getAdminPermission()) { Chris@0: return AccessResult::allowedIfHasPermission($account, $admin_permission); Chris@0: } Chris@0: else { Chris@0: // No opinion. Chris@0: return AccessResult::neutral(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Loads the current account object, if it does not exist yet. Chris@0: * Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The account interface instance. Chris@0: * Chris@0: * @return \Drupal\Core\Session\AccountInterface Chris@0: * Returns the current account object. Chris@0: */ Chris@0: protected function prepareUser(AccountInterface $account = NULL) { Chris@0: if (!$account) { Chris@0: $account = \Drupal::currentUser(); Chris@0: } Chris@0: return $account; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function fieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account = NULL, FieldItemListInterface $items = NULL, $return_as_object = FALSE) { Chris@0: $account = $this->prepareUser($account); Chris@0: Chris@0: // Get the default access restriction that lives within this field. Chris@0: $default = $items ? $items->defaultAccess($operation, $account) : AccessResult::allowed(); Chris@0: Chris@0: // Explicitly disallow changing the entity ID and entity UUID. Chris@14: $entity = $items ? $items->getEntity() : NULL; Chris@14: if ($operation === 'edit' && $entity) { Chris@0: if ($field_definition->getName() === $this->entityType->getKey('id')) { Chris@14: // String IDs can be set when creating the entity. Chris@14: if (!($entity->isNew() && $field_definition->getType() === 'string')) { Chris@17: return $return_as_object ? AccessResult::forbidden('The entity ID cannot be changed.')->addCacheableDependency($entity) : FALSE; Chris@14: } Chris@0: } Chris@0: elseif ($field_definition->getName() === $this->entityType->getKey('uuid')) { Chris@0: // UUIDs can be set when creating an entity. Chris@14: if (!$entity->isNew()) { Chris@17: return $return_as_object ? AccessResult::forbidden('The entity UUID cannot be changed.')->addCacheableDependency($entity) : FALSE; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Get the default access restriction as specified by the access control Chris@0: // handler. Chris@0: $entity_default = $this->checkFieldAccess($operation, $field_definition, $account, $items); Chris@0: Chris@0: // Combine default access, denying access wins. Chris@0: $default = $default->andIf($entity_default); Chris@0: Chris@0: // Invoke hook and collect grants/denies for field access from other Chris@0: // modules. Our default access flag is masked under the ':default' key. Chris@0: $grants = [':default' => $default]; Chris@0: $hook_implementations = $this->moduleHandler()->getImplementations('entity_field_access'); Chris@0: foreach ($hook_implementations as $module) { Chris@0: $grants = array_merge($grants, [$module => $this->moduleHandler()->invoke($module, 'entity_field_access', [$operation, $field_definition, $account, $items])]); Chris@0: } Chris@0: Chris@0: // Also allow modules to alter the returned grants/denies. Chris@0: $context = [ Chris@0: 'operation' => $operation, Chris@0: 'field_definition' => $field_definition, Chris@0: 'items' => $items, Chris@0: 'account' => $account, Chris@0: ]; Chris@0: $this->moduleHandler()->alter('entity_field_access', $grants, $context); Chris@0: Chris@0: $result = $this->processAccessHookResults($grants); Chris@0: return $return_as_object ? $result : $result->isAllowed(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Default field access as determined by this access control handler. Chris@0: * Chris@0: * @param string $operation Chris@0: * The operation access should be checked for. Chris@0: * Usually one of "view" or "edit". Chris@0: * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition Chris@0: * The field definition. Chris@0: * @param \Drupal\Core\Session\AccountInterface $account Chris@0: * The user session for which to check access. Chris@0: * @param \Drupal\Core\Field\FieldItemListInterface $items Chris@0: * (optional) The field values for which to check access, or NULL if access Chris@0: * is checked for the field definition, without any specific value Chris@0: * available. Defaults to NULL. Chris@0: * Chris@0: * @return \Drupal\Core\Access\AccessResultInterface Chris@0: * The access result. Chris@0: */ Chris@0: protected function checkFieldAccess($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) { Chris@0: return AccessResult::allowed(); Chris@0: } Chris@0: Chris@0: }