Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,45 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessArgumentsResolverFactory.
*/
namespace Drupal\Core\Access;
use Drupal\Component\Utility\ArgumentsResolver;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Resolves the arguments to pass to an access check callable.
*/
class AccessArgumentsResolverFactory implements AccessArgumentsResolverFactoryInterface {
/**
* {@inheritdoc}
*/
public function getArgumentsResolver(RouteMatchInterface $route_match, AccountInterface $account, Request $request = NULL) {
$route = $route_match->getRouteObject();
// Defaults for the parameters defined on the route object need to be added
// to the raw arguments.
$raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
$upcasted_route_arguments = $route_match->getParameters()->all();
// Parameters which are not defined on the route object, but still are
// essential for access checking are passed as wildcards to the argument
// resolver. An access-check method with a parameter of type Route,
// RouteMatchInterface, AccountInterface or Request will receive those
// arguments regardless of the parameter name.
$wildcard_arguments = [$route, $route_match, $account];
if (isset($request)) {
$wildcard_arguments[] = $request;
}
return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Constructs the arguments resolver instance to use when running access checks.
*/
interface AccessArgumentsResolverFactoryInterface {
/**
* Returns the arguments resolver to use when running access checks.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object to be checked.
* @param \Drupal\Core\Session\AccountInterface $account
* The account being checked.
* @param \Symfony\Component\HttpFoundation\Request $request
* Optional, the request object.
*
* @return \Drupal\Component\Utility\ArgumentsResolverInterface
* The parametrized arguments resolver instance.
*/
public function getArgumentsResolver(RouteMatchInterface $route_match, AccountInterface $account, Request $request = NULL);
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessCheckInterface.
*/
namespace Drupal\Core\Access;
use Symfony\Component\Routing\Route;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
/**
* An access check service determines access rules for particular routes.
*/
interface AccessCheckInterface extends RoutingAccessInterface {
/**
* Declares whether the access check applies to a specific route or not.
*
* @param \Symfony\Component\Routing\Route $route
* The route to consider attaching to.
*
* @return array
* An array of route requirement keys this access checker applies to.
*/
public function applies(Route $route);
}

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessException.
*/
namespace Drupal\Core\Access;
/**
* An exception thrown for access errors.
*
* Examples could be invalid access callback return values, or invalid access
* objects being used.
*/
class AccessException extends \RuntimeException {
}

View file

@ -0,0 +1,178 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessManager.
*/
namespace Drupal\Core\Access;
use Drupal\Core\ParamConverter\ParamConverterManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\RouteMatch;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Utility\ArgumentsResolverInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
/**
* Attaches access check services to routes and runs them on request.
*
* @see \Drupal\Tests\Core\Access\AccessManagerTest
*/
class AccessManager implements AccessManagerInterface {
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* The paramconverter manager.
*
* @var \Drupal\Core\ParamConverter\ParamConverterManagerInterface
*/
protected $paramConverterManager;
/**
* The access arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface
*/
protected $argumentsResolverFactory;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The check provider.
*
* @var \Drupal\Core\Access\CheckProviderInterface
*/
protected $checkProvider;
/**
* Constructs a AccessManager instance.
*
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
* @param \Drupal\Core\ParamConverter\ParamConverterManagerInterface $paramconverter_manager
* The param converter manager.
* @param \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface $arguments_resolver_factory
* The access arguments resolver.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
* @param CheckProviderInterface $check_provider
*/
public function __construct(RouteProviderInterface $route_provider, ParamConverterManagerInterface $paramconverter_manager, AccessArgumentsResolverFactoryInterface $arguments_resolver_factory, AccountInterface $current_user, CheckProviderInterface $check_provider) {
$this->routeProvider = $route_provider;
$this->paramConverterManager = $paramconverter_manager;
$this->argumentsResolverFactory = $arguments_resolver_factory;
$this->currentUser = $current_user;
$this->checkProvider = $check_provider;
}
/**
* {@inheritdoc}
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, $return_as_object = FALSE) {
try {
$route = $this->routeProvider->getRouteByName($route_name, $parameters);
// ParamConverterManager relies on the route name and object being
// available from the parameters array.
$parameters[RouteObjectInterface::ROUTE_NAME] = $route_name;
$parameters[RouteObjectInterface::ROUTE_OBJECT] = $route;
$upcasted_parameters = $this->paramConverterManager->convert($parameters + $route->getDefaults());
$route_match = new RouteMatch($route_name, $route, $upcasted_parameters, $parameters);
return $this->check($route_match, $account, NULL, $return_as_object);
}
catch (RouteNotFoundException $e) {
// Cacheable until extensions change.
$result = AccessResult::forbidden()->addCacheTags(['config:core.extension']);
return $return_as_object ? $result : $result->isAllowed();
}
catch (ParamNotConvertedException $e) {
// Uncacheable because conversion of the parameter may not have been
// possible due to dynamic circumstances.
$result = AccessResult::forbidden()->setCacheMaxAge(0);
return $return_as_object ? $result : $result->isAllowed();
}
}
/**
* {@inheritdoc}
*/
public function checkRequest(Request $request, AccountInterface $account = NULL, $return_as_object = FALSE) {
$route_match = RouteMatch::createFromRequest($request);
return $this->check($route_match, $account, $request, $return_as_object);
}
/**
* {@inheritdoc}
*/
public function check(RouteMatchInterface $route_match, AccountInterface $account = NULL, Request $request = NULL, $return_as_object = FALSE) {
if (!isset($account)) {
$account = $this->currentUser;
}
$route = $route_match->getRouteObject();
$checks = $route->getOption('_access_checks') ?: array();
// Filter out checks which require the incoming request.
if (!isset($request)) {
$checks = array_diff($checks, $this->checkProvider->getChecksNeedRequest());
}
$result = AccessResult::neutral();
if (!empty($checks)) {
$arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account, $request);
if (!$checks) {
return AccessResult::neutral();
}
$result = AccessResult::allowed();
foreach ($checks as $service_id) {
$result = $result->andIf($this->performCheck($service_id, $arguments_resolver));
}
}
return $return_as_object ? $result : $result->isAllowed();
}
/**
* Performs the specified access check.
*
* @param string $service_id
* The access check service ID to use.
* @param \Drupal\Component\Utility\ArgumentsResolverInterface $arguments_resolver
* The parametrized arguments resolver instance.
*
* @throws \Drupal\Core\Access\AccessException
* Thrown when the access check returns an invalid value.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
protected function performCheck($service_id, ArgumentsResolverInterface $arguments_resolver) {
$callable = $this->checkProvider->loadCheck($service_id);
$arguments = $arguments_resolver->getArguments($callable);
/** @var \Drupal\Core\Access\AccessResultInterface $service_access **/
$service_access = call_user_func_array($callable, $arguments);
if (!$service_access instanceof AccessResultInterface) {
throw new AccessException("Access error in $service_id. Access services must return an object that implements AccessResultInterface.");
}
return $service_access;
}
}

View file

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessManagerInterface.
*/
namespace Drupal\Core\Access;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Provides an interface for attaching and running access check services.
*/
interface AccessManagerInterface {
/**
* Checks a named route with parameters against applicable access check services.
*
* Determines whether the route is accessible or not.
*
* @param string $route_name
* The route to check access to.
* @param array $parameters
* Optional array of values to substitute into the route path pattern.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* Execute access checks against the incoming request.
*
* @param Request $request
* The incoming request.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function checkRequest(Request $request, AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* Checks a route against applicable access check services.
*
* Determines whether the route is accessible or not.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
* @param \Symfony\Component\HttpFoundation\Request $request
* Optional, a request. Only supply this parameter when checking the
* incoming request, do not specify when checking routes on output.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function check(RouteMatchInterface $route_match, AccountInterface $account = NULL, Request $request = NULL, $return_as_object = FALSE);
}

View file

@ -0,0 +1,451 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessResult.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Config\ConfigBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Value object for passing an access result with cacheability metadata.
*
* 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
*
* When using ::orIf() and ::andIf(), cacheability metadata will be merged
* accordingly as well.
*/
abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
/**
* The cache context IDs (to vary a cache item ID based on active contexts).
*
* @see \Drupal\Core\Cache\Context\CacheContextInterface
* @see \Drupal\Core\Cache\Context\CacheContextsManager::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() {
$this->resetCacheContexts()
->resetCacheTags()
// Max-age must be non-zero for an access result to be cacheable.
// Typically, cache items are invalidated via associated cache tags, not
// via a maximum age.
->setCacheMaxAge(Cache::PERMANENT);
}
/**
* Creates an AccessResultInterface object with isNeutral() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* isNeutral() will be TRUE.
*/
public static function neutral() {
return new AccessResultNeutral();
}
/**
* Creates an AccessResultInterface object with isAllowed() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* isAllowed() will be TRUE.
*/
public static function allowed() {
return new AccessResultAllowed();
}
/**
* Creates an AccessResultInterface object with isForbidden() === TRUE.
*
* @return \Drupal\Core\Access\AccessResult
* isForbidden() will be TRUE.
*/
public static function forbidden() {
return new AccessResultForbidden();
}
/**
* Creates an allowed or neutral access result.
*
* @param bool $condition
* The condition to evaluate.
*
* @return \Drupal\Core\Access\AccessResult
* If $condition is TRUE, isAllowed() will be TRUE, otherwise isNeutral()
* will be TRUE.
*/
public static function allowedIf($condition) {
return $condition ? static::allowed() : static::neutral();
}
/**
* Creates a forbidden or neutral access result.
*
* @param bool $condition
* The condition to evaluate.
*
* @return \Drupal\Core\Access\AccessResult
* If $condition is TRUE, isForbidden() will be TRUE, otherwise isNeutral()
* will be TRUE.
*/
public static function forbiddenIf($condition) {
return $condition ? static::forbidden(): static::neutral();
}
/**
* Creates an allowed access result if the permission is present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache context.
*
* @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
* isNeutral() will be TRUE.
*/
public static function allowedIfHasPermission(AccountInterface $account, $permission) {
return static::allowedIf($account->hasPermission($permission))->addCacheContexts(['user.permissions']);
}
/**
* Creates an allowed access result if the permissions are present, neutral otherwise.
*
* Checks the permission and adds a 'user.permissions' cache contexts.
*
* @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;
}
}
}
return static::allowedIf($access)->addCacheContexts(empty($permissions) ? [] : ['user.permissions']);
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultAllowed
*/
public function isAllowed() {
return FALSE;
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultForbidden
*/
public function isForbidden() {
return FALSE;
}
/**
* {@inheritdoc}
*
* @see \Drupal\Core\Access\AccessResultNeutral
*/
public function isNeutral() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
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) {
$this->tags = Cache::mergeTags($this->tags, $tags);
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;
}
/**
* Convenience method, adds the "user.permissions" cache context.
*
* @return $this
*/
public function cachePerPermissions() {
$this->addCacheContexts(array('user.permissions'));
return $this;
}
/**
* Convenience method, adds the "user" cache context.
*
* @return $this
*/
public function cachePerUser() {
$this->addCacheContexts(array('user'));
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) {
$this->addCacheTags($entity->getCacheTags());
return $this;
}
/**
* 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;
}
/**
* {@inheritdoc}
*/
public function orIf(AccessResultInterface $other) {
$merge_other = FALSE;
// $other's cacheability metadata is merged if $merge_other gets set to TRUE
// and this happens in three 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.
// 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!
if ($this->isForbidden() || $other->isForbidden()) {
$result = static::forbidden();
if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
$merge_other = TRUE;
}
}
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)) {
$merge_other = TRUE;
}
}
else {
$result = static::neutral();
if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral()) || ($this->getCacheMaxAge() !== 0 && $other instanceof CacheableDependencyInterface && $other->getCacheMaxAge() !== 0)) {
$merge_other = TRUE;
}
}
$result->inheritCacheability($this);
if ($merge_other) {
$result->inheritCacheability($other);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function andIf(AccessResultInterface $other) {
// 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;
}
else {
$result = static::neutral();
if (!$this->isNeutral()) {
$merge_other = TRUE;
}
}
$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.
if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
$result->setCacheMaxAge(0);
}
}
return $result;
}
/**
* Inherits the cacheability of the other access result, if any.
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result, whose cacheability (if any) to inherit.
*
* @return $this
*/
public function inheritCacheability(AccessResultInterface $other) {
if ($other instanceof CacheableDependencyInterface) {
if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
$this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
}
else {
$this->setCacheMaxAge($other->getCacheMaxAge());
}
$this->addCacheContexts($other->getCacheContexts());
$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 {
$this->setCacheMaxAge(0);
}
return $this;
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessResultAllowed.
*/
namespace Drupal\Core\Access;
/**
* Value object indicating an allowed access result, with cacheability metadata.
*/
class AccessResultAllowed extends AccessResult {
/**
* {@inheritdoc}
*/
public function isAllowed() {
return TRUE;
}
}

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessResultForbidden.
*/
namespace Drupal\Core\Access;
/**
* Value object indicating a forbidden access result, with cacheability metadata.
*/
class AccessResultForbidden extends AccessResult {
/**
* {@inheritdoc}
*/
public function isForbidden() {
return TRUE;
}
}

View file

@ -0,0 +1,103 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessResultInterface.
*/
namespace Drupal\Core\Access;
/**
* Interface for access result value objects.
*
* IMPORTANT NOTE: You have to call isAllowed() when you want to know whether
* someone has access. Just using
* @code
* if ($access_result) {
* // The user has access!
* }
* else {
* // The user doesn't have access!
* }
* @endcode
* would never enter the else-statement and hence introduce a critical security
* issue.
*/
interface AccessResultInterface {
/**
* Checks whether this access result indicates access is explicitly allowed.
*
* @return bool
* When TRUE then isForbidden() and isNeutral() are FALSE.
*/
public function isAllowed();
/**
* Checks whether this access result indicates access is explicitly forbidden.
*
* This is a kill switch both orIf() and andIf() will result in
* isForbidden() if either results are isForbidden().
*
* @return bool
* When TRUE then isAllowed() and isNeutral() are FALSE.
*/
public function isForbidden();
/**
* Checks whether this access result indicates access is not yet determined.
*
* @return bool
* When TRUE then isAllowed() and isForbidden() are FALSE.
*/
public function isNeutral();
/**
* Combine this access result with another using OR.
*
* When OR-ing two access results, the result is:
* - isForbidden() in either isForbidden()
* - otherwise if isAllowed() in either isAllowed()
* - otherwise both must be isNeutral() isNeutral()
*
* Truth table:
* @code
* |A N F
* --+-----
* A |A A F
* N |A N F
* F |F F F
* @endcode
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result to OR this one with.
*
* @return static
*/
public function orIf(AccessResultInterface $other);
/**
* Combine this access result with another using AND.
*
* When AND-ing two access results, the result is:
* - isForbidden() in either isForbidden()
* - otherwise, if isAllowed() in both isAllowed()
* - otherwise, one of them is isNeutral() isNeutral()
*
* Truth table:
* @code
* |A N F
* --+-----
* A |A N F
* N |N N F
* F |F F F
* @endcode
*
* @param \Drupal\Core\Access\AccessResultInterface $other
* The other access result to AND this one with.
*
* @return static
*/
public function andIf(AccessResultInterface $other);
}

View file

@ -0,0 +1,22 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessResultNeutral.
*/
namespace Drupal\Core\Access;
/**
* Value object indicating a neutral access result, with cacheability metadata.
*/
class AccessResultNeutral extends AccessResult {
/**
* {@inheritdoc}
*/
public function isNeutral() {
return TRUE;
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\AccessibleInterface.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Session\AccountInterface;
/**
* Interface for checking access.
*
* @ingroup entity_api
*/
interface AccessibleInterface {
/**
* Checks data value access.
*
* @param string $operation
* The operation to be performed.
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) The user for which to check access, or NULL to check access
* for the current user. Defaults to NULL.
* @param bool $return_as_object
* (optional) Defaults to FALSE.
*
* @return bool|\Drupal\Core\Access\AccessResultInterface
* The access result. Returns a boolean if $return_as_object is FALSE (this
* is the default) and otherwise an AccessResultInterface object.
* When a boolean is returned, the result of AccessInterface::isAllowed() is
* returned, i.e. TRUE means access is explicitly allowed, FALSE means
* access is either explicitly forbidden or "no opinion".
*/
public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE);
}

View file

@ -0,0 +1,170 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\CheckProvider.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface;
use Symfony\Component\DependencyInjection\ContainerAware;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
* Loads access checkers from the container.
*/
class CheckProvider extends ContainerAware implements CheckProviderInterface {
/**
* Array of registered access check service ids.
*
* @var array
*/
protected $checkIds = array();
/**
* Array of access check objects keyed by service id.
*
* @var \Drupal\Core\Routing\Access\AccessInterface[]
*/
protected $checks;
/**
* Array of access check method names keyed by service ID.
*
* @var array
*/
protected $checkMethods = array();
/**
* Array of access checks which only will be run on the incoming request.
*/
protected $checksNeedsRequest = array();
/**
* An array to map static requirement keys to service IDs.
*
* @var array
*/
protected $staticRequirementMap;
/**
* An array to map dynamic requirement keys to service IDs.
*
* @var array
*/
protected $dynamicRequirementMap;
/**
* {@inheritdoc}
*/
public function addCheckService($service_id, $service_method, array $applies_checks = array(), $needs_incoming_request = FALSE) {
$this->checkIds[] = $service_id;
$this->checkMethods[$service_id] = $service_method;
if ($needs_incoming_request) {
$this->checksNeedsRequest[$service_id] = $service_id;
}
foreach ($applies_checks as $applies_check) {
$this->staticRequirementMap[$applies_check][] = $service_id;
}
}
/**
* {@inheritdoc}
*/
public function getChecksNeedRequest() {
return $this->checksNeedsRequest;
}
/**
* {@inheritdoc}
*/
public function setChecks(RouteCollection $routes) {
$this->loadDynamicRequirementMap();
foreach ($routes as $route) {
if ($checks = $this->applies($route)) {
$route->setOption('_access_checks', $checks);
}
}
}
/**
* {@inheritdoc}
*/
public function loadCheck($service_id) {
if (empty($this->checks[$service_id])) {
if (!in_array($service_id, $this->checkIds)) {
throw new \InvalidArgumentException(sprintf('No check has been registered for %s', $service_id));
}
$check = $this->container->get($service_id);
if (!($check instanceof AccessInterface)) {
throw new AccessException('All access checks must implement AccessInterface.');
}
if (!is_callable(array($check, $this->checkMethods[$service_id]))) {
throw new AccessException(sprintf('Access check method %s in service %s must be callable.', $this->checkMethods[$service_id], $service_id));
}
$this->checks[$service_id] = $check;
}
return [$this->checks[$service_id], $this->checkMethods[$service_id]];
}
/**
* Determine which registered access checks apply to a route.
*
* @param \Symfony\Component\Routing\Route $route
* The route to get list of access checks for.
*
* @return array
* An array of service ids for the access checks that apply to passed
* route.
*/
protected function applies(Route $route) {
$checks = array();
// Iterate through map requirements from appliesTo() on access checkers.
// Only iterate through all checkIds if this is not used.
foreach ($route->getRequirements() as $key => $value) {
if (isset($this->staticRequirementMap[$key])) {
foreach ($this->staticRequirementMap[$key] as $service_id) {
$checks[] = $service_id;
}
}
}
// Finally, see if any dynamic access checkers apply.
foreach ($this->dynamicRequirementMap as $service_id) {
if ($this->checks[$service_id]->applies($route)) {
$checks[] = $service_id;
}
}
return $checks;
}
/**
* Compiles a mapping of requirement keys to access checker service IDs.
*/
protected function loadDynamicRequirementMap() {
if (isset($this->dynamicRequirementMap)) {
return;
}
// Set them here, so we can use the isset() check above.
$this->dynamicRequirementMap = array();
foreach ($this->checkIds as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
}
// Add the service ID to an array that will be iterated over.
if ($this->checks[$service_id] instanceof AccessCheckInterface) {
$this->dynamicRequirementMap[] = $service_id;
}
}
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\CheckProviderInterface.
*/
namespace Drupal\Core\Access;
use Symfony\Component\Routing\RouteCollection;
/**
* Provides the available access checkers by service IDs.
*
* Access checker services are added by ::addCheckService calls and are loaded
* by ::loadCheck.
*
* The checker provider service and the actual checking is separated in order
* to not require the full access manager on route build time.
*/
interface CheckProviderInterface {
/**
* For each route, saves a list of applicable access checks to the route.
*
* @param \Symfony\Component\Routing\RouteCollection $routes
* A collection of routes to apply checks to.
*/
public function setChecks(RouteCollection $routes);
/**
* Registers a new AccessCheck by service ID.
*
* @param string $service_id
* The ID of the service in the Container that provides a check.
* @param string $service_method
* The method to invoke on the service object for performing the check.
* @param array $applies_checks
* (optional) An array of route requirement keys the checker service applies
* to.
* @param bool $needs_incoming_request
* (optional) True if access-check method only acts on an incoming request.
*/
public function addCheckService($service_id, $service_method, array $applies_checks = array(), $needs_incoming_request = FALSE);
/**
* Lazy-loads access check services.
*
* @param string $service_id
* The service id of the access check service to load.
*
* @return callable
* A callable access check.
*
* @throws \InvalidArgumentException
* Thrown when the service hasn't been registered in addCheckService().
* @throws \Drupal\Core\Access\AccessException
* Thrown when the service doesn't implement the required interface.
*/
public function loadCheck($service_id);
/**
* A list of checks that needs the request.
*
* @return array
*/
public function getChecksNeedRequest();
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\CsrfAccessCheck.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Allows access to routes to be controlled by a '_csrf_token' parameter.
*
* To use this check, add a "token" GET parameter to URLs of which the value is
* a token generated by \Drupal::csrfToken()->get() using the same value as the
* "_csrf_token" parameter in the route.
*/
class CsrfAccessCheck implements RoutingAccessInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a CsrfAccessCheck object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
public function __construct(CsrfTokenGenerator $csrf_token) {
$this->csrfToken = $csrf_token;
}
/**
* Checks access based on a CSRF token for the request.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, Request $request, RouteMatchInterface $route_match) {
$parameters = $route_match->getRawParameters();
$path = ltrim($route->getPath(), '/');
// Replace the path parameters with values from the parameters array.
foreach ($parameters as $param => $value) {
$path = str_replace("{{$param}}", $value, $path);
}
if ($this->csrfToken->validate($request->query->get('token'), $path)) {
$result = AccessResult::allowed();
}
else {
$result = AccessResult::forbidden();
}
// Not cacheable because the CSRF token is highly dynamic.
return $result->setCacheMaxAge(0);
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\CsrfTokenGenerator.
*/
namespace Drupal\Core\Access;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\PrivateKey;
use Drupal\Core\Session\MetadataBag;
use Drupal\Core\Site\Settings;
/**
* Generates and validates CSRF tokens.
*
* @see \Drupal\Tests\Core\Access\CsrfTokenGeneratorTest
*/
class CsrfTokenGenerator {
/**
* The private key service.
*
* @var \Drupal\Core\PrivateKey
*/
protected $privateKey;
/**
* The session metadata bag.
*
* @var \Drupal\Core\Session\MetadataBag
*/
protected $sessionMetadata;
/**
* Constructs the token generator.
*
* @param \Drupal\Core\PrivateKey $private_key
* The private key service.
* @param \Drupal\Core\Session\MetadataBag $session_metadata
* The session metadata bag.
*/
public function __construct(PrivateKey $private_key, MetadataBag $session_metadata) {
$this->privateKey = $private_key;
$this->sessionMetadata = $session_metadata;
}
/**
* Generates a token based on $value, the user session, and the private key.
*
* The generated token is based on the session of the current user. Normally,
* anonymous users do not have a session, so the generated token will be
* different on every page request. To generate a token for users without a
* session, manually start a session prior to calling this function.
*
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the token seed,
* the hash salt provided by Settings::getHashSalt(), and the
* 'drupal_private_key' configuration variable.
*
* @see \Drupal\Core\Site\Settings::getHashSalt()
* @see \Symfony\Component\HttpFoundation\Session\SessionInterface::start()
*/
public function get($value = '') {
$seed = $this->sessionMetadata->getCsrfTokenSeed();
if (empty($seed)) {
$seed = Crypt::randomBytesBase64();
$this->sessionMetadata->setCsrfTokenSeed($seed);
}
return $this->computeToken($seed, $value);
}
/**
* Validates a token based on $value, the user session, and the private key.
*
* @param string $token
* The token to be validated.
* @param string $value
* (optional) An additional value to base the token on.
*
* @return bool
* TRUE for a valid token, FALSE for an invalid token.
*/
public function validate($token, $value = '') {
$seed = $this->sessionMetadata->getCsrfTokenSeed();
if (empty($seed)) {
return FALSE;
}
return $token === $this->computeToken($seed, $value);
}
/**
* Generates a token based on $value, the token seed, and the private key.
*
* @param string $seed
* The per-session token seed.
* @param string $value
* (optional) An additional value to base the token on.
*
* @return string
* A 43-character URL-safe token for validation, based on the token seed,
* the hash salt provided by Settings::getHashSalt(), and the
* 'drupal_private_key' configuration variable.
*
* @see \Drupal\Core\Site\Settings::getHashSalt()
*/
protected function computeToken($seed, $value = '') {
return Crypt::hmacBase64($value, $seed . $this->privateKey->get() . Settings::getHashSalt());
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\CustomAccessCheck.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\Routing\Route;
/**
* Defines an access checker that allows specifying a custom method for access.
*
* You should only use it when you are sure that the access callback will not be
* reused. Good examples in core are Edit or Toolbar module.
*
* The method is called on another instance of the controller class, so you
* cannot reuse any stored property of your actual controller instance used
* to generate the output.
*/
class CustomAccessCheck implements RoutingAccessInterface {
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The arguments resolver.
*
* @var \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface
*/
protected $argumentsResolverFactory;
/**
* Constructs a CustomAccessCheck instance.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Access\AccessArgumentsResolverFactoryInterface $arguments_resolver_factory
* The arguments resolver factory.
*/
public function __construct(ControllerResolverInterface $controller_resolver, AccessArgumentsResolverFactoryInterface $arguments_resolver_factory) {
$this->controllerResolver = $controller_resolver;
$this->argumentsResolverFactory = $arguments_resolver_factory;
}
/**
* Checks access for the account and route using the custom access checker.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match object to be checked.
* @param \Drupal\Core\Session\AccountInterface $account
* The account being checked.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
$callable = $this->controllerResolver->getControllerFromDefinition($route->getRequirement('_custom_access'));
$arguments_resolver = $this->argumentsResolverFactory->getArgumentsResolver($route_match, $account);
$arguments = $arguments_resolver->getArguments($callable);
return call_user_func_array($callable, $arguments);
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\DefaultAccessCheck.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Routing\Access\AccessInterface as RoutingAccessInterface;
use Symfony\Component\Routing\Route;
/**
* Allows access to routes to be controlled by an '_access' boolean parameter.
*/
class DefaultAccessCheck implements RoutingAccessInterface {
/**
* Checks access to the route based on the _access parameter.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route) {
if ($route->getRequirement('_access') === 'TRUE') {
return AccessResult::allowed();
}
elseif ($route->getRequirement('_access') === 'FALSE') {
return AccessResult::forbidden();
}
else {
return AccessResult::neutral();
}
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\Core\Access\RouteProcessorCsrf.
*/
namespace Drupal\Core\Access;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Symfony\Component\Routing\Route;
/**
* Processes the outbound route to handle the CSRF token.
*/
class RouteProcessorCsrf implements OutboundRouteProcessorInterface {
/**
* The CSRF token generator.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* Constructs a RouteProcessorCsrf object.
*
* @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
* The CSRF token generator.
*/
function __construct(CsrfTokenGenerator $csrf_token) {
$this->csrfToken = $csrf_token;
}
/**
* {@inheritdoc}
*/
public function processOutbound($route_name, Route $route, array &$parameters, CacheableMetadata $cacheable_metadata = NULL) {
if ($route->hasRequirement('_csrf_token')) {
$path = ltrim($route->getPath(), '/');
// Replace the path parameters with values from the parameters array.
foreach ($parameters as $param => $value) {
$path = str_replace("{{$param}}", $value, $path);
}
// Adding this to the parameters means it will get merged into the query
// string when the route is compiled.
$parameters['token'] = $this->csrfToken->get($path);
if ($cacheable_metadata) {
// Tokens are per user and per session, so not cacheable.
// @todo Improve in https://www.drupal.org/node/2351015.
$cacheable_metadata->setCacheMaxAge(0);
}
}
}
}