diff -u b/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php --- b/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -15,8 +15,9 @@ /** * Provides a class for CRUD operations on path aliases. * - * All queries use case-insensitive implementations, so '/test-alias' and - * '/test-Alias' is treated as equal. + * All queries perform case-insensitive matching on the 'source' and 'alias' + * fields, so the the aliases '/test-alias' and '/test-Alias' are considered + * to be the same, and will both refer to the same internal system path. */ class AliasStorage implements AliasStorageInterface { /** @@ -101,8 +102,13 @@ public function load($conditions) { $select = $this->connection->select('url_alias'); foreach ($conditions as $field => $value) { - // Use LIKE for case-insensitive matching. - $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + if ($field == 'source' || $field == 'alias') { + // Use LIKE for case-insensitive matching. + $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + } + else { + $select->condition($field, $value); + } } return $select ->fields('url_alias') @@ -119,8 +125,13 @@ $path = $this->load($conditions); $query = $this->connection->delete('url_alias'); foreach ($conditions as $field => $value) { - // Use LIKE for case-insensitive matching. - $query->condition($field, $this->connection->escapeLike($value), 'LIKE'); + if ($field == 'source' || $field == 'alias') { + // Use LIKE for case-insensitive matching. + $query->condition($field, $this->connection->escapeLike($value), 'LIKE'); + } + else { + $query->condition($field, $value); + } } $deleted = $query->execute(); // @todo Switch to using an event for this instead of a hook. @@ -164,21 +175,38 @@ * {@inheritdoc} */ public function lookupPathAlias($path, $langcode) { - $args = array( - ':source' => $this->connection->escapeLike($path), - ':langcode' => $langcode, - ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); + $source = $this->connection->escapeLike($path); + $langcode_undetermined = LanguageInterface::LANGCODE_NOT_SPECIFIED; + // See the queries above. Use LIKE for case-insensitive matching. if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - unset($args[':langcode']); - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source LIKE :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField(); + $alias = $this->connection->select('url_alias') + ->fields('url_alias', ['alias']) + ->condition('source', $source, 'LIKE') + ->condition('langcode', $langcode_undetermined) + ->orderBy('pid', 'DESC') + ->execute() + ->fetchField(); } elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source LIKE :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField(); + $alias = $this->connection->select('url_alias') + ->fields('url_alias', ['alias']) + ->condition('source', $source, 'LIKE') + ->condition('langcode', [$langcode, $langcode_undetermined], 'IN') + ->orderBy('langcode', 'DESC') + ->orderBy('pid', 'DESC') + ->execute() + ->fetchField(); } else { - $alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source LIKE :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField(); + $alias = $this->connection->select('url_alias') + ->fields('url_alias', ['alias']) + ->condition('source', $source, 'LIKE') + ->condition('langcode', [$langcode, $langcode_undetermined], 'IN') + ->orderBy('langcode', 'ASC') + ->orderBy('pid', 'DESC') + ->execute() + ->fetchField(); } return $alias; @@ -188,21 +216,35 @@ * {@inheritdoc} */ public function lookupPathSource($path, $langcode) { - $args = array( - ':alias' => $this->connection->escapeLike($path), - ':langcode' => $langcode, - ':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); + $alias = $this->connection->escapeLike($path); + $langcode_undetermined = LanguageInterface::LANGCODE_NOT_SPECIFIED; + // See the queries above. Use LIKE for case-insensitive matching. if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - unset($args[':langcode']); - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias LIKE :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args); + $result = $this->connection->select('url_alias') + ->fields('url_alias', ['source']) + ->condition('alias', $alias, 'LIKE') + ->condition('langcode', $langcode_undetermined) + ->orderBy('pid', 'DESC') + ->execute(); } elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias LIKE :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args); + $result = $this->connection->select('url_alias') + ->fields('url_alias', ['source']) + ->condition('alias', $alias, 'LIKE') + ->condition('langcode', [$langcode, $langcode_undetermined], 'IN') + ->orderBy('langcode', 'DESC') + ->orderBy('pid', 'DESC') + ->execute(); } else { - $result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias LIKE :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args); + $result = $this->connection->select('url_alias') + ->fields('url_alias', ['source']) + ->condition('alias', $alias, 'LIKE') + ->condition('langcode', [$langcode, $langcode_undetermined], 'IN') + ->orderBy('langcode', 'ASC') + ->orderBy('pid', 'DESC') + ->execute(); } return $result->fetchField(); diff -u b/core/lib/Drupal/Core/Path/AliasStorageInterface.php b/core/lib/Drupal/Core/Path/AliasStorageInterface.php --- b/core/lib/Drupal/Core/Path/AliasStorageInterface.php +++ b/core/lib/Drupal/Core/Path/AliasStorageInterface.php @@ -44,7 +44,8 @@ /** * Fetches a specific URL alias from the database. * - * The default implementation provides a case-insensitive implementation. + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. * * @param array $conditions * An array of query conditions. @@ -62,7 +63,8 @@ /** * Deletes a URL alias. * - * The default implementation provides a case-insensitive implementation. + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. * * @param array $conditions * An array of criteria. @@ -86,7 +88,8 @@ /** * Returns an alias of Drupal system URL. * - * The default implementation provides a case-insensitive implementation. + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. * * @param string $path * The path to investigate for corresponding path aliases. @@ -102,7 +105,8 @@ /** * Returns Drupal system URL of an alias. * - * The default implementation provides a case-insensitive implementation. + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. * * @param string $path * The path to investigate for corresponding system URLs. @@ -118,7 +122,8 @@ /** * Checks if alias already exists. * - * The default implementation provides a case-insensitive implementation. + * The default implementation performs case-insensitive matching on the + * 'source' and 'alias' strings. * * @param string $alias * Alias to check against. @@ -146,8 +151,8 @@ * @param array $header * Table header. * @param string|null $keys - * (optional) Search keyword that may include one or more '*' as a wildcard - * value. + * (optional) Search keyword that may include one or more '*' as wildcard + * values. * * @return array * Array of items to be displayed on the current page. diff -u b/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php --- b/core/modules/path/src/Form/PathFormBase.php +++ b/core/modules/path/src/Form/PathFormBase.php @@ -182,10 +182,16 @@ if ($this->aliasStorage->aliasExists($alias, $langcode, $this->path['source'])) { $stored_alias = $this->aliasStorage->load(['alias' => $alias, 'langcode' => $langcode]); if ($stored_alias['alias'] !== $alias) { - $form_state->setErrorByName('alias', t('There is an alias with other case-sensitivity: %actual_alias is already in use in this language.', ['%actual_alias' => $stored_alias['alias']])); + // The alias already exists with different capitalization as the default + // implementation of AliasStorageInterface::aliasExists is + // case-insensitive. + $form_state->setErrorByName('alias', t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [ + '%alias' => $alias, + '%stored_alias' => $stored_alias['alias'], + ])); } else { - $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', array('%alias' => $alias))); + $form_state->setErrorByName('alias', t('The alias %alias is already in use in this language.', ['%alias' => $alias])); } } diff -u b/core/modules/path/src/Tests/PathAliasTest.php b/core/modules/path/src/Tests/PathAliasTest.php --- b/core/modules/path/src/Tests/PathAliasTest.php +++ b/core/modules/path/src/Tests/PathAliasTest.php @@ -9,6 +9,8 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; +use Drupal\simpletest\RandomGeneratorTrait; +use Drupal\Core\Database\Database; /** * Add, edit, delete, and change alias and verify its consistency in the @@ -18,6 +20,8 @@ */ class PathAliasTest extends PathTestBase { + use RandomGeneratorTrait; + /** * Modules to enable. * @@ -76,7 +80,7 @@ // Create alias. $edit = array(); $edit['source'] = '/node/' . $node1->id(); - $edit['alias'] = '/' . $this->randomMachineName(8); + $edit['alias'] = '/' . $this->getRandomGenerator()->word(8); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm that the alias works. @@ -84,7 +88,8 @@ $this->assertText($node1->label(), 'Alias works.'); $this->assertResponse(200); // Confirm that the alias works in a case-insensitive way. - $this->drupalGet(Unicode::strtolower($edit['alias'])); + $this->assertTrue(ctype_lower(ltrim($edit['alias'], '/'))); + $this->drupalGet($edit['alias']); $this->assertText($node1->label(), 'Alias works lower case.'); $this->assertResponse(200); $this->drupalGet(Unicode::strtoupper($edit['alias'])); @@ -95,13 +100,20 @@ $pid = $this->getPID($edit['alias']); $previous = $edit['alias']; - $edit['alias'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. + $edit['alias'] = '/alias' . // Lower-case letters. + "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. - "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. + "中國書۞"; // Characters from various non-ASCII alphabets. + $connection = Database::getConnection(); + if ($connection->databaseType() != 'sqlite') { + // When using LIKE for case-insensitivity, the SQLite driver is + // currently unable to find the upper-case versions of these characters. + $edit['alias'] .= "ïвβéø"; // Lower-case characters from various non-ASCII alphabets. + } $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet(Unicode::strtolower($edit['alias'])); + $this->drupalGet(Unicode::strtoupper($edit['alias'])); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); @@ -122,6 +134,14 @@ // Confirm no duplicate was created. $this->assertRaw(t('The alias %alias is already in use in this language.', array('%alias' => $edit['alias'])), 'Attempt to move alias was rejected.'); + $edit_upper = $edit; + $edit_upper['alias'] = Unicode::strtoupper($edit['alias']); + $this->drupalPostForm('admin/config/search/path/add', $edit_upper, t('Save')); + $this->assertRaw(t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [ + '%alias' => $edit_upper['alias'], + '%stored_alias' => $edit['alias'], + ]), 'Attempt to move upper-case alias was rejected.'); + // Delete alias. $this->drupalPostForm('admin/config/search/path/edit/' . $pid, array(), t('Delete')); $this->drupalPostForm(NULL, array(), t('Confirm')); @@ -227,13 +247,20 @@ // Change alias to one containing "exotic" characters. $previous = $edit['path[0][alias]']; - $edit['path[0][alias]'] = "/- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. + $edit['path[0][alias]'] = '/alias' . // Lower-case letters. + "- ._~!$'\"()*@[]?&+%#,;=:" . // "Special" ASCII characters. "%23%25%26%2B%2F%3F" . // Characters that look like a percent-escaped string. - "éøïвβ中國書۞"; // Characters from various non-ASCII alphabets. + "中國書۞"; // Characters from various non-ASCII alphabets. + $connection = Database::getConnection(); + if ($connection->databaseType() != 'sqlite') { + // When using LIKE for case-insensitivity, the SQLite driver is + // currently unable to find the upper-case versions of these characters. + $edit['path[0][alias]'] .= "ïвβéø"; // Lower-case characters from various non-ASCII alphabets. + } $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet(Unicode::strtolower($edit['path[0][alias]'])); + $this->drupalGet(Unicode::strtoupper($edit['path[0][alias]'])); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200);