diff --git a/core/core.services.yml b/core/core.services.yml index 12f3a3c..beef3f9 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -261,7 +261,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']] @@ -446,6 +446,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: @@ -504,6 +509,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: @@ -522,6 +529,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 0000000..1f329ed --- /dev/null +++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php @@ -0,0 +1,71 @@ +get() using the same value as the + * "_csrf" 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'); + } + + /** + * {@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')) ? 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 0000000..e9633a7 --- /dev/null +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -0,0 +1,49 @@ +csrfToken = $csrf_token; + } + + /** + * {@inheritdoc} + */ + public function processOutbound(Route $route, array &$parameters) { + if ($route->hasRequirement('_csrf')) { + // 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')); + } + } + +} + diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 9cd8f5a..8a525c9 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 0000000..dd95869 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/RegisterRouteProcessorsPass.php @@ -0,0 +1,34 @@ +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 09261a6..0c31999 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 347a877..9e69001 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 0000000..c9bda24 --- /dev/null +++ b/core/lib/Drupal/Core/RouteProcessor/OutboundRouteProcessorInterface.php @@ -0,0 +1,32 @@ +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 b6e2609..1430f1f 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 bf6956f..1bb4b53 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. @@ -40,6 +41,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. * * @var string @@ -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; @@ -336,6 +349,19 @@ protected function processPath($path, &$options = array()) { } /** + * 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. * * @return bool diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php b/core/modules/shortcut/lib/Drupal/shortcut/Controller/ShortcutSetController.php index 7123ae5..d2f38eb 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 dc9bd86..4f1245c 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -456,14 +456,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')) { @@ -476,7 +477,8 @@ function shortcut_preprocess_page(&$variables) { '#prefix' => '', ); diff --git a/core/modules/shortcut/shortcut.routing.yml b/core/modules/shortcut/shortcut.routing.yml index 73decdd..4f902a5 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: '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 0000000..2a12387 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php @@ -0,0 +1,135 @@ + '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->accessCheck = new CsrfAccessCheck($this->csrfToken); + } + + /** + * Tests CsrfAccessCheck::appliesTo(). + */ + public function testAppliesTo() { + $this->assertEquals($this->accessCheck->appliesTo(), array('_csrf'), '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' => '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)); + } + + /** + * 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' => '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)); + } + + /** + * 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' => 'test')); + $request = new Request(array( + 'token' => 'test_query', + )); + + $this->assertSame(AccessInterface::DENY, $this->accessCheck->access($route, $request)); + } + + /** + * 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' => 'test'), array('_access_mode' => 'ALL')); + $request = new Request(array( + 'token' => 'test_query', + )); + + $this->assertSame(AccessInterface::ALLOW, $this->accessCheck->access($route, $request)); + } + +} 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 0000000..f75cd5f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Access/RouteProcessorCsrfTest.php @@ -0,0 +1,87 @@ + '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 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 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' => '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 0000000..6d23ca6 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/RouteProcessor/RouteProcessorManagerTest.php @@ -0,0 +1,83 @@ + '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 75d051d..d1c59c9 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);