Newer
Older
catch
committed
<?php
namespace Drupal\Core\Access;
use Drupal\Core\Cache\Cache;
Alex Pott
committed
use Drupal\Core\Cache\CacheableDependencyInterface;
Alex Pott
committed
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Config\ConfigBase;
catch
committed
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Value object for passing an access result with cacheability metadata.
*
Alex Pott
committed
* 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
*
catch
committed
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
* accordingly as well.
*/
Alex Pott
committed
abstract class AccessResult implements AccessResultInterface, RefinableCacheableDependencyInterface {
catch
committed
Alex Pott
committed
use RefinableCacheableDependencyTrait;
catch
committed
/**
Alex Pott
committed
* Creates an AccessResultInterface object with isNeutral() === TRUE.
catch
committed
*
Alex Pott
committed
* @param string|null $reason
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable.
*
* @return \Drupal\Core\Access\AccessResultNeutral
Alex Pott
committed
* isNeutral() will be TRUE.
catch
committed
*/
Alex Pott
committed
public static function neutral($reason = NULL) {
assert(is_string($reason) || is_null($reason));
Alex Pott
committed
return new AccessResultNeutral($reason);
catch
committed
}
/**
Alex Pott
committed
* Creates an AccessResultInterface object with isAllowed() === TRUE.
catch
committed
*
Alex Pott
committed
* @return \Drupal\Core\Access\AccessResultAllowed
Alex Pott
committed
* isAllowed() will be TRUE.
catch
committed
*/
public static function allowed() {
Alex Pott
committed
return new AccessResultAllowed();
catch
committed
}
/**
Alex Pott
committed
* Creates an AccessResultInterface object with isForbidden() === TRUE.
catch
committed
*
* @param string|null $reason
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable.
*
Alex Pott
committed
* @return \Drupal\Core\Access\AccessResultForbidden
Alex Pott
committed
* isForbidden() will be TRUE.
catch
committed
*/
public static function forbidden($reason = NULL) {
assert(is_string($reason) || is_null($reason));
return new AccessResultForbidden($reason);
catch
committed
}
/**
Alex Pott
committed
* Creates an allowed or neutral access result.
catch
committed
*
* @param bool $condition
Alex Pott
committed
* The condition to evaluate.
catch
committed
*
* @return \Drupal\Core\Access\AccessResult
Alex Pott
committed
* If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
* will be TRUE.
catch
committed
*/
public static function allowedIf($condition) {
Alex Pott
committed
return $condition ? static::allowed() : static::neutral();
catch
committed
}
/**
Alex Pott
committed
* Creates a forbidden or neutral access result.
catch
committed
*
* @param bool $condition
Alex Pott
committed
* The condition to evaluate.
* @param string|null $reason
* (optional) The reason why access is forbidden. Intended for developers,
* hence not translatable
catch
committed
*
* @return \Drupal\Core\Access\AccessResult
Alex Pott
committed
* If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
* will be TRUE.
catch
committed
*/
public static function forbiddenIf($condition, $reason = NULL) {
return $condition ? static::forbidden($reason) : static::neutral();
catch
committed
}
/**
Alex Pott
committed
* Creates an allowed access result if the permission is present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache context.
catch
committed
*
* @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
* If the account has the permission, isAllowed() will be TRUE, otherwise
Alex Pott
committed
* isNeutral() will be TRUE.
catch
committed
*/
public static function allowedIfHasPermission(AccountInterface $account, $permission) {
Alex Pott
committed
$access_result = static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
if ($access_result instanceof AccessResultReasonInterface) {
$access_result->setReason("The '$permission' permission is required.");
}
return $access_result;
catch
committed
}
/**
* Creates an allowed access result if the permissions are present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache contexts.
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
*
* @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;
}
}
}
Alex Pott
committed
$access_result = static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
if ($access_result instanceof AccessResultReasonInterface) {
if (count($permissions) === 1) {
$access_result->setReason("The '$permission' permission is required.");
}
elseif (count($permissions) > 1) {
$quote = function ($s) {
return "'$s'";
};
$access_result->setReason(sprintf("The following permissions are required: %s.", implode(" $conjunction ", array_map($quote, $permissions))));
}
}
return $access_result;
}
/**
catch
committed
* {@inheritdoc}
Alex Pott
committed
*
* @see \Drupal\Core\Access\AccessResultAllowed
catch
committed
*/
public function isAllowed() {
Alex Pott
committed
return FALSE;
catch
committed
}
/**
* {@inheritdoc}
*
Alex Pott
committed
* @see \Drupal\Core\Access\AccessResultForbidden
catch
committed
*/
Alex Pott
committed
public function isForbidden() {
return FALSE;
catch
committed
}
/**
Alex Pott
committed
* {@inheritdoc}
catch
committed
*
Alex Pott
committed
* @see \Drupal\Core\Access\AccessResultNeutral
catch
committed
*/
Alex Pott
committed
public function isNeutral() {
return FALSE;
catch
committed
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
Alex Pott
committed
return $this->cacheContexts;
catch
committed
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
Alex Pott
committed
return $this->cacheTags;
catch
committed
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
Alex Pott
committed
return $this->cacheMaxAge;
catch
committed
}
/**
* Resets cache contexts (to the empty array).
*
* @return $this
*/
public function resetCacheContexts() {
Alex Pott
committed
$this->cacheContexts = [];
catch
committed
return $this;
}
/**
* Resets cache tags (to the empty array).
*
* @return $this
*/
public function resetCacheTags() {
Alex Pott
committed
$this->cacheTags = [];
catch
committed
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) {
Alex Pott
committed
$this->cacheMaxAge = $max_age;
catch
committed
return $this;
}
/**
* Convenience method, adds the "user.permissions" cache context.
catch
committed
*
* @return $this
*/
public function cachePerPermissions() {
$this->addCacheContexts(['user.permissions']);
catch
committed
return $this;
}
/**
* Convenience method, adds the "user" cache context.
catch
committed
*
* @return $this
*/
public function cachePerUser() {
$this->addCacheContexts(['user']);
catch
committed
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
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
* ::addCacheableDependency() instead.
catch
committed
*/
public function cacheUntilEntityChanges(EntityInterface $entity) {
return $this->addCacheableDependency($entity);
catch
committed
}
/**
* 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
*
* @deprecated in Drupal 8.0.x-dev, will be removed before Drupal 9.0.0. Use
* ::addCacheableDependency() instead.
*/
public function cacheUntilConfigurationChanges(ConfigBase $configuration) {
return $this->addCacheableDependency($configuration);
}
catch
committed
/**
* {@inheritdoc}
*/
public function orIf(AccessResultInterface $other) {
$merge_other = FALSE;
Alex Pott
committed
// $other's cacheability metadata is merged if $merge_other gets set to TRUE
// and this happens in three cases:
Alex Pott
committed
// 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.
// 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!
Alex Pott
committed
if ($this->isForbidden() || $other->isForbidden()) {
$result = static::forbidden();
Alex Pott
committed
if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
Alex Pott
committed
$merge_other = TRUE;
}
Alex Pott
committed
if ($this->isForbidden() && $this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
Alex Pott
committed
$result->setReason($this->getReason());
}
elseif ($other->isForbidden() && $other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
Alex Pott
committed
$result->setReason($other->getReason());
}
catch
committed
}
Alex Pott
committed
elseif ($this->isAllowed() || $other->isAllowed()) {
$result = static::allowed();
if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
Alex Pott
committed
$merge_other = TRUE;
catch
committed
}
Alex Pott
committed
}
else {
$result = static::neutral();
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
Alex Pott
committed
$merge_other = TRUE;
Alex Pott
committed
}
if ($this instanceof AccessResultReasonInterface && !is_null($this->getReason())) {
$result->setReason($this->getReason());
}
elseif ($other instanceof AccessResultReasonInterface && !is_null($other->getReason())) {
$result->setReason($other->getReason());
catch
committed
}
}
Alex Pott
committed
$result->inheritCacheability($this);
if ($merge_other) {
$result->inheritCacheability($other);
}
return $result;
catch
committed
}
/**
* {@inheritdoc}
*/
public function andIf(AccessResultInterface $other) {
Alex Pott
committed
// 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()) {
if ($other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
Alex Pott
committed
$merge_other = TRUE;
}
else {
if ($this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
}
Alex Pott
committed
}
elseif ($this->isAllowed() && $other->isAllowed()) {
$result = static::allowed();
$merge_other = TRUE;
catch
committed
}
else {
Alex Pott
committed
$result = static::neutral();
if (!$this->isNeutral()) {
$merge_other = TRUE;
Alex Pott
committed
if ($other instanceof AccessResultReasonInterface) {
$result->setReason($other->getReason());
}
}
else {
if ($this instanceof AccessResultReasonInterface) {
$result->setReason($this->getReason());
}
catch
committed
}
Alex Pott
committed
}
$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.
Alex Pott
committed
if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
$result->setCacheMaxAge(0);
catch
committed
}
}
Alex Pott
committed
return $result;
catch
committed
}
/**
Alex Pott
committed
* Inherits the cacheability of the other access result, if any.
catch
committed
*
* This method 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
Alex Pott
committed
* "allowed, max-age=0 OR allowed, max-age=1000" needs to yield max-age 1000
* as the end result.
*
catch
committed
* @param \Drupal\Core\Access\AccessResultInterface $other
Alex Pott
committed
* The other access result, whose cacheability (if any) to inherit.
*
* @return $this
catch
committed
*/
Alex Pott
committed
public function inheritCacheability(AccessResultInterface $other) {
Alex Pott
committed
$this->addCacheableDependency($other);
Alex Pott
committed
if ($other instanceof CacheableDependencyInterface) {
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
else {
$this->setCacheMaxAge($other->getCacheMaxAge());
}
catch
committed
}
Alex Pott
committed
return $this;