diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php index 9732cc8..3574977 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -59,7 +59,7 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO } $fields = array( - 'source' => $source, + 'source' => Unicode::strtolower($source), 'alias' => Unicode::strtolower($alias), 'langcode' => $langcode, ); @@ -99,6 +99,9 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO public function load($conditions) { $select = $this->connection->select('url_alias'); foreach ($conditions as $field => $value) { + if ($field != 'pid') { + $value = Unicode::strtolower($value); + } $select->condition($field, $value); } return $select @@ -116,6 +119,9 @@ public function delete($conditions) { $path = $this->load($conditions); $query = $this->connection->delete('url_alias'); foreach ($conditions as $field => $value) { + if ($field != 'pid') { + $value = Unicode::strtolower($value); + } $query->condition($field, $value); } $deleted = $query->execute(); @@ -161,7 +167,7 @@ public function preloadPathAlias($preloaded, $langcode) { */ public function lookupPathAlias($path, $langcode) { $args = array( - ':source' => $path, + ':source' => Unicode::strtolower($path), ':langcode' => $langcode, ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, ); @@ -185,7 +191,7 @@ public function lookupPathAlias($path, $langcode) { */ public function lookupPathSource($path, $langcode) { $args = array( - ':alias' => $path, + ':alias' => Unicode::strtolower($path), ':langcode' => $langcode, ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, ); @@ -209,10 +215,10 @@ public function lookupPathSource($path, $langcode) { */ public function aliasExists($alias, $langcode, $source = NULL) { $query = $this->connection->select('url_alias') - ->condition('alias', $alias) + ->condition('alias', Unicode::strtolower($alias)) ->condition('langcode', $langcode); if (!empty($source)) { - $query->condition('source', $source, '<>'); + $query->condition('source', Unicode::strtolower($source), '<>'); } $query->addExpression('1'); $query->range(0, 1); @@ -235,7 +241,7 @@ public function getAliasesForAdminListing($header, $keys = NULL) { ->extend('Drupal\Core\Database\Query\TableSortExtender'); if ($keys) { // Replace wildcards with PDO wildcards. - $query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE'); + $query->condition('alias', '%' . preg_replace('!\*+!', '%', Unicode::strtolower($keys)) . '%', 'LIKE'); } return $query ->fields('url_alias') @@ -252,7 +258,7 @@ public function pathHasMatchingAlias($initial_substring) { $query = $this->connection->select('url_alias', 'u'); $query->addExpression(1); return (bool) $query - ->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE') + ->condition('u.source', $this->connection->escapeLike(Unicode::strtolower($initial_substring)) . '%', 'LIKE') ->range(0, 1) ->execute() ->fetchField(); diff --git a/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php index 5ac77a3..fbd0695 100644 --- a/core/lib/Drupal/Core/Path/AliasStorageInterface.php +++ b/core/lib/Drupal/Core/Path/AliasStorageInterface.php @@ -20,7 +20,7 @@ * @param string $source * The internal system path. * @param string $alias - * The URL alias. + * The URL alias. This will be converted to lower case. * @param string $langcode * (optional) The language code of the alias. * @param int|null $pid @@ -83,7 +83,8 @@ public function preloadPathAlias($preloaded, $langcode); * Returns an alias of Drupal system URL. * * @param string $path - * The path to investigate for corresponding path aliases. + * The path to investigate for corresponding path aliases. This will be + * converted to lower case. * @param string $langcode * Language code to search the path with. If there's no path defined for * that language it will search paths without language. @@ -97,7 +98,8 @@ public function lookupPathAlias($path, $langcode); * Returns Drupal system URL of an alias. * * @param string $path - * The path to investigate for corresponding system URLs. + * The path to investigate for corresponding system URLs. This will be + * converted to lower case. * @param string $langcode * Language code to search the path with. If there's no path defined for * that language it will search paths without language. @@ -111,7 +113,7 @@ public function lookupPathSource($path, $langcode); * Checks if alias already exists. * * @param string $alias - * Alias to check against. + * Alias to check against. This will be converted to lower case. * @param string $langcode * Language of the alias. * @param string|null $source @@ -135,8 +137,9 @@ public function languageAliasExists(); * * @param array $header * Table header. - * @param string[]|null $keys - * (optional) Search keys. + * @param string|null $keys + * (optional) Search keyword that may include one or more '*' as a wildcard + * value. Will be converted to lower case. * * @return array * Array of items to be displayed on the current page. @@ -147,7 +150,8 @@ public function getAliasesForAdminListing($header, $keys = NULL); * Check if any alias exists starting with $initial_substring. * * @param string $initial_substring - * Initial path substring to test against. + * Initial path substring to test against. This will be converted to lower + * case. * * @return bool * TRUE if any alias exists, FALSE otherwise. diff --git a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php index ebad3ab..d7bd416 100644 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php +++ b/core/lib/Drupal/Core/PathProcessor/PathProcessorAlias.php @@ -7,8 +7,6 @@ namespace Drupal\Core\PathProcessor; -use Drupal\Component\Utility\Unicode; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Path\AliasManagerInterface; use Drupal\Core\Render\BubbleableMetadata; use Symfony\Component\HttpFoundation\Request; diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php index a78cf0c..0404cd4 100644 --- a/core/lib/Drupal/Core/Routing/RouteBuilder.php +++ b/core/lib/Drupal/Core/Routing/RouteBuilder.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Routing; use Drupal\Component\Discovery\YamlDiscovery; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Access\CheckProviderInterface; use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -174,7 +175,9 @@ public function rebuild() { 'condition' => '', ); - $route = new Route($route_info['path'], $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']); + // Lowercase the path here so that the events get a consistent path, + // even though we force them all to be lower later. + $route = new Route(Unicode::strtolower($route_info['path']), $route_info['defaults'], $route_info['requirements'], $route_info['options'], $route_info['host'], $route_info['schemes'], $route_info['methods'], $route_info['condition']); $collection->add($name, $route); } } @@ -182,11 +185,15 @@ public function rebuild() { // DYNAMIC is supposed to be used to add new routes based upon all the // static defined ones. $this->dispatcher->dispatch(RoutingEvents::DYNAMIC, new RouteBuildEvent($collection)); + // Process the whole collection since we cannot tell what was newly added. + $this->processCollection($collection); // ALTER is the final step to alter all the existing routes. We cannot stop // people from adding new routes here, but we define two separate steps to // make it clear. $this->dispatcher->dispatch(RoutingEvents::ALTER, new RouteBuildEvent($collection)); + // Process the whole collection again since we cannot tell what was changed. + $this->processCollection($collection); $this->checkProvider->setChecks($collection); @@ -221,6 +228,20 @@ public function destruct() { // user. $this->rebuildIfNeeded(); } + /** + * Apply additional processing to each route in the collection. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * A route collection. + */ + protected function processCollection(RouteCollection $collection) { + /** @var \Symfony\Component\Routing\Route $route */ + foreach ($collection as $route) { + // Force each path to be lower case. + $path = Unicode::strtolower($route->getPath()); + $route->setPath($path); + } + } /** * Retrieves all defined routes from .routing.yml files. diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 3213bff..639feff 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -53,7 +53,7 @@ public static function compile(Route $route) { $num_parts, // These are the Symfony compiled parts. $symfony_compiled->getStaticPrefix(), - $symfony_compiled->getRegex() . 'i', + $symfony_compiled->getRegex(), $symfony_compiled->getTokens(), $symfony_compiled->getPathVariables(), $symfony_compiled->getHostRegex(), diff --git a/core/lib/Drupal/Core/Routing/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 3081f3b..26b7b48 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -150,8 +150,10 @@ 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. Convert the path to lower case for consistent and + // case-insensitive handling. + $path = Unicode::strtolower($request->getPathInfo()); + $cid = 'route:' . $path . ':' . $request->getQueryString(); if ($cached = $this->cache->get($cid)) { $this->currentPath->setPath($cached->data['path'], $request); $request->query->replace($cached->data['query']); @@ -159,8 +161,7 @@ public function getRouteCollectionForRequest(Request $request) { } else { // Just trim on the right side. - $path = $request->getPathInfo(); - $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); + $path = $path === '/' ? $path : rtrim($path, '/'); $path = $this->pathProcessor->processInbound($path, $request); $this->currentPath->setPath($path, $request); // Incoming path processors may also set query parameters. @@ -311,7 +312,7 @@ public function getCandidateOutlines(array $parts) { * {@inheritdoc} */ public function getRoutesByPattern($pattern) { - $path = RouteCompiler::getPatternOutline($pattern); + $path = RouteCompiler::getPatternOutline(Unicode::strtolower($pattern)); return $this->getRoutesByPath($path); } @@ -320,7 +321,8 @@ 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 (contains % as placeholders). Must be + * lower case. * * @return \Symfony\Component\Routing\RouteCollection * Returns a route collection of matching routes. @@ -328,7 +330,7 @@ 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. - $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY); + $parts = preg_split('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); $collection = new RouteCollection(); diff --git a/core/lib/Drupal/Core/Routing/UrlMatcher.php b/core/lib/Drupal/Core/Routing/UrlMatcher.php index 1251787..51d833e 100644 --- a/core/lib/Drupal/Core/Routing/UrlMatcher.php +++ b/core/lib/Drupal/Core/Routing/UrlMatcher.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Routing; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Path\CurrentPathStack; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\RouteCollection; @@ -46,8 +47,31 @@ public function finalMatch(RouteCollection $collection, Request $request) { return $this->match($this->currentPath->getPath($request)); } + /** + * Tries to match a URL with a set of routes. + * + * This version differe from the Symfony parent version in two respects. + * First, the $pathinfo string is converted to lower case. In addition, we + * remove the check against any static prefix since we would already have + * matched the static prefix in \Drupal\Core\Routing\RouteProvider before + * arriving here. + * + * @param string $pathinfo + * The path info to be parsed + * @param 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) { + // Make sure the incoming path is lower case for comparison. + $pathinfo = Unicode::strtolower($pathinfo); foreach ($routes as $name => $route) { $compiledRoute = $route->compile();