diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 937bf48..72694e7 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -2,6 +2,7 @@ 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; @@ -37,7 +38,8 @@ public static function compile(Route $route) { // The Drupal-specific compiled information. $stripped_path = static::getPathWithoutDefaults($route); $fit = static::getFit($stripped_path); - $pattern_outline = static::getPatternOutline($stripped_path); + // Store a lower-case pattern outline to enable case-insensitive matching. + $pattern_outline = Unicode::strtolower(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(), '/'))); @@ -46,23 +48,28 @@ public static function compile(Route $route) { $fit, $pattern_outline, $num_parts, - // These are the Symfony compiled parts. - $symfony_compiled->getStaticPrefix(), - $symfony_compiled->getRegex(), + // These are the compiled parts 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', $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 equal strings and default values are removed. + * placeholders are the string '%'. * * @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 53ed626..aa20f7e 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -5,6 +5,7 @@ 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; @@ -143,22 +144,23 @@ public function __construct(Connection $connection, StateInterface $state, Curre */ public function getRouteCollectionForRequest(Request $request) { // Cache both the system path as well as route parameters and matching - // routes. - $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); + // routes. We can not yet convert the path to lowercase since wildcard path + // portions may be case sensitive if they contain data like a base64 encoded + // token. We remove repeated and trailing slashes to normalize the path. + $parts = preg_split('@/+@', $request->getPathInfo(), NULL, PREG_SPLIT_NO_EMPTY); + $path = '/' . implode('/', $parts); + $cid = 'route:' . $path . ':' . $request->getQueryString(); if ($cached = $this->cache->get($cid)) { $this->currentPath->setPath($cached->data['path'], $request); $request->query->replace($cached->data['query']); return $cached->data['routes']; } else { - // Just trim on the right side. - $path = $request->getPathInfo(); - $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); $path = $this->pathProcessor->processInbound($path, $request); $this->currentPath->setPath($path, $request); // Incoming path processors may also set query parameters. $query_parameters = $request->query->all(); - $routes = $this->getRoutesByPath(rtrim($path, '/')); + $routes = $this->getRoutesByPath($path); $cache_value = [ 'path' => $path, 'query' => $query_parameters, @@ -317,15 +319,17 @@ public function getRoutesByPattern($pattern) { * Get all routes which match a certain pattern. * * @param string $path - * The route pattern to search for (contains % as placeholders). + * The route pattern to search for. * * @return \Symfony\Component\Routing\RouteCollection * Returns a route collection of matching routes. */ protected function getRoutesByPath($path) { // Split the path up on the slashes, ignoring multiple slashes in a row - // or leading or trailing slashes. - $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); + // 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); $collection = new RouteCollection(); diff --git a/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php b/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php new file mode 100644 index 0000000..83c58b5 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Routing/CaseInsensitivePathTest.php @@ -0,0 +1,36 @@ +drupalGet('user/login'); + $this->assertEquals($this->getSession()->getStatusCode(), 200); + $this->drupalGet('User/Login'); + $this->assertEquals($this->getSession()->getStatusCode(), 200); + } + + public function testViewsPath() { + $admin = $this->drupalCreateUser(['access administration pages', 'administer nodes', 'access content overview']); + $this->drupalLogin($admin); + + $this->drupalGet('admin/content'); + $this->assertEquals($this->getSession()->getStatusCode(), 200); + $this->drupalGet('Admin/Content'); + $this->assertEquals($this->getSession()->getStatusCode(), 200); + } + +}