diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php index 899c39e..b9f65fe 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Path; use Drupal\Core\Cache\Cache; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageInterface; @@ -59,7 +60,7 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO $fields = array( 'source' => $source, - 'alias' => $alias, + 'alias' => Unicode::strtolower($alias), 'langcode' => $langcode, ); @@ -98,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 == 'alias') { + $value = Unicode::strtolower($value); + } $select->condition($field, $value); } return $select @@ -115,6 +119,9 @@ public function delete($conditions) { $path = $this->load($conditions); $query = $this->connection->delete('url_alias'); foreach ($conditions as $field => $value) { + if ($field == 'alias') { + $value = Unicode::strtolower($value); + } $query->condition($field, $value); } $deleted = $query->execute(); @@ -184,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, ); @@ -208,7 +215,7 @@ 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, '<>'); @@ -234,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') diff --git a/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php index 5ac77a3..63cb942 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 lowercase. * @param string $langcode * (optional) The language code of the alias. * @param int|null $pid @@ -97,7 +97,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 lowercase. * @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 +112,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 lowercase. * @param string $langcode * Language of the alias. * @param string|null $source @@ -135,8 +136,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. This will be converted to lowercase. * * @return array * Array of items to be displayed on the current page. diff --git a/core/lib/Drupal/Core/Routing/CompiledRoute.php b/core/lib/Drupal/Core/Routing/CompiledRoute.php index 2e2ff6b..6f3d84d 100644 --- a/core/lib/Drupal/Core/Routing/CompiledRoute.php +++ b/core/lib/Drupal/Core/Routing/CompiledRoute.php @@ -36,6 +36,13 @@ class CompiledRoute extends SymfonyCompiledRoute { protected $numParts; /** + * The mapping of numeric path parts to path variable names. + * + * @var array + */ + protected $mappedPathVariables; + + /** * Constructs a new compiled route object. * * This is a ridiculously long set of constructor parameters, but as this @@ -56,7 +63,8 @@ class CompiledRoute extends SymfonyCompiledRoute { * @param array $tokens * An array of tokens to use to generate URL for this route * @param array $pathVariables - * An array of path variables + * An array of path variable names where the numeric array keys correspond + * to the path part where the named variable is found in the path pattern. * @param string|null $hostRegex * Host regex * @param array $hostTokens @@ -72,6 +80,7 @@ public function __construct($fit, $pattern_outline, $num_parts, $staticPrefix, $ $this->fit = $fit; $this->patternOutline = $pattern_outline; $this->numParts = $num_parts; + $this->mappedPathVariables = $pathVariables; } /** @@ -143,6 +152,19 @@ public function getRequirements() { } /** + * Returns the path variables. + * + * This differs from the parent method in that the numeric array keys + * correspond to a matching path part containing that variable. + * + * @return array + * The path variable names keyed by numeric path part. + */ + public function getPathVariables() { + return $this->mappedPathVariables; + } + + /** * {@inheritdoc} */ public function serialize() { @@ -166,6 +188,7 @@ public function unserialize($serialized) { $this->fit = $data['fit']; $this->patternOutline = $data['patternOutline']; $this->numParts = $data['numParts']; + $this->mappedPathVariables = $data['path_vars']; } diff --git a/core/lib/Drupal/Core/Routing/RouteBuilder.php b/core/lib/Drupal/Core/Routing/RouteBuilder.php index a78cf0c..c88a033 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 lowercase 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->lowercaseCollection($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 since we cannot tell what was changed. + $this->lowercaseCollection($collection); $this->checkProvider->setChecks($collection); @@ -223,6 +230,20 @@ public function destruct() { } /** + * Lowercases the path for each route in the collection. + * + * @param \Symfony\Component\Routing\RouteCollection $collection + * A route collection. + */ + protected function lowercaseCollection(RouteCollection $collection) { + foreach ($collection->all() as $route) { + // Force each path to be lowercase. + $path = Unicode::strtolower($route->getPath()); + $route->setPath($path); + } + } + + /** * Retrieves all defined routes from .routing.yml files. * * @return array diff --git a/core/lib/Drupal/Core/Routing/RouteCompiler.php b/core/lib/Drupal/Core/Routing/RouteCompiler.php index 639feff..86e7ffb 100644 --- a/core/lib/Drupal/Core/Routing/RouteCompiler.php +++ b/core/lib/Drupal/Core/Routing/RouteCompiler.php @@ -55,7 +55,7 @@ public static function compile(Route $route) { $symfony_compiled->getStaticPrefix(), $symfony_compiled->getRegex(), $symfony_compiled->getTokens(), - $symfony_compiled->getPathVariables(), + static::getMappedPathVariables($route->getPath(), $symfony_compiled->getPathVariables()), $symfony_compiled->getHostRegex(), $symfony_compiled->getHostTokens(), $symfony_compiled->getHostVariables(), @@ -64,6 +64,34 @@ 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. + * + * @param string $path + * The path 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($path, array $variables) { + $map = []; + // 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); + 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/RouteProvider.php b/core/lib/Drupal/Core/Routing/RouteProvider.php index 58b8320..77b9449 100644 --- a/core/lib/Drupal/Core/Routing/RouteProvider.php +++ b/core/lib/Drupal/Core/Routing/RouteProvider.php @@ -10,6 +10,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; @@ -149,7 +150,9 @@ 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. + // 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. $cid = 'route:' . $request->getPathInfo() . ':' . $request->getQueryString(); if ($cached = $this->cache->get($cid)) { $this->currentPath->setPath($cached->data['path'], $request); @@ -157,9 +160,9 @@ public function getRouteCollectionForRequest(Request $request) { return $cached->data['routes']; } else { - // Just trim on the right side. $path = $request->getPathInfo(); - $path = $path === '/' ? $path : rtrim($request->getPathInfo(), '/'); + // Just trim path on the right side. + $path = $path === '/' ? $path : rtrim($path, '/'); $path = $this->pathProcessor->processInbound($path, $request); $this->currentPath->setPath($path, $request); // Incoming path processors may also set query parameters. @@ -316,10 +319,12 @@ public function getRoutesByPattern($pattern) { } /** - * Get all routes which match a certain pattern. + * Get all routes which match a certain path pattern. + * + * The path will be converted to lowercase before performing the match. * * @param string $path - * The route pattern to search for (contains % as placeholders). + * The route path pattern to search for (contains % as placeholders). * * @return \Symfony\Component\Routing\RouteCollection * Returns a route collection of matching routes. @@ -327,7 +332,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('@/+@', $path, NULL, PREG_SPLIT_NO_EMPTY); + $parts = preg_split('@/+@', Unicode::strtolower($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 49bff8f..f82cd35 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,4 +47,82 @@ 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. 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) { + foreach ($routes as $name => $route) { + /** @var \Symfony\Component\Routing\Route $route */ + $compiledRoute = $route->compile(); + + // Convert the path to lowercase, so that we match the patterns from + // routes where we forced all paths to be lowercase. + // @see \Drupal\Core\Routing\RouteBuilder::rebuild() + // @see \Drupal\Core\Routing\RouteProvider::getRoutesByPath() + if (!preg_match($compiledRoute->getRegex(), Unicode::strtolower($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. + $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)); + } + } + } diff --git a/core/lib/Drupal/Core/Routing/routing.api.php b/core/lib/Drupal/Core/Routing/routing.api.php index 7c5e80a..f5a38e3 100644 --- a/core/lib/Drupal/Core/Routing/routing.api.php +++ b/core/lib/Drupal/Core/Routing/routing.api.php @@ -43,7 +43,7 @@ * 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). + * base URL). Paths are handled with case-insensitive matching. * - 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 diff --git a/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php b/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php index 13b56d5..387aeaa 100644 --- a/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php +++ b/core/modules/config_translation/src/Tests/ConfigTranslationOverviewTest.php @@ -8,6 +8,7 @@ namespace Drupal\config_translation\Tests; use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\Unicode; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\simpletest\WebTestBase; @@ -96,7 +97,7 @@ public function testMapperListPage() { foreach ($labels as $label) { $test_entity = entity_create('config_test', array( - 'id' => $this->randomMachineName(), + 'id' => Unicode::strtolower($this->randomMachineName()), 'label' => $label, )); $test_entity->save(); diff --git a/core/modules/locale/src/Tests/LocalePathTest.php b/core/modules/locale/src/Tests/LocalePathTest.php index 820814e..b04d4e3 100644 --- a/core/modules/locale/src/Tests/LocalePathTest.php +++ b/core/modules/locale/src/Tests/LocalePathTest.php @@ -7,6 +7,7 @@ namespace Drupal\locale\Tests; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -72,7 +73,7 @@ public function testPathLanguageConfiguration() { // Create a path alias in default language (English). $path = 'admin/config/search/path/add'; - $english_path = $this->randomMachineName(8); + $english_path = Unicode::strtolower($this->randomMachineName(8)); $edit = array( 'source' => '/node/' . $node->id(), 'alias' => '/' . $english_path, @@ -81,7 +82,7 @@ public function testPathLanguageConfiguration() { $this->drupalPostForm($path, $edit, t('Save')); // Create a path alias in new custom language. - $custom_language_path = $this->randomMachineName(8); + $custom_language_path = Unicode::strtolower($this->randomMachineName(8)); $edit = array( 'source' => '/node/' . $node->id(), 'alias' => '/' . $custom_language_path, @@ -98,7 +99,7 @@ public function testPathLanguageConfiguration() { $this->assertText($node->label(), 'Custom language alias works.'); // Create a custom path. - $custom_path = $this->randomMachineName(8); + $custom_path = Unicode::strtolower($this->randomMachineName(8)); // Check priority of language for alias by source path. $edit = array( diff --git a/core/modules/path/path.module b/core/modules/path/path.module index 307a08b..dbb8d97 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -21,12 +21,13 @@ function path_help($route_name, RouteMatchInterface $route_match) { $output = ''; $output .= '

' . t('About') . '

'; $output .= '

' . t('The Path module allows you to specify an alias, or custom URL, for any existing internal system path. Aliases should not be confused with URL redirects, which allow you to forward a changed or inactive URL to a new URL. In addition to making URLs more readable, aliases also help search engines index content more effectively. Multiple aliases may be used for a single internal system path. To automate the aliasing of paths, you can install the contributed module Pathauto. For more information, see the online documentation for the Path module.', array(':path' => 'https://www.drupal.org/documentation/modules/path', ':pathauto' => 'https://www.drupal.org/project/pathauto')) . '

'; + $output .= '

' . t('Aliases must be unique, are converted to lowercase, and are matched in a case insensitive fashion.'); $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Creating aliases') . '
'; - $output .= '
' . t('If you create or edit a taxonomy term you can add an alias (for example music/jazz) in the field "URL alias". When creating or editing content you can add an alias (for example about-us/team) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page URL aliases. To add aliases a user needs the permission Create and edit URL aliases.', array(':aliases' => \Drupal::url('path.admin_overview'), ':permissions' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-path')))) . '
'; + $output .= '
' . t('If you create or edit a taxonomy term you can add a unique alias (for example /music/jazz) in the field "URL alias". When creating or editing content you can add an alias (for example /about-us/team) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page URL aliases. To add aliases a user needs the permission Create and edit URL aliases.', array(':aliases' => \Drupal::url('path.admin_overview'), ':permissions' => \Drupal::url('user.admin_permissions', array(), array('fragment' => 'module-path')))) . '
'; $output .= '
' . t('Managing aliases') . '
'; - $output .= '
' . t('The Path module provides a way to search and view a list of all aliases that are in use on your website. Aliases can be added, edited and deleted through this list.', array(':aliases' => \Drupal::url('path.admin_overview'))) . '
'; + $output .= '
' . t('The Path module provides a way to search and view a list of all aliases that are in use on your website. Aliases need to be unique and are converted to lowercase. Aliases can be added, edited and deleted through this list.', array(':aliases' => \Drupal::url('path.admin_overview'))) . '
'; $output .= '
'; return $output; diff --git a/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php index 8c84841..e296b6c 100644 --- a/core/modules/path/src/Form/PathFormBase.php +++ b/core/modules/path/src/Form/PathFormBase.php @@ -116,7 +116,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $pid = NU '#default_value' => $this->path['alias'], '#maxlength' => 255, '#size' => 45, - '#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page. Use a relative path with a slash in front..'), + '#description' => $this->t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page. Use a relative path with a slash in front. The path alias will be converted to lowercase and must be unique.'), '#field_prefix' => $this->requestContext->getCompleteBaseUrl(), '#required' => TRUE, ); diff --git a/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php index 651c11f..2b74562 100644 --- a/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -7,6 +7,7 @@ namespace Drupal\path\Tests; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; /** @@ -75,7 +76,7 @@ function testAdminAlias() { // Create alias. $edit = array(); $edit['source'] = '/node/' . $node1->id(); - $edit['alias'] = '/' . $this->randomMachineName(8); + $edit['alias'] = '/' . Unicode::strtolower($this->randomMachineName(8)); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm that the alias works. @@ -126,7 +127,7 @@ function testAdminAlias() { // Create a really long alias. $edit = array(); $edit['source'] = '/node/' . $node1->id(); - $alias = '/' . $this->randomMachineName(128); + $alias = '/' . Unicode::strtolower($this->randomMachineName(128)); $edit['alias'] = $alias; // The alias is shortened to 50 characters counting the ellipsis. $truncated_alias = substr($alias, 0, 47); @@ -141,7 +142,7 @@ function testAdminAlias() { // Create absolute path alias. $edit = array(); $edit['source'] = '/node/' . $node3->id(); - $node3_alias = '/' . $this->randomMachineName(8); + $node3_alias = '/' . Unicode::strtolower($this->randomMachineName(8)); $edit['alias'] = $node3_alias; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); @@ -151,7 +152,7 @@ function testAdminAlias() { // Create alias with trailing slash. $edit = array(); $edit['source'] = '/node/' . $node4->id(); - $node4_alias = '/' . $this->randomMachineName(8); + $node4_alias = '/' . Unicode::strtolower($this->randomMachineName(8)); $edit['alias'] = $node4_alias . '/'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); @@ -185,7 +186,7 @@ function testAdminAlias() { $edit = array(); $edit['source'] = 'node/' . $node5->id(); - $node5_alias = $this->randomMachineName(8); + $node5_alias = Unicode::strtolower($this->randomMachineName(8)); $edit['alias'] = $node5_alias . '/'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); @@ -203,7 +204,7 @@ function testNodeAlias() { // Create alias. $edit = array(); - $edit['path[0][alias]'] = '/' . $this->randomMachineName(8); + $edit['path[0][alias]'] = '/' . Unicode::strtolower($this->randomMachineName(8)); $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save')); // Confirm that the alias works. diff --git a/core/modules/path/src/Tests/PathLanguageTest.php b/core/modules/path/src/Tests/PathLanguageTest.php index 67463f2..0b159f3 100644 --- a/core/modules/path/src/Tests/PathLanguageTest.php +++ b/core/modules/path/src/Tests/PathLanguageTest.php @@ -7,6 +7,8 @@ namespace Drupal\path\Tests; +use Drupal\Component\Utility\Unicode; + /** * Confirm that paths work with translated nodes. * @@ -79,7 +81,7 @@ protected function setUp() { function testAliasTranslation() { $node_storage = $this->container->get('entity.manager')->getStorage('node'); $english_node = $this->drupalCreateNode(array('type' => 'page', 'langcode' => 'en')); - $english_alias = $this->randomMachineName(); + $english_alias = Unicode::strtolower($this->randomMachineName()); // Edit the node to set language and path. $edit = array(); @@ -97,7 +99,7 @@ function testAliasTranslation() { $edit = array(); $edit['title[0][value]'] = $this->randomMachineName(); $edit['body[0][value]'] = $this->randomMachineName(); - $french_alias = $this->randomMachineName(); + $french_alias = Unicode::strtolower($this->randomMachineName()); $edit['path[0][alias]'] = '/' . $french_alias; $this->drupalPostForm(NULL, $edit, t('Save (this translation)')); @@ -125,7 +127,7 @@ function testAliasTranslation() { $languages = $this->container->get('language_manager')->getLanguages(); $url = $english_node_french_translation->url('canonical', array('language' => $languages['fr'])); - $this->assertTrue(strpos($url, $edit['path[0][alias]']), 'URL contains the path alias.'); + $this->assertTrue(strpos($url, Unicode::strtolower($edit['path[0][alias]'])), 'URL contains the path alias.'); // Confirm that the alias works even when changing language negotiation // options. Enable User language detection and selection over URL one. diff --git a/core/modules/path/src/Tests/PathTaxonomyTermTest.php b/core/modules/path/src/Tests/PathTaxonomyTermTest.php index 98372df..b545323 100644 --- a/core/modules/path/src/Tests/PathTaxonomyTermTest.php +++ b/core/modules/path/src/Tests/PathTaxonomyTermTest.php @@ -7,6 +7,7 @@ namespace Drupal\path\Tests; +use Drupal\Component\Utility\Unicode; use Drupal\taxonomy\Entity\Vocabulary; /** @@ -48,7 +49,7 @@ function testTermAlias() { $edit = array( 'name[0][value]' => $this->randomMachineName(), 'description[0][value]' => $description, - 'path[0][alias]' => '/' . $this->randomMachineName(), + 'path[0][alias]' => '/' . Unicode::strtolower($this->randomMachineName()), ); $this->drupalPostForm('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add', $edit, t('Save')); $tid = db_query("SELECT tid FROM {taxonomy_term_field_data} WHERE name = :name AND default_langcode = 1", array(':name' => $edit['name[0][value]']))->fetchField(); diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php index 3d27687..f143de6 100644 --- a/core/modules/simpletest/src/AssertContentTrait.php +++ b/core/modules/simpletest/src/AssertContentTrait.php @@ -10,6 +10,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Xss; use Drupal\Core\Render\RenderContext; use Symfony\Component\CssSelector\CssSelector; @@ -359,7 +360,7 @@ protected function assertNoLink($label, $message = '', $group = 'Other') { * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertLinkByHref($href, $index = 0, $message = '', $group = 'Other') { - $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href)); + $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => Unicode::strtolower($href))); $message = ($message ? $message : SafeMarkup::format('Link containing href %href found.', array('%href' => $href))); return $this->assert(isset($links[$index]), $message, $group); } @@ -384,7 +385,7 @@ protected function assertLinkByHref($href, $index = 0, $message = '', $group = ' * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertNoLinkByHref($href, $message = '', $group = 'Other') { - $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => $href)); + $links = $this->xpath('//a[contains(@href, :href)]', array(':href' => Unicode::strtolower($href))); $message = ($message ? $message : SafeMarkup::format('No link containing href %href found.', array('%href' => $href))); return $this->assert(empty($links), $message, $group); } diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php index 42332e0..deefc99 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Menu; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; use Drupal\simpletest\WebTestBase; @@ -66,7 +67,7 @@ public function testMenuIntegration() { */ protected function doTestHookMenuIntegration() { // Generate base path with random argument. - $machine_name = $this->randomMachineName(8); + $machine_name = Unicode::strtolower($this->randomMachineName(8)); $base_path = 'foo/' . $machine_name; $this->drupalGet($base_path); // Confirm correct controller activated. diff --git a/core/modules/system/src/Tests/Path/AliasTest.php b/core/modules/system/src/Tests/Path/AliasTest.php index 2ceb4c9..76cd859 100644 --- a/core/modules/system/src/Tests/Path/AliasTest.php +++ b/core/modules/system/src/Tests/Path/AliasTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Path; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\MemoryCounterBackend; use Drupal\Core\Path\AliasStorage; use Drupal\Core\Database\Database; @@ -107,7 +108,8 @@ function testLookupPath() { $aliasStorage->save($path['source'], $path['alias'], $path['langcode']); // Hook that clears cache is not executed with unit tests. \Drupal::service('path.alias_manager')->cacheClear(); - $this->assertEqual($aliasManager->getAliasByPath($path['source']), $path['alias'], 'English alias overrides language-neutral alias.'); + + $this->assertEqual($aliasManager->getAliasByPath($path['source']), Unicode::strtolower($path['alias']), 'English alias overrides language-neutral alias.'); $this->assertEqual($aliasManager->getPathByAlias($path['alias']), $path['source'], 'English source overrides language-neutral source.'); // Create a language-neutral alias for the same path, again. @@ -127,7 +129,7 @@ function testLookupPath() { $aliasStorage->save($path['source'], $path['alias'], $path['langcode']); $this->assertEqual($aliasManager->getAliasByPath($path['source']), "/users/Dries", 'English alias still returned after entering a LOLspeak alias.'); // The LOLspeak alias should be returned if we really want LOLspeak. - $this->assertEqual($aliasManager->getAliasByPath($path['source'], 'xx-lolspeak'), '/LOL', 'LOLspeak alias returned if we specify xx-lolspeak to the alias manager.'); + $this->assertEqual($aliasManager->getAliasByPath($path['source'], 'xx-lolspeak'), '/lol', 'LOLspeak alias returned if we specify xx-lolspeak to the alias manager.'); // Create a new alias for this path in English, which should override the // previous alias for "user/1". diff --git a/core/modules/system/src/Tests/Routing/RouterTest.php b/core/modules/system/src/Tests/Routing/RouterTest.php index 076d0ce..0b24960 100644 --- a/core/modules/system/src/Tests/Routing/RouterTest.php +++ b/core/modules/system/src/Tests/Routing/RouterTest.php @@ -212,6 +212,71 @@ public function testRouterMatching() { } /** + * Tests the case insensitivity of route paths and path variable handling. + * + * The un-routed URLs are constructed using base: so that we are sure they are + * used as written without being processed by the routing system. + */ + public function testRoutePathMixedCase() { + $this->drupalGet(Url::fromUri('base:router_test/TEST14/1')); + $this->assertResponse(200); + $this->assertText('User route "entity.user.canonical" was matched.'); + + /** @var \Drupal\Core\Routing\RouteProvider $route_provider */ + $route_provider = $this->container->get('router.route_provider'); + $route2 = $route_provider->getRouteByName('router_test.mixedcase.2'); + // Verify that the route path is stored as lowercase despite being defined + // with mixed case in the YAML file. + $this->assertIdentical('/router_test/mixedcase2', $route2->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/mixEDCASE2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The correct string was returned because the route was successful.'); + + $this->drupalGet(Url::fromUri('base:router_TEST/MIXEDcase2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The correct string was returned because the route was successful.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase2')); + $this->assertResponse(200); + $this->assertRaw('test2', 'The correct string was returned because the route was successful.'); + + $route3 = $route_provider->getRouteByName('router_test.mixedcase.3'); + // Verify that the variable name in the route path is stored as lowercase + // despite being defined with mixed case in the YAML file. + $this->assertIdentical('/router_test/mixedcase3/{value}', $route3->getPath()); + + // Verify that data in variable path parts is retained as the original, + // mixed-case value. Matches route path /router_test/mixedcase3/{value} + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/mixedCASEstring')); + $this->assertResponse(200); + $this->assertRaw('mixedCASEstring', 'The correct string was returned because the route was successful.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/mixedcasestring')); + $this->assertResponse(200); + $this->assertRaw('mixedcasestring', 'The correct string was returned because the route was successful.'); + + $this->drupalGet(Url::fromUri('base:router_test/mixedcase3/MIXEDCASESTRING')); + $this->assertResponse(200); + $this->assertRaw('MIXEDCASESTRING', 'The correct string was returned because the route was successful.'); + // Test routes added by \Drupal\router_test\RouteTestSubscriber. + // Check that a dynamically added route has path changed to lower-case. + $route4 = $route_provider->getRouteByName('router_test.mixedcase.4'); + $this->assertIdentical('/router_test/mixedcase4', $route4->getPath()); + $this->drupalGet(Url::fromUri('base:router_TEST/MixedCASE4')); + $this->assertResponse(200); + // Check that a route added dynamically and altered has path lower-cased. + $route5 = $route_provider->getRouteByName('router_test.mixedcase.5'); + $this->assertIdentical('/router_test/mixedcase5/altered', $route5->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/MixedCASE5/altereD')); + $this->assertResponse(200); + // Check that a route added during the alter event has path lower-cased. + $route6 = $route_provider->getRouteByName('router_test.mixedcase.6'); + $this->assertIdentical('/router_test/mixedcase6', $route6->getPath()); + $this->drupalGet(Url::fromUri('base:router_test/MixedCASE6')); + $this->assertResponse(200); + } + + /** * Tests that a PSR-7 response works. */ public function testRouterResponsePsr7() { diff --git a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml index e0c91dd..7bf05b7 100644 --- a/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml +++ b/core/modules/system/tests/modules/router_test_directory/router_test.routing.yml @@ -189,3 +189,17 @@ router_test.hierarchy_parent_child2: _controller: '\Drupal\router_test\TestControllers::test' requirements: _access: 'TRUE' + +router_test.mixedcase.2: + path: '/router_test/mixedCASE2' + defaults: + _controller: '\Drupal\router_test\TestControllers::test2' + requirements: + _access: 'TRUE' + +router_test.mixedcase.3: + path: '/router_test/mixedcase3/{ValUe}' + defaults: + _controller: '\Drupal\router_test\TestControllers::test3' + requirements: + _access: 'TRUE' diff --git a/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php b/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php index 3bc703b..72b824b 100644 --- a/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php +++ b/core/modules/system/tests/modules/router_test_directory/src/RouteTestSubscriber.php @@ -7,21 +7,82 @@ namespace Drupal\router_test; -use Drupal\Core\Routing\RouteSubscriberBase; -use Symfony\Component\Routing\RouteCollection; +use Drupal\Core\Routing\RouteBuildEvent; +use Drupal\Core\Routing\RoutingEvents; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Routing\Route; /** * Listens to the dynamic route event and add a test route. */ -class RouteTestSubscriber extends RouteSubscriberBase { +class RouteTestSubscriber implements EventSubscriberInterface { /** * {@inheritdoc} */ - protected function alterRoutes(RouteCollection $collection) { + public static function getSubscribedEvents() { + $events[RoutingEvents::DYNAMIC] = 'onDynamicRoutes'; + $events[RoutingEvents::ALTER] = 'onAlterRoutes'; + return $events; + } + + /** + * Adds dynamic routes for a specific collection from an event. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onDynamicRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + $route = new Route( + '/router_test/mixedCASE4', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.4", $route); + $route = new Route( + '/router_test/tobealtered', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.5", $route); + } + + /** + * Alters existing routes for a specific collection from an event. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function onAlterRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); $route = $collection->get('router_test.6'); // Change controller method from test1 to test5. $route->setDefault('_controller', '\Drupal\router_test\TestControllers::test5'); + // Change a route path from lower case to mixed case. + $route = $collection->get('router_test.mixedcase.5'); + $route->setPath('/router_tesT/MIXedcase5/ALTERED'); + // We can also add routes in the alter, which happens in classes like + // \Drupal\content_translation\Routing\ContentTranslationRouteSubscriber + // that need to add routes based on other routes that are added dynamically. + $route = new Route( + '/router_test/mixedCASE6', + [ + '_controller' => '\Drupal\router_test\TestControllers::test2', + ], + [ + '_access' => 'TRUE', + ] + ); + $collection->add("router_test.mixedcase.6", $route); } } diff --git a/core/modules/views/src/Plugin/views/display/PathPluginBase.php b/core/modules/views/src/Plugin/views/display/PathPluginBase.php index 599e7f0..615f6e7f 100644 --- a/core/modules/views/src/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php @@ -263,7 +263,7 @@ public function alterRoutes(RouteCollection $collection) { $argument_map = array(); // We assume that the numeric ids of the parameters match the one from // the view argument handlers. - foreach ($parameters as $position => $parameter_name) { + foreach (array_values($parameters) as $position => $parameter_name) { $path = str_replace('{arg_' . $position . '}', '{' . $parameter_name . '}', $path); $argument_map['arg_' . $position] = $parameter_name; } diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php index 8a64df8..4216594 100644 --- a/core/modules/views/src/Tests/Wizard/BasicTest.php +++ b/core/modules/views/src/Tests/Wizard/BasicTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; use Drupal\views\Views; @@ -66,9 +67,9 @@ function testViewsWizardAndListing() { $view2['description'] = $this->randomMachineName(16); $view2['page[create]'] = 1; $view2['page[title]'] = $this->randomMachineName(16); - $view2['page[path]'] = $this->randomMachineName(16); + $view2['page[path]'] = Unicode::strtolower($this->randomMachineName(16)); $view2['page[feed]'] = 1; - $view2['page[feed_properties][path]'] = $this->randomMachineName(16); + $view2['page[feed_properties][path]'] = Unicode::strtolower($this->randomMachineName(16)); $this->drupalPostForm('admin/structure/views/add', $view2, t('Save and edit')); $this->drupalGet($view2['page[path]']); $this->assertResponse(200); @@ -115,7 +116,7 @@ function testViewsWizardAndListing() { $view3['show[type]'] = 'page'; $view3['page[create]'] = 1; $view3['page[title]'] = $this->randomMachineName(16); - $view3['page[path]'] = $this->randomMachineName(16); + $view3['page[path]'] = Unicode::strtolower($this->randomMachineName(16)); $view3['block[create]'] = 1; $view3['block[title]'] = $this->randomMachineName(16); $this->drupalPostForm('admin/structure/views/add', $view3, t('Save and edit')); diff --git a/core/modules/views/src/Tests/Wizard/MenuTest.php b/core/modules/views/src/Tests/Wizard/MenuTest.php index af6a98d..ffae5bc 100644 --- a/core/modules/views/src/Tests/Wizard/MenuTest.php +++ b/core/modules/views/src/Tests/Wizard/MenuTest.php @@ -8,6 +8,7 @@ namespace Drupal\views\Tests\Wizard; use Drupal\Component\Utility\SafeMarkup; +use Drupal\Component\Utility\Unicode; use Drupal\Core\Url; /** @@ -30,7 +31,7 @@ function testMenus() { $view['description'] = $this->randomMachineName(16); $view['page[create]'] = 1; $view['page[title]'] = $this->randomMachineName(16); - $view['page[path]'] = $this->randomMachineName(16); + $view['page[path]'] = Unicode::strtolower($this->randomMachineName(16)); $view['page[link]'] = 1; $view['page[link_properties][menu_name]'] = 'main'; $view['page[link_properties][title]'] = $this->randomMachineName(16);