3 namespace Drupal\Core\Access;
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheableDependencyInterface;
7 use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
8 use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
9 use Drupal\Core\Config\ConfigBase;
10 use Drupal\Core\Entity\EntityInterface;
11 use Drupal\Core\Session\AccountInterface;
14 * Value object for passing an access result with cacheability metadata.
16 * The access result itself — excluding the cacheability metadata — is
17 * immutable. There are subclasses for each of the three possible access results
20 * @see \Drupal\Core\Access\AccessResultAllowed
21 * @see \Drupal\Core\Access\AccessResultForbidden
22 * @see \Drupal\Core\Access\AccessResultNeutral
24 * When using ::orIf() and ::andIf(), cacheability metadata will be merged
25 * accordingly as well.
27 abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
29 use RefinableCacheableDependencyTrait;
32 * Creates an AccessResultInterface object with isNeutral() === TRUE.
34 * @param string|null $reason
35 * (optional) The reason why access is forbidden. Intended for developers,
36 * hence not translatable.
38 * @return \Drupal\Core\Access\AccessResultNeutral
39 * isNeutral() will be TRUE.
41 public static function neutral($reason = NULL) {
42 assert(is_string($reason) || is_null($reason));
43 return new AccessResultNeutral($reason);
47 * Creates an AccessResultInterface object with isAllowed() === TRUE.
49 * @return \Drupal\Core\Access\AccessResultAllowed
50 * isAllowed() will be TRUE.
52 public static function allowed() {
53 return new AccessResultAllowed();
57 * Creates an AccessResultInterface object with isForbidden() === TRUE.
59 * @param string|null $reason
60 * (optional) The reason why access is forbidden. Intended for developers,
61 * hence not translatable.
63 * @return \Drupal\Core\Access\AccessResultForbidden
64 * isForbidden() will be TRUE.
66 public static function forbidden($reason = NULL) {
67 assert(is_string($reason) || is_null($reason));
68 return new AccessResultForbidden($reason);
72 * Creates an allowed or neutral access result.
74 * @param bool $condition
75 * The condition to evaluate.
77 * @return \Drupal\Core\Access\AccessResult
78 * If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
81 public static function allowedIf($condition) {
82 return $condition ? static::allowed() : static::neutral();
86 * Creates a forbidden or neutral access result.
88 * @param bool $condition
89 * The condition to evaluate.
90 * @param string|null $reason
91 * (optional) The reason why access is forbidden. Intended for developers,
92 * hence not translatable
94 * @return \Drupal\Core\Access\AccessResult
95 * If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
98 public static function forbiddenIf($condition, $reason = NULL) {
99 return $condition ? static::forbidden($reason) : static::neutral();
103 * Creates an allowed access result if the permission is present, neutral otherwise.
105 * Checks the permission and adds a 'user.permissions' cache context.
107 * @param \Drupal\Core\Session\AccountInterface $account
108 * The account for which to check a permission.
109 * @param string $permission
110 * The permission to check for.
112 * @return \Drupal\Core\Access\AccessResult
113 * If the account has the permission, isAllowed() will be TRUE, otherwise
114 * isNeutral() will be TRUE.
116 public static function allowedIfHasPermission(AccountInterface $account, $permission) {
117 $access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
119 if ($access_result instanceof AccessResultReasonInterface) {
120 $access_result->setReason("The '$permission' permission is required.");
122 return $access_result;
126 * Creates an allowed access result if the permissions are present, neutral otherwise.
128 * Checks the permission and adds a 'user.permissions' cache contexts.
130 * @param \Drupal\Core\Session\AccountInterface $account
131 * The account for which to check permissions.
132 * @param array $permissions
133 * The permissions to check.
134 * @param string $conjunction
135 * (optional) 'AND' if all permissions are required, 'OR' in case just one.
138 * @return \Drupal\Core\Access\AccessResult
139 * If the account has the permissions, isAllowed() will be TRUE, otherwise
140 * isNeutral() will be TRUE.
142 public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') {
145 if ($conjunction == 'AND' && !empty($permissions)) {
147 foreach ($permissions as $permission) {
148 if (!$permission_access = $account->hasPermission($permission)) {
155 foreach ($permissions as $permission) {
156 if ($permission_access = $account->hasPermission($permission)) {
163 $access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
165 if ($access_result instanceof AccessResultReasonInterface) {
166 if (count($permissions) === 1) {
167 $access_result->setReason("The '$permission' permission is required.");
169 elseif (count($permissions) > 1) {
170 $quote = function ($s) {
173 $access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
177 return $access_result;
183 * @see \Drupal\Core\Access\AccessResultAllowed
185 public function isAllowed() {
192 * @see \Drupal\Core\Access\AccessResultForbidden
194 public function isForbidden() {
201 * @see \Drupal\Core\Access\AccessResultNeutral
203 public function isNeutral() {
210 public function getCacheContexts() {
211 return $this->cacheContexts;
217 public function getCacheTags() {
218 return $this->cacheTags;
224 public function getCacheMaxAge() {
225 return $this->cacheMaxAge;
229 * Resets cache contexts (to the empty array).
233 public function resetCacheContexts() {
234 $this->cacheContexts = [];
239 * Resets cache tags (to the empty array).
243 public function resetCacheTags() {
244 $this->cacheTags = [];
249 * Sets the maximum age for which this access result may be cached.
251 * @param int $max_age
252 * The maximum time in seconds that this access result may be cached.
256 public function setCacheMaxAge($max_age) {
257 $this->cacheMaxAge = $max_age;
262 * Convenience method, adds the "user.permissions" cache context.
266 public function cachePerPermissions() {
267 $this->addCacheContexts(['user.permissions']);
272 * Convenience method, adds the "user" cache context.
276 public function cachePerUser() {
277 $this->addCacheContexts(['user']);
282 * Convenience method, adds the entity's cache tag.
284 * @param \Drupal\Core\Entity\EntityInterface $entity
285 * The entity whose cache tag to set on the access result.
289 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
290 * ::addCacheableDependency() instead.
292 public function cacheUntilEntityChanges(EntityInterface $entity) {
293 return $this->addCacheableDependency($entity);
297 * Convenience method, adds the configuration object's cache tag.
299 * @param \Drupal\Core\Config\ConfigBase $configuration
300 * The configuration object whose cache tag to set on the access result.
304 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
305 * ::addCacheableDependency() instead.
307 public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
308 return $this->addCacheableDependency($configuration);
314 public function orIf(AccessResultInterface $other) {
315 $merge_other = FALSE;
316 // $other's cacheability metadata is merged if $merge_other gets set to TRUE
317 // and this happens in three cases:
318 // 1. $other's access result is the one that determines the combined access
320 // 2. This access result is not cacheable and $other's access result is the
321 // same. i.e. attempt to return a cacheable access result.
322 // 3. Neither access result is 'forbidden' and both are cacheable: inherit
323 // the other's cacheability metadata because it may turn into a
324 // 'forbidden' for another value of the cache contexts in the
325 // cacheability metadata. In other words: this is necessary to respect
326 // the contagious nature of the 'forbidden' access result.
327 // e.g. we have two access results A and B. Neither is forbidden. A is
328 // globally cacheable (no cache contexts). B is cacheable per role. If we
329 // don't have merging case 3, then A->orIf(B) will be globally cacheable,
330 // which means that even if a user of a different role logs in, the
331 // cached access result will be used, even though for that other role, B
333 if ($this->isForbidden() || $other->isForbidden()) {
334 $result = static::forbidden();
335 if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
339 if ($this->isForbidden() && $this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
340 $result->setReason($this->getReason());
342 elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
343 $result->setReason($other->getReason());
346 elseif ($this->isAllowed() || $other->isAllowed()) {
347 $result = static::allowed();
348 if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
353 $result = static::neutral();
354 if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
358 if ($this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
359 $result->setReason($this->getReason());
361 elseif ($other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
362 $result->setReason($other->getReason());
365 $result->inheritCacheability($this);
367 $result->inheritCacheability($other);
375 public function andIf(AccessResultInterface $other) {
376 // The other access result's cacheability metadata is merged if $merge_other
377 // gets set to TRUE. It gets set to TRUE in one case: if the other access
379 $merge_other = FALSE;
380 if ($this->isForbidden() || $other->isForbidden()) {
381 $result = static::forbidden();
382 if (!$this->isForbidden()) {
383 if ($other instanceof AccessResultReasonInterface) {
384 $result->setReason($other->getReason());
389 if ($this instanceof AccessResultReasonInterface) {
390 $result->setReason($this->getReason());
394 elseif ($this->isAllowed() && $other->isAllowed()) {
395 $result = static::allowed();
399 $result = static::neutral();
400 if (!$this->isNeutral()) {
402 if ($other instanceof AccessResultReasonInterface) {
403 $result->setReason($other->getReason());
407 if ($this instanceof AccessResultReasonInterface) {
408 $result->setReason($this->getReason());
412 $result->inheritCacheability($this);
414 $result->inheritCacheability($other);
415 // If this access result is not cacheable, then an AND with another access
416 // result must also not be cacheable, except if the other access result
417 // has isForbidden() === TRUE. isForbidden() access results are contagious
418 // in that they propagate regardless of the other value.
419 if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
420 $result->setCacheMaxAge(0);
427 * Inherits the cacheability of the other access result, if any.
429 * This method differs from addCacheableDependency() in how it handles
430 * max-age, because it is designed to inherit the cacheability of the second
431 * operand in the andIf() and orIf() operations. There, the situation
432 * "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
435 * @param \Drupal\Core\Access\AccessResultInterface $other
436 * The other access result, whose cacheability (if any) to inherit.
440 public function inheritCacheability(AccessResultInterface $other) {
441 $this->addCacheableDependency($other);
442 if ($other instanceof CacheableDependencyInterface) {
443 if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
444 $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
447 $this->setCacheMaxAge($other->getCacheMaxAge());