diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php index 775d4ae..ff76b11 100644 --- a/core/lib/Drupal/Core/Routing/CompiledRoute.php +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Routing; +use Drupal\Component\Utility\Unicode; use Symfony\Component\Routing\CompiledRoute as SymfonyCompiledRoute; /** @@ -65,7 +66,10 @@ public function __construct($fit, $pattern_outline, $num_parts, $staticPrefix, $ parent::__construct($staticPrefix, $regex, $tokens, $pathVariables, $hostRegex, $hostTokens, $hostVariables, $variables); $this->fit = $fit; - $this->patternOutline = $pattern_outline; + // Support case insensitive route matching by ensuring the pattern outline + // is lowercase. + // @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath + $this->patternOutline = Unicode::strtolower($pattern_outline); $this->numParts = $num_parts; } diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 52d0041..7185aa4 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -2,7 +2,6 @@ namespace Drupal\Core\Routing; -use Drupal\Component\Utility\Unicode; use Symfony\Component\Routing\RouteCompilerInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCompiler as SymfonyRouteCompiler; @@ -38,8 +37,7 @@ public static function compile(Route $route) { // The Drupal-specific compiled information. $stripped_path = static::getPathWithoutDefaults($route); $fit = static::getFit($stripped_path); - // Store a lower-case pattern outline to enable case-insensitive matching. - $pattern_outline = Unicode::strtolower(static::getPatternOutline($stripped_path)); + $pattern_outline = static::getPatternOutline($stripped_path); // We count the number of parts including any optional trailing parts. This // allows the RouteProvider to filter candidate routes more efficiently. $num_parts = count(explode('/', trim($route->getPath(), '/'))); @@ -56,22 +54,21 @@ public static function compile(Route $route) { // the matching in \Drupal\Core\Routing\RouteProvider::getRoutesByPath() // and by skipping it we more easily make the routing case insensitive. '', - // Set the regex to use UTF-8 and be case-insensitive. - $symfony_compiled->getRegex() . 'ui', + $symfony_compiled->getRegex(), $symfony_compiled->getTokens(), $symfony_compiled->getPathVariables(), $symfony_compiled->getHostRegex(), $symfony_compiled->getHostTokens(), $symfony_compiled->getHostVariables(), $symfony_compiled->getVariables() - ); + ); } /** * Returns the pattern outline. * * The pattern outline is the path pattern but normalized so that all - * placeholders are the string '%'. + * placeholders are equal strings and default values are removed. * * @param string $path * The path for which we want the normalized outline. diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 4586652..99f8d15 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -2,10 +2,10 @@ namespace Drupal\Core\Routing; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\CacheTagsInvalidatorInterface; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Path\CurrentPathStack; use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\State\StateInterface; @@ -332,6 +332,7 @@ protected function getRoutesByPath($path) { // or leading or trailing slashes. Convert to lower case here so we can // have a case insensitive match from the incoming path to the lower case // pattern outlines from \Drupal\Core\Routing\RouteCompiler::compile(). + // @see \Drupal\Core\Routing\CompiledRoute::__construct() $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); $collection = new RouteCollection(); diff --git a/core/lib/Drupal/Core/Routing/Router.php b/core/lib/Drupal/Core/Routing/Router.php index 80a31f7..861ccda 100644 --- a/core/lib/Drupal/Core/Routing/Router.php +++ b/core/lib/Drupal/Core/Routing/Router.php @@ -160,6 +160,104 @@ public function matchRequest(Request $request) { } /** + * Tries to match a URL with a set of routes. + * + * @param string $pathinfo + * The path info to be parsed + * @param \Symfony\Component\Routing\RouteCollection $routes + * The set of routes. + * + * @return array + * An array of parameters + * + * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException + * If the resource could not be found. + * @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException + * If the resource was found but the request method is not allowed. + */ + protected function matchCollection($pathinfo, RouteCollection $routes) { + // Try a case sensitive match. + $match = $this->_matchCollection($pathinfo, $routes, TRUE); + // Try a case in-sensitive match. + if ($match === NULL) { + $match = $this->_matchCollection($pathinfo, $routes, FALSE); + } + return $match; + } + + /** + * Tries to match a URL with a set of routes. + * + * This code is very similar to Symfony's UrlMatcher::matchCollection() but it + * supports case insensitive matching. Also the static prefix optimisation is + * removed as this duplicates work done by the query in + * RouteProvider::getRoutesByPath(). + * + * @param string $pathinfo + * The path info to be parsed + * @param \Symfony\Component\Routing\RouteCollection $routes + * The set of routes. + * @param bool $case_sensitive + * Determines if the match should be case sensitive of not. + * + * @return array|null + * An array of parameters. NULL when there is no match. + * + * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException + * If the resource could not be found. + * @throws \Symfony\Component\Routing\Exception\MethodNotAllowedException + * If the resource was found but the request method is not allowed. + * + * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection() + * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath() + */ + protected function _matchCollection($pathinfo, RouteCollection $routes, $case_sensitive) { + foreach ($routes as $name => $route) { + $compiledRoute = $route->compile(); + + // Set the regex to use UTF-8. + $regex = $compiledRoute->getRegex() . 'u'; + if (!$case_sensitive) { + $regex = $regex . 'i'; + } + if (!preg_match($regex, $pathinfo, $matches)) { + continue; + } + + $hostMatches = array(); + if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { + continue; + } + + // check HTTP method requirement + if ($requiredMethods = $route->getMethods()) { + // HEAD and GET are equivalent as per RFC + if ('HEAD' === $method = $this->context->getMethod()) { + $method = 'GET'; + } + + if (!in_array($method, $requiredMethods)) { + $this->allow = array_merge($this->allow, $requiredMethods); + + continue; + } + } + + $status = $this->handleRouteRequirements($pathinfo, $name, $route); + + if (self::ROUTE_MATCH === $status[0]) { + return $status[1]; + } + + if (self::REQUIREMENT_MISMATCH === $status[0]) { + continue; + } + + return $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + } + } + + /** * Returns a collection of potential matching routes for a request. * * @param \Symfony\Component\HttpFoundation\Request $request diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index db8a8af9..400ee0f 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -113,13 +113,18 @@ public function testDuplicateRoutePaths() { $this->assertRaw('router_test.two_duplicate1'); // Tests three routes with same the path. One of the routes the path has a - // different case. The route with the maximum fit and lowest sorting route - // name will match, regardless of the order the routes are declared. + // different case. $this->drupalGet('router-test/duplicate-path3'); $this->assertResponse(200); $this->assertRaw('router_test.three_duplicate1'); + // While case-insensitive matching works, exact matches are preferred. $this->drupalGet('router-test/Duplicate-PATH3'); $this->assertResponse(200); + $this->assertRaw('router_test.three_duplicate3'); + // Test that case-insensitive matching works, falling back to the first + // route defined. + $this->drupalGet('router-test/Duplicate-Path3'); + $this->assertResponse(200); $this->assertRaw('router_test.three_duplicate1'); }