diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 52d0041..937bf48 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(), '/'))); @@ -48,30 +46,23 @@ public static function compile(Route $route) { $fit, $pattern_outline, $num_parts, - - // The following parameters are what Symfony uses in - // \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection(). - - // Set the static prefix to an empty string since it is redundant to - // 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', + // These are the Symfony compiled parts. + $symfony_compiled->getStaticPrefix(), + $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..9c078ce 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -5,7 +5,6 @@ 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; @@ -329,10 +328,8 @@ public function getRoutesByPattern($pattern) { */ protected function getRoutesByPath($path) { // Split the path up on the slashes, ignoring multiple slashes in a row - // 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(). - $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); + // or leading or trailing slashes. + $parts = preg_split('@/+@', $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..b99689b 100644 --- a/core/lib/Drupal/Core/Routing/Router.php +++ b/core/lib/Drupal/Core/Routing/Router.php @@ -160,6 +160,101 @@ 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. + * + * @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. + */ + protected function _matchCollection($pathinfo, RouteCollection $routes, $case_sensitive) { + foreach ($routes as $name => $route) { + $compiledRoute = $route->compile(); + + // check the static prefix of the URL first. Only use the more expensive preg_match when it matches + $function = $case_sensitive ? 'mb_strpos' : 'mb_stripos'; + if ('' !== $compiledRoute->getStaticPrefix() && 0 !== $function($pathinfo, $compiledRoute->getStaticPrefix())) { + continue; + } + + // 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'); }