diff --git a/core/core.services.yml b/core/core.services.yml index 37a1c482d334a6a2817406576e7058031c9e263b..8b4eed020ab46897e694c926e23bb3b59050b90f 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -264,7 +264,7 @@ services: - [setFinalMatcher, ['@router.matcher.final_matcher']] url_generator: class: Drupal\Core\Routing\UrlGenerator - arguments: ['@router.route_provider', '@path_processor_manager', '@config.factory', '@settings'] + arguments: ['@router.route_provider', '@path_processor_manager', '@route_processor_manager', '@config.factory', '@settings'] calls: - [setRequest, ['@?request']] - [setContext, ['@?router.request_context']] @@ -449,6 +449,11 @@ services: arguments: ['@controller_resolver'] tags: - { name: access_check } + access_check.csrf: + class: Drupal\Core\Access\CsrfAccessCheck + tags: + - { name: access_check } + arguments: ['@csrf_token'] maintenance_mode_subscriber: class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber tags: @@ -507,6 +512,8 @@ services: tags: - { name: event_subscriber } arguments: [['@exception_controller', execute]] + route_processor_manager: + class: Drupal\Core\RouteProcessor\RouteProcessorManager path_processor_manager: class: Drupal\Core\PathProcessor\PathProcessorManager path_processor_decode: @@ -525,6 +532,11 @@ services: - { name: path_processor_inbound, priority: 100 } - { name: path_processor_outbound, priority: 300 } arguments: ['@path.alias_manager'] + route_processer_csrf: + class: Drupal\Core\Access\RouteProcessorCsrf + tags: + - { name: route_processor_outbound } + arguments: ['@csrf_token'] transliteration: class: Drupal\Core\Transliteration\PHPTransliteration flood: diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php new file mode 100644 index 0000000000000000000000000000000000000000..d6ba3a846e22cf62492241f3c5348dbdc8b6f4ce --- /dev/null +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php @@ -0,0 +1,71 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Access\CsrfAccessCheck. + */ + +namespace Drupal\Core\Access; + +use Drupal\Core\Access\CsrfTokenGenerator; +use Drupal\Core\Session\AccountInterface; +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 StaticAccessCheckInterface { + + /** + * 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. + */ + function __construct(CsrfTokenGenerator $csrf_token) { + $this->csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function appliesTo() { + return array('_csrf_token'); + } + + /** + * {@inheritdoc} + */ + public function access(Route $route, Request $request, AccountInterface $account) { + // If this is the controller request, check CSRF access as normal. + if ($request->attributes->get('_controller_request')) { + return $this->csrfToken->validate($request->query->get('token'), $route->getRequirement('_csrf_token')) ? static::ALLOW : static::KILL; + } + + // Otherwise, this could be another requested access check that we don't + // want to check CSRF tokens on. + $conjunction = $route->getOption('_access_mode') ?: 'ANY'; + // Return ALLOW if all access checks are needed. + if ($conjunction == 'ALL') { + return static::ALLOW; + } + // Return DENY otherwise, as another access checker should grant access + // for the route. + else { + return static::DENY; + } + } + +} diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php new file mode 100644 index 0000000000000000000000000000000000000000..0fb075cedfd9cc0dc5c30fde2149576178fbd934 --- /dev/null +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -0,0 +1,49 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Access\RouteProcessorCsrf. + */ + +namespace Drupal\Core\Access; + +use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; +use Drupal\Core\Access\CsrfTokenGenerator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; + +/** + * Processes the inbound path by resolving it to the front page if empty. + */ +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 $route, array &$parameters) { + if ($route->hasRequirement('_csrf_token')) { + // Adding this to the parameters means it will get merged into the query + // string when the route is compiled. + $parameters['token'] = $this->csrfToken->get($route->getRequirement('_csrf_token')); + } + } + +} + diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 9cd8f5a87bbd62d790e5a096952b72d26edc6800..8a525c9abdbcd96eaa0d80cfe5642ba41408399c 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -14,6 +14,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterKernelListenersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterAccessChecksPass; use Drupal\Core\DependencyInjection\Compiler\RegisterPathProcessorsPass; +use Drupal\Core\DependencyInjection\Compiler\RegisterRouteProcessorsPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteFiltersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterRouteEnhancersPass; use Drupal\Core\DependencyInjection\Compiler\RegisterParamConvertersPass; @@ -63,6 +64,7 @@ public function register(ContainerBuilder $container) { $container->addCompilerPass(new RegisterServicesForDestructionPass()); // Add the compiler pass that will process the tagged services. $container->addCompilerPass(new RegisterPathProcessorsPass()); + $container->addCompilerPass(new RegisterRouteProcessorsPass()); $container->addCompilerPass(new ListCacheBinsPass()); // Add the compiler pass for appending string translators. $container->addCompilerPass(new RegisterStringTranslatorsPass()); diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php new file mode 100644 index 0000000000000000000000000000000000000000..dd958695b1932c9d302a681566ddb64bb1763385 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\DependencyInjection\Compiler\RegisterRouteProcessorsPass. + */ + +namespace Drupal\Core\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Adds services to the route_processor_manager service. + */ +class RegisterRouteProcessorsPass implements CompilerPassInterface { + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) { + if (!$container->hasDefinition('route_processor_manager')) { + return; + } + $manager = $container->getDefinition('route_processor_manager'); + // Add outbound route processors. + foreach ($container->findTaggedServiceIds('route_processor_outbound') as $id => $attributes) { + $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $manager->addMethodCall('addOutbound', array(new Reference($id), $priority)); + } + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php index 09261a6c509887cc9a22e9ef9bda188e004335ab..0c319994395bd6adca01c2f7463ea155102a868a 100644 --- a/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/AccessSubscriber.php @@ -11,6 +11,7 @@ use Drupal\Core\Session\AccountInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -59,13 +60,29 @@ public function __construct(AccessManager $access_manager, AccountInterface $cur */ public function onKernelRequestAccessCheck(GetResponseEvent $event) { $request = $event->getRequest(); + + // The controller is being handled by the HTTP kernel, so add an attribute + // to tell us this is the controller request. + $request->attributes->set('_controller_request', TRUE); + if (!$request->attributes->has(RouteObjectInterface::ROUTE_OBJECT)) { // If no Route is available it is likely a static resource and access is // handled elsewhere. return; } - $access = $this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT), $request, $this->currentUser); + // Wrap this in a try/catch to ensure the '_controller_request' attribute + // can always be removed. + try { + $access = $this->accessManager->check($request->attributes->get(RouteObjectInterface::ROUTE_OBJECT), $request, $this->currentUser); + } + catch (\Exception $e) { + $request->attributes->remove('_controller_request'); + throw $e; + } + + $request->attributes->remove('_controller_request'); + if (!$access) { throw new AccessDeniedHttpException(); } diff --git a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php index 347a8777574bcd5e7be655aef30afd1dad5c0b1f..9e6900181498bb2a4b4bffb47920c1eb25f4a6ad 100644 --- a/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php +++ b/core/lib/Drupal/Core/PathProcessor/OutboundPathProcessorInterface.php @@ -8,6 +8,7 @@ namespace Drupal\Core\PathProcessor; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; /** * Defines an interface for classes that process the outbound path. diff --git a/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c9bda247fe8dec9788f97afd57b721a726c5806f --- /dev/null +++ b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface. + */ + +namespace Drupal\Core\RouteProcessor; + +use Symfony\Component\Routing\Route; + +/** + * Defines an interface for classes that process the outbound route. + */ +interface OutboundRouteProcessorInterface { + + /** + * Processes the outbound route. + * + * @param \Symfony\Component\Routing\Route $route + * The outbound route to process. + * + * @param array $parameters + * An array of parameters to be passed to the route compiler. Passed by + * reference. + * + * @return + * The processed path. + */ + public function processOutbound(Route $route, array &$parameters); + +} diff --git a/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php new file mode 100644 index 0000000000000000000000000000000000000000..43071be3d739df0c506182e63ce44a3221861d49 --- /dev/null +++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorManager.php @@ -0,0 +1,88 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\RouteProcessor\RouteProcessorManager. + */ + +namespace Drupal\Core\RouteProcessor; + +use Symfony\Component\Routing\Route; + +/** + * Route processor manager. + * + * Holds an array of route processor objects and uses them to sequentially + * process an outbound route, in order of processor priority. + */ +class RouteProcessorManager implements OutboundRouteProcessorInterface { + + /** + * Holds the array of outbound processors to cycle through. + * + * @var array + * An array whose keys are priorities and whose values are arrays of path + * processor objects. + */ + protected $outboundProcessors = array(); + + /** + * Holds the array of outbound processors, sorted by priority. + * + * @var array + * An array of path processor objects. + */ + protected $sortedOutbound = array(); + + /** + * Adds an outbound processor object to the $outboundProcessors property. + * + * @param \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface $processor + * The processor object to add. + * + * @param int $priority + * The priority of the processor being added. + */ + public function addOutbound(OutboundRouteProcessorInterface $processor, $priority = 0) { + $this->outboundProcessors[$priority][] = $processor; + $this->sortedOutbound = array(); + } + + /** + * {@inheritdoc} + */ + public function processOutbound(Route $route, array &$parameters) { + $processors = $this->getOutbound(); + foreach ($processors as $processor) { + $processor->processOutbound($route, $parameters); + } + } + + /** + * Returns the sorted array of outbound processors. + * + * @return array + * An array of processor objects. + */ + protected function getOutbound() { + if (empty($this->sortedOutbound)) { + $this->sortedOutbound = $this->sortProcessors(); + } + + return $this->sortedOutbound; + } + + /** + * Sorts the processors according to priority. + */ + protected function sortProcessors() { + $sorted = array(); + krsort($this->outboundProcessors); + + foreach ($this->outboundProcessors as $processors) { + $sorted = array_merge($sorted, $processors); + } + return $sorted; + } + +} diff --git a/core/lib/Drupal/Core/Routing/NullGenerator.php b/core/lib/Drupal/Core/Routing/NullGenerator.php index b6e26097fde15a11c7ab8e8c7d5648540effef02..1430f1f744dce5e1622c2738f8151ed0973e36c2 100644 --- a/core/lib/Drupal/Core/Routing/NullGenerator.php +++ b/core/lib/Drupal/Core/Routing/NullGenerator.php @@ -9,6 +9,7 @@ use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Route; /** * No-op implementation of a Url Generator, needed for backward compatibility. diff --git a/core/lib/Drupal/Core/Routing/UrlGenerator.php b/core/lib/Drupal/Core/Routing/UrlGenerator.php index bf6956fd0877b23583a00d9d4a0a8aef883a8f08..1bb4b53b50353253688af34f860a06c9eed76034 100644 --- a/core/lib/Drupal/Core/Routing/UrlGenerator.php +++ b/core/lib/Drupal/Core/Routing/UrlGenerator.php @@ -19,6 +19,7 @@ use Drupal\Component\Utility\Url; use Drupal\Core\Config\ConfigFactory; use Drupal\Core\PathProcessor\OutboundPathProcessorInterface; +use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; /** * Generates URLs from route names and parameters. @@ -39,6 +40,13 @@ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterfa */ protected $pathProcessor; + /** + * The route processor. + * + * @var \Drupal\Tests\Core\RouteProcessor\OutboundRouteProcessorInterface + */ + protected $routeProcessor; + /** * The base path to use for urls. * @@ -77,10 +85,11 @@ class UrlGenerator extends ProviderBasedGenerator implements UrlGeneratorInterfa * @param \Symfony\Component\HttpKernel\Log\LoggerInterface $logger * An optional logger for recording errors. */ - public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, ConfigFactory $config, Settings $settings, LoggerInterface $logger = NULL) { + public function __construct(RouteProviderInterface $provider, OutboundPathProcessorInterface $path_processor, OutboundRouteProcessorInterface $route_processor, ConfigFactory $config, Settings $settings, LoggerInterface $logger = NULL) { parent::__construct($provider, $logger); $this->pathProcessor = $path_processor; + $this->routeProcessor = $route_processor; $this->mixedModeSessions = $settings->get('mixed_mode_sessions', FALSE); $allowed_protocols = $config->get('system.filter')->get('protocols') ?: array('http', 'https'); Url::setAllowedProtocols($allowed_protocols); @@ -167,10 +176,13 @@ public function generate($name, $parameters = array(), $absolute = FALSE) { public function generateFromRoute($name, $parameters = array(), $options = array()) { $absolute = !empty($options['absolute']); $route = $this->getRoute($name); + $this->processRoute($route, $parameters); + // Symfony adds any parameters that are not path slugs as query strings. if (isset($options['query']) && is_array($options['query'])) { $parameters = (array) $parameters + $options['query']; } + $path = $this->getInternalPathFromRoute($route, $parameters); $path = $this->processPath($path, $options); $fragment = ''; @@ -179,6 +191,7 @@ public function generateFromRoute($name, $parameters = array(), $options = array $fragment = '#' . $fragment; } } + $base_url = $this->context->getBaseUrl(); if (!$absolute || !$host = $this->context->getHost()) { return $base_url . $path . $fragment; @@ -335,6 +348,19 @@ protected function processPath($path, &$options = array()) { return $path; } + /** + * Passes the route to the processor manager for altering before complation. + * + * @param \Symfony\Component\Routing\Route $route + * The route object to process. + * + * @param array $parameters + * An array of parameters to be passed to the route compiler. + */ + protected function processRoute(SymfonyRoute $route, array &$parameters) { + $this->routeProcessor->processOutbound($route, $parameters); + } + /** * Returns whether or not the url generator has been initialized. * diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php index 7123ae57e20b2eb85066ffce940e71be41c88620..d2f38eb3236eee482c13d7c190c0a30f727d9a43 100644 --- a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php +++ b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php @@ -33,9 +33,8 @@ class ShortcutSetController extends ControllerBase { * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function addShortcutLinkInline(ShortcutSetInterface $shortcut_set, Request $request) { - $token = $request->query->get('token'); $link = $request->query->get('link'); - if (isset($token) && drupal_valid_token($token, 'shortcut-add-link') && shortcut_valid_link($link)) { + if (shortcut_valid_link($link)) { $item = menu_get_item($link); $title = ($item && $item['title']) ? $item['title'] : $link; $link = array( diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index 724309c15d0e1a90be8d0ddacc5e7a2787d6c056..5579d55b75372220ce999f8bd3a6a588382ad344 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -451,14 +451,15 @@ function shortcut_preprocess_page(&$variables) { $link_mode = isset($mlid) ? "remove" : "add"; if ($link_mode == "add") { - $query['token'] = drupal_get_token('shortcut-add-link'); $link_text = shortcut_set_switch_access() ? t('Add to %shortcut_set shortcuts', array('%shortcut_set' => $shortcut_set->label())) : t('Add to shortcuts'); - $link_path = 'admin/config/user-interface/shortcut/manage/' . $shortcut_set->id() . '/add-link-inline'; + $route_name = 'shortcut.link_add_inline'; + $route_parameters = array('shortcut_set' => $shortcut_set->id()); } else { $query['mlid'] = $mlid; $link_text = shortcut_set_switch_access() ? t('Remove from %shortcut_set shortcuts', array('%shortcut_set' => $shortcut_set->label())) : t('Remove from shortcuts'); - $link_path = 'admin/config/user-interface/shortcut/link/' . $mlid . '/delete'; + $route_name = 'shortcut.link_delete'; + $route_parameters = array('menu_link' => $mlid); } if (theme_get_setting('shortcut_module_link')) { @@ -471,7 +472,8 @@ function shortcut_preprocess_page(&$variables) { '#prefix' => '<div class="add-or-remove-shortcuts ' . $link_mode . '-shortcut">', '#type' => 'link', '#title' => '<span class="icon">'. t('Add or remove shortcut') .'</span><span class="text">' . $link_text . '</span>', - '#href' => $link_path, + '#route_name' => $route_name, + '#route_parameters' => $route_parameters, '#options' => array('query' => $query, 'html' => TRUE), '#suffix' => '</div>', ); diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml index 73decdd9a0aa3e56425b619942b43c55b56b91ae..34dbac181dab1a4326f84dd0886e9322727d6e86 100644 --- a/core/modules/shortcut/shortcut.routing.yml +++ b/core/modules/shortcut/shortcut.routing.yml @@ -40,6 +40,7 @@ shortcut.link_add_inline: _controller: 'Drupal\shortcut\Controller\ShortcutSetController::addShortcutLinkInline' requirements: _entity_access: 'shortcut_set.update' + _csrf_token: 'shortcut-add-link' shortcut.set_customize: path: '/admin/config/user-interface/shortcut/manage/{shortcut_set}' diff --git a/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8c1066944b04265cab65ca73ade9da89fd9ffa59 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php @@ -0,0 +1,144 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Access\CsrfAccessCheckTest. + */ + +namespace Drupal\Tests\Core\Access; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Route; +use Drupal\Core\Access\CsrfAccessCheck; +use Drupal\Core\Access\AccessInterface; +use Drupal\Tests\UnitTestCase; + +/** + * Tests the CSRF access checker.. + * + * @group Drupal + * @group Access + * + * @see \Drupal\Core\Access\CsrfAccessCheck + */ +class CsrfAccessCheckTest extends UnitTestCase { + + /** + * The mock CSRF token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + protected $csrfToken; + + /** + * The access checker. + * + * @var \Drupal\Core\Access\CsrfAccessCheck + */ + protected $accessCheck; + + /** + * The mock user account. + * + * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $account; + + public static function getInfo() { + return array( + 'name' => 'CSRF access checker', + 'description' => 'Tests CSRF access control for routes.', + 'group' => 'Routing', + ); + } + + public function setUp() { + $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator') + ->disableOriginalConstructor() + ->getMock(); + + $this->account = $this->getMock('Drupal\Core\Session\AccountInterface'); + + $this->accessCheck = new CsrfAccessCheck($this->csrfToken); + } + + /** + * Tests CsrfAccessCheck::appliesTo(). + */ + public function testAppliesTo() { + $this->assertEquals($this->accessCheck->appliesTo(), array('_csrf_token'), 'Access checker returned the expected appliesTo() array.'); + } + + /** + * Tests the access() method with a valid token. + */ + public function testAccessTokenPass() { + $this->csrfToken->expects($this->once()) + ->method('validate') + ->with('test_query', 'test') + ->will($this->returnValue(TRUE)); + + $route = new Route('', array(), array('_csrf_token' => 'test')); + $request = new Request(array( + 'token' => 'test_query', + )); + // Set the _controller_request flag so tokens are validated. + $request->attributes->set('_controller_request', TRUE); + + $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request, $this->account)); + } + + /** + * Tests the access() method with an invalid token. + */ + public function testAccessTokenFail() { + $this->csrfToken->expects($this->once()) + ->method('validate') + ->with('test_query', 'test') + ->will($this->returnValue(FALSE)); + + $route = new Route('', array(), array('_csrf_token' => 'test')); + $request = new Request(array( + 'token' => 'test_query', + )); + // Set the _controller_request flag so tokens are validated. + $request->attributes->set('_controller_request', TRUE); + + $this->assertSame(AccessInterface::KILL, $this->accessCheck->access($route, $request, $this->account)); + } + + /** + * Tests the access() method with no _controller_request attribute set. + * + * This will default to the 'ANY' access conjuction. + */ + public function testAccessTokenMissAny() { + $this->csrfToken->expects($this->never()) + ->method('validate'); + + $route = new Route('', array(), array('_csrf_token' => 'test')); + $request = new Request(array( + 'token' => 'test_query', + )); + + $this->assertSame(AccessInterface::DENY, $this->accessCheck->access($route, $request, $this->account)); + } + + /** + * Tests the access() method with no _controller_request attribute set. + * + * This will use the 'ALL' access conjuction. + */ + public function testAccessTokenMissAll() { + $this->csrfToken->expects($this->never()) + ->method('validate'); + + $route = new Route('', array(), array('_csrf_token' => 'test'), array('_access_mode' => 'ALL')); + $request = new Request(array( + 'token' => 'test_query', + )); + + $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request, $this->account)); + } + +} diff --git a/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php new file mode 100644 index 0000000000000000000000000000000000000000..37eec23ab95d94c92e4f09047e4b41a420956ee5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php @@ -0,0 +1,87 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Access\RouteProcessorCsrfTest. + */ + +namespace Drupal\Tests\Core\Access; + +use Drupal\Tests\UnitTestCase; +use Drupal\Core\Access\RouteProcessorCsrf; +use Symfony\Component\Routing\Route; + +/** + * Tests the CSRF route processor. + * + * @see Drupal + * @see Routing + * + * @see \Drupal\Core\Access\RouteProcessorCsrf + */ +class RouteProcessorCsrfTest extends UnitTestCase { + + /** + * The mock CSRF token generator. + * + * @var \Drupal\Core\Access\CsrfTokenGenerator|\PHPUnit_Framework_MockObject_MockObject + */ + protected $csrfToken; + + /** + * The route processor. + * + * @var \Drupal\Core\Access\RouteProcessorCsrf + */ + protected $processor; + + public static function getInfo() { + return array( + 'name' => 'CSRF access checker', + 'description' => 'Tests CSRF access control for routes.', + 'group' => 'Routing', + ); + } + + public function setUp() { + $this->csrfToken = $this->getMockBuilder('Drupal\Core\Access\CsrfTokenGenerator') + ->disableOriginalConstructor() + ->getMock(); + + $this->processor = new RouteProcessorCsrf($this->csrfToken); + } + + /** + * Tests the processOutbound() method with no _csrf_token route requirement. + */ + public function testProcessOutboundNoRequirement() { + $this->csrfToken->expects($this->never()) + ->method('get'); + + $route = new Route(''); + $parameters = array(); + + $this->processor->processOutbound($route, $parameters); + // No parameters should be added to the parameters array. + $this->assertEmpty($parameters); + } + + /** + * Tests the processOutbound() method with a _csrf_token route requirement. + */ + public function testProcessOutbound() { + $this->csrfToken->expects($this->once()) + ->method('get') + ->with('test') + ->will($this->returnValue('test_token')); + + $route = new Route('', array(), array('_csrf_token' => 'test')); + $parameters = array(); + + $this->processor->processOutbound($route, $parameters); + // 'token' should be added to the parameters array. + $this->assertArrayHasKey('token', $parameters); + $this->assertSame($parameters['token'], 'test_token'); + } + +} diff --git a/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..6d23ca6b57aac5291cfc640a6bfa3ff607d55742 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php @@ -0,0 +1,83 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\RouteProcessor\RouteProcessorManagerTest. + */ + +namespace Drupal\Tests\Core\RouteProcessor; + +use Drupal\Core\RouteProcessor\RouteProcessorManager; +use Drupal\Tests\UnitTestCase; +use Symfony\Component\Routing\Route; + +/** + * Tests the RouteProcessorManager class. + * + * @group Drupal + * @group Routing + * + * @see \Drupal\Core\RouteProcessor\RouteProcessorManager + */ +class RouteProcessorManagerTest extends UnitTestCase { + + /** + * The route processor manager. + * + * @var \Drupal\Core\RouteProcessor\RouteProcessorManager + */ + protected $processorManager; + + public static function getInfo() { + return array( + 'name' => 'Route processor manager', + 'description' => 'Tests the RouteProcessorManager class.', + 'group' => 'Routing', + ); + } + + public function setUp() { + $this->processorManager = new RouteProcessorManager(); + } + + /** + * Tests the Route process manager functionality. + */ + public function testRouteProcessorManager() { + $route = new Route(''); + $parameters = array('test' => 'test'); + + $processors = array( + 10 => $this->getMockProcessor($route, $parameters), + 5 => $this->getMockProcessor($route, $parameters), + 0 => $this->getMockProcessor($route, $parameters), + ); + + // Add the processors in reverse order. + foreach ($processors as $priority => $processor) { + $this->processorManager->addOutbound($processor, $priority); + } + + $this->processorManager->processOutbound($route, $parameters); + } + + /** + * Returns a mock Route processor object. + * + * @param \Symfony\Component\Routing\Route $route + * The Route to use in mock with() expectation. + * @param array $parameters + * The parameters to use in mock with() expectation. + * + * @return \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockProcessor($route, $parameters) { + $processor = $this->getMock('Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface'); + $processor->expects($this->once()) + ->method('processOutbound') + ->with($route, $parameters); + + return $processor; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php index 75d051dea8bb57d047c0891de7c00580dd0703d1..d1c59c93fd38321017fa9cf8e46fbbff92e4b640 100644 --- a/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php +++ b/core/tests/Drupal/Tests/Core/Routing/UrlGeneratorTest.php @@ -8,21 +8,15 @@ namespace Drupal\Tests\Core\Routing; use Drupal\Component\Utility\Settings; -use Drupal\Core\Config\ConfigFactory; -use Drupal\Core\Config\NullStorage; -use Drupal\Core\Config\Context\ConfigContextFactory; use Drupal\Core\PathProcessor\PathProcessorAlias; use Drupal\Core\PathProcessor\PathProcessorManager; -use Symfony\Component\EventDispatcher\EventDispatcher; +use Drupal\Core\Routing\UrlGenerator; +use Drupal\Tests\UnitTestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\RequestContext; -use Drupal\Tests\UnitTestCase; - -use Drupal\Core\Routing\UrlGenerator; - /** * Basic tests for the Route. * @@ -44,8 +38,20 @@ class UrlGeneratorTest extends UnitTestCase { */ protected $generatorMixedMode; + /** + * The alias manager. + * + * @var \Drupal\Core\Path\AliasManager|\PHPUnit_Framework_MockObject_MockObject + */ protected $aliasManager; + /** + * The mock route processor manager. + * + * @var \Drupal\Core\RouteProcessor\RouteProcessorManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $routeProcessorManager; + public static function getInfo() { return array( 'name' => 'UrlGenerator', @@ -121,14 +127,18 @@ function setUp() { $processor_manager = new PathProcessorManager(); $processor_manager->addOutbound($processor, 1000); + $this->routeProcessorManager = $this->getMockBuilder('Drupal\Core\RouteProcessor\RouteProcessorManager') + ->disableOriginalConstructor() + ->getMock(); + $config_factory_stub = $this->getConfigFactoryStub(array('system.filter' => array('protocols' => array('http', 'https')))); - $generator = new UrlGenerator($provider, $processor_manager, $config_factory_stub, new Settings(array())); + $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, new Settings(array())); $generator->setContext($context); $this->generator = $generator; // Second generator for mixed-mode sessions. - $generator = new UrlGenerator($provider, $processor_manager, $config_factory_stub, new Settings(array('mixed_mode_sessions' => TRUE))); + $generator = new UrlGenerator($provider, $processor_manager, $this->routeProcessorManager, $config_factory_stub, new Settings(array('mixed_mode_sessions' => TRUE))); $generator->setContext($context); $this->generatorMixedMode = $generator; } @@ -163,6 +173,11 @@ public function testAliasGeneration() { $url = $this->generator->generate('test_1'); $this->assertEquals('/hello/world', $url); + $this->routeProcessorManager->expects($this->once()) + ->method('processOutbound') + ->with($this->anything()); + + // Check that the two generate methods return the same result. $url_from_route = $this->generator->generateFromRoute('test_1'); $this->assertEquals($url_from_route, $url); @@ -177,6 +192,9 @@ public function testAliasGeneration() { public function testGetPathFromRouteWithSubdirectory() { $this->generator->setBasePath('/test-base-path'); + $this->routeProcessorManager->expects($this->never()) + ->method('processOutbound'); + $path = $this->generator->getPathFromRoute('test_1'); $this->assertEquals('test/one', $path); } @@ -188,6 +206,10 @@ public function testAliasGenerationWithParameters() { $url = $this->generator->generate('test_2', array('narf' => '5')); $this->assertEquals('/goodbye/cruel/world', $url); + $this->routeProcessorManager->expects($this->exactly(3)) + ->method('processOutbound') + ->with($this->anything()); + $options = array('fragment' => 'top'); // Extra parameters should appear in the query string. $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options); @@ -209,6 +231,9 @@ public function testAliasGenerationWithParameters() { * Tests URL generation from route with trailing start and end slashes. */ public function testGetPathFromRouteTrailing() { + $this->routeProcessorManager->expects($this->never()) + ->method('processOutbound'); + $path = $this->generator->getPathFromRoute('test_3'); $this->assertEquals($path, 'test/two'); } @@ -220,6 +245,10 @@ public function testAbsoluteURLGeneration() { $url = $this->generator->generate('test_1', array(), TRUE); $this->assertEquals('http://localhost/hello/world', $url); + $this->routeProcessorManager->expects($this->once()) + ->method('processOutbound') + ->with($this->anything()); + $options = array('absolute' => TRUE, 'fragment' => 'top'); // Extra parameters should appear in the query string. $url = $this->generator->generateFromRoute('test_1', array('zoo' => '5'), $options); @@ -233,6 +262,10 @@ public function testUrlGenerationWithHttpsRequirement() { $url = $this->generator->generate('test_4', array(), TRUE); $this->assertEquals('https://localhost/test/four', $url); + $this->routeProcessorManager->expects($this->exactly(2)) + ->method('processOutbound') + ->with($this->anything()); + $options = array('absolute' => TRUE, 'https' => TRUE); // Mixed-mode sessions are not enabled, so the https option is ignored. $url = $this->generator->generateFromRoute('test_1', array(), $options);