hasPermission($permission))->addCacheContexts(['user.permissions']); if ($access_result instanceof AccessResultReasonInterface) { $access_result->setReason("The '$permission' permission is required."); } return $access_result; } /** * Creates an allowed access result if the permissions are present, neutral otherwise. * * Checks the permission and adds a 'user.permissions' cache contexts. * * @param \Drupal\Core\Session\AccountInterface $account * The account for which to check permissions. * @param array $permissions * The permissions to check. * @param string $conjunction * (optional) 'AND' if all permissions are required, 'OR' in case just one. * Defaults to 'AND' * * @return \Drupal\Core\Access\AccessResult * If the account has the permissions, isAllowed() will be TRUE, otherwise * isNeutral() will be TRUE. */ public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') { $access = FALSE; if ($conjunction == 'AND' && !empty($permissions)) { $access = TRUE; foreach ($permissions as $permission) { if (!$account->hasPermission($permission)) { $access = FALSE; break; } } } else { foreach ($permissions as $permission) { if ($account->hasPermission($permission)) { $access = TRUE; break; } } } $access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']); if ($access_result instanceof AccessResultReasonInterface) { if (count($permissions) === 1) { $access_result->setReason("The '$permission' permission is required."); } elseif (count($permissions) > 1) { $quote = function ($s) { return "'$s'"; }; $access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions)))); } } return $access_result; } /** * {@inheritdoc} * * @see \Drupal\Core\Access\AccessResultAllowed */ public function isAllowed() { return FALSE; } /** * {@inheritdoc} * * @see \Drupal\Core\Access\AccessResultForbidden */ public function isForbidden() { return FALSE; } /** * {@inheritdoc} * * @see \Drupal\Core\Access\AccessResultNeutral */ public function isNeutral() { return FALSE; } /** * {@inheritdoc} */ public function getCacheContexts() { return $this->cacheContexts; } /** * {@inheritdoc} */ public function getCacheTags() { return $this->cacheTags; } /** * {@inheritdoc} */ public function getCacheMaxAge() { return $this->cacheMaxAge; } /** * Resets cache contexts (to the empty array). * * @return $this */ public function resetCacheContexts() { $this->cacheContexts = []; return $this; } /** * Resets cache tags (to the empty array). * * @return $this */ public function resetCacheTags() { $this->cacheTags = []; return $this; } /** * Sets the maximum age for which this access result may be cached. * * @param int $max_age * The maximum time in seconds that this access result may be cached. * * @return $this */ public function setCacheMaxAge($max_age) { $this->cacheMaxAge = $max_age; return $this; } /** * Convenience method, adds the "user.permissions" cache context. * * @return $this */ public function cachePerPermissions() { $this->addCacheContexts(['user.permissions']); return $this; } /** * Convenience method, adds the "user" cache context. * * @return $this */ public function cachePerUser() { $this->addCacheContexts(['user']); return $this; } /** * {@inheritdoc} */ public function orIf(AccessResultInterface $other) { $merge_other = FALSE; // $other's cacheability metadata is merged if $merge_other gets set to TRUE // and this happens in three cases: // 1. $other's access result is the one that determines the combined access // result. // 2. This access result is not cacheable and $other's access result is the // same. i.e. attempt to return a cacheable access result. // 3. Neither access result is 'forbidden' and both are cacheable: inherit // the other's cacheability metadata because it may turn into a // 'forbidden' for another value of the cache contexts in the // cacheability metadata. In other words: this is necessary to respect // the contagious nature of the 'forbidden' access result. // e.g. we have two access results A and B. Neither is forbidden. A is // globally cacheable (no cache contexts). B is cacheable per role. If we // don't have merging case 3, then A->orIf(B) will be globally cacheable, // which means that even if a user of a different role logs in, the // cached access result will be used, even though for that other role, B // is forbidden! if ($this->isForbidden() || $other->isForbidden()) { $result = static::forbidden(); if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) { $merge_other = TRUE; } if ($this->isForbidden() && $this instanceof AccessResultReasonInterface && $this->getReason() !== '') { $result->setReason($this->getReason()); } elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface && $other->getReason() !== '') { $result->setReason($other->getReason()); } } elseif ($this->isAllowed() || $other->isAllowed()) { $result = static::allowed(); if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) { $merge_other = TRUE; } } else { $result = static::neutral(); if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) { $merge_other = TRUE; } if ($this instanceof AccessResultReasonInterface && $this->getReason() !== '') { $result->setReason($this->getReason()); } elseif ($other instanceof AccessResultReasonInterface && $other->getReason() !== '') { $result->setReason($other->getReason()); } } $result->inheritCacheability($this); if ($merge_other) { $result->inheritCacheability($other); } return $result; } /** * {@inheritdoc} */ public function andIf(AccessResultInterface $other) { // The other access result's cacheability metadata is merged if $merge_other // gets set to TRUE. It gets set to TRUE in one case: if the other access // result is used. $merge_other = FALSE; if ($this->isForbidden() || $other->isForbidden()) { $result = static::forbidden(); if (!$this->isForbidden()) { if ($other instanceof AccessResultReasonInterface) { $result->setReason($other->getReason()); } $merge_other = TRUE; } else { if ($this instanceof AccessResultReasonInterface) { $result->setReason($this->getReason()); } } } elseif ($this->isAllowed() && $other->isAllowed()) { $result = static::allowed(); $merge_other = TRUE; } else { $result = static::neutral(); if (!$this->isNeutral()) { $merge_other = TRUE; if ($other instanceof AccessResultReasonInterface) { $result->setReason($other->getReason()); } } else { if ($this instanceof AccessResultReasonInterface) { $result->setReason($this->getReason()); } } } $result->inheritCacheability($this); if ($merge_other) { $result->inheritCacheability($other); // If this access result is not cacheable, then an AND with another access // result must also not be cacheable, except if the other access result // has isForbidden() === TRUE. isForbidden() access results are contagious // in that they propagate regardless of the other value. if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) { $result->setCacheMaxAge(0); } } return $result; } /** * Inherits the cacheability of the other access result, if any. * * This method differs from addCacheableDependency() in how it handles * max-age, because it is designed to inherit the cacheability of the second * operand in the andIf() and orIf() operations. There, the situation * "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000 * as the end result. * * @param \Drupal\Core\Access\AccessResultInterface $other * The other access result, whose cacheability (if any) to inherit. * * @return $this */ public function inheritCacheability(AccessResultInterface $other) { $this->addCacheableDependency($other); if ($other instanceof CacheableDependencyInterface) { if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) { $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge())); } else { $this->setCacheMaxAge($other->getCacheMaxAge()); } } return $this; } }