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.
91 * @return \Drupal\Core\Access\AccessResult
92 * If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
95 public static function forbiddenIf($condition) {
96 return $condition ? static::forbidden() : static::neutral();
100 * Creates an allowed access result if the permission is present, neutral otherwise.
102 * Checks the permission and adds a 'user.permissions' cache context.
104 * @param \Drupal\Core\Session\AccountInterface $account
105 * The account for which to check a permission.
106 * @param string $permission
107 * The permission to check for.
109 * @return \Drupal\Core\Access\AccessResult
110 * If the account has the permission, isAllowed() will be TRUE, otherwise
111 * isNeutral() will be TRUE.
113 public static function allowedIfHasPermission(AccountInterface $account, $permission) {
114 $access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
116 if ($access_result instanceof AccessResultReasonInterface) {
117 $access_result->setReason("The '$permission' permission is required.");
119 return $access_result;
123 * Creates an allowed access result if the permissions are present, neutral otherwise.
125 * Checks the permission and adds a 'user.permissions' cache contexts.
127 * @param \Drupal\Core\Session\AccountInterface $account
128 * The account for which to check permissions.
129 * @param array $permissions
130 * The permissions to check.
131 * @param string $conjunction
132 * (optional) 'AND' if all permissions are required, 'OR' in case just one.
135 * @return \Drupal\Core\Access\AccessResult
136 * If the account has the permissions, isAllowed() will be TRUE, otherwise
137 * isNeutral() will be TRUE.
139 public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') {
142 if ($conjunction == 'AND' && !empty($permissions)) {
144 foreach ($permissions as $permission) {
145 if (!$permission_access = $account->hasPermission($permission)) {
152 foreach ($permissions as $permission) {
153 if ($permission_access = $account->hasPermission($permission)) {
160 $access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
162 if ($access_result instanceof AccessResultReasonInterface) {
163 if (count($permissions) === 1) {
164 $access_result->setReason("The '$permission' permission is required.");
166 elseif (count($permissions) > 1) {
167 $quote = function ($s) {
170 $access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
174 return $access_result;
180 * @see \Drupal\Core\Access\AccessResultAllowed
182 public function isAllowed() {
189 * @see \Drupal\Core\Access\AccessResultForbidden
191 public function isForbidden() {
198 * @see \Drupal\Core\Access\AccessResultNeutral
200 public function isNeutral() {
207 public function getCacheContexts() {
208 return $this->cacheContexts;
214 public function getCacheTags() {
215 return $this->cacheTags;
221 public function getCacheMaxAge() {
222 return $this->cacheMaxAge;
226 * Resets cache contexts (to the empty array).
230 public function resetCacheContexts() {
231 $this->cacheContexts = [];
236 * Resets cache tags (to the empty array).
240 public function resetCacheTags() {
241 $this->cacheTags = [];
246 * Sets the maximum age for which this access result may be cached.
248 * @param int $max_age
249 * The maximum time in seconds that this access result may be cached.
253 public function setCacheMaxAge($max_age) {
254 $this->cacheMaxAge = $max_age;
259 * Convenience method, adds the "user.permissions" cache context.
263 public function cachePerPermissions() {
264 $this->addCacheContexts(['user.permissions']);
269 * Convenience method, adds the "user" cache context.
273 public function cachePerUser() {
274 $this->addCacheContexts(['user']);
279 * Convenience method, adds the entity's cache tag.
281 * @param \Drupal\Core\Entity\EntityInterface $entity
282 * The entity whose cache tag to set on the access result.
286 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
287 * ::addCacheableDependency() instead.
289 public function cacheUntilEntityChanges(EntityInterface $entity) {
290 return $this->addCacheableDependency($entity);
294 * Convenience method, adds the configuration object's cache tag.
296 * @param \Drupal\Core\Config\ConfigBase $configuration
297 * The configuration object whose cache tag to set on the access result.
301 * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
302 * ::addCacheableDependency() instead.
304 public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
305 return $this->addCacheableDependency($configuration);
311 public function orIf(AccessResultInterface $other) {
312 $merge_other = FALSE;
313 // $other's cacheability metadata is merged if $merge_other gets set to TRUE
314 // and this happens in three cases:
315 // 1. $other's access result is the one that determines the combined access
317 // 2. This access result is not cacheable and $other's access result is the
318 // same. i.e. attempt to return a cacheable access result.
319 // 3. Neither access result is 'forbidden' and both are cacheable: inherit
320 // the other's cacheability metadata because it may turn into a
321 // 'forbidden' for another value of the cache contexts in the
322 // cacheability metadata. In other words: this is necessary to respect
323 // the contagious nature of the 'forbidden' access result.
324 // e.g. we have two access results A and B. Neither is forbidden. A is
325 // globally cacheable (no cache contexts). B is cacheable per role. If we
326 // don't have merging case 3, then A->orIf(B) will be globally cacheable,
327 // which means that even if a user of a different role logs in, the
328 // cached access result will be used, even though for that other role, B
330 if ($this->isForbidden() || $other->isForbidden()) {
331 $result = static::forbidden();
332 if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
336 if ($this->isForbidden() && $this instanceof AccessResultReasonInterface) {
337 $result->setReason($this->getReason());
339 elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface) {
340 $result->setReason($other->getReason());
343 elseif ($this->isAllowed() || $other->isAllowed()) {
344 $result = static::allowed();
345 if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
350 $result = static::neutral();
351 if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
353 if ($other instanceof AccessResultReasonInterface) {
354 $result->setReason($other->getReason());
358 if ($this instanceof AccessResultReasonInterface) {
359 $result->setReason($this->getReason());
363 $result->inheritCacheability($this);
365 $result->inheritCacheability($other);
373 public function andIf(AccessResultInterface $other) {
374 // The other access result's cacheability metadata is merged if $merge_other
375 // gets set to TRUE. It gets set to TRUE in one case: if the other access
377 $merge_other = FALSE;
378 if ($this->isForbidden() || $other->isForbidden()) {
379 $result = static::forbidden();
380 if (!$this->isForbidden()) {
381 if ($other instanceof AccessResultReasonInterface) {
382 $result->setReason($other->getReason());
387 if ($this instanceof AccessResultReasonInterface) {
388 $result->setReason($this->getReason());
392 elseif ($this->isAllowed() && $other->isAllowed()) {
393 $result = static::allowed();
397 $result = static::neutral();
398 if (!$this->isNeutral()) {
400 if ($other instanceof AccessResultReasonInterface) {
401 $result->setReason($other->getReason());
405 if ($this instanceof AccessResultReasonInterface) {
406 $result->setReason($this->getReason());
410 $result->inheritCacheability($this);
412 $result->inheritCacheability($other);
413 // If this access result is not cacheable, then an AND with another access
414 // result must also not be cacheable, except if the other access result
415 // has isForbidden() === TRUE. isForbidden() access results are contagious
416 // in that they propagate regardless of the other value.
417 if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
418 $result->setCacheMaxAge(0);
425 * Inherits the cacheability of the other access result, if any.
427 * inheritCacheability() differs from addCacheableDependency() in how it
428 * handles max-age, because it is designed to inherit the cacheability of the
429 * second operand in the andIf() and orIf() operations. There, the situation
430 * "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
433 * @param \Drupal\Core\Access\AccessResultInterface $other
434 * The other access result, whose cacheability (if any) to inherit.
438 public function inheritCacheability(AccessResultInterface $other) {
439 $this->addCacheableDependency($other);
440 if ($other instanceof CacheableDependencyInterface) {
441 if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
442 $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
445 $this->setCacheMaxAge($other->getCacheMaxAge());