AccessResult.php 11.8 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php
/**
 * @file
 * Contains \Drupal\Core\Access\AccessResult.
 */

namespace Drupal\Core\Access;

use Drupal\Core\Cache\Cache;
10
use Drupal\Core\Cache\CacheableDependencyInterface;
11
use Drupal\Core\Config\ConfigBase;
12 13 14 15 16 17
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;

/**
 * Value object for passing an access result with cacheability metadata.
 *
18 19 20 21 22 23 24 25
 * 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
 *
26 27 28
 * When using ::orIf() and ::andIf(), cacheability metadata will be merged
 * accordingly as well.
 */
29
abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

  /**
   * The cache context IDs (to vary a cache item ID based on active contexts).
   *
   * @see \Drupal\Core\Cache\CacheContextInterface
   * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
   *
   * @var string[]
   */
  protected $contexts;

  /**
   * The cache tags.
   *
   * @var array
   */
  protected $tags;

  /**
   * The maximum caching time in seconds.
   *
   * @var int
   */
  protected $maxAge;

  /**
   * Constructs a new AccessResult object.
   */
  public function __construct() {
59
    $this->resetCacheContexts()
60
      ->resetCacheTags()
61
      // Max-age must be non-zero for an access result to be cacheable.
62 63 64 65 66 67
      // Typically, cache items are invalidated via associated cache tags, not
      // via a maximum age.
      ->setCacheMaxAge(Cache::PERMANENT);
  }

  /**
68
   * Creates an AccessResultInterface object with isNeutral() === TRUE.
69 70
   *
   * @return \Drupal\Core\Access\AccessResult
71
   *   isNeutral() will be TRUE.
72
   */
73 74
  public static function neutral() {
    return new AccessResultNeutral();
75 76 77
  }

  /**
78
   * Creates an AccessResultInterface object with isAllowed() === TRUE.
79 80
   *
   * @return \Drupal\Core\Access\AccessResult
81
   *   isAllowed() will be TRUE.
82 83
   */
  public static function allowed() {
84
    return new AccessResultAllowed();
85 86 87
  }

  /**
88
   * Creates an AccessResultInterface object with isForbidden() === TRUE.
89 90
   *
   * @return \Drupal\Core\Access\AccessResult
91
   *   isForbidden() will be TRUE.
92 93
   */
  public static function forbidden() {
94
    return new AccessResultForbidden();
95 96 97
  }

  /**
98
   * Creates an allowed or neutral access result.
99 100
   *
   * @param bool $condition
101
   *   The condition to evaluate.
102 103
   *
   * @return \Drupal\Core\Access\AccessResult
104 105
   *   If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
   *   will be TRUE.
106 107
   */
  public static function allowedIf($condition) {
108
    return $condition ? static::allowed() : static::neutral();
109 110 111
  }

  /**
112
   * Creates a forbidden or neutral access result.
113 114
   *
   * @param bool $condition
115
   *   The condition to evaluate.
116 117
   *
   * @return \Drupal\Core\Access\AccessResult
118 119
   *   If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
   *   will be TRUE.
120 121
   */
  public static function forbiddenIf($condition) {
122
    return $condition ? static::forbidden(): static::neutral();
123 124 125
  }

  /**
126 127
   * Creates an allowed access result if the permission is present, neutral otherwise.
   *
128
   * Checks the permission and adds a 'user.permissions' cache context.
129 130 131 132 133 134 135
   *
   * @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
136
   *   If the account has the permission, isAllowed() will be TRUE, otherwise
137
   *   isNeutral() will be TRUE.
138 139
   */
  public static function allowedIfHasPermission(AccountInterface $account, $permission) {
140
    return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
141 142 143
  }

  /**
144 145
   * Creates an allowed access result if the permissions are present, neutral otherwise.
   *
146
   * Checks the permission and adds a 'user.permissions' cache contexts.
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
   *
   * @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;
        }
      }
    }

181
    return static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
182 183 184
  }

  /**
185
   * {@inheritdoc}
186 187
   *
   * @see \Drupal\Core\Access\AccessResultAllowed
188 189
   */
  public function isAllowed() {
190
    return FALSE;
191 192 193 194 195
  }

  /**
   * {@inheritdoc}
   *
196
   * @see \Drupal\Core\Access\AccessResultForbidden
197
   */
198 199
  public function isForbidden() {
    return FALSE;
200 201 202
  }

  /**
203
   * {@inheritdoc}
204
   *
205
   * @see \Drupal\Core\Access\AccessResultNeutral
206
   */
207 208
  public function isNeutral() {
    return FALSE;
209 210
  }

211 212 213 214
  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
    sort($this->contexts);
    return $this->contexts;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return $this->tags;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return $this->maxAge;
  }

  /**
   * Adds cache contexts associated with the access result.
   *
   * @param string[] $contexts
   *   An array of cache context IDs, used to generate a cache ID.
   *
   * @return $this
   */
  public function addCacheContexts(array $contexts) {
    $this->contexts = array_unique(array_merge($this->contexts, $contexts));
    return $this;
  }

  /**
   * Resets cache contexts (to the empty array).
   *
   * @return $this
   */
  public function resetCacheContexts() {
    $this->contexts = array();
    return $this;
  }

  /**
   * Adds cache tags associated with the access result.
   *
   * @param array $tags
   *   An array of cache tags.
   *
   * @return $this
   */
  public function addCacheTags(array $tags) {
265
    $this->tags = Cache::mergeTags($this->tags, $tags);
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
    return $this;
  }

  /**
   * Resets cache tags (to the empty array).
   *
   * @return $this
   */
  public function resetCacheTags() {
    $this->tags = array();
    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) {
    $this->maxAge = $max_age;
    return $this;
  }

  /**
293
   * Convenience method, adds the "user.permissions" cache context.
294 295 296
   *
   * @return $this
   */
297 298
  public function cachePerPermissions() {
    $this->addCacheContexts(array('user.permissions'));
299 300 301 302
    return $this;
  }

  /**
303
   * Convenience method, adds the "user" cache context.
304 305 306 307
   *
   * @return $this
   */
  public function cachePerUser() {
308
    $this->addCacheContexts(array('user'));
309 310 311 312 313 314 315 316 317 318 319 320
    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
   */
  public function cacheUntilEntityChanges(EntityInterface $entity) {
321
    $this->addCacheTags($entity->getCacheTags());
322 323 324
    return $this;
  }

325 326 327 328 329 330 331 332 333 334 335 336 337
  /**
   * 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
   */
  public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
    $this->addCacheTags($configuration->getCacheTags());
    return $this;
  }

338 339 340 341
  /**
   * {@inheritdoc}
   */
  public function orIf(AccessResultInterface $other) {
342 343 344 345 346 347 348 349 350
    // $other's cacheability metadata is merged if $merge_other gets set to TRUE
    // and this happens in two cases:
    // 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.
    $merge_other = FALSE;
    if ($this->isForbidden() || $other->isForbidden()) {
      $result = static::forbidden();
351
      if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
352 353
        $merge_other = TRUE;
      }
354
    }
355 356
    elseif ($this->isAllowed() || $other->isAllowed()) {
      $result = static::allowed();
357
      if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed())) {
358
        $merge_other = TRUE;
359
      }
360 361 362
    }
    else {
      $result = static::neutral();
363
      if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral())) {
364
        $merge_other = TRUE;
365 366
      }
    }
367 368 369 370 371
    $result->inheritCacheability($this);
    if ($merge_other) {
      $result->inheritCacheability($other);
    }
    return $result;
372 373 374 375 376 377
  }

  /**
   * {@inheritdoc}
   */
  public function andIf(AccessResultInterface $other) {
378 379 380 381 382 383 384 385 386 387 388 389 390
    // 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()) {
        $merge_other = TRUE;
      }
    }
    elseif ($this->isAllowed() && $other->isAllowed()) {
      $result = static::allowed();
      $merge_other = TRUE;
391 392
    }
    else {
393 394 395
      $result = static::neutral();
      if (!$this->isNeutral()) {
        $merge_other = TRUE;
396
      }
397 398 399 400 401 402 403 404
    }
    $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.
405 406
      if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
        $result->setCacheMaxAge(0);
407 408
      }
    }
409
    return $result;
410 411 412
  }

  /**
413
   * Inherits the cacheability of the other access result, if any.
414 415
   *
   * @param \Drupal\Core\Access\AccessResultInterface $other
416 417 418
   *   The other access result, whose cacheability (if any) to inherit.
   *
   * @return $this
419
   */
420
  public function inheritCacheability(AccessResultInterface $other) {
421 422 423 424 425 426 427
    if ($other instanceof CacheableDependencyInterface) {
      if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
        $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
      }
      else {
        $this->setCacheMaxAge($other->getCacheMaxAge());
      }
428
      $this->addCacheContexts($other->getCacheContexts());
429 430 431 432 433 434
      $this->addCacheTags($other->getCacheTags());
    }
    // If any of the access results don't provide cacheability metadata, then
    // we cannot cache the combined access result, for we may not make
    // assumptions.
    else {
435
      $this->setCacheMaxAge(0);
436
    }
437
    return $this;
438 439 440
  }

}