Upgraded drupal core with security updates
[yaffs-website] / web / core / lib / Drupal / Core / Access / AccessResult.php
1 <?php
2
3 namespace Drupal\Core\Access;
4
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;
12
13 /**
14  * Value object for passing an access result with cacheability metadata.
15  *
16  * The access result itself — excluding the cacheability metadata — is
17  * immutable. There are subclasses for each of the three possible access results
18  * themselves:
19  *
20  * @see \Drupal\Core\Access\AccessResultAllowed
21  * @see \Drupal\Core\Access\AccessResultForbidden
22  * @see \Drupal\Core\Access\AccessResultNeutral
23  *
24  * When using ::orIf() and ::andIf(), cacheability metadata will be merged
25  * accordingly as well.
26  */
27 abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
28
29   use RefinableCacheableDependencyTrait;
30
31   /**
32    * Creates an AccessResultInterface object with isNeutral() === TRUE.
33    *
34    * @param string|null $reason
35    *   (optional) The reason why access is forbidden. Intended for developers,
36    *   hence not translatable.
37    *
38    * @return \Drupal\Core\Access\AccessResultNeutral
39    *   isNeutral() will be TRUE.
40    */
41   public static function neutral($reason = NULL) {
42     assert('is_string($reason) || is_null($reason)');
43     return new AccessResultNeutral($reason);
44   }
45
46   /**
47    * Creates an AccessResultInterface object with isAllowed() === TRUE.
48    *
49    * @return \Drupal\Core\Access\AccessResultAllowed
50    *   isAllowed() will be TRUE.
51    */
52   public static function allowed() {
53     return new AccessResultAllowed();
54   }
55
56   /**
57    * Creates an AccessResultInterface object with isForbidden() === TRUE.
58    *
59    * @param string|null $reason
60    *   (optional) The reason why access is forbidden. Intended for developers,
61    *   hence not translatable.
62    *
63    * @return \Drupal\Core\Access\AccessResultForbidden
64    *   isForbidden() will be TRUE.
65    */
66   public static function forbidden($reason = NULL) {
67     assert('is_string($reason) || is_null($reason)');
68     return new AccessResultForbidden($reason);
69   }
70
71   /**
72    * Creates an allowed or neutral access result.
73    *
74    * @param bool $condition
75    *   The condition to evaluate.
76    *
77    * @return \Drupal\Core\Access\AccessResult
78    *   If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
79    *   will be TRUE.
80    */
81   public static function allowedIf($condition) {
82     return $condition ? static::allowed() : static::neutral();
83   }
84
85   /**
86    * Creates a forbidden or neutral access result.
87    *
88    * @param bool $condition
89    *   The condition to evaluate.
90    *
91    * @return \Drupal\Core\Access\AccessResult
92    *   If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
93    *   will be TRUE.
94    */
95   public static function forbiddenIf($condition) {
96     return $condition ? static::forbidden() : static::neutral();
97   }
98
99   /**
100    * Creates an allowed access result if the permission is present, neutral otherwise.
101    *
102    * Checks the permission and adds a 'user.permissions' cache context.
103    *
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.
108    *
109    * @return \Drupal\Core\Access\AccessResult
110    *   If the account has the permission, isAllowed() will be TRUE, otherwise
111    *   isNeutral() will be TRUE.
112    */
113   public static function allowedIfHasPermission(AccountInterface $account, $permission) {
114     $access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
115
116     if ($access_result instanceof AccessResultReasonInterface) {
117       $access_result->setReason("The '$permission' permission is required.");
118     }
119     return $access_result;
120   }
121
122   /**
123    * Creates an allowed access result if the permissions are present, neutral otherwise.
124    *
125    * Checks the permission and adds a 'user.permissions' cache contexts.
126    *
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.
133    *   Defaults to 'AND'
134    *
135    * @return \Drupal\Core\Access\AccessResult
136    *   If the account has the permissions, isAllowed() will be TRUE, otherwise
137    *   isNeutral() will be TRUE.
138    */
139   public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') {
140     $access = FALSE;
141
142     if ($conjunction == 'AND' && !empty($permissions)) {
143       $access = TRUE;
144       foreach ($permissions as $permission) {
145         if (!$permission_access = $account->hasPermission($permission)) {
146           $access = FALSE;
147           break;
148         }
149       }
150     }
151     else {
152       foreach ($permissions as $permission) {
153         if ($permission_access = $account->hasPermission($permission)) {
154           $access = TRUE;
155           break;
156         }
157       }
158     }
159
160     $access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
161
162     if ($access_result instanceof AccessResultReasonInterface) {
163       if (count($permissions) === 1) {
164         $access_result->setReason("The '$permission' permission is required.");
165       }
166       elseif (count($permissions) > 1) {
167         $quote = function ($s) {
168           return "'$s'";
169         };
170         $access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
171       }
172     }
173
174     return $access_result;
175   }
176
177   /**
178    * {@inheritdoc}
179    *
180    * @see \Drupal\Core\Access\AccessResultAllowed
181    */
182   public function isAllowed() {
183     return FALSE;
184   }
185
186   /**
187    * {@inheritdoc}
188    *
189    * @see \Drupal\Core\Access\AccessResultForbidden
190    */
191   public function isForbidden() {
192     return FALSE;
193   }
194
195   /**
196    * {@inheritdoc}
197    *
198    * @see \Drupal\Core\Access\AccessResultNeutral
199    */
200   public function isNeutral() {
201     return FALSE;
202   }
203
204   /**
205    * {@inheritdoc}
206    */
207   public function getCacheContexts() {
208     return $this->cacheContexts;
209   }
210
211   /**
212    * {@inheritdoc}
213    */
214   public function getCacheTags() {
215     return $this->cacheTags;
216   }
217
218   /**
219    * {@inheritdoc}
220    */
221   public function getCacheMaxAge() {
222     return $this->cacheMaxAge;
223   }
224
225   /**
226    * Resets cache contexts (to the empty array).
227    *
228    * @return $this
229    */
230   public function resetCacheContexts() {
231     $this->cacheContexts = [];
232     return $this;
233   }
234
235   /**
236    * Resets cache tags (to the empty array).
237    *
238    * @return $this
239    */
240   public function resetCacheTags() {
241     $this->cacheTags = [];
242     return $this;
243   }
244
245   /**
246    * Sets the maximum age for which this access result may be cached.
247    *
248    * @param int $max_age
249    *   The maximum time in seconds that this access result may be cached.
250    *
251    * @return $this
252    */
253   public function setCacheMaxAge($max_age) {
254     $this->cacheMaxAge = $max_age;
255     return $this;
256   }
257
258   /**
259    * Convenience method, adds the "user.permissions" cache context.
260    *
261    * @return $this
262    */
263   public function cachePerPermissions() {
264     $this->addCacheContexts(['user.permissions']);
265     return $this;
266   }
267
268   /**
269    * Convenience method, adds the "user" cache context.
270    *
271    * @return $this
272    */
273   public function cachePerUser() {
274     $this->addCacheContexts(['user']);
275     return $this;
276   }
277
278   /**
279    * Convenience method, adds the entity's cache tag.
280    *
281    * @param \Drupal\Core\Entity\EntityInterface $entity
282    *   The entity whose cache tag to set on the access result.
283    *
284    * @return $this
285    *
286    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
287    *   ::addCacheableDependency() instead.
288    */
289   public function cacheUntilEntityChanges(EntityInterface $entity) {
290     return $this->addCacheableDependency($entity);
291   }
292
293   /**
294    * Convenience method, adds the configuration object's cache tag.
295    *
296    * @param \Drupal\Core\Config\ConfigBase $configuration
297    *   The configuration object whose cache tag to set on the access result.
298    *
299    * @return $this
300    *
301    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
302    *   ::addCacheableDependency() instead.
303    */
304   public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
305     return $this->addCacheableDependency($configuration);
306   }
307
308   /**
309    * {@inheritdoc}
310    */
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
316     //    result.
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
329     //    is forbidden!
330     if ($this->isForbidden() || $other->isForbidden()) {
331       $result = static::forbidden();
332       if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
333         $merge_other = TRUE;
334       }
335
336       if ($this->isForbidden() && $this instanceof AccessResultReasonInterface) {
337         $result->setReason($this->getReason());
338       }
339       elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface) {
340         $result->setReason($other->getReason());
341       }
342     }
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)) {
346         $merge_other = TRUE;
347       }
348     }
349     else {
350       $result = static::neutral();
351       if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
352         $merge_other = TRUE;
353         if ($other instanceof AccessResultReasonInterface) {
354           $result->setReason($other->getReason());
355         }
356       }
357       else {
358         if ($this instanceof AccessResultReasonInterface) {
359           $result->setReason($this->getReason());
360         }
361       }
362     }
363     $result->inheritCacheability($this);
364     if ($merge_other) {
365       $result->inheritCacheability($other);
366     }
367     return $result;
368   }
369
370   /**
371    * {@inheritdoc}
372    */
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
376     // result is used.
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());
383         }
384         $merge_other = TRUE;
385       }
386       else {
387         if ($this instanceof AccessResultReasonInterface) {
388           $result->setReason($this->getReason());
389         }
390       }
391     }
392     elseif ($this->isAllowed() && $other->isAllowed()) {
393       $result = static::allowed();
394       $merge_other = TRUE;
395     }
396     else {
397       $result = static::neutral();
398       if (!$this->isNeutral()) {
399         $merge_other = TRUE;
400         if ($other instanceof AccessResultReasonInterface) {
401           $result->setReason($other->getReason());
402         }
403       }
404       else {
405         if ($this instanceof AccessResultReasonInterface) {
406           $result->setReason($this->getReason());
407         }
408       }
409     }
410     $result->inheritCacheability($this);
411     if ($merge_other) {
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);
419       }
420     }
421     return $result;
422   }
423
424   /**
425    * Inherits the cacheability of the other access result, if any.
426    *
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
431    * as the end result.
432    *
433    * @param \Drupal\Core\Access\AccessResultInterface $other
434    *   The other access result, whose cacheability (if any) to inherit.
435    *
436    * @return $this
437    */
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()));
443       }
444       else {
445         $this->setCacheMaxAge($other->getCacheMaxAge());
446       }
447     }
448     return $this;
449   }
450
451 }