summaryrefslogtreecommitdiffstats
path: root/core/lib/Drupal/Core/Routing
diff options
context:
space:
mode:
authorNathaniel Catchpole2017-02-28 11:49:30 (GMT)
committerNathaniel Catchpole2017-02-28 11:49:30 (GMT)
commitb1242b2928e7830412fa8fde0d2e916bc09e0d1b (patch)
tree9c7e8c9b156d0cce4e75377f30926975b18c52b6 /core/lib/Drupal/Core/Routing
parent1fb7dc9c23b9443702aff0b61924b4415f555583 (diff)
Issue #2075889 by pwolanin, kgoel, alexpott, catch, mpdonadio, YesCT, himanshu-dixit, dawehner, xjm, johnshortess, Crell, jhodgdon, Wim Leers, mradcliffe, attiks, tstoeckler, webchick: Make Drupal handle incoming paths in a case-insensitive fashion for routing
Diffstat (limited to 'core/lib/Drupal/Core/Routing')
-rw-r--r--core/lib/Drupal/Core/Routing/CompiledRoute.php6
-rw-r--r--core/lib/Drupal/Core/Routing/RouteCompiler.php14
-rw-r--r--core/lib/Drupal/Core/Routing/RouteProvider.php21
-rw-r--r--core/lib/Drupal/Core/Routing/RouteProviderInterface.php4
-rw-r--r--core/lib/Drupal/Core/Routing/Router.php90
-rw-r--r--core/lib/Drupal/Core/Routing/routing.api.php14
6 files changed, 133 insertions, 16 deletions
diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php
index 775d4ae..76d6ef1 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 @@ class CompiledRoute extends SymfonyCompiledRoute {
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 937bf48..e4aef43 100644
--- a/core/lib/Drupal/Core/Routing/RouteCompiler.php
+++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php
@@ -46,8 +46,14 @@ class RouteCompiler extends SymfonyRouteCompiler implements RouteCompilerInterfa
$fit,
$pattern_outline,
$num_parts,
- // These are the Symfony compiled parts.
- $symfony_compiled->getStaticPrefix(),
+
+ // 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.
+ '',
$symfony_compiled->getRegex(),
$symfony_compiled->getTokens(),
$symfony_compiled->getPathVariables(),
@@ -55,14 +61,14 @@ class RouteCompiler extends SymfonyRouteCompiler implements RouteCompilerInterfa
$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..f16aa9f 100644
--- a/core/lib/Drupal/Core/Routing/RouteProvider.php
+++ b/core/lib/Drupal/Core/Routing/RouteProvider.php
@@ -2,6 +2,7 @@
namespace Drupal\Core\Routing;
+use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
@@ -139,7 +140,9 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
*
* @return \Symfony\Component\Routing\RouteCollection with all urls that
* could potentially match $request. Empty collection if nothing can
- * match.
+ * match. The collection will be sorted from highest to lowest fit (match
+ * of path parts) and then in ascending order by route name for routes
+ * with the same fit.
*/
public function getRouteCollectionForRequest(Request $request) {
// Cache both the system path as well as route parameters and matching
@@ -317,15 +320,20 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
* 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.
+ * Returns a route collection of matching routes. The collection may be
+ * empty and will be sorted from highest to lowest fit (match of path parts)
+ * and then in ascending order by route name for routes with the same fit.
*/
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().
+ // @see \Drupal\Core\Routing\CompiledRoute::__construct()
+ $parts = preg_split('@/+@', Unicode::strtolower($path), NULL, PREG_SPLIT_NO_EMPTY);
$collection = new RouteCollection();
@@ -347,7 +355,8 @@ class RouteProvider implements PreloadableRouteProviderInterface, PagedRouteProv
$routes = [];
}
- // We sort by fit and name in PHP to avoid a SQL filesort.
+ // We sort by fit and name in PHP to avoid a SQL filesort and avoid any
+ // difference in the sorting behavior of SQL back-ends.
usort($routes, array($this, 'routeProviderRouteCompare'));
foreach ($routes as $row) {
diff --git a/core/lib/Drupal/Core/Routing/RouteProviderInterface.php b/core/lib/Drupal/Core/Routing/RouteProviderInterface.php
index ce07a6d..f53593a 100644
--- a/core/lib/Drupal/Core/Routing/RouteProviderInterface.php
+++ b/core/lib/Drupal/Core/Routing/RouteProviderInterface.php
@@ -18,7 +18,9 @@ interface RouteProviderInterface extends RouteProviderBaseInterface {
* The route pattern to search for (contains {} as placeholders).
*
* @return \Symfony\Component\Routing\RouteCollection
- * Returns a route collection of matching routes.
+ * Returns a route collection of matching routes. The collection may be
+ * empty and will be sorted from highest to lowest fit (match of path parts)
+ * and then in ascending order by route name for routes with the same fit.
*/
public function getRoutesByPattern($pattern);
diff --git a/core/lib/Drupal/Core/Routing/Router.php b/core/lib/Drupal/Core/Routing/Router.php
index 80a31f7..dc724bc 100644
--- a/core/lib/Drupal/Core/Routing/Router.php
+++ b/core/lib/Drupal/Core/Routing/Router.php
@@ -160,6 +160,96 @@ class Router extends UrlMatcher implements RequestMatcherInterface, RouterInterf
}
/**
+ * 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|null
+ * An array of parameters. NULL when there is no match.
+ */
+ protected function matchCollection($pathinfo, RouteCollection $routes) {
+ // Try a case-sensitive match.
+ $match = $this->doMatchCollection($pathinfo, $routes, TRUE);
+ // Try a case-insensitive match.
+ if ($match === NULL && $routes->count() > 0) {
+ $match = $this->doMatchCollection($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. The static prefix optimization 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.
+ *
+ * @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
+ * @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath()
+ */
+ protected function doMatchCollection($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)) {
+ $routes->remove($name);
+ 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);
+ $routes->remove($name);
+ continue;
+ }
+ }
+
+ $status = $this->handleRouteRequirements($pathinfo, $name, $route);
+
+ if (self::ROUTE_MATCH === $status[0]) {
+ return $status[1];
+ }
+
+ if (self::REQUIREMENT_MISMATCH === $status[0]) {
+ $routes->remove($name);
+ 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/lib/Drupal/Core/Routing/routing.api.php b/core/lib/Drupal/Core/Routing/routing.api.php
index 6f47520..b505465 100644
--- a/core/lib/Drupal/Core/Routing/routing.api.php
+++ b/core/lib/Drupal/Core/Routing/routing.api.php
@@ -43,10 +43,16 @@
* by the machine name of the module that defines the route, or the name of
* a subsystem.
* - The 'path' line gives the URL path of the route (relative to the site's
- * base URL). Note: The path in Drupal is treated case insensitive so
- * /example and /EXAmplE should return the same page.
- * @todo Fix https://www.drupal.org/node/2075889 to actually get this
- * behaviour.
+ * base URL). Generally, paths in Drupal are treated as case-insensitive,
+ * which overrides the default Symfony behavior. Specifically:
+ * - If different routes are defined for /example and /EXAmplE, the exact
+ * match is respected.
+ * - If there is no exact match, the route falls back to a case-insensitive
+ * match, so /example and /EXAmplE will return the same page.
+ * Relying on case-sensitive path matching is not recommended because it
+ * negatively affects user experience, and path aliases do not support case-
+ * sensitive matches. The case-sensitive exact match is currently supported
+ * only for backwards compatibility and may be deprecated in a later release.
* - The 'defaults' section tells how to build the main content of the route,
* and can also give other information, such as the page title and additional
* arguments for the route controller method. There are several possibilities