Commit 61150517 authored by alexpott's avatar alexpott

Issue #2302563 by chx, dawehner: Fixed Access check Url objects.

parent 12bb051b
......@@ -664,7 +664,7 @@ services:
class: Drupal\Core\Access\AccessArgumentsResolver
access_manager:
class: Drupal\Core\Access\AccessManager
arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager', '@access_arguments_resolver', '@request_stack']
arguments: ['@router.route_provider', '@url_generator', '@paramconverter_manager', '@access_arguments_resolver', '@request_stack', '@current_user']
calls:
- [setContainer, ['@service_container']]
access_route_subscriber:
......
......@@ -2809,6 +2809,14 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
$current->postRenderCache = NestedArray::mergeDeep($current->postRenderCache, $parent->postRenderCache);
$stack->push($current);
};
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
$elements['#access_callback'] = $controller_resolver->getControllerFromDefinition($elements['#access_callback']);
}
$elements['#access'] = call_user_func($elements['#access_callback'], $elements);
}
// Early-return nothing if user does not have access.
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
......@@ -2856,8 +2864,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
......
......@@ -102,6 +102,13 @@ class AccessManager implements ContainerAwareInterface, AccessManagerInterface {
*/
protected $requestStack;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a AccessManager instance.
*
......@@ -115,13 +122,16 @@ class AccessManager implements ContainerAwareInterface, AccessManagerInterface {
* The access arguments resolver.
* @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
* The request stack object.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(RouteProviderInterface $route_provider, UrlGeneratorInterface $url_generator, ParamConverterManagerInterface $paramconverter_manager, AccessArgumentsResolverInterface $arguments_resolver, RequestStack $requestStack) {
public function __construct(RouteProviderInterface $route_provider, UrlGeneratorInterface $url_generator, ParamConverterManagerInterface $paramconverter_manager, AccessArgumentsResolverInterface $arguments_resolver, RequestStack $requestStack, AccountInterface $current_user) {
$this->routeProvider = $route_provider;
$this->urlGenerator = $url_generator;
$this->paramConverterManager = $paramconverter_manager;
$this->argumentsResolver = $arguments_resolver;
$this->requestStack = $requestStack;
$this->currentUser = $current_user;
}
/**
......@@ -182,7 +192,7 @@ protected function applies(Route $route) {
/**
* {@inheritdoc}
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account, Request $route_request = NULL) {
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, Request $route_request = NULL) {
try {
$route = $this->routeProvider->getRouteByName($route_name, $parameters);
if (empty($route_request)) {
......@@ -210,7 +220,10 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
/**
* {@inheritdoc}
*/
public function check(Route $route, Request $request, AccountInterface $account) {
public function check(Route $route, Request $request, AccountInterface $account = NULL) {
if (!isset($account)) {
$account = $this->currentUser;
}
$checks = $route->getOption('_access_checks') ?: array();
$conjunction = $route->getOption('_access_mode') ?: static::ACCESS_MODE_ALL;
......
......@@ -45,7 +45,8 @@ interface AccessManagerInterface {
* @param array $parameters
* Optional array of values to substitute into the route path patern.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* (optional) Run access checks for this account. Defaults to the current
* user.
* @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.
......@@ -53,7 +54,7 @@ interface AccessManagerInterface {
* @return bool
* Returns TRUE if the user has access to the route, otherwise FALSE.
*/
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account, Request $route_request = NULL);
public function checkNamedRoute($route_name, array $parameters = array(), AccountInterface $account = NULL, Request $route_request = NULL);
/**
* For each route, saves a list of applicable access checks to the route.
......@@ -86,11 +87,12 @@ public function addCheckService($service_id, $service_method, array $applies_che
* @param \Symfony\Component\HttpFoundation\Request $request
* The incoming request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The current account.
* (optional) Run access checks for this account. Defaults to the current
* user.
*
* @return bool
* Returns TRUE if the user has access to the route, otherwise FALSE.
*/
public function check(Route $route, Request $request, AccountInterface $account);
public function check(Route $route, Request $request, AccountInterface $account = NULL);
}
......@@ -10,6 +10,7 @@
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
......@@ -26,6 +27,13 @@ class Url {
*/
protected $urlGenerator;
/**
* The access manager
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The route name.
*
......@@ -386,6 +394,7 @@ public function toRenderArray() {
'#route_name' => $this->getRouteName(),
'#route_parameters' => $this->getRouteParameters(),
'#options' => $this->getOptions(),
'#access_callback' => array(get_class(), 'renderAccess'),
);
}
}
......@@ -411,6 +420,45 @@ public function getInternalPath() {
return $this->urlGenerator()->getPathFromRoute($this->getRouteName(), $this->getRouteParameters());
}
/**
* Checks this Url object against applicable access check services.
*
* Determines whether the route is accessible or not.
*
* @param \Drupal\Core\Session\AccountInterface $account
* (optional) Run access checks for this account. Defaults to the current
* user.
*
* @return bool
* Returns TRUE if the user has access to the url, otherwise FALSE.
*/
public function access(AccountInterface $account = NULL) {
return $this->accessManager()->checkNamedRoute($this->getRouteName(), $this->getRouteParameters(), $account);
}
/**
* Checks a Url render element against applicable access check services.
*
* @param array $element
* A render element as returned from \Drupal\Core\Url::toRenderArray().
*
* @return bool
* Returns TRUE if the current user has access to the url, otherwise FALSE.
*/
public static function renderAccess(array $element) {
return (new static($element['#route_name'], $element['#route_parameters'], $element['#options']))->access();
}
/**
* @return \Drupal\Core\Access\AccessManagerInterface
*/
protected function accessManager() {
if (!isset($this->accessManager)) {
$this->accessManager = \Drupal::service('access_manager');
}
return $this->accessManager;
}
/**
* Gets the URL generator.
*
......
......@@ -7,9 +7,8 @@
namespace Drupal\rdf\Tests\Field;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\rdf\Tests\Field\FieldRdfaTestBase;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\user\Entity\Role;
/**
* Tests the RDFa output of the taxonomy term reference field formatter.
......@@ -100,6 +99,10 @@ protected function setUp() {
public function testAllFormatters() {
// Tests the plain formatter.
$this->assertFormatterRdfa(array('type' => 'taxonomy_term_reference_plain'), 'http://schema.org/about', array('value' => $this->term->getName(), 'type' => 'literal'));
// Grant the access content permission to the anonymous user.
Role::create(array('id' => DRUPAL_ANONYMOUS_RID))
->grantPermission('access content')
->save();
// Tests the link formatter.
$term_uri = $this->getAbsoluteUri($this->term);
$this->assertFormatterRdfa(array('type'=>'taxonomy_term_reference_link'), 'http://schema.org/about', array('value' => $term_uri, 'type' => 'uri'));
......
......@@ -12,9 +12,8 @@
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -27,13 +26,6 @@
*/
class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* Stores the configuration factory.
*
......@@ -41,13 +33,6 @@ class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInt
*/
protected $configFactory;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Creates a SystemBrandingBlock instance.
*
......@@ -59,16 +44,10 @@ class SystemBrandingBlock extends BlockBase implements ContainerFactoryPluginInt
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The factory for configuration objects.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The url generator service.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, UrlGeneratorInterface $url_generator, AccountInterface $current_user) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->configFactory = $config_factory;
$this->urlGenerator = $url_generator;
$this->currentUser = $current_user;
}
/**
......@@ -79,9 +58,7 @@ public static function create(ContainerInterface $container, array $configuratio
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('url_generator'),
$container->get('current_user')
$container->get('config.factory')
);
}
......@@ -111,26 +88,26 @@ public function blockForm($form, FormStateInterface $form_state) {
$theme = $form_state['block_theme'];
// Get permissions.
$administer_themes_access = $this->currentUser->hasPermission('administer themes');
$administer_site_configuration_access = $this->currentUser->hasPermission('administer site configuration');
if ($administer_themes_access) {
// Get paths to theme settings pages.
$appearance_settings_url = $this->urlGenerator->generateFromRoute('system.theme_settings');
$theme_settings_url = $this->urlGenerator->generateFromRoute('system.theme_settings_theme', array('theme' => $theme));
$url_system_theme_settings = new Url('system.theme_settings');
$url_system_theme_settings_theme = new Url('system.theme_settings_theme', array('theme' => $theme));
if ($url_system_theme_settings->access() && $url_system_theme_settings_theme->access()) {
// Provide links to the Appearance Settings and Theme Settings pages
// if the user has access to administer themes.
$site_logo_description = $this->t('Defined on the <a href="@appearance">Appearance Settings</a> or <a href="@theme">Theme Settings</a> page.', array('@appearance' => $appearance_settings_url, '@theme' => $theme_settings_url));
$site_logo_description = $this->t('Defined on the <a href="@appearance">Appearance Settings</a> or <a href="@theme">Theme Settings</a> page.', array(
'@appearance' => $url_system_theme_settings->toString(),
'@theme' => $url_system_theme_settings_theme->toString(),
));
}
else {
// Explain that the user does not have access to the Appearance and Theme
// Settings pages.
$site_logo_description = $this->t('Defined on the Appearance or Theme Settings page. You do not have the appropriate permissions to change the site logo.');
}
if ($administer_site_configuration_access) {
$url_system_site_information_settings = new Url('system.site_information_settings');
if ($url_system_site_information_settings->access()) {
// Get paths to settings pages.
$site_information_url = $this->urlGenerator->generateFromRoute('system.site_information_settings');
$site_information_url = $url_system_site_information_settings->toString();
// Provide link to Site Information page if the user has access to
// administer site configuration.
......
......@@ -53,6 +53,31 @@ function testDrupalRenderBasics() {
),
'expected' => '',
),
array(
'name' => 'access denied via callback',
'value' => array(
'#markup' => 'foo',
'#access_callback' => 'is_bool',
),
'expected' => '',
),
array(
'name' => 'access granted via callback',
'value' => array(
'#markup' => 'foo',
'#access_callback' => 'is_array',
),
'expected' => 'foo',
),
array(
'name' => 'access FALSE is honored',
'value' => array(
'#markup' => 'foo',
'#access' => FALSE,
'#access_callback' => 'is_array',
),
'expected' => '',
),
array(
'name' => 'previously printed',
'value' => array(
......
......@@ -33,7 +33,7 @@ function testConfirmForm() {
// Test canelling the form.
$this->clickLink(t('ConfirmFormTestForm::getCancelText().'));
$this->assertUrl('admin', array(), "The form's cancel link was followed.");
$this->assertUrl('form-test/autocomplete', array(), "The form's cancel link was followed.");
// Test submitting the form.
$this->drupalPostForm('form-test/confirm-form', NULL, t('ConfirmFormTestForm::getConfirmText().'));
......@@ -47,7 +47,7 @@ function testConfirmForm() {
// Test cancelling the form with a complex destination.
$this->drupalGet('form-test/confirm-form-array-path');
$this->clickLink(t('ConfirmFormArrayPathTestForm::getCancelText().'));
$this->assertUrl('admin', array('query' => array('destination' => 'admin/config')), "The form's complex cancel link was followed.");
$this->assertUrl('form-test/confirm-form', array('query' => array('destination' => 'admin/config')), "The form's complex cancel link was followed.");
}
}
<?php
/**
* @file
* Contains \Drupal\system\Tests\Routing\UrlIntegrationTest.
*/
namespace Drupal\system\Tests\Routing;
use Drupal\Core\Url;
use Drupal\simpletest\KernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
/**
* Tests the URL object integration into the access system.
*
* @group Url
*/
class UrlIntegrationTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('user', 'router_test', 'system');
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installSchema('system', ['router']);
}
/**
* Ensures that the access() method on \Drupal\Core\Url objects works.
*/
public function testAccess() {
\Drupal::service('router.builder')->rebuild();
/** @var \Drupal\user\RoleInterface $role_with_access */
$role_with_access = Role::create(['id' => 'role_with_access']);
$role_with_access->grantPermission('administer users');
$role_with_access->save();
/** @var \Drupal\user\RoleInterface $role_without_access */
$role_without_access = Role::create(['id' => 'role_without_access']);
$role_without_access->save();
$user_with_access = User::create(['roles' => ['role_with_access']]);
$user_without_access = User::create(['roles' => ['role_without_access']]);
$url_always_access = new Url('router_test.1');
$this->assertTrue($url_always_access->access($user_with_access));
$this->assertTrue($url_always_access->access($user_without_access));
$url_none_access = new Url('router_test.15');
$this->assertFalse($url_none_access->access($user_with_access));
$this->assertFalse($url_none_access->access($user_without_access));
$url_access = new Url('router_test.16');
$this->assertTrue($url_access->access($user_with_access));
$this->assertFalse($url_access->access($user_without_access));
}
}
......@@ -25,7 +25,7 @@ public function getFormId() {
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.admin', array(), array(
return new Url('form_test.route6', array(), array(
'query' => array(
'destination' => 'admin/config',
),
......
......@@ -34,7 +34,7 @@ public function getQuestion() {
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('system.admin');
return new Url('form_test.route8');
}
/**
......
......@@ -93,6 +93,20 @@ router_test.14:
defaults:
_controller: '\Drupal\router_test\TestControllers::test9'
router_test.15:
path: '/router_test/test15'
defaults:
_controller: '\Drupal\router_test\TestControllers::test1'
requirements:
_access: 'FALSE'
router_test.16:
path: '/router_test/test16'
defaults:
_controller: '\Drupal\router_test\TestControllers::test1'
requirements:
_permission: 'administer users'
router_test.hierarchy_parent:
path: '/menu-test/parent'
defaults:
......
......@@ -91,6 +91,11 @@ class AccessManagerTest extends UnitTestCase {
*/
protected $requestStack;
/**
* @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $currentUser;
/**
* {@inheritdoc}
*/
......@@ -129,11 +134,12 @@ protected function setUp() {
$this->paramConverter = $this->getMock('Drupal\Core\ParamConverter\ParamConverterManagerInterface');
$this->account = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface');
$this->argumentsResolver = $this->getMock('Drupal\Core\Access\AccessArgumentsResolverInterface');
$this->requestStack = new RequestStack();
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$this->accessManager->setContainer($this->container);
}
......@@ -162,7 +168,7 @@ public function testSetChecks() {
*/
public function testSetChecksWithDynamicAccessChecker() {
// Setup the access manager.
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$this->accessManager->setContainer($this->container);
// Setup the dynamic access checker.
......@@ -219,6 +225,25 @@ public function testCheck() {
$this->assertFalse($this->accessManager->check($this->routeCollection->get('test_route_3'), $request, $this->account));
}
/**
* Tests \Drupal\Core\Access\AccessManager::check() with no account specified.
*
* @covers ::check
*/
public function testCheckWithNullAccount() {
$this->setupAccessChecker();
$this->accessManager->setChecks($this->routeCollection);
$request = new Request;
$route = $this->routeCollection->get('test_route_2');
$this->argumentsResolver->expects($this->once())
->method('getArguments')
->with(array($this->container->get('test_access_default'), 'access'), $route, $request, $this->currentUser)
->will($this->returnCallback(function ($callable, $route, $request, $account) {
return array($route);
}));
$this->accessManager->check($route, $request);
}
/**
* Provides data for the conjunction test.
*
......@@ -472,7 +497,7 @@ public function testCheckNamedRouteWithUpcastedValues() {
$subrequest = Request::create('/test-route-1/example');
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$this->accessManager->setContainer($this->container);
$this->requestStack->push(new Request());
......@@ -532,7 +557,7 @@ public function testCheckNamedRouteWithDefaultValue() {
$subrequest = Request::create('/test-route-1/example');
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$this->accessManager->setContainer($this->container);
$this->requestStack->push(new Request());
......@@ -610,7 +635,7 @@ public function testCheckException($return_value, $access_mode) {
->will($this->returnValue($return_value));
$container->set('test_incorrect_value', $access_check);
$access_manager = new AccessManager($route_provider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$access_manager = new AccessManager($route_provider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$access_manager->setContainer($container);
$access_manager->addCheckService('test_incorrect_value', 'access');
......@@ -665,7 +690,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->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack);
$this->accessManager = new AccessManager($this->routeProvider, $this->urlGenerator, $this->paramConverter, $this->argumentsResolver, $this->requestStack, $this->currentUser);
$this->accessManager->setContainer($this->container);
$access_check = new DefaultAccessCheck();
$this->container->register('test_access_default', $access_check);
......
......@@ -7,6 +7,7 @@
namespace Drupal\Tests\Core;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
......@@ -21,6 +22,11 @@
*/
class UrlTest extends UnitTestCase {
/**
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* The URL generator
*
......@@ -60,10 +66,10 @@ protected function setUp() {
->will($this->returnValueMap($this->map));
$this->router = $this->getMock('Drupal\Tests\Core\Routing\TestRouterInterface');
$container = new ContainerBuilder();
$container->set('router.no_access_checks', $this->router);
$container->set('url_generator', $this->urlGenerator);
\Drupal::setContainer($container);
$this->container = new ContainerBuilder();
$this->container->set('router.no_access_checks', $this->router);
$this->container->set('url_generator', $this->urlGenerator);
\Drupal::setContainer($this->container);
}
/**
......@@ -333,4 +339,81 @@ public function testGetOptions($urls) {
}
}
/**
* Tests the access() method.
*
* @param bool $access
*
* @covers ::access
* @covers ::getAccessManager
* @covers ::setAccessManager
* @dataProvider accessProvider
*/
public function testAccess($access) {
$account = $this->getMock('Drupal\Core\Session\AccountInterface');