diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index dfe2e76..00b426c 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -33,7 +33,16 @@ class RouteCompiler extends SymfonyRouteCompiler implements RouteCompilerInterfa */ public static function compile(Route $route) { - $symfony_compiled = parent::compile($route); + // Split the path up on the slashes, ignoring multiple slashes in a row + // or leading or trailing slashes. + $parts = preg_split('@/+@', Unicode::strtolower($route->getPath()), NULL, PREG_SPLIT_NO_EMPTY); + // This seemingly round-about approach accounts for the fact that Drupal + // may be installed on PHP without multibyte support, so we need to be + // consistent and transform paths parts using Unicode::strtolower() to + // perform any matching. + $lowered_path_route = clone($route); + $lowered_path_route->setPath('/' . implode('/', $parts)); + $symfony_compiled = parent::compile($lowered_path_route); // The Drupal-specific compiled information. $stripped_path = static::getPathWithoutDefaults($route); @@ -41,7 +50,7 @@ public static function compile(Route $route) { $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(), '/'))); + $num_parts = count($parts); return new CompiledRoute( $fit, @@ -51,7 +60,7 @@ public static function compile(Route $route) { $symfony_compiled->getStaticPrefix(), $symfony_compiled->getRegex(), $symfony_compiled->getTokens(), - $symfony_compiled->getPathVariables(), + static::getMappedPathVariables($parts, $symfony_compiled->getPathVariables()), $symfony_compiled->getHostRegex(), $symfony_compiled->getHostTokens(), $symfony_compiled->getHostVariables(), @@ -60,6 +69,33 @@ public static function compile(Route $route) { } /** + * Returns the mapping of route variables to numeric path part. + * + * Drupal only supports path wildcard (variables) that consist of a complete + * part of the path separated by slashes. + * + * @see \Symfony\Component\Routing\RouteCompiler::compilePattern() + * + * @param array $parts + * The path parts for which we want the mapping. + * @param array $variables + * The names of placeholder variables in the path. + * + * @return array + * The mapping of numeric path part to variable name. + */ + public static function getMappedPathVariables($parts, array $variables) { + $map = []; + foreach ($variables as $name) { + $index = array_search('{' . $name . '}', $parts, TRUE); + if ($index !== FALSE) { + $map[$index] = $name; + } + } + return $map; + } + + /** * Returns the pattern outline. * * The pattern outline is the path pattern but normalized so that all diff --git a/core/lib/Drupal/Core/Routing/UrlMatcher.php b/core/lib/Drupal/Core/Routing/UrlMatcher.php index 9056903..ad1ff00 100644 --- a/core/lib/Drupal/Core/Routing/UrlMatcher.php +++ b/core/lib/Drupal/Core/Routing/UrlMatcher.php @@ -2,6 +2,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; @@ -41,4 +42,85 @@ 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 differs from the Symfony parent version in two respects. + * First, the $pathinfo string is converted to lowercase when matching the + * route path regular expression. 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 to be parsed. + * @param \Symfony\Component\Routing\RouteCollection $routes + * The set of routes. + * + * @return array + * An array of route 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) { + // Convert the path to lowercase, so that we match the patterns from + // routes where we forced all paths to be lowercase. Also remove any + // repeated slashes so the pattern and path are consistent. + // @see \Drupal\Core\Routing\RouteCompiler::compile() + $lowered_parts = preg_split('@/+@', Unicode::strtolower($pathinfo), NULL, PREG_SPLIT_NO_EMPTY); + $lowered_pathinfo = '/' . implode('/', $lowered_parts); + foreach ($routes as $name => $route) { + /** @var \Symfony\Component\Routing\Route $route */ + $compiledRoute = $route->compile(); + + if (!preg_match($compiledRoute->getRegex(), $lowered_pathinfo, $matches)) { + continue; + } + // Recover the original value for wildcard (named variable) portions + // of the path, since they may be case-sensitive data like a base64 + // encoded token. We create this positional mapping in the route compiler. + // @see \Drupal\Core\Routing\RouteCompiler::getMappedPathVariables() + $parts = preg_split('@/+@', $pathinfo, NULL, PREG_SPLIT_NO_EMPTY); + foreach ($compiledRoute->getPathVariables() as $position => $variable_name) { + if (isset($matches[$variable_name])) { + $matches[$variable_name] = $parts[$position]; + } + } + + $hostMatches = array(); + if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { + continue; + } + + // Check the HTTP method requirement. + if ($requiredMethods = $route->getMethods()) { + // HEAD and GET are equivalent as per RFC. + $method = $this->context->getMethod(); + if ('HEAD' === $method) { + $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)); + } + } + }