Commit c6f02381 authored by catch's avatar catch

Issue #2354657 by chx, dawehner: Separate access manager from loading checks.

parent e8d6588e
......@@ -696,14 +696,17 @@ services:
class: Drupal\Core\Access\AccessArgumentsResolverFactory
access_manager:
class: Drupal\Core\Access\AccessManager
arguments: ['@router.route_provider', '@paramconverter_manager', '@access_arguments_resolver_factory', '@current_user']
arguments: ['@router.route_provider', '@paramconverter_manager', '@access_arguments_resolver_factory', '@current_user', '@access_manager.check_provider']
access_manager.check_provider:
class: Drupal\Core\Access\CheckProvider
calls:
- [setContainer, ['@service_container']]
public: false
access_route_subscriber:
class: Drupal\Core\EventSubscriber\AccessRouteSubscriber
tags:
- { name: event_subscriber }
arguments: ['@access_manager']
arguments: ['@access_manager.check_provider']
access_check.default:
class: Drupal\Core\Access\DefaultAccessCheck
tags:
......
......@@ -9,16 +9,11 @@
use Drupal\Core\ParamConverter\ParamConverterManagerInterface;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\Routing\Access\AccessInterface;
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\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\RouteNotFoundException;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
......@@ -28,50 +23,7 @@
*
* @see \Drupal\Tests\Core\Access\AccessManagerTest
*/
class AccessManager implements ContainerAwareInterface, AccessManagerInterface {
use ContainerAwareTrait;
/**
* 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 $checkNeedsRequest = 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;
class AccessManager implements AccessManagerInterface {
/**
* The route provider.
*
......@@ -100,6 +52,13 @@ class AccessManager implements ContainerAwareInterface, AccessManagerInterface {
*/
protected $currentUser;
/**
* The check provider.
*
* @var \Drupal\Core\Access\CheckProviderInterface
*/
protected $checkProvider;
/**
* Constructs a AccessManager instance.
*
......@@ -111,70 +70,14 @@ class AccessManager implements ContainerAwareInterface, AccessManagerInterface {
* 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) {
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;
}
/**
* {@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->checkNeedsRequest[$service_id] = $service_id;
}
foreach ($applies_checks as $applies_check) {
$this->staticRequirementMap[$applies_check][] = $service_id;
}
}
/**
* {@inheritdoc}
*/
public function setChecks(RouteCollection $routes) {
$this->loadDynamicRequirementMap();
foreach ($routes as $route) {
if ($checks = $this->applies($route)) {
$route->setOption('_access_checks', $checks);
}
}
}
/**
* 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;
$this->checkProvider = $check_provider;
}
/**
......@@ -226,7 +129,7 @@ public function check(RouteMatchInterface $route_match, AccountInterface $accoun
// Filter out checks which require the incoming request.
if (!isset($request)) {
$checks = array_diff($checks, $this->checkNeedsRequest);
$checks = array_diff($checks, $this->checkProvider->getChecksNeedRequest());
}
$result = AccessResult::neutral();
......@@ -262,9 +165,6 @@ protected function checkAll(array $checks, ArgumentsResolverInterface $arguments
}
$result = AccessResult::allowed();
foreach ($checks as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
}
$result = $result->andIf($this->performCheck($service_id, $arguments_resolver));
}
return $result;
......@@ -288,9 +188,6 @@ protected function checkAny(array $checks, ArgumentsResolverInterface $arguments
$result = AccessResult::neutral();
foreach ($checks as $service_id) {
if (empty($this->checks[$service_id])) {
$this->loadCheck($service_id);
}
$result = $result->orIf($this->performCheck($service_id, $arguments_resolver));
}
......@@ -312,7 +209,7 @@ protected function checkAny(array $checks, ArgumentsResolverInterface $arguments
* The access result.
*/
protected function performCheck($service_id, ArgumentsResolverInterface $arguments_resolver) {
$callable = array($this->checks[$service_id], $this->checkMethods[$service_id]);
$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);
......@@ -324,55 +221,5 @@ protected function performCheck($service_id, ArgumentsResolverInterface $argumen
return $service_access;
}
/**
* Lazy-loads access check services.
*
* @param string $service_id
* The service id of the access check service to load.
*
* @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.
*/
protected function loadCheck($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;
}
/**
* 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;
}
}
}
}
......@@ -8,7 +8,6 @@
namespace Drupal\Core\Access;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouteCollection;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -76,29 +75,6 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
*/
public function checkRequest(Request $request, AccountInterface $account = NULL, $return_as_object = FALSE);
/**
* 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);
/**
* Checks a route against applicable access check services.
*
......
<?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;
}
}
}
}
<?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();
}
......@@ -24,7 +24,7 @@ public function process(ContainerBuilder $container) {
if (!$container->hasDefinition('access_manager')) {
return;
}
$access_manager = $container->getDefinition('access_manager');
$access_manager = $container->getDefinition('access_manager.check_provider');
foreach ($container->findTaggedServiceIds('access_check') as $id => $attributes) {
$applies = array();
$method = 'access';
......
......@@ -7,7 +7,7 @@
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Access\CheckProviderInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -20,19 +20,19 @@ class AccessRouteSubscriber implements EventSubscriberInterface {
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
* @var \Drupal\Core\Access\checkProviderInterface
*/
protected $accessManager;
protected $checkProvider;
/**
* Constructs a new AccessSubscriber.
*
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access check manager that will be responsible for applying
* AccessCheckers against routes.
* @param \Drupal\Core\Access\CheckProviderInterface $check_provider
* The check provider that will be responsible for applying
* access checkers against routes.
*/
public function __construct(AccessManagerInterface $access_manager) {
$this->accessManager = $access_manager;
public function __construct(CheckProviderInterface $check_provider) {
$this->checkProvider = $check_provider;
}
/**
......@@ -42,7 +42,7 @@ public function __construct(AccessManagerInterface $access_manager) {
* The event to process.
*/
public function onRoutingRouteAlterSetAccessCheck(RouteBuildEvent $event) {
$this->accessManager->setChecks($event->getRouteCollection());
$this->checkProvider->setChecks($event->getRouteCollection());
}
/**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment