diff --git a/core/core.services.yml b/core/core.services.yml index 0e556865e9297969c7ce74bac1e4799091fe485d..f070b69ed7cad165a2de2e11b05eb273f46f9fce 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -379,8 +379,10 @@ services: - [setRequest, ['@?request']] access_manager: class: Drupal\Core\Access\AccessManager + arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager'] calls: - [setContainer, ['@service_container']] + - [setRequest, ['@?request']] access_subscriber: class: Drupal\Core\EventSubscriber\AccessSubscriber tags: diff --git a/core/lib/Drupal/Core/Access/AccessManager.php b/core/lib/Drupal/Core/Access/AccessManager.php index 14729a51ea3dad9d94f1371b8e548b0857860380..a3b61915dcbd61d4e757bcf78fb57fa782110898 100644 --- a/core/lib/Drupal/Core/Access/AccessManager.php +++ b/core/lib/Drupal/Core/Access/AccessManager.php @@ -7,11 +7,17 @@ namespace Drupal\Core\Access; +use Drupal\Core\ParamConverter\ParamConverterManager; +use Drupal\Core\Routing\RouteProviderInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; /** * Attaches access check services to routes and runs them on request. @@ -48,6 +54,63 @@ class AccessManager extends ContainerAware { */ protected $dynamicRequirementMap; + /** + * The route provider. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * The url generator. + * + * @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * The paramconverter manager. + * + * @var \Drupal\Core\ParamConverter\ParamConverterManager + */ + protected $paramConverterManager; + + /** + * A request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + protected $request; + + /** + * Constructs a AccessManager instance. + * + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + * @param \Symfony\Component\Routing\Generator\UrlGeneratorInterface $url_generator + * The url generator. + * @param \Drupal\Core\ParamConverter\ParamConverterManager $paramconverter_manager + * The param converter manager. + */ + public function __construct(RouteProviderInterface $route_provider, UrlGeneratorInterface $url_generator, ParamConverterManager $paramconverter_manager) { + $this->routeProvider = $route_provider; + $this->urlGenerator = $url_generator; + $this->paramConverterManager = $paramconverter_manager; + } + + /** + * Sets the request object to use. + * + * This is used by the RouterListener to make additional request attributes + * available. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object. + */ + public function setRequest(Request $request) { + $this->request = $request; + } + /** * Registers a new AccessCheck by service ID. * @@ -107,6 +170,43 @@ protected function applies(Route $route) { return $checks; } + /** + * 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 patern. + * @param \Symfony\Component\HttpFoundation\Request $route_request + * Optional incoming request object. If not provided, one will be built + * using the route information and the current request from the container. + * + * @return bool + * Returns TRUE if the user has access to the route, otherwise FALSE. + */ + public function checkNamedRoute($route_name, array $parameters = array(), Request $route_request = NULL) { + try { + $route = $this->routeProvider->getRouteByName($route_name, $parameters); + if (empty($route_request)) { + // Create a request and copy the account from the current request. + $route_request = Request::create($this->urlGenerator->generate($route_name, $parameters)); + $defaults = $parameters; + $defaults['_account'] = $this->request->attributes->get('_account'); + $defaults[RouteObjectInterface::ROUTE_OBJECT] = $route; + $route_request->attributes->add($this->paramConverterManager->enhance($defaults, $route_request)); + } + return $this->check($route, $route_request); + } + catch (RouteNotFoundException $e) { + return FALSE; + } + catch (NotFoundHttpException $e) { + return FALSE; + } + } + /** * Checks a route against applicable access check services. * @@ -118,7 +218,7 @@ protected function applies(Route $route) { * The incoming request object. * * @return bool - * Returns TRUE if the user has access to the route, otherwise FALSE. + * Returns TRUE if the user has access to the route, otherwise FALSE. * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException * If any access check denies access or none explicitly approve. diff --git a/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php b/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php index f9cab9a9822bcce07dd80af8a03f39ea44e96184..097cdd35f7160555385480c205cc613ea135794e 100644 --- a/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Access/AccessManagerTest.php @@ -8,12 +8,16 @@ namespace Drupal\Tests\Core\Access; use Drupal\Core\Access\AccessCheckInterface; +use Drupal\Core\Access\AccessInterface; use Drupal\Core\Access\AccessManager; use Drupal\Core\Access\DefaultAccessCheck; +use Drupal\system\Tests\Routing\MockRouteProvider; use Drupal\Tests\UnitTestCase; use Drupal\router_test\Access\DefinedTestAccessCheck; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\RouteNotFoundException; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -45,6 +49,27 @@ class AccessManagerTest extends UnitTestCase { */ protected $accessManager; + /** + * The route provider. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $routeProvider; + + /** + * The url generator + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $urlGenerator; + + /** + * The parameter converter. + * + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $paramConverter; + public static function getInfo() { return array( 'name' => 'Access manager tests', @@ -60,13 +85,38 @@ protected function setUp() { parent::setUp(); $this->container = new ContainerBuilder(); - $this->accessManager = new AccessManager(); - $this->accessManager->setContainer($this->container); $this->routeCollection = new RouteCollection(); $this->routeCollection->add('test_route_1', new Route('/test-route-1')); $this->routeCollection->add('test_route_2', new Route('/test-route-2', array(), array('_access' => 'TRUE'))); $this->routeCollection->add('test_route_3', new Route('/test-route-3', array(), array('_access' => 'FALSE'))); + $this->routeCollection->add('test_route_4', new Route('/test-route-4/{value}', array(), array('_access' => 'TRUE'))); + + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + $map = array(); + foreach ($this->routeCollection->all() as $name => $route) { + $map[] = array($name, array(), $route); + } + $map[] = array('test_route_4', array('value' => 'example'), $this->routeCollection->get('test_route_4')); + $this->routeProvider->expects($this->any()) + ->method('getRouteByName') + ->will($this->returnValueMap($map)); + + $map = array(); + $map[] = array('test_route_1', array(), '/test-route-1'); + $map[] = array('test_route_2', array(), '/test-route-2'); + $map[] = array('test_route_3', array(), '/test-route-3'); + $map[] = array('test_route_4', array('value' => 'example'), '/test-route-4/example'); + + $this->urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); + $this->urlGenerator->expects($this->any()) + ->method('generate') + ->will($this->returnValueMap($map)); + + $this->paramConverter = $this->getMock('\Drupal\Core\ParamConverter\ParamConverterManager'); + + $this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter); + $this->accessManager->setContainer($this->container); } /** @@ -255,6 +305,119 @@ public function testStaticAccessCheckInterface() { $this->accessManager->setChecks($this->routeCollection); } + /** + * Tests the checkNamedRoute method. + * + * @see \Drupal\Core\Access\AccessManager::checkNamedRoute() + */ + public function testCheckNamedRoute() { + $this->setupAccessChecker(); + $this->accessManager->setChecks($this->routeCollection); + + // Tests the access with routes without parameters. + $request = new Request(); + $this->assertTrue($this->accessManager->checkNamedRoute('test_route_2', array(), $request)); + $this->assertFalse($this->accessManager->checkNamedRoute('test_route_3', array(), $request)); + + // Tests the access with routes with parameters with given request. + $request = new Request(); + $request->attributes->set('value', 'example'); + $request->attributes->set('value2', 'example2'); + $this->assertTrue($this->accessManager->checkNamedRoute('test_route_4', array(), $request)); + + // Tests the access with routes without given request. + $account = $this->getMock('Drupal\Core\Session\AccountInterface'); + $this->accessManager->setRequest(new Request(array(), array(), array('_account' => $account))); + + $this->paramConverter->expects($this->at(0)) + ->method('enhance') + ->will($this->returnValue(array())); + + $this->paramConverter->expects($this->at(1)) + ->method('enhance') + ->will($this->returnValue(array())); + + // Tests the access with routes with parameters without given request. + $this->assertTrue($this->accessManager->checkNamedRoute('test_route_2', array())); + $this->assertTrue($this->accessManager->checkNamedRoute('test_route_4', array('value' => 'example'))); + } + + /** + * Tests the checkNamedRoute with upcasted values. + * + * @see \Drupal\Core\Access\AccessManager::checkNamedRoute() + */ + public function testCheckNamedRouteWithUpcastedValues() { + $account = $this->getMock('Drupal\Core\Session\AccountInterface'); + + $this->routeCollection = new RouteCollection(); + $route = new Route('/test-route-1/{value}', array(), array('_test_access' => 'TRUE')); + $this->routeCollection->add('test_route_1', $route); + + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + $this->routeProvider->expects($this->any()) + ->method('getRouteByName') + ->with('test_route_1', array('value' => 'example')) + ->will($this->returnValue($route)); + + $map = array(); + $map[] = array('test_route_1', array('value' => 'example'), '/test-route-1/example'); + + $this->urlGenerator = $this->getMock('Symfony\Component\Routing\Generator\UrlGeneratorInterface'); + $this->urlGenerator->expects($this->any()) + ->method('generate') + ->will($this->returnValueMap($map)); + + $this->paramConverter = $this->getMock('\Drupal\Core\ParamConverter\ParamConverterManager'); + $this->paramConverter->expects($this->at(0)) + ->method('enhance') + ->will($this->returnValue(array('value' => 'upcasted_value'))); + + + $subrequest = Request::create('/test-route-1/example'); + $class = $this->getMockClass('Symfony\Component\HttpFoundation\Request', array('create')); + $class::staticExpects($this->any()) + ->method('create') + ->with('/test-route-1/example') + ->will($this->returnValue($subrequest)); + + $this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter); + $this->accessManager->setContainer($this->container); + $this->accessManager->setRequest(new Request(array(), array(), array('_account' => $account))); + + $access_check = $this->getMock('Drupal\Core\Access\AccessCheckInterface'); + $access_check->expects($this->any()) + ->method('applies') + ->will($this->returnValue(TRUE)); + $access_check->expects($this->any()) + ->method('access') + ->with($route, $subrequest) + ->will($this->returnValue(AccessInterface::KILL)); + + $subrequest->attributes->set('value', 'upcasted_value'); + $this->container->register('test_access', $access_check); + + $this->accessManager->addCheckService('test_access'); + $this->accessManager->setChecks($this->routeCollection); + + $this->assertFalse($this->accessManager->checkNamedRoute('test_route_1', array('value' => 'example'))); + } + + /** + * Tests checkNamedRoute given an invalid/non existing route name. + */ + public function testCheckNamedRouteWithNonExistingRoute() { + $this->routeProvider = $this->getMock('Drupal\Core\Routing\RouteProviderInterface'); + + $this->routeProvider->expects($this->any()) + ->method('getRouteByName') + ->will($this->throwException(new RouteNotFoundException())); + + $this->setupAccessChecker(); + + $this->assertFalse($this->accessManager->checkNamedRoute('test_route_1'), 'A non existing route lead to access.'); + } + /** * Converts AccessCheckInterface constants to a string. * @@ -283,7 +446,7 @@ protected static function convertAccessCheckInterfaceToString($constant) { * Adds a default access check service to the container and the access manager. */ protected function setupAccessChecker() { - $this->accessManager = new AccessManager(); + $this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter); $this->accessManager->setContainer($this->container); $access_check = new DefaultAccessCheck(); $this->container->register('test_access_default', $access_check);