AccessResult.php 12.2 KB
Newer Older
1 2 3 4 5
<?php

namespace Drupal\Core\Access;

use Drupal\Core\Cache\Cache;
6
use Drupal\Core\Cache\CacheableDependencyInterface;
7 8
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
9
use Drupal\Core\Config\ConfigBase;
10 11 12 13 14 15
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Value object for passing an access result with cacheability metadata.
 *
16 17 18 19 20 21 22 23
 * The access result itself — excluding the cacheability metadata — is
 * immutable. There are subclasses for each of the three possible access results
 * themselves:
 *
 * @see \Drupal\Core\Access\AccessResultAllowed
 * @see \Drupal\Core\Access\AccessResultForbidden
 * @see \Drupal\Core\Access\AccessResultNeutral
 *
24 25 26
 * When using ::orIf() and ::andIf(), cacheability metadata will be merged
 * accordingly as well.
 */
27
abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
28

29
  use RefinableCacheableDependencyTrait;
30 31

  /**
32
   * Creates an AccessResultInterface object with isNeutral() === TRUE.
33 34
   *
   * @return \Drupal\Core\Access\AccessResult
35
   *   isNeutral() will be TRUE.
36
   */
37 38
  public static function neutral() {
    return new AccessResultNeutral();
39 40 41
  }

  /**
42
   * Creates an AccessResultInterface object with isAllowed() === TRUE.
43 44
   *
   * @return \Drupal\Core\Access\AccessResult
45
   *   isAllowed() will be TRUE.
46 47
   */
  public static function allowed() {
48
    return new AccessResultAllowed();
49 50 51
  }

  /**
52
   * Creates an AccessResultInterface object with isForbidden() === TRUE.
53
   *
54 55 56 57
   * @param string|null $reason
   *   (optional) The reason why access is forbidden. Intended for developers,
   *   hence not translatable.
   *
58
   * @return \Drupal\Core\Access\AccessResult
59
   *   isForbidden() will be TRUE.
60
   */
61 62 63
  public static function forbidden($reason = NULL) {
    assert('is_string($reason) || is_null($reason)');
    return new AccessResultForbidden($reason);
64 65 66
  }

  /**
67
   * Creates an allowed or neutral access result.
68 69
   *
   * @param bool $condition
70
   *   The condition to evaluate.
71 72
   *
   * @return \Drupal\Core\Access\AccessResult
73 74
   *   If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
   *   will be TRUE.
75 76
   */
  public static function allowedIf($condition) {
77
    return $condition ? static::allowed() : static::neutral();
78 79 80
  }

  /**
81
   * Creates a forbidden or neutral access result.
82 83
   *
   * @param bool $condition
84
   *   The condition to evaluate.
85 86
   *
   * @return \Drupal\Core\Access\AccessResult
87 88
   *   If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
   *   will be TRUE.
89 90
   */
  public static function forbiddenIf($condition) {
91
    return $condition ? static::forbidden() : static::neutral();
92 93 94
  }

  /**
95 96
   * Creates an allowed access result if the permission is present, neutral otherwise.
   *
97
   * Checks the permission and adds a 'user.permissions' cache context.
98 99 100 101 102 103 104
   *
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The account for which to check a permission.
   * @param string $permission
   *   The permission to check for.
   *
   * @return \Drupal\Core\Access\AccessResult
105
   *   If the account has the permission, isAllowed() will be TRUE, otherwise
106
   *   isNeutral() will be TRUE.
107 108
   */
  public static function allowedIfHasPermission(AccountInterface $account, $permission) {
109
    return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
110 111 112
  }

  /**
113 114
   * Creates an allowed access result if the permissions are present, neutral otherwise.
   *
115
   * Checks the permission and adds a 'user.permissions' cache contexts.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
   *
   * @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 (!$permission_access = $account->hasPermission($permission)) {
          $access = FALSE;
          break;
        }
      }
    }
    else {
      foreach ($permissions as $permission) {
        if ($permission_access = $account->hasPermission($permission)) {
          $access = TRUE;
          break;
        }
      }
    }

150
    return static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
151 152 153
  }

  /**
154
   * {@inheritdoc}
155 156
   *
   * @see \Drupal\Core\Access\AccessResultAllowed
157 158
   */
  public function isAllowed() {
159
    return FALSE;
160 161 162 163 164
  }

  /**
   * {@inheritdoc}
   *
165
   * @see \Drupal\Core\Access\AccessResultForbidden
166
   */
167 168
  public function isForbidden() {
    return FALSE;
169 170 171
  }

  /**
172
   * {@inheritdoc}
173
   *
174
   * @see \Drupal\Core\Access\AccessResultNeutral
175
   */
176 177
  public function isNeutral() {
    return FALSE;
178 179
  }

180 181 182 183
  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
184
    return $this->cacheContexts;
185 186 187 188 189 190
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
191
    return $this->cacheTags;
192 193 194 195 196 197
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
198
    return $this->cacheMaxAge;
199 200 201 202 203 204 205 206
  }

  /**
   * Resets cache contexts (to the empty array).
   *
   * @return $this
   */
  public function resetCacheContexts() {
207
    $this->cacheContexts = [];
208 209 210 211 212 213 214 215 216
    return $this;
  }

  /**
   * Resets cache tags (to the empty array).
   *
   * @return $this
   */
  public function resetCacheTags() {
217
    $this->cacheTags = [];
218 219 220 221 222 223 224 225 226 227 228 229
    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) {
230
    $this->cacheMaxAge = $max_age;
231 232 233 234
    return $this;
  }

  /**
235
   * Convenience method, adds the "user.permissions" cache context.
236 237 238
   *
   * @return $this
   */
239 240
  public function cachePerPermissions() {
    $this->addCacheContexts(array('user.permissions'));
241 242 243 244
    return $this;
  }

  /**
245
   * Convenience method, adds the "user" cache context.
246 247 248 249
   *
   * @return $this
   */
  public function cachePerUser() {
250
    $this->addCacheContexts(array('user'));
251 252 253 254 255 256 257 258 259 260
    return $this;
  }

  /**
   * Convenience method, adds the entity's cache tag.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity whose cache tag to set on the access result.
   *
   * @return $this
261 262 263
   *
   * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
   *   ::addCacheableDependency() instead.
264 265
   */
  public function cacheUntilEntityChanges(EntityInterface $entity) {
266
    return $this->addCacheableDependency($entity);
267 268
  }

269 270 271 272 273 274 275
  /**
   * Convenience method, adds the configuration object's cache tag.
   *
   * @param \Drupal\Core\Config\ConfigBase $configuration
   *   The configuration object whose cache tag to set on the access result.
   *
   * @return $this
276 277 278
   *
   * @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
   *   ::addCacheableDependency() instead.
279 280
   */
  public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
281 282 283
    return $this->addCacheableDependency($configuration);
  }

284 285 286 287
  /**
   * {@inheritdoc}
   */
  public function orIf(AccessResultInterface $other) {
288
    $merge_other = FALSE;
289
    // $other's cacheability metadata is merged if $merge_other gets set to TRUE
290
    // and this happens in three cases:
291 292 293 294
    // 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.
295 296 297 298 299 300 301 302 303 304 305
    // 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!
306 307
    if ($this->isForbidden() || $other->isForbidden()) {
      $result = static::forbidden();
308
      if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
309 310
        $merge_other = TRUE;
      }
311
    }
312 313
    elseif ($this->isAllowed() || $other->isAllowed()) {
      $result = static::allowed();
314
      if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
315
        $merge_other = TRUE;
316
      }
317 318 319
    }
    else {
      $result = static::neutral();
320
      if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
321
        $merge_other = TRUE;
322 323
      }
    }
324 325 326 327 328
    $result->inheritCacheability($this);
    if ($merge_other) {
      $result->inheritCacheability($other);
    }
    return $result;
329 330 331 332 333 334
  }

  /**
   * {@inheritdoc}
   */
  public function andIf(AccessResultInterface $other) {
335 336 337 338 339 340 341
    // 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()) {
342 343 344
        if ($other instanceof AccessResultReasonInterface) {
          $result->setReason($other->getReason());
        }
345 346
        $merge_other = TRUE;
      }
347 348 349 350 351
      else {
        if ($this instanceof AccessResultReasonInterface) {
          $result->setReason($this->getReason());
        }
      }
352 353 354 355
    }
    elseif ($this->isAllowed() && $other->isAllowed()) {
      $result = static::allowed();
      $merge_other = TRUE;
356 357
    }
    else {
358 359 360
      $result = static::neutral();
      if (!$this->isNeutral()) {
        $merge_other = TRUE;
361
      }
362 363 364 365 366 367 368 369
    }
    $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.
370 371
      if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
        $result->setCacheMaxAge(0);
372 373
      }
    }
374
    return $result;
375 376 377
  }

  /**
378
   * Inherits the cacheability of the other access result, if any.
379
   *
380 381 382 383 384 385
   * inheritCacheability() 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.
   *
386
   * @param \Drupal\Core\Access\AccessResultInterface $other
387 388 389
   *   The other access result, whose cacheability (if any) to inherit.
   *
   * @return $this
390
   */
391
  public function inheritCacheability(AccessResultInterface $other) {
392
    $this->addCacheableDependency($other);
393 394 395 396 397 398 399
    if ($other instanceof CacheableDependencyInterface) {
      if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
        $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
      }
      else {
        $this->setCacheMaxAge($other->getCacheMaxAge());
      }
400
    }
401
    return $this;
402 403 404
  }

}