Pull merge.
[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    * @param string|null $reason
91    *   (optional) The reason why access is forbidden. Intended for developers,
92    *   hence not translatable
93    *
94    * @return \Drupal\Core\Access\AccessResult
95    *   If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
96    *   will be TRUE.
97    */
98   public static function forbiddenIf($condition, $reason = NULL) {
99     return $condition ? static::forbidden($reason) : static::neutral();
100   }
101
102   /**
103    * Creates an allowed access result if the permission is present, neutral otherwise.
104    *
105    * Checks the permission and adds a 'user.permissions' cache context.
106    *
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.
111    *
112    * @return \Drupal\Core\Access\AccessResult
113    *   If the account has the permission, isAllowed() will be TRUE, otherwise
114    *   isNeutral() will be TRUE.
115    */
116   public static function allowedIfHasPermission(AccountInterface $account, $permission) {
117     $access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
118
119     if ($access_result instanceof AccessResultReasonInterface) {
120       $access_result->setReason("The '$permission' permission is required.");
121     }
122     return $access_result;
123   }
124
125   /**
126    * Creates an allowed access result if the permissions are present, neutral otherwise.
127    *
128    * Checks the permission and adds a 'user.permissions' cache contexts.
129    *
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.
136    *   Defaults to 'AND'
137    *
138    * @return \Drupal\Core\Access\AccessResult
139    *   If the account has the permissions, isAllowed() will be TRUE, otherwise
140    *   isNeutral() will be TRUE.
141    */
142   public static function allowedIfHasPermissions(AccountInterface $account, array $permissions, $conjunction = 'AND') {
143     $access = FALSE;
144
145     if ($conjunction == 'AND' && !empty($permissions)) {
146       $access = TRUE;
147       foreach ($permissions as $permission) {
148         if (!$permission_access = $account->hasPermission($permission)) {
149           $access = FALSE;
150           break;
151         }
152       }
153     }
154     else {
155       foreach ($permissions as $permission) {
156         if ($permission_access = $account->hasPermission($permission)) {
157           $access = TRUE;
158           break;
159         }
160       }
161     }
162
163     $access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
164
165     if ($access_result instanceof AccessResultReasonInterface) {
166       if (count($permissions) === 1) {
167         $access_result->setReason("The '$permission' permission is required.");
168       }
169       elseif (count($permissions) > 1) {
170         $quote = function ($s) {
171           return "'$s'";
172         };
173         $access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
174       }
175     }
176
177     return $access_result;
178   }
179
180   /**
181    * {@inheritdoc}
182    *
183    * @see \Drupal\Core\Access\AccessResultAllowed
184    */
185   public function isAllowed() {
186     return FALSE;
187   }
188
189   /**
190    * {@inheritdoc}
191    *
192    * @see \Drupal\Core\Access\AccessResultForbidden
193    */
194   public function isForbidden() {
195     return FALSE;
196   }
197
198   /**
199    * {@inheritdoc}
200    *
201    * @see \Drupal\Core\Access\AccessResultNeutral
202    */
203   public function isNeutral() {
204     return FALSE;
205   }
206
207   /**
208    * {@inheritdoc}
209    */
210   public function getCacheContexts() {
211     return $this->cacheContexts;
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function getCacheTags() {
218     return $this->cacheTags;
219   }
220
221   /**
222    * {@inheritdoc}
223    */
224   public function getCacheMaxAge() {
225     return $this->cacheMaxAge;
226   }
227
228   /**
229    * Resets cache contexts (to the empty array).
230    *
231    * @return $this
232    */
233   public function resetCacheContexts() {
234     $this->cacheContexts = [];
235     return $this;
236   }
237
238   /**
239    * Resets cache tags (to the empty array).
240    *
241    * @return $this
242    */
243   public function resetCacheTags() {
244     $this->cacheTags = [];
245     return $this;
246   }
247
248   /**
249    * Sets the maximum age for which this access result may be cached.
250    *
251    * @param int $max_age
252    *   The maximum time in seconds that this access result may be cached.
253    *
254    * @return $this
255    */
256   public function setCacheMaxAge($max_age) {
257     $this->cacheMaxAge = $max_age;
258     return $this;
259   }
260
261   /**
262    * Convenience method, adds the "user.permissions" cache context.
263    *
264    * @return $this
265    */
266   public function cachePerPermissions() {
267     $this->addCacheContexts(['user.permissions']);
268     return $this;
269   }
270
271   /**
272    * Convenience method, adds the "user" cache context.
273    *
274    * @return $this
275    */
276   public function cachePerUser() {
277     $this->addCacheContexts(['user']);
278     return $this;
279   }
280
281   /**
282    * Convenience method, adds the entity's cache tag.
283    *
284    * @param \Drupal\Core\Entity\EntityInterface $entity
285    *   The entity whose cache tag to set on the access result.
286    *
287    * @return $this
288    *
289    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
290    *   ::addCacheableDependency() instead.
291    */
292   public function cacheUntilEntityChanges(EntityInterface $entity) {
293     return $this->addCacheableDependency($entity);
294   }
295
296   /**
297    * Convenience method, adds the configuration object's cache tag.
298    *
299    * @param \Drupal\Core\Config\ConfigBase $configuration
300    *   The configuration object whose cache tag to set on the access result.
301    *
302    * @return $this
303    *
304    * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
305    *   ::addCacheableDependency() instead.
306    */
307   public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
308     return $this->addCacheableDependency($configuration);
309   }
310
311   /**
312    * {@inheritdoc}
313    */
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
319     //    result.
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
332     //    is forbidden!
333     if ($this->isForbidden() || $other->isForbidden()) {
334       $result = static::forbidden();
335       if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
336         $merge_other = TRUE;
337       }
338
339       if ($this->isForbidden() && $this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
340         $result->setReason($this->getReason());
341       }
342       elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
343         $result->setReason($other->getReason());
344       }
345     }
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)) {
349         $merge_other = TRUE;
350       }
351     }
352     else {
353       $result = static::neutral();
354       if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
355         $merge_other = TRUE;
356       }
357
358       if ($this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
359         $result->setReason($this->getReason());
360       }
361       elseif ($other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
362         $result->setReason($other->getReason());
363       }
364     }
365     $result->inheritCacheability($this);
366     if ($merge_other) {
367       $result->inheritCacheability($other);
368     }
369     return $result;
370   }
371
372   /**
373    * {@inheritdoc}
374    */
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
378     // result is used.
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());
385         }
386         $merge_other = TRUE;
387       }
388       else {
389         if ($this instanceof AccessResultReasonInterface) {
390           $result->setReason($this->getReason());
391         }
392       }
393     }
394     elseif ($this->isAllowed() && $other->isAllowed()) {
395       $result = static::allowed();
396       $merge_other = TRUE;
397     }
398     else {
399       $result = static::neutral();
400       if (!$this->isNeutral()) {
401         $merge_other = TRUE;
402         if ($other instanceof AccessResultReasonInterface) {
403           $result->setReason($other->getReason());
404         }
405       }
406       else {
407         if ($this instanceof AccessResultReasonInterface) {
408           $result->setReason($this->getReason());
409         }
410       }
411     }
412     $result->inheritCacheability($this);
413     if ($merge_other) {
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);
421       }
422     }
423     return $result;
424   }
425
426   /**
427    * Inherits the cacheability of the other access result, if any.
428    *
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
433    * as the end result.
434    *
435    * @param \Drupal\Core\Access\AccessResultInterface $other
436    *   The other access result, whose cacheability (if any) to inherit.
437    *
438    * @return $this
439    */
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()));
445       }
446       else {
447         $this->setCacheMaxAge($other->getCacheMaxAge());
448       }
449     }
450     return $this;
451   }
452
453 }