diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index c6aa2b8..36a5641 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -99,6 +99,26 @@ public function testFinishResponseSubscriber() { $this->assertFalse(isset($headers['x-drupal-cache-contexts'])); $this->assertFalse(isset($headers['x-drupal-cache-tags'])); } + /** + * Confirms that multiple routes with the same path do not cause an error. + */ + public function testDuplicateRoutePaths() { + // Tests two routes with exactly the same path. The first route declared in + // the routing.yml will respond. + $this->drupalGet('router-test/duplicate-path2'); + $this->assertResponse(200); + $this->assertRaw('router_test.two_duplicate1'); + + // Tests three routes with same the path. On one of the routes the path has + // a different case. The first route declared in the routing.yml will + // respond regardless of case. + $this->drupalGet('router-test/duplicate-path3'); + $this->assertResponse(200); + $this->assertRaw('router_test.three_duplicate1'); + $this->drupalGet('router-test/Duplicate-PATH3'); + $this->assertResponse(200); + $this->assertRaw('router_test.three_duplicate1'); + } /** * Confirms that placeholders in paths work correctly. diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml index ad2d418..ee8745f 100644 --- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml @@ -212,3 +212,38 @@ router_test.hierarchy_parent_child2: _controller: '\Drupal\router_test\TestControllers::test' requirements: _access: 'TRUE' + +router_test.two_duplicate1: + path: '/router-test/duplicate-path2' + defaults: + _controller: '\Drupal\router_test\TestControllers::testRouteName' + requirements: + _access: 'TRUE' + +router_test.two_duplicate2: + path: '/router-test/duplicate-path2' + defaults: + _controller: '\Drupal\router_test\TestControllers::testRouteName' + requirements: + _access: 'TRUE' + +router_test.three_duplicate1: + path: '/router-test/duplicate-path3' + defaults: + _controller: '\Drupal\router_test\TestControllers::testRouteName' + requirements: + _access: 'TRUE' + +router_test.three_duplicate2: + path: '/router-test/duplicate-path3' + defaults: + _controller: '\Drupal\router_test\TestControllers::testRouteName' + requirements: + _access: 'TRUE' + +router_test.three_duplicate3: + path: '/router-test/Duplicate-PATH3' + defaults: + _controller: '\Drupal\router_test\TestControllers::testRouteName' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php index c3bcac7..151c140 100644 --- a/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php +++ b/core/modules/system/tests/modules/router_test_directory/src/TestControllers.php @@ -6,6 +6,7 @@ use Drupal\Core\ParamConverter\ParamNotConvertedException; use Drupal\user\UserInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Zend\Diactoros\Response\HtmlResponse; @@ -110,6 +111,12 @@ public function test25() { ]; } + public function testRouteName(Request $request) { + return [ + '#markup' => $request->attributes->get(RouteObjectInterface::ROUTE_NAME), + ]; + } + /** * Throws an exception. * diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index eb8709e..8ef6457 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -313,6 +313,21 @@ public function configureTitle($foo) { } /** + * Simple argument echo. + * + * @param string $text + * Any string for the {text} slug. + * + * @return array + * A render array. + */ + public function simpleEcho($text) { + return [ + '#plain_text' => $text, + ]; + } + + /** * Shows permission-dependent content. * * @return array diff --git a/core/modules/system/tests/modules/system_test/system_test.routing.yml b/core/modules/system/tests/modules/system_test/system_test.routing.yml index f35f546..579b511 100644 --- a/core/modules/system/tests/modules/system_test/system_test.routing.yml +++ b/core/modules/system/tests/modules/system_test/system_test.routing.yml @@ -182,3 +182,17 @@ system_test.header: _controller: '\Drupal\system_test\Controller\SystemTestController::getTestHeader' requirements: _access: 'TRUE' + +system_test.echo: + path: '/system-test/echo/{text}' + defaults: + _controller: '\Drupal\system_test\Controller\SystemTestController::simpleEcho' + requirements: + _access: 'TRUE' + +system_test.echo_utf8: + path: '/system-test/Ȅchȏ/meφΩ/{text}' + defaults: + _controller: '\Drupal\system_test\Controller\SystemTestController::simpleEcho' + requirements: + _access: 'TRUE' diff --git a/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php b/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php new file mode 100644 index 0000000..183d140 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php @@ -0,0 +1,113 @@ +set('system_test.module_hidden', FALSE); + $this->createContentType(['type' => 'page']); + } + + /** + * Tests mixed case paths. + */ + public function testMixedCasePaths() { + // Tests paths defined by routes from standard modules as anonymous. + $this->drupalGet('user/login'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/Log in/'); + $this->drupalGet('User/Login'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/Log in/'); + + // Tests paths defined by routes from the Views module. + $admin = $this->drupalCreateUser(['access administration pages', 'administer nodes', 'access content overview']); + $this->drupalLogin($admin); + + $this->drupalGet('admin/content'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/Content/'); + $this->drupalGet('Admin/Content'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/Content/'); + + // Tests paths with query arguments. + + // Make sure our node title doesn't exist. + $this->drupalGet('admin/content'); + $this->assertSession()->linkNotExists('FooBarBaz'); + $this->assertSession()->linkNotExists('foobarbaz'); + + // Create a node, and make sure it shows up on admin/content. + $node = $this->createNode([ + 'title' => 'FooBarBaz', + 'type' => 'page', + ]); + + $this->drupalGet('admin/content', [ + 'query' => [ + 'title' => 'FooBarBaz' + ] + ]); + + $this->assertSession()->linkExists('FooBarBaz'); + $this->assertSession()->linkByHrefExists($node->toUrl()->toString()); + + // Make sure the path is case insensitive, and query case is preserved. + + $this->drupalGet('Admin/Content', [ + 'query' => [ + 'title' => 'FooBarBaz' + ] + ]); + + $this->assertSession()->linkExists('FooBarBaz'); + $this->assertSession()->linkByHrefExists($node->toUrl()->toString()); + $this->assertSession()->fieldValueEquals('edit-title', 'FooBarBaz'); + // Check that we can access the node with a mixed case path. + $this->drupalGet('NOdE/' . $node->id()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/FooBarBaz/'); + } + + /** + * Tests paths with slugs. + */ + public function testPathsWithArguments() { + $this->drupalGet('system-test/echo/foobarbaz'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/foobarbaz/'); + $this->assertSession()->pageTextNotMatches('/FooBarBaz/'); + + $this->drupalGet('system-test/echo/FooBarBaz'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/FooBarBaz/'); + $this->assertSession()->pageTextNotMatches('/foobarbaz/'); + + // Test utf-8 characters in the route path. + $this->drupalGet('/system-test/Ȅchȏ/meΦω/ABc123'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/ABc123/'); + $this->drupalGet('/system-test/ȅchȎ/MEΦΩ/ABc123'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextMatches('/ABc123/'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php index fa135f0..6a98a8c 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -7,6 +7,7 @@ namespace Drupal\KernelTests\Core\Routing; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\MemoryBackend; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; @@ -195,7 +196,102 @@ function testOutlinePathMatch() { } /** - * Confirms that a trailing slash on the request doesn't result in a 404. + * Data provider for testMixedCasePaths() + */ + public function providerMixedCaseRoutePaths() { + return [ + ['/path/one', 'route_a'], + ['/path/two', NULL], + ['/PATH/one', 'route_a'], + ['/path/2/one', 'route_b', 'PUT'], + ['/paTH/3/one', 'route_b', 'PUT'], + // There should be no lower case of a Hebrew letter. + ['/somewhere/4/over/the/קainbow', 'route_c'], + ['/Somewhere/5/over/the/קainboW', 'route_c'], + ['/another/llama/aboUT/22', 'route_d'], + ['/another/llama/about/22', 'route_d'], + ['/place/meΦω', 'route_e', 'HEAD'], + ['/place/meφΩ', 'route_e', 'HEAD'], + ]; + } + + /** + * Confirms that we find routes using a case insensitive path match. + * + * @dataProvider providerMixedCaseRoutePaths + */ + public function testMixedCasePaths($path, $expected_route_name, $method = 'GET') { + // The case-insensitive behavior for higher UTF-8 characters depends on + // \Drupal\Component\Utility\Unicode::strtolower() using mb_strtolower() + // but kernel tests do not currently run the check that enables it. + // @todo remove this when https://www.drupal.org/node/2849669 is fixed. + Unicode::check(); + + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->mixedCaseRouteCollection()); + $dumper->dump(); + + $request = Request::create($path, $method); + + $routes = $provider->getRouteCollectionForRequest($request); + + if ($expected_route_name) { + $this->assertEquals(1, count($routes), 'The correct number of routes was found.'); + $this->assertNotNull($routes->get($expected_route_name), 'The first matching route was found.'); + } + else { + $this->assertEquals(0, count($routes), 'No routes matched.'); + } + } + + /** + * Data provider for testMixedCasePaths() + */ + public function providerDuplicateRoutePaths() { + return [ + ['/path/one', 3], + ['/PATH/one', 3], + ['/path/two', 1], + ['/PATH/three', 0], + ['/place/meΦω', 2], + ['/placE/meφΩ', 2], + ]; + } + + /** + * Confirms that we find all routes with the same path. + * + * @dataProvider providerDuplicateRoutePaths + */ + public function testDuplicateRoutePaths($path, $number) { + + // The case-insensitive behavior for higher UTF-8 characters depends on + // \Drupal\Component\Utility\Unicode::strtolower() using mb_strtolower() + // but kernel tests do not currently run the check that enables it. + // @todo remove this when https://www.drupal.org/node/2849669 is fixed. + Unicode::check(); + + $connection = Database::getConnection(); + $provider = new RouteProvider($connection, $this->state, $this->currentPath, $this->cache, $this->pathProcessor, $this->cacheTagsInvalidator, 'test_routes'); + + $this->fixtures->createTables($connection); + + $dumper = new MatcherDumper($connection, $this->state, 'test_routes'); + $dumper->addRoutes($this->fixtures->duplicatePathsRouteCollection()); + $dumper->dump(); + + $request = Request::create($path); + $routes = $provider->getRouteCollectionForRequest($request); + $this->assertEquals($number, count($routes), 'The correct number of routes was found.'); + } + + /** + * Confirms that a trailing slash on the request does not result in a 404. */ function testOutlinePathMatchTrailingSlash() { $connection = Database::getConnection(); diff --git a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php index d1826ed..72714b7 100644 --- a/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php +++ b/core/tests/Drupal/Tests/Core/Routing/RoutingFixtures.php @@ -140,6 +140,73 @@ public function complexRouteCollection() { } /** + * Returns a complex set of routes for testing. + * + * @return \Symfony\Component\Routing\RouteCollection + */ + public function mixedCaseRouteCollection() { + $collection = new RouteCollection(); + + $route = new Route('/path/one'); + $route->setMethods(['GET']); + $collection->add('route_a', $route); + + $route = new Route('/path/{thing}/one'); + $route->setMethods(['PUT']); + $collection->add('route_b', $route); + + // Uses Hewbrew letter QOF (U+05E7) + $route = new Route('/somewhere/{item}/over/the/קainbow'); + $route->setMethods(['GET']); + $collection->add('route_c', $route); + + $route = new Route('/another/{thing}/aboUT/{item}'); + $collection->add('route_d', $route); + + // Greek letters lower case phi (U+03C6) and lower case omega (U+03C9) + $route = new Route('/place/meφω'); + $route->setMethods(['GET', 'HEAD']); + $collection->add('route_e', $route); + + return $collection; + } + + /** + * Returns a complex set of routes for testing. + * + * @return \Symfony\Component\Routing\RouteCollection + */ + public function duplicatePathsRouteCollection() { + $collection = new RouteCollection(); + + $route = new Route('/path/one'); + $route->setMethods(['GET']); + $collection->add('route_a', $route); + + $route = new Route('/path/one'); + $route->setMethods(['GET']); + $collection->add('route_b', $route); + + $route = new Route('/path/one'); + $route->setMethods(['GET']); + $collection->add('route_c', $route); + + $route = new Route('/path/TWO'); + $route->setMethods(['GET']); + $collection->add('route_d', $route); + + $route = new Route('/PLACE/meφω'); + $collection->add('route_e', $route); + + // Greek letters lower case phi (U+03C6) and lower case omega (U+03C9) + $route = new Route('/place/meφω'); + $route->setMethods(['GET', 'HEAD']); + $collection->add('route_f', $route); + + return $collection; + } + + /** * Returns a Content-type restricted set of routes for testing. * * @return \Symfony\Component\Routing\RouteCollection