diff --git a/core/core.services.yml b/core/core.services.yml index 9c60943..4e4b6ed 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -947,7 +947,7 @@ services: - { name: event_subscriber } path.alias_storage: class: Drupal\Core\Path\AliasStorage - arguments: ['@database', '@module_handler'] + arguments: ['@entity_type.manager'] tags: - { name: backend_overridable } path.matcher: diff --git a/core/lib/Drupal/Core/Path/AliasStorage.php b/core/lib/Drupal/Core/Path/AliasStorage.php index a02bc35..6e69e12 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -2,12 +2,8 @@ namespace Drupal\Core\Path; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Database\Connection; -use Drupal\Core\Database\SchemaObjectExistsException; -use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Database\Query\Condition; /** * Provides a class for CRUD operations on path aliases. @@ -19,42 +15,33 @@ class AliasStorage implements AliasStorageInterface { /** - * The table for the url_alias storage. - */ - const TABLE = 'url_alias'; - - /** - * The database connection. + * The entity type manager. * - * @var \Drupal\Core\Database\Connection + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $connection; + protected $entityTypeManager; /** - * The module handler. + * The storage handler for path_alias entities. * - * @var \Drupal\Core\Extension\ModuleHandlerInterface + * @var \Drupal\Core\Path\PathAliasStorageInterface */ - protected $moduleHandler; + protected $pathAliasEntityStorage; /** * Constructs a Path CRUD object. * - * @param \Drupal\Core\Database\Connection $connection - * A database connection for reading and writing path aliases. - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(Connection $connection, ModuleHandlerInterface $module_handler) { - $this->connection = $connection; - $this->moduleHandler = $module_handler; + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL) { - if ($source[0] !== '/') { throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $source)); } @@ -63,390 +50,170 @@ public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NO throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias)); } - $fields = [ - 'source' => $source, - 'alias' => $alias, - 'langcode' => $langcode, - ]; - - // Insert or update the alias. - if (empty($pid)) { - $try_again = FALSE; - try { - $query = $this->connection->insert(static::TABLE) - ->fields($fields); - $pid = $query->execute(); - } - catch (\Exception $e) { - // If there was an exception, try to create the table. - if (!$try_again = $this->ensureTableExists()) { - // If the exception happened for other reason than the missing table, - // propagate the exception. - throw $e; + if ($pid) { + /** @var \Drupal\Core\Path\PathAliasInterface $path_alias */ + $path_alias = $this->getPathAliasEntityStorage()->load($pid); + $original_values = [ + 'source' => $path_alias->getPath(), + 'alias' => $path_alias->getAlias(), + 'langcode' => $path_alias->language()->getId(), + ]; + + if ($path_alias->language()->getId() != $langcode) { + if ($path_alias->hasTranslation($langcode)) { + $path_alias = $path_alias->getTranslation($langcode); + } + else { + $path_alias = $path_alias->addTranslation($langcode); } } - // Now that the table has been created, try again if necessary. - if ($try_again) { - $query = $this->connection->insert(static::TABLE) - ->fields($fields); - $pid = $query->execute(); - } - - $fields['pid'] = $pid; - $operation = 'insert'; + $path_alias->setPath($source); + $path_alias->setAlias($alias); } else { - // Fetch the current values so that an update hook can identify what - // exactly changed. - try { - $original = $this->connection->query('SELECT source, alias, langcode FROM {url_alias} WHERE pid = :pid', [':pid' => $pid]) - ->fetchAssoc(); - } - catch (\Exception $e) { - $this->catchException($e); - $original = FALSE; - } - $fields['pid'] = $pid; - $query = $this->connection->update(static::TABLE) - ->fields($fields) - ->condition('pid', $pid); - $pid = $query->execute(); - $fields['original'] = $original; - $operation = 'update'; + $path_alias = $this->getPathAliasEntityStorage()->create([ + 'path' => $source, + 'alias' => $alias, + 'langcode' => $langcode, + ]); } - if ($pid) { - // @todo Switch to using an event for this instead of a hook. - $this->moduleHandler->invokeAll('path_' . $operation, [$fields]); - Cache::invalidateTags(['route_match']); - return $fields; + + $path_alias->save(); + + $path_alias_values = [ + 'pid' => $path_alias->id(), + 'source' => $path_alias->getPath(), + 'alias' => $path_alias->getAlias(), + 'langcode' => $path_alias->language()->getId(), + ]; + + if (isset($original_values)) { + $path_alias_values['original'] = $original_values; } - return FALSE; + + return $path_alias_values; } /** * {@inheritdoc} */ public function load($conditions) { - $select = $this->connection->select(static::TABLE); + $query = $this->getPathAliasEntityStorage()->getQuery(); foreach ($conditions as $field => $value) { - if ($field == 'source' || $field == 'alias') { - // Use LIKE for case-insensitive matching. - $select->condition($field, $this->connection->escapeLike($value), 'LIKE'); + if ($field === 'source') { + $field = 'path'; } - else { - $select->condition($field, $value); + if ($field === 'pid') { + $field = 'id'; } + + $query->condition($field, $value, '='); } - try { - return $select - ->fields(static::TABLE) - ->orderBy('pid', 'DESC') - ->range(0, 1) - ->execute() - ->fetchAssoc(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; + + $result = $query + ->sort('id', 'DESC') + ->range(0, 1) + ->execute(); + $entities = $this->getPathAliasEntityStorage()->loadMultiple($result); + + /** @var \Drupal\Core\Path\PathAliasInterface $path_alias */ + $path_alias = reset($entities); + if ($path_alias) { + return [ + 'pid' => $path_alias->id(), + 'source' => $path_alias->getPath(), + 'alias' => $path_alias->getAlias(), + 'langcode' => $path_alias->language()->getId(), + ]; } + + return FALSE; } /** * {@inheritdoc} */ public function delete($conditions) { - $path = $this->load($conditions); - $query = $this->connection->delete(static::TABLE); + $query = $this->getPathAliasEntityStorage()->getQuery(); foreach ($conditions as $field => $value) { - if ($field == 'source' || $field == 'alias') { - // Use LIKE for case-insensitive matching. - $query->condition($field, $this->connection->escapeLike($value), 'LIKE'); + if ($field === 'source') { + $field = 'path'; } - else { - $query->condition($field, $value); + if ($field === 'pid') { + $field = 'id'; } + + $query->condition($field, $value, '='); } - try { - $deleted = $query->execute(); - } - catch (\Exception $e) { - $this->catchException($e); - $deleted = FALSE; - } - // @todo Switch to using an event for this instead of a hook. - $this->moduleHandler->invokeAll('path_delete', [$path]); - Cache::invalidateTags(['route_match']); - return $deleted; + + $result = $query->execute(); + $entities = $this->getPathAliasEntityStorage()->loadMultiple($result); + $this->getPathAliasEntityStorage()->delete($entities); } /** * {@inheritdoc} */ public function preloadPathAlias($preloaded, $langcode) { - $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; - $select = $this->connection->select(static::TABLE) - ->fields(static::TABLE, ['source', 'alias']); - - if (!empty($preloaded)) { - $conditions = new Condition('OR'); - foreach ($preloaded as $preloaded_item) { - $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE'); - } - $select->condition($conditions); - } - - // Always get the language-specific alias before the language-neutral one. - // For example 'de' is less than 'und' so the order needs to be ASC, while - // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also - // order by pid ASC so that fetchAllKeyed() returns the most recently - // created alias for each source. Subsequent queries using fetchField() must - // use pid DESC to have the same effect. - if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - array_pop($langcode_list); - } - elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $select->orderBy('langcode', 'ASC'); - } - else { - $select->orderBy('langcode', 'DESC'); - } - - $select->orderBy('pid', 'ASC'); - $select->condition('langcode', $langcode_list, 'IN'); - try { - return $select->execute()->fetchAllKeyed(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return $this->getPathAliasEntityStorage()->preloadPathAlias($preloaded, $langcode); } /** * {@inheritdoc} */ public function lookupPathAlias($path, $langcode) { - $source = $this->connection->escapeLike($path); - $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; - - // See the queries above. Use LIKE for case-insensitive matching. - $select = $this->connection->select(static::TABLE) - ->fields(static::TABLE, ['alias']) - ->condition('source', $source, 'LIKE'); - if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - array_pop($langcode_list); - } - elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $select->orderBy('langcode', 'DESC'); - } - else { - $select->orderBy('langcode', 'ASC'); - } - - $select->orderBy('pid', 'DESC'); - $select->condition('langcode', $langcode_list, 'IN'); - try { - return $select->execute()->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return $this->getPathAliasEntityStorage()->lookupPathAlias($path, $langcode); } /** * {@inheritdoc} */ public function lookupPathSource($path, $langcode) { - $alias = $this->connection->escapeLike($path); - $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; - - // See the queries above. Use LIKE for case-insensitive matching. - $select = $this->connection->select(static::TABLE) - ->fields(static::TABLE, ['source']) - ->condition('alias', $alias, 'LIKE'); - if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { - array_pop($langcode_list); - } - elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $select->orderBy('langcode', 'DESC'); - } - else { - $select->orderBy('langcode', 'ASC'); - } - - $select->orderBy('pid', 'DESC'); - $select->condition('langcode', $langcode_list, 'IN'); - try { - return $select->execute()->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return $this->getPathAliasEntityStorage()->lookupPathSource($path, $langcode); } /** * {@inheritdoc} */ public function aliasExists($alias, $langcode, $source = NULL) { - // Use LIKE and NOT LIKE for case-insensitive matching. - $query = $this->connection->select(static::TABLE) - ->condition('alias', $this->connection->escapeLike($alias), 'LIKE') - ->condition('langcode', $langcode); - if (!empty($source)) { - $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE'); - } - $query->addExpression('1'); - $query->range(0, 1); - try { - return (bool) $query->execute()->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return $this->getPathAliasEntityStorage()->aliasExists($alias, $langcode, $source); } /** * {@inheritdoc} */ public function languageAliasExists() { - try { - return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return $this->getPathAliasEntityStorage()->languageAliasExists(); } /** * {@inheritdoc} */ public function getAliasesForAdminListing($header, $keys = NULL) { - $query = $this->connection->select(static::TABLE) - ->extend('Drupal\Core\Database\Query\PagerSelectExtender') - ->extend('Drupal\Core\Database\Query\TableSortExtender'); - if ($keys) { - // Replace wildcards with PDO wildcards. - $query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE'); - } - try { - return $query - ->fields(static::TABLE) - ->orderByHeader($header) - ->limit(50) - ->execute() - ->fetchAll(); - } - catch (\Exception $e) { - $this->catchException($e); - return []; - } + return $this->getPathAliasEntityStorage()->getAliasesForAdminListing($header, $keys); } /** * {@inheritdoc} */ public function pathHasMatchingAlias($initial_substring) { - $query = $this->connection->select(static::TABLE, 'u'); - $query->addExpression(1); - try { - return (bool) $query - ->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE') - ->range(0, 1) - ->execute() - ->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } - } - - /** - * Check if the table exists and create it if not. - */ - protected function ensureTableExists() { - try { - $database_schema = $this->connection->schema(); - if (!$database_schema->tableExists(static::TABLE)) { - $schema_definition = $this->schemaDefinition(); - $database_schema->createTable(static::TABLE, $schema_definition); - return TRUE; - } - } - // If another process has already created the table, attempting to recreate - // it will throw an exception. In this case just catch the exception and do - // nothing. - catch (SchemaObjectExistsException $e) { - return TRUE; - } - return FALSE; + return $this->getPathAliasEntityStorage()->pathHasMatchingAlias($initial_substring); } /** - * Act on an exception when url_alias might be stale. + * Returns the path alias entity storage handler. * - * If the table does not yet exist, that's fine, but if the table exists and - * yet the query failed, then the url_alias is stale and the exception needs - * to propagate. + * We can not store it in the constructor because that leads to a circular + * dependency in the service container. * - * @param $e - * The exception. - * - * @throws \Exception + * @return \Drupal\Core\Path\PathAliasStorageInterface + * The path alias entity storage. */ - protected function catchException(\Exception $e) { - if ($this->connection->schema()->tableExists(static::TABLE)) { - throw $e; + protected function getPathAliasEntityStorage() { + if (!$this->pathAliasEntityStorage) { + $this->pathAliasEntityStorage = $this->entityTypeManager->getStorage('path_alias'); } - } - - /** - * Defines the schema for the {url_alias} table. - * - * @internal - */ - public static function schemaDefinition() { - return [ - 'description' => 'A list of URL aliases for Drupal paths; a user may visit either the source or destination path.', - 'fields' => [ - 'pid' => [ - 'description' => 'A unique path alias identifier.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'source' => [ - 'description' => 'The Drupal path this alias is for; e.g. node/12.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'alias' => [ - 'description' => 'The alias for this path; e.g. title-of-the-story.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ], - 'langcode' => [ - 'description' => "The language code this alias is for; if 'und', the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.", - 'type' => 'varchar_ascii', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ], - ], - 'primary key' => ['pid'], - 'indexes' => [ - 'alias_langcode_pid' => ['alias', 'langcode', 'pid'], - 'source_langcode_pid' => ['source', 'langcode', 'pid'], - ], - ]; + return $this->pathAliasEntityStorage; } } diff --git a/core/lib/Drupal/Core/Path/Entity/PathAlias.php b/core/lib/Drupal/Core/Path/Entity/PathAlias.php new file mode 100644 index 0000000..d989c2f --- /dev/null +++ b/core/lib/Drupal/Core/Path/Entity/PathAlias.php @@ -0,0 +1,172 @@ +setDisplayOptions('form', [ + 'type' => 'language_select', + 'weight' => 0, + 'settings' => [ + 'include_locked' => FALSE, + ], + ]); + + $fields['path'] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('System path')) + ->setDescription(new TranslatableMarkup('The path that this alias belongs to.')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => 5, + 'settings' => [ + 'size' => 45, + ], + ]) + ->addPropertyConstraints('value', [ + 'Regex' => [ + 'pattern' => '/^\//i', + 'message' => new TranslatableMarkup('The source path has to start with a slash.'), + ], + ]) + ->addPropertyConstraints('value', ['ValidPath' => []]); + + $fields['alias'] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('Path alias')) + ->setDescription(new TranslatableMarkup('The alias used with this path.')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => 10, + 'settings' => [ + 'size' => 45, + ], + ]) + ->addPropertyConstraints('value', [ + 'Regex' => [ + 'pattern' => '/^\//i', + 'message' => new TranslatableMarkup('The alias path has to start with a slash.'), + ], + ]); + + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + return $this->get('path')->value; + } + + /** + * {@inheritdoc} + */ + public function setPath($path) { + $this->set('path', $path); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAlias() { + return $this->get('alias')->value; + } + + /** + * {@inheritdoc} + */ + public function setAlias($alias) { + $this->set('alias', $alias); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + return ['route_match']; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Trim the alias value of whitespace and slashes. Ensure to not trim the + // slash on the left side. + $alias = $this->getAlias(); + $alias = rtrim(trim(trim($alias), ''), "\\/"); + $this->setAlias($alias); + } + +} diff --git a/core/lib/Drupal/Core/Path/PathAliasInterface.php b/core/lib/Drupal/Core/Path/PathAliasInterface.php new file mode 100644 index 0000000..73d2b45 --- /dev/null +++ b/core/lib/Drupal/Core/Path/PathAliasInterface.php @@ -0,0 +1,48 @@ + $entity->id(), + 'source' => $entity->getPath(), + 'alias' => $entity->getAlias(), + 'langcode' => $entity->language()->getId(), + ]; + + if ($hook === 'update') { + $values['original'] = [ + 'pid' => $entity->id(), + 'source' => $entity->original->getPath(), + 'alias' => $entity->original->getAlias(), + 'langcode' => $entity->original->language()->getId(), + ]; + } + + $this->moduleHandler()->invokeAll('path_' . $hook, [$values]); + } + } + + /** + * {@inheritdoc} + */ + public function preloadPathAlias($preloaded, $langcode) { + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + $select = $this->database->select($this->dataTable) + ->fields($this->dataTable, ['path', 'alias']); + + if (!empty($preloaded)) { + $conditions = new Condition('OR'); + foreach ($preloaded as $preloaded_item) { + $conditions->condition('path', $this->database->escapeLike($preloaded_item), 'LIKE'); + } + $select->condition($conditions); + } + + // Always get the language-specific alias before the language-neutral one. + // For example 'de' is less than 'und' so the order needs to be ASC, while + // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also + // order by ID ASC so that fetchAllKeyed() returns the most recently + // created alias for each source. Subsequent queries using fetchField() must + // use ID DESC to have the same effect. + if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { + array_pop($langcode_list); + } + elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $select->orderBy('langcode', 'ASC'); + } + else { + $select->orderBy('langcode', 'DESC'); + } + + $select->orderBy('id', 'ASC'); + $select->condition('langcode', $langcode_list, 'IN'); + + return $select->execute()->fetchAllKeyed(); + } + + /** + * {@inheritdoc} + */ + public function lookupPathAlias($path, $langcode) { + $path = $this->database->escapeLike($path); + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->database->select($this->dataTable) + ->fields($this->dataTable, ['alias']) + ->condition('path', $path, 'LIKE'); + if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { + array_pop($langcode_list); + } + elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $select->orderBy('langcode', 'DESC'); + } + else { + $select->orderBy('langcode', 'ASC'); + } + + $select->orderBy('id', 'DESC'); + $select->condition('langcode', $langcode_list, 'IN'); + + return $select->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function lookupPathSource($path, $langcode) { + $alias = $this->database->escapeLike($path); + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->database->select($this->dataTable, 'pafd'); + $select->addField('pafd', 'path', 'source'); + $select->condition('alias', $alias, 'LIKE'); + if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) { + array_pop($langcode_list); + } + elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { + $select->orderBy('langcode', 'DESC'); + } + else { + $select->orderBy('langcode', 'ASC'); + } + + $select->orderBy('id', 'DESC'); + $select->condition('langcode', $langcode_list, 'IN'); + + return $select->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function aliasExists($alias, $langcode, $path = NULL) { + // Use LIKE and NOT LIKE for case-insensitive matching. + $query = $this->database->select($this->dataTable) + ->condition('alias', $this->database->escapeLike($alias), 'LIKE') + ->condition('langcode', $langcode); + if (!empty($path)) { + $query->condition('path', $this->database->escapeLike($path), 'NOT LIKE'); + } + $query->addExpression('1'); + $query->range(0, 1); + + return (bool) $query->execute()->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function languageAliasExists() { + return (bool) $this->database->queryRange('SELECT 1 FROM {' . $this->dataTable . '} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getAliasesForAdminListing($header, $keys = NULL) { + $query = $this->database->select($this->dataTable, 'pafd') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender') + ->extend('Drupal\Core\Database\Query\TableSortExtender'); + if ($keys) { + // Replace wildcards with PDO wildcards. + $query->condition('alias', '%' . preg_replace('!\*+!', '%', $this->database->escapeLike($keys)) . '%', 'LIKE'); + } + + $query->addField('pafd', 'id', 'pid'); + $query->addField('pafd', 'path', 'source'); + return $query + ->fields('pafd') + ->orderByHeader($header) + ->limit(50) + ->execute() + ->fetchAll(); + } + + /** + * {@inheritdoc} + */ + public function pathHasMatchingAlias($initial_substring) { + $query = $this->database->select($this->dataTable); + $query->addExpression(1); + + return (bool) $query + ->condition('path', $this->database->escapeLike($initial_substring) . '%', 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + } + +} diff --git a/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php b/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php new file mode 100644 index 0000000..051256c --- /dev/null +++ b/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php @@ -0,0 +1,111 @@ +storage->getDataTable()) { + $schema[$data_table]['indexes'] += [ + 'path_alias__alias_langcode_id' => ['alias', 'langcode', 'id'], + 'path_alias__path_langcode_id' => ['path', 'langcode', 'id'], + ]; + } + + return $schema; + } + +} diff --git a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php index ad3d4df..c0fea15 100644 --- a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceModifierInterface; use Drupal\Core\DependencyInjection\ServiceProviderInterface; +use Drupal\Core\PathProcessor\NullPathProcessorManager; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -32,6 +33,8 @@ public function alter(ContainerBuilder $container) { $definition = $container->getDefinition('library.discovery.collector'); $argument = new Reference('cache.null'); $definition->replaceArgument(0, $argument); + + $container->register('path_processor_manager', NullPathProcessorManager::class); } } diff --git a/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php b/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php index 6a7cf4a..028d23a 100644 --- a/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php +++ b/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php @@ -27,6 +27,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('path_alias'); // Ensure that the weight of module_link_content is higher than system. // @see menu_link_content_install() diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php index 29219d8..a3f6ac4 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php @@ -84,6 +84,7 @@ protected function getEntityCounts() { 'shortcut_set' => 1, 'action' => 23, 'menu' => 8, + 'path_alias' => 8, 'taxonomy_term' => 8, 'taxonomy_vocabulary' => 7, 'tour' => 5, diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php index b171801..e0e7b19 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php @@ -86,6 +86,7 @@ protected function getEntityCounts() { 'shortcut_set' => 2, 'action' => 17, 'menu' => 6, + 'path_alias' => 6, 'taxonomy_term' => 18, 'taxonomy_vocabulary' => 4, 'tour' => 5, diff --git a/core/modules/path/config/optional/language.content_settings.path_alias.path_alias.yml b/core/modules/path/config/optional/language.content_settings.path_alias.path_alias.yml new file mode 100644 index 0000000..d83e501 --- /dev/null +++ b/core/modules/path/config/optional/language.content_settings.path_alias.path_alias.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - language +id: path_alias.path_alias +target_entity_type_id: path_alias +target_bundle: path_alias +default_langcode: und +language_alterable: true diff --git a/core/modules/path/path.links.action.yml b/core/modules/path/path.links.action.yml index 9c58984..985482e 100644 --- a/core/modules/path/path.links.action.yml +++ b/core/modules/path/path.links.action.yml @@ -1,5 +1,5 @@ -path.admin_add: - route_name: path.admin_add +entity.path_alias.add_form: + route_name: entity.path_alias.add_form title: 'Add alias' appears_on: - - path.admin_overview + - entity.path_alias.collection diff --git a/core/modules/path/path.links.menu.yml b/core/modules/path/path.links.menu.yml index 4f394a0..549a4eb 100644 --- a/core/modules/path/path.links.menu.yml +++ b/core/modules/path/path.links.menu.yml @@ -1,6 +1,6 @@ -path.admin_overview: +entity.path_alias.collection: title: 'URL aliases' description: 'Add custom URLs to existing paths.' - route_name: path.admin_overview + route_name: entity.path_alias.collection parent: system.admin_config_search weight: -5 diff --git a/core/modules/path/path.links.task.yml b/core/modules/path/path.links.task.yml index bc59857..e9328fe 100644 --- a/core/modules/path/path.links.task.yml +++ b/core/modules/path/path.links.task.yml @@ -1,4 +1,4 @@ -path.admin_overview: +entity.path_alias.collection: title: List - route_name: path.admin_overview - base_route: path.admin_overview + route_name: entity.path_alias.collection + base_route: entity.path_alias.collection diff --git a/core/modules/path/path.module b/core/modules/path/path.module index 03a563e..02eca3e 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -8,6 +8,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Routing\RouteMatchInterface; /** @@ -28,10 +29,10 @@ function path_help($route_name, RouteMatchInterface $route_match) { $output .= ''; return $output; - case 'path.admin_overview': + case 'entity.path_alias.collection': return '

' . t("An alias defines a different name for an existing URL path - for example, the alias 'about' for the URL path 'node/1'. A URL path can have multiple aliases.") . '

'; - case 'path.admin_add': + case 'entity.path_alias.add_form': return '

' . t('Enter the path you wish to create the alias for, followed by the name of the new alias.') . '

'; } } @@ -68,3 +69,33 @@ function path_entity_translation_create(ContentEntityInterface $translation) { } } } + +/** + * Implements hook_field_widget_form_alter(). + */ +function path_field_widget_form_alter(&$element, \Drupal\Core\Form\FormStateInterface $form_state, $context) { + $field_definition = $context['items']->getFieldDefinition(); + $field_name = $field_definition->getName(); + $entity_type = $field_definition->getTargetEntityTypeId(); + $widget_name = $context['widget']->getPluginId(); + + if ($entity_type === 'path_alias') { + if (($field_name === 'path' || $field_name === 'alias') && $widget_name === 'string_textfield') { + $element['value']['#field_prefix'] = \Drupal::service('router.request_context')->getCompleteBaseUrl(); + } + + if ($field_name === 'langcode') { + $element['value']['#description'] = t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set as - Not specified -.'); + $element['value']['#empty_value'] = LanguageInterface::LANGCODE_NOT_SPECIFIED; + $element['value']['#empty_option'] = t('- Not specified -'); + } + + if ($field_name === 'path') { + $element['value']['#description'] = t('Specify the existing path you wish to alias. For example: /node/28, /forum/1, /taxonomy/term/1.'); + } + + if ($field_name === 'alias') { + $element['value']['#description'] = t('Specify an alternative path by which this data can be accessed. For example, type "/about" when writing an about page.'); + } + } +} diff --git a/core/modules/path/path.routing.yml b/core/modules/path/path.routing.yml index 9634541..a4005ce 100644 --- a/core/modules/path/path.routing.yml +++ b/core/modules/path/path.routing.yml @@ -1,12 +1,4 @@ -path.delete: - path: '/admin/config/search/path/delete/{pid}' - defaults: - _form: '\Drupal\path\Form\DeleteForm' - _title: 'Delete alias' - requirements: - _permission: 'administer url aliases' - -path.admin_overview: +entity.path_alias.collection: path: '/admin/config/search/path' defaults: _title: 'URL aliases' @@ -22,19 +14,3 @@ path.admin_overview_filter: _controller: '\Drupal\path\Controller\PathController::adminOverview' requirements: _permission: 'administer url aliases' - -path.admin_add: - path: '/admin/config/search/path/add' - defaults: - _title: 'Add alias' - _form: '\Drupal\path\Form\AddForm' - requirements: - _permission: 'administer url aliases' - -path.admin_edit: - path: '/admin/config/search/path/edit/{pid}' - defaults: - _title: 'Edit alias' - _form: '\Drupal\path\Form\EditForm' - requirements: - _permission: 'administer url aliases' diff --git a/core/modules/path/src/Controller/PathController.php b/core/modules/path/src/Controller/PathController.php index 26fe851..dd01a3e 100644 --- a/core/modules/path/src/Controller/PathController.php +++ b/core/modules/path/src/Controller/PathController.php @@ -98,11 +98,11 @@ public function adminOverview(Request $request) { $operations = []; $operations['edit'] = [ 'title' => $this->t('Edit'), - 'url' => Url::fromRoute('path.admin_edit', ['pid' => $data->pid], ['query' => $destination]), + 'url' => Url::fromRoute('entity.path_alias.edit_form', ['path_alias' => $data->pid], ['query' => $destination]), ]; $operations['delete'] = [ 'title' => $this->t('Delete'), - 'url' => Url::fromRoute('path.delete', ['pid' => $data->pid], ['query' => $destination]), + 'url' => Url::fromRoute('entity.path_alias.delete_form', ['path_alias' => $data->pid], ['query' => $destination]), ]; $row['data']['operations'] = [ 'data' => [ @@ -124,7 +124,7 @@ public function adminOverview(Request $request) { '#type' => 'table', '#header' => $header, '#rows' => $rows, - '#empty' => $this->t('No URL aliases available. Add URL alias.', [':link' => $this->url('path.admin_add')]), + '#empty' => $this->t('No URL aliases available. Add URL alias.', [':link' => $this->url('entity.path_alias.add_form')]), ]; $build['path_pager'] = ['#type' => 'pager']; diff --git a/core/modules/path/src/Form/AddForm.php b/core/modules/path/src/Form/AddForm.php deleted file mode 100644 index 59db6f9..0000000 --- a/core/modules/path/src/Form/AddForm.php +++ /dev/null @@ -1,33 +0,0 @@ - '', - 'alias' => '', - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - 'pid' => NULL, - ]; - } - -} diff --git a/core/modules/path/src/Form/DeleteForm.php b/core/modules/path/src/Form/DeleteForm.php deleted file mode 100644 index aac413f..0000000 --- a/core/modules/path/src/Form/DeleteForm.php +++ /dev/null @@ -1,92 +0,0 @@ -aliasStorage = $alias_storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('path.alias_storage') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'path_alias_delete'; - } - - /** - * {@inheritdoc} - */ - public function getQuestion() { - return t('Are you sure you want to delete path alias %title?', ['%title' => $this->pathAlias['alias']]); - } - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - return new Url('path.admin_overview'); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) { - $this->pathAlias = $this->aliasStorage->load(['pid' => $pid]); - - $form = parent::buildForm($form, $form_state); - - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $this->aliasStorage->delete(['pid' => $this->pathAlias['pid']]); - - $form_state->setRedirect('path.admin_overview'); - } - -} diff --git a/core/modules/path/src/Form/EditForm.php b/core/modules/path/src/Form/EditForm.php deleted file mode 100644 index 9638b57..0000000 --- a/core/modules/path/src/Form/EditForm.php +++ /dev/null @@ -1,61 +0,0 @@ -aliasStorage->load(['pid' => $pid]); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) { - $form = parent::buildForm($form, $form_state, $pid); - - $form['#title'] = $this->path['alias']; - $form['pid'] = [ - '#type' => 'hidden', - '#value' => $this->path['pid'], - ]; - - $url = new Url('path.delete', [ - 'pid' => $this->path['pid'], - ]); - - if ($this->getRequest()->query->has('destination')) { - $url->setOption('query', $this->getDestinationArray()); - } - - $form['actions']['delete'] = [ - '#type' => 'link', - '#title' => $this->t('Delete'), - '#url' => $url, - '#attributes' => [ - 'class' => ['button', 'button--danger'], - ], - ]; - - return $form; - } - -} diff --git a/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php deleted file mode 100644 index 4b1cbd5..0000000 --- a/core/modules/path/src/Form/PathFormBase.php +++ /dev/null @@ -1,219 +0,0 @@ -aliasStorage = $alias_storage; - $this->aliasManager = $alias_manager; - $this->pathValidator = $path_validator; - $this->requestContext = $request_context; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('path.alias_storage'), - $container->get('path.alias_manager'), - $container->get('path.validator'), - $container->get('router.request_context') - ); - } - - /** - * Builds the path used by the form. - * - * @param int|null $pid - * Either the unique path ID, or NULL if a new one is being created. - */ - abstract protected function buildPath($pid); - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $pid = NULL) { - $this->path = $this->buildPath($pid); - $form['source'] = [ - '#type' => 'textfield', - '#title' => $this->t('Existing system path'), - '#default_value' => $this->path['source'], - '#maxlength' => 255, - '#size' => 45, - '#description' => $this->t('Specify the existing path you wish to alias. For example: /node/28, /forum/1, /taxonomy/term/1.'), - '#field_prefix' => $this->requestContext->getCompleteBaseUrl(), - '#required' => TRUE, - ]; - $form['alias'] = [ - '#type' => 'textfield', - '#title' => $this->t('Path alias'), - '#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.'), - '#field_prefix' => $this->requestContext->getCompleteBaseUrl(), - '#required' => TRUE, - ]; - - // A hidden value unless language.module is enabled. - if (\Drupal::moduleHandler()->moduleExists('language')) { - $languages = \Drupal::languageManager()->getLanguages(); - $language_options = []; - foreach ($languages as $langcode => $language) { - $language_options[$langcode] = $language->getName(); - } - - $form['langcode'] = [ - '#type' => 'select', - '#title' => $this->t('Language'), - '#options' => $language_options, - '#empty_value' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - '#empty_option' => $this->t('- None -'), - '#default_value' => $this->path['langcode'], - '#weight' => -10, - '#description' => $this->t('A path alias set for a specific language will always be used when displaying this page in that language, and takes precedence over path aliases set as - None -.'), - ]; - } - else { - $form['langcode'] = [ - '#type' => 'value', - '#value' => $this->path['langcode'], - ]; - } - - $form['actions'] = ['#type' => 'actions']; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Save'), - '#button_type' => 'primary', - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $source = &$form_state->getValue('source'); - $source = $this->aliasManager->getPathByAlias($source); - $alias = &$form_state->getValue('alias'); - - // Trim the submitted value of whitespace and slashes. Ensure to not trim - // the slash on the left side. - $alias = rtrim(trim(trim($alias), ''), "\\/"); - - if ($source[0] !== '/') { - $form_state->setErrorByName('source', 'The source path has to start with a slash.'); - } - if ($alias[0] !== '/') { - $form_state->setErrorByName('alias', 'The alias path has to start with a slash.'); - } - - // Language is only set if language.module is enabled, otherwise save for all - // languages. - $langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED); - - if ($this->aliasStorage->aliasExists($alias, $langcode, $this->path['source'])) { - $stored_alias = $this->aliasStorage->load(['alias' => $alias, 'langcode' => $langcode]); - if ($stored_alias['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.', ['%alias' => $alias])); - } - } - - if (!$this->pathValidator->isValid(trim($source, '/'))) { - $form_state->setErrorByName('source', t("Either the path '@link_path' is invalid or you do not have access to it.", ['@link_path' => $source])); - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - // Remove unnecessary values. - $form_state->cleanValues(); - - $pid = $form_state->getValue('pid', 0); - $source = $form_state->getValue('source'); - $alias = $form_state->getValue('alias'); - // Language is only set if language.module is enabled, otherwise save for all - // languages. - $langcode = $form_state->getValue('langcode', LanguageInterface::LANGCODE_NOT_SPECIFIED); - - $this->aliasStorage->save($source, $alias, $langcode, $pid); - - $this->messenger()->addStatus($this->t('The alias has been saved.')); - $form_state->setRedirect('path.admin_overview'); - } - -} diff --git a/core/modules/path/src/PathAliasForm.php b/core/modules/path/src/PathAliasForm.php new file mode 100644 index 0000000..6a44309 --- /dev/null +++ b/core/modules/path/src/PathAliasForm.php @@ -0,0 +1,32 @@ +messenger()->addStatus($this->t('The alias has been saved.')); + $form_state->setRedirect('entity.path_alias.collection'); + } + +} diff --git a/core/modules/path/src/Plugin/Validation/Constraint/UniquePathAliasConstraint.php b/core/modules/path/src/Plugin/Validation/Constraint/UniquePathAliasConstraint.php new file mode 100644 index 0000000..977d4d7 --- /dev/null +++ b/core/modules/path/src/Plugin/Validation/Constraint/UniquePathAliasConstraint.php @@ -0,0 +1,31 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function validate($entity, Constraint $constraint) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + $path = $entity->getPath(); + $alias = $entity->getAlias(); + $langcode = $entity->language()->getId(); + + $storage = $this->entityTypeManager->getStorage('path_alias'); + $query = $storage->getQuery(); + if (!$entity->isNew()) { + $query->condition('id', $entity->id(), '<>'); + } + $result = $query + ->condition('alias', $alias, '=') + ->condition('path', $path, '<>') + ->condition('langcode', $langcode, '=') + ->range(0, 1) + ->execute(); + + if ($result) { + $existing_alias_id = reset($result); + $existing_alias = $storage->load($existing_alias_id); + + if ($existing_alias->getAlias() !== $alias) { + $this->context->buildViolation($constraint->differentCapitalizationMessage, [ + '%alias' => $alias, + '%stored_alias' => $existing_alias->getAlias(), + ])->addViolation(); + } + else { + $this->context->buildViolation($constraint->message, [ + '%alias' => $alias, + ])->addViolation(); + } + } + } + +} diff --git a/core/modules/path/src/Plugin/Validation/Constraint/ValidPathConstraint.php b/core/modules/path/src/Plugin/Validation/Constraint/ValidPathConstraint.php new file mode 100644 index 0000000..76e5887 --- /dev/null +++ b/core/modules/path/src/Plugin/Validation/Constraint/ValidPathConstraint.php @@ -0,0 +1,24 @@ +pathValidator = $path_validator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('path.validator') + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + if (!isset($value)) { + return; + } + + $path = trim($value, '/'); + if (!$this->pathValidator->isValid($path)) { + $this->context->addViolation($constraint->message, [ + '%link_path' => $value, + ]); + } + } + +} diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index 19115cd..13b5268 100644 --- a/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -38,8 +38,8 @@ public function testPathCache() { // Create alias. $edit = []; - $edit['source'] = '/node/' . $node1->id(); - $edit['alias'] = '/' . $this->randomMachineName(8); + $edit['path[0][value]'] = '/node/' . $node1->id(); + $edit['alias[0][value]'] = '/' . $this->randomMachineName(8); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Check the path alias whitelist cache. @@ -51,15 +51,15 @@ public function testPathCache() { // created. \Drupal::cache('data')->deleteAll(); // Make sure the path is not converted to the alias. - $this->drupalGet(trim($edit['source'], '/'), ['alias' => TRUE]); - $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); + $this->drupalGet(trim($edit['path[0][value]'], '/'), ['alias' => TRUE]); + $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.'); // Visit the alias for the node and confirm a cache entry is created. \Drupal::cache('data')->deleteAll(); // @todo Remove this once https://www.drupal.org/node/2480077 lands. Cache::invalidateTags(['rendered']); - $this->drupalGet(trim($edit['alias'], '/')); - $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.'); + $this->drupalGet(trim($edit['alias[0][value]'], '/')); + $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['path[0][value]']), 'Cache entry was created.'); } /** @@ -71,29 +71,29 @@ public function testAdminAlias() { // Create alias. $edit = []; - $edit['source'] = '/node/' . $node1->id(); - $edit['alias'] = '/' . $this->getRandomGenerator()->word(8); + $edit['path[0][value]'] = '/node/' . $node1->id(); + $edit['alias[0][value]'] = '/' . $this->getRandomGenerator()->word(8); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet($edit['alias']); + $this->drupalGet($edit['alias[0][value]']); $this->assertText($node1->label(), 'Alias works.'); $this->assertResponse(200); // Confirm that the alias works in a case-insensitive way. - $this->assertTrue(ctype_lower(ltrim($edit['alias'], '/'))); - $this->drupalGet($edit['alias']); + $this->assertTrue(ctype_lower(ltrim($edit['alias[0][value]'], '/'))); + $this->drupalGet($edit['alias[0][value]']); $this->assertText($node1->label(), 'Alias works lower case.'); $this->assertResponse(200); - $this->drupalGet(mb_strtoupper($edit['alias'])); + $this->drupalGet(mb_strtoupper($edit['alias[0][value]'])); $this->assertText($node1->label(), 'Alias works upper case.'); $this->assertResponse(200); // Change alias to one containing "exotic" characters. - $pid = $this->getPID($edit['alias']); + $pid = $this->getPID($edit['alias[0][value]']); - $previous = $edit['alias']; + $previous = $edit['alias[0][value]']; // Lower-case letters. - $edit['alias'] = '/alias' . + $edit['alias[0][value]'] = '/alias' . // "Special" ASCII characters. "- ._~!$'\"()*@[]?&+%#,;=:" . // Characters that look like a percent-escaped string. @@ -106,12 +106,12 @@ public function testAdminAlias() { // currently unable to find the upper-case versions of non-ASCII // characters. // @todo fix this in https://www.drupal.org/node/2607432 - $edit['alias'] .= "ïвβéø"; + $edit['alias[0][value]'] .= "ïвβéø"; } $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save')); // Confirm that the alias works. - $this->drupalGet(mb_strtoupper($edit['alias'])); + $this->drupalGet(mb_strtoupper($edit['alias[0][value]'])); $this->assertText($node1->label(), 'Changed alias works.'); $this->assertResponse(200); @@ -125,37 +125,37 @@ public function testAdminAlias() { $node2 = $this->drupalCreateNode(); // Set alias to second test node. - $edit['source'] = '/node/' . $node2->id(); + $edit['path[0][value]'] = '/node/' . $node2->id(); // leave $edit['alias'] the same $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm no duplicate was created. - $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']]), 'Attempt to move alias was rejected.'); + $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias[0][value]']]), 'Attempt to move alias was rejected.'); $edit_upper = $edit; - $edit_upper['alias'] = mb_strtoupper($edit['alias']); + $edit_upper['alias[0][value]'] = mb_strtoupper($edit['alias[0][value]']); $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'], + '%alias' => $edit_upper['alias[0][value]'], + '%stored_alias' => $edit['alias[0][value]'], ]), 'Attempt to move upper-case alias was rejected.'); // Delete alias. $this->drupalGet('admin/config/search/path/edit/' . $pid); $this->clickLink(t('Delete')); - $this->assertRaw(t('Are you sure you want to delete path alias %name?', ['%name' => $edit['alias']])); - $this->drupalPostForm(NULL, [], t('Confirm')); + $this->assertRaw(t('Are you sure you want to delete the path alias %name?', ['%name' => $edit['alias[0][value]']])); + $this->drupalPostForm(NULL, [], t('Delete')); // Confirm that the alias no longer works. - $this->drupalGet($edit['alias']); + $this->drupalGet($edit['alias[0][value]']); $this->assertNoText($node1->label(), 'Alias was successfully deleted.'); $this->assertResponse(404); // Create a really long alias. $edit = []; - $edit['source'] = '/node/' . $node1->id(); + $edit['path[0][value]'] = '/node/' . $node1->id(); $alias = '/' . $this->randomMachineName(128); - $edit['alias'] = $alias; + $edit['alias[0][value]'] = $alias; // The alias is shortened to 50 characters counting the ellipsis. $truncated_alias = substr($alias, 0, 47); $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); @@ -168,9 +168,9 @@ public function testAdminAlias() { // Create absolute path alias. $edit = []; - $edit['source'] = '/node/' . $node3->id(); + $edit['path[0][value]'] = '/node/' . $node3->id(); $node3_alias = '/' . $this->randomMachineName(8); - $edit['alias'] = $node3_alias; + $edit['alias[0][value]'] = $node3_alias; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Create fourth test node. @@ -178,24 +178,24 @@ public function testAdminAlias() { // Create alias with trailing slash. $edit = []; - $edit['source'] = '/node/' . $node4->id(); + $edit['path[0][value]'] = '/node/' . $node4->id(); $node4_alias = '/' . $this->randomMachineName(8); - $edit['alias'] = $node4_alias . '/'; + $edit['alias[0][value]'] = $node4_alias . '/'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); // Confirm that the alias with trailing slash is not found. - $this->assertNoText($edit['alias'], 'The absolute alias was not found.'); + $this->assertNoText($edit['alias[0][value]'], 'The absolute alias was not found.'); // The alias without trailing flash is found. - $this->assertText(trim($edit['alias'], '/'), 'The alias without trailing slash was found.'); + $this->assertText(trim($edit['alias[0][value]'], '/'), 'The alias without trailing slash was found.'); // Update an existing alias to point to a different source. $pid = $this->getPID($node4_alias); $edit = []; - $edit['alias'] = $node4_alias; - $edit['source'] = '/node/' . $node2->id(); + $edit['alias[0][value]'] = $node4_alias; + $edit['path[0][value]'] = '/node/' . $node2->id(); $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save')); $this->assertText('The alias has been saved.'); - $this->drupalGet($edit['alias']); + $this->drupalGet($edit['alias[0][value]']); $this->assertNoText($node4->label(), 'Previous alias no longer works.'); $this->assertText($node2->label(), 'Alias works.'); $this->assertResponse(200); @@ -203,18 +203,18 @@ public function testAdminAlias() { // Update an existing alias to use a duplicate alias. $pid = $this->getPID($node3_alias); $edit = []; - $edit['alias'] = $node4_alias; - $edit['source'] = '/node/' . $node3->id(); + $edit['alias[0][value]'] = $node4_alias; + $edit['path[0][value]'] = '/node/' . $node3->id(); $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save')); - $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']])); + $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias[0][value]']])); // Create an alias without a starting slash. $node5 = $this->drupalCreateNode(); $edit = []; - $edit['source'] = 'node/' . $node5->id(); + $edit['path[0][value]'] = 'node/' . $node5->id(); $node5_alias = $this->randomMachineName(8); - $edit['alias'] = $node5_alias . '/'; + $edit['alias[0][value]'] = $node5_alias . '/'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->assertUrl('admin/config/search/path/add'); @@ -368,7 +368,11 @@ public function testNodeAlias() { * Integer representing the path ID. */ public function getPID($alias) { - return db_query("SELECT pid FROM {url_alias} WHERE alias = :alias", [':alias' => $alias])->fetchField(); + $result = \Drupal::entityTypeManager()->getStorage('path_alias')->getQuery() + ->condition('alias', $alias, '=') + ->accessCheck(FALSE) + ->execute(); + return reset($result); } /** diff --git a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php index 28f22ec..2676844 100644 --- a/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php +++ b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php @@ -31,6 +31,7 @@ class MigrateUrlAliasTest extends MigrateDrupal6TestBase { protected function setUp() { parent::setUp(); $this->installEntitySchema('node'); + $this->installEntitySchema('path_alias'); $this->installConfig(['node']); $this->installSchema('node', ['node_access']); $this->migrateUsers(FALSE); diff --git a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php index b8d92e0..587709c 100644 --- a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php +++ b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php @@ -32,6 +32,7 @@ protected function setUp() { parent::setUp(); $this->installEntitySchema('node'); + $this->installEntitySchema('path_alias'); $this->installConfig('node'); $this->installSchema('node', ['node_access']); diff --git a/core/modules/path/tests/src/Kernel/PathItemTest.php b/core/modules/path/tests/src/Kernel/PathItemTest.php index 224a842..f001dc6 100644 --- a/core/modules/path/tests/src/Kernel/PathItemTest.php +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php @@ -29,6 +29,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('user'); + $this->installEntitySchema('path_alias'); $this->installSchema('node', ['node_access']); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index 17e0c0a..d5119fd 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -874,9 +874,10 @@ public function testPost() { // DX: 422 when invalid entity: multiple values sent for single-value field. $response = $this->request('POST', $url, $request_options); - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + if ($label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName) { + $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -961,7 +962,9 @@ public function testPost() { // 500 when creating an entity with a duplicate UUID. $normalized_entity = $this->getModifiedEntityForPostTesting(); $normalized_entity[$created_entity->getEntityType()->getKey('uuid')] = [['value' => $created_entity->uuid()]]; - $normalized_entity[$label_field] = [['value' => $this->randomMachineName()]]; + if ($label_field) { + $normalized_entity[$label_field] = [['value' => $this->randomMachineName()]]; + } $request_options[RequestOptions::BODY] = $this->serializer->encode($normalized_entity, static::$format); $response = $this->request('POST', $url, $request_options); @@ -972,7 +975,9 @@ public function testPost() { $normalized_entity = $this->getModifiedEntityForPostTesting(); $new_uuid = \Drupal::service('uuid')->generate(); $normalized_entity[$created_entity->getEntityType()->getKey('uuid')] = [['value' => $new_uuid]]; - $normalized_entity[$label_field] = [['value' => $this->randomMachineName()]]; + if ($label_field) { + $normalized_entity[$label_field] = [['value' => $this->randomMachineName()]]; + } $request_options[RequestOptions::BODY] = $this->serializer->encode($normalized_entity, static::$format); $response = $this->request('POST', $url, $request_options); @@ -1103,9 +1108,10 @@ public function testPatch() { // DX: 422 when invalid entity: multiple values sent for single-value field. $response = $this->request('PATCH', $url, $request_options); - $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); - $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + if ($label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName) { + $label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel(); + $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response); + } $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2; @@ -1472,8 +1478,9 @@ protected function makeNormalizationInvalid(array $normalization, $entity_key) { switch ($entity_key) { case 'label': // Add a second label to this entity to make it invalid. - $label_field = $entity_type->hasKey('label') ? $entity_type->getKey('label') : static::$labelFieldName; - $normalization[$label_field][1]['value'] = 'Second Title'; + if ($label_field = $entity_type->hasKey('label') ? $entity_type->getKey('label') : static::$labelFieldName) { + $normalization[$label_field][1]['value'] = 'Second Title'; + } break; case 'id': $normalization[$entity_type->getKey('id')][0]['value'] = $this->anotherEntity->id(); diff --git a/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php b/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php index 739089a..205c0d1 100644 --- a/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php +++ b/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php @@ -33,16 +33,13 @@ public function setDatabaseDumpFiles() { * Tests rest_update_8203(). */ public function testBcEntityResourcePermissionSettingAdded() { - $permission_handler = $this->container->get('user.permissions'); - - $is_rest_resource_permission = function ($permission) { - return $permission['provider'] === 'rest' && (string) $permission['title'] !== 'Administer REST resource configuration'; - }; - // Make sure we have the expected values before the update. $rest_settings = $this->config('rest.settings'); $this->assertFalse(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData())); - $this->assertEqual([], array_filter($permission_handler->getPermissions(), $is_rest_resource_permission)); + + $rest_permissions_callback = \Drupal::service('controller_resolver')->getControllerFromDefinition('Drupal\rest\RestPermissions::permissions'); + $rest_permissions = array_keys(call_user_func($rest_permissions_callback)); + $this->assertEquals([], $rest_permissions); $this->runUpdates(); @@ -50,8 +47,10 @@ public function testBcEntityResourcePermissionSettingAdded() { $rest_settings = $this->config('rest.settings'); $this->assertTrue(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData())); $this->assertTrue($rest_settings->get('bc_entity_resource_permissions')); - $rest_permissions = array_keys(array_filter($permission_handler->getPermissions(), $is_rest_resource_permission)); - $this->assertEqual(['restful delete entity:node', 'restful get entity:node', 'restful patch entity:node', 'restful post entity:node'], $rest_permissions); + + $rest_permissions_callback = \Drupal::service('controller_resolver')->getControllerFromDefinition('Drupal\rest\RestPermissions::permissions'); + $rest_permissions = array_keys(call_user_func($rest_permissions_callback)); + $this->assertEquals(['restful get entity:node', 'restful post entity:node', 'restful delete entity:node', 'restful patch entity:node'], $rest_permissions); } } diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index 832cc82..62b0fee 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -384,7 +384,7 @@ public function containerBuild(ContainerBuilder $container) { } if ($container->hasDefinition('path_processor_alias')) { - // Prevent the alias-based path processor, which requires a url_alias db + // Prevent the alias-based path processor, which requires a path_alias db // table, from being registered to the path processor manager. We do this // by removing the tags that the compiler pass looks for. This means the // url generator can safely be used within tests. diff --git a/core/modules/system/src/Tests/Path/UrlAliasFixtures.php b/core/modules/system/src/Tests/Path/UrlAliasFixtures.php deleted file mode 100644 index 41fd896..0000000 --- a/core/modules/system/src/Tests/Path/UrlAliasFixtures.php +++ /dev/null @@ -1,96 +0,0 @@ -tableDefinition(); - $schema = $connection->schema(); - - foreach ($tables as $name => $table) { - $schema->dropTable($name); - $schema->createTable($name, $table); - } - } - - /** - * Drop the tables used for the sample data. - * - * @param \Drupal\Core\Database\Connection $connection - * The connection to use to drop the tables. - */ - public function dropTables(Connection $connection) { - $tables = $this->tableDefinition(); - $schema = $connection->schema(); - - foreach ($tables as $name => $table) { - $schema->dropTable($name); - } - } - - /** - * Returns an array of URL aliases for testing. - * - * @return array of URL alias definitions. - */ - public function sampleUrlAliases() { - return [ - [ - 'source' => '/node/1', - 'alias' => '/alias_for_node_1_en', - 'langcode' => 'en', - ], - [ - 'source' => '/node/2', - 'alias' => '/alias_for_node_2_en', - 'langcode' => 'en', - ], - [ - 'source' => '/node/1', - 'alias' => '/alias_for_node_1_fr', - 'langcode' => 'fr', - ], - [ - 'source' => '/node/1', - 'alias' => '/alias_for_node_1_und', - 'langcode' => 'und', - ], - ]; - } - - /** - * Returns the table definition for the URL alias fixtures. - * - * @return array - * Table definitions. - */ - public function tableDefinition() { - $tables = []; - - // Prime the drupal_get_filename() cache with the location of the system - // module as its location is known and shouldn't change. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml'); - module_load_install('system'); - $schema = system_schema(); - - $tables['url_alias'] = AliasStorage::schemaDefinition(); - $tables['key_value'] = $schema['key_value']; - - return $tables; - } - -} diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 2cddcbf..91aa36a 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -11,7 +11,6 @@ use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; -use Drupal\Core\Path\AliasStorage; use Drupal\Core\Url; use Drupal\Core\Database\Database; use Drupal\Core\Entity\ContentEntityTypeInterface; @@ -1181,11 +1180,6 @@ function system_schema() { ], ]; - // Create the url_alias table. The alias_storage service can auto-create its - // table, but this relies on exceptions being thrown. These exceptions will be - // thrown every request until an alias is created. - $schema['url_alias'] = AliasStorage::schemaDefinition(); - return $schema; } diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 0662853..4e26d6e 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -6,10 +6,13 @@ */ use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\Display\EntityDisplayInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Site\Settings; +use Drupal\Core\StringTranslation\TranslatableMarkup; /** * Re-save all configuration entities to recalculate dependencies. @@ -169,3 +172,74 @@ function system_post_update_extra_fields(&$sandbox = NULL) { $config_entity_updater->update($sandbox, 'entity_form_display', $callback); $config_entity_updater->update($sandbox, 'entity_view_display', $callback); } + +/** + * Convert path aliases to entities. + */ +function system_post_update_convert_path_aliases_to_entities(&$sandbox = NULL) { + $database = \Drupal::database(); + + if (!isset($sandbox['current'])) { + // This must be the first run. Initialize the sandbox. + $sandbox['progress'] = 0; + $sandbox['current'] = 0; + + $entity_type = new ContentEntityType([ + 'id' => 'path_alias', + 'class' => '\Drupal\Core\Path\Entity\PathAlias', + 'label' => new TranslatableMarkup('Path alias'), + 'handlers' => [ + 'storage' => 'Drupal\Core\Path\PathAliasStorage', + 'storage_schema' => 'Drupal\Core\Path\PathAliasStorageSchema', + 'translation' => 'Drupal\content_translation\ContentTranslationHandler', + ], + 'base_table' => 'path_alias', + 'data_table' => 'path_alias_field_data', + 'revision_table' => 'path_alias_revision', + 'revision_data_table' => 'path_alias_field_revision', + 'translatable' => TRUE, + 'entity_keys' => [ + 'id' => 'id', + 'revision' => 'revision_id', + 'langcode' => 'langcode', + 'uuid' => 'uuid', + ], + ]); + \Drupal::entityDefinitionUpdateManager()->installEntityType($entity_type); + } + + $step_size = Settings::get('entity_update_batch_size', 50); + $url_aliases = $database->select('url_alias', 'ua') + ->condition("ua.pid", $sandbox['current'], '>') + ->fields('ua') + ->orderBy('pid', 'ASC') + ->range(0, $step_size) + ->execute() + ->fetchAll(); + + $storage = \Drupal::entityTypeManager()->getStorage('path_alias'); + foreach ($url_aliases as $url_alias) { + $path_alias = $storage->create([ + 'id' => $url_alias->pid, + 'path' => $url_alias->source, + 'alias' => $url_alias->alias, + 'langcode' => $url_alias->langcode, + ]); + $path_alias->enforceIsNew(TRUE); + $path_alias->save(); + + $sandbox['progress']++; + $sandbox['current'] = $url_alias->pid; + } + + // If we're not in maintenance mode, the number of path aliasa could change at + // any time so make sure that we always use the latest record count. + $sandbox['max'] = (int) $database->select('url_alias', 't')->countQuery()->execute()->fetchField(); + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + + if ($sandbox['#finished'] >= 1) { + $database->schema()->dropTable('url_alias'); + + return t('Path aliases have been converted to entities.'); + } +} diff --git a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php index afd697a..6e22a4a 100644 --- a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php +++ b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php @@ -25,8 +25,8 @@ class UrlAlterFunctionalTest extends BrowserTestBase { * Test that URL altering works and that it occurs in the correct order. */ public function testUrlAlter() { - // Ensure that the url_alias table exists after Drupal installation. - $this->assertTrue(Database::getConnection()->schema()->tableExists('url_alias'), 'The url_alias table exists after Drupal installation.'); + // Ensure that the path_alias table exists after Drupal installation. + $this->assertTrue(Database::getConnection()->schema()->tableExists('path_alias'), 'The path_alias table exists after Drupal installation.'); // User names can have quotes and plus signs so we should ensure that URL // altering works with this. diff --git a/core/modules/system/tests/src/Kernel/PathHooksTest.php b/core/modules/system/tests/src/Kernel/PathHooksTest.php index fcbb499..afa6176 100644 --- a/core/modules/system/tests/src/Kernel/PathHooksTest.php +++ b/core/modules/system/tests/src/Kernel/PathHooksTest.php @@ -18,6 +18,15 @@ class PathHooksTest extends KernelTestBase { static public $modules = ['system']; /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('path_alias'); + } + + /** * Test system_path_*() correctly clears caches. */ public function testPathHooks() { diff --git a/core/modules/user/src/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php b/core/modules/user/src/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php index 5618605..676661d 100644 --- a/core/modules/user/src/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php +++ b/core/modules/user/src/Plugin/LanguageNegotiation/LanguageNegotiationUserAdmin.php @@ -2,7 +2,7 @@ namespace Drupal\user\Plugin\LanguageNegotiation; -use Drupal\Core\PathProcessor\PathProcessorManager; +use Drupal\Core\PathProcessor\InboundPathProcessorInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Routing\AdminContext; use Drupal\Core\Routing\StackedRouteMatchInterface; @@ -70,12 +70,12 @@ class LanguageNegotiationUserAdmin extends LanguageNegotiationMethodBase impleme * The admin context. * @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $router * The router. - * @param \Drupal\Core\PathProcessor\PathProcessorManager $path_processor_manager + * @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor_manager * The path processor manager. * @param \Drupal\Core\Routing\StackedRouteMatchInterface $stacked_route_match * The stacked route match. */ - public function __construct(AdminContext $admin_context, UrlMatcherInterface $router, PathProcessorManager $path_processor_manager, StackedRouteMatchInterface $stacked_route_match) { + public function __construct(AdminContext $admin_context, UrlMatcherInterface $router, InboundPathProcessorInterface $path_processor_manager, StackedRouteMatchInterface $stacked_route_match) { $this->adminContext = $admin_context; $this->router = $router; $this->pathProcessorManager = $path_processor_manager; diff --git a/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php b/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php new file mode 100644 index 0000000..5c8c16c --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php @@ -0,0 +1,29 @@ +applyHalFieldNormalization($default_normalization); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => '', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/path_alias/path_alias', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/path_alias/path_alias', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return [ + 'url.site', + 'user.permissions', + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php new file mode 100644 index 0000000..9febf9e --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php @@ -0,0 +1,26 @@ +grantPermissionsToTestedRole(['administer url aliases']); + break; + case 'POST': + $this->grantPermissionsToTestedRole(['administer url aliases']); + break; + case 'PATCH': + $this->grantPermissionsToTestedRole(['administer url aliases']); + break; + case 'DELETE': + $this->grantPermissionsToTestedRole(['administer url aliases']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $path_alias = PathAlias::create([ + 'path' => '/admin', + 'alias' => '/admin2', + ]); + $path_alias->save(); + return $path_alias; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'id' => [ + [ + 'value' => 1, + ], + ], + 'revision_id' => [ + [ + 'value' => 1, + ], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + 'revision_translation_affected' => [ + [ + 'value' => TRUE, + ], + ], + 'path' => [ + [ + 'value' => '/admin', + ], + ], + 'alias' => [ + [ + 'value' => '/admin2', + ], + ], + 'uuid' => [ + [ + 'value' => $this->entity->uuid(), + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'path' => [ + [ + 'value' => '/admin', + ], + ], + 'alias' => [ + [ + 'value' => '/admin2', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) { + return parent::getExpectedUnauthorizedAccessMessage($method); + } + + switch ($method) { + case 'GET': + return "The 'administer url aliases' permission is required."; + break; + case 'POST': + return "The 'administer url aliases' permission is required."; + break; + case 'PATCH': + return "The 'administer url aliases' permission is required."; + break; + case 'DELETE': + return "The 'administer url aliases' permission is required."; + break; + } + return parent::getExpectedUnauthorizedAccessMessage($method); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return ['user.permissions']; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php new file mode 100644 index 0000000..60c624c --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php @@ -0,0 +1,36 @@ +markTestSkipped(); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlBasicAuthTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlBasicAuthTest.php new file mode 100644 index 0000000..4eef5af --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlBasicAuthTest.php @@ -0,0 +1,46 @@ +markTestSkipped(); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlCookieTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlCookieTest.php new file mode 100644 index 0000000..514d234 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlCookieTest.php @@ -0,0 +1,41 @@ +markTestSkipped(); + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php index 99659af..943be7f 100644 --- a/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php +++ b/core/tests/Drupal/FunctionalTests/Update/UpdatePathTestBase.php @@ -161,7 +161,7 @@ protected function setUp() { // Set the update url. This must be set here rather than in // self::__construct() or the old URL generator will leak additional test // sites. - $this->updateUrl = Url::fromRoute('system.db_update'); + $this->updateUrl = Url::fromRoute('system.db_update', [], ['path_processing' => FALSE]); $this->setupBaseUrl(); diff --git a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php index 16070c0..7d7148f 100644 --- a/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php +++ b/core/tests/Drupal/KernelTests/Core/Command/DbDumpTest.php @@ -93,6 +93,7 @@ protected function setUp() { $this->installEntitySchema('user'); $this->installEntitySchema('file'); $this->installEntitySchema('menu_link_content'); + $this->installEntitySchema('path_alias'); $this->installSchema('system', 'sequences'); // Place some sample config to test for in the export. @@ -107,7 +108,7 @@ protected function setUp() { $account = User::create(['mail' => 'q\'uote$dollar@example.com', 'name' => '$dollar']); $account->save(); - // Create url_alias (this will create 'url_alias'). + // Create a path alias. $this->container->get('path.alias_storage')->save('/user/' . $account->id(), '/user/example'); // Create a cache table (this will create 'cache_discovery'). @@ -132,7 +133,10 @@ protected function setUp() { 'menu_link_content_data', 'sequences', 'sessions', - 'url_alias', + 'path_alias', + 'path_alias_field_data', + 'path_alias_revision', + 'path_alias_field_revision', 'user__roles', 'users', 'users_field_data', diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php index d7b24b4..2e986d1 100644 --- a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php +++ b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php @@ -27,6 +27,7 @@ class AliasStorageTest extends KernelTestBase { protected function setUp() { parent::setUp(); + $this->installEntitySchema('path_alias'); $this->storage = $this->container->get('path.alias_storage'); } diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php index 48d6b7d..1cccc56 100644 --- a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php +++ b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php @@ -7,35 +7,48 @@ use Drupal\Core\Database\Database; use Drupal\Core\Path\AliasManager; use Drupal\Core\Path\AliasWhitelist; +use Drupal\KernelTests\KernelTestBase; /** * Tests path alias CRUD and lookup functionality. * * @group Path */ -class AliasTest extends PathUnitTestBase { +class AliasTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + // The alias whitelist expects that the menu path roots are set by a + // menu router rebuild. + \Drupal::state()->set('router.path_roots', ['user', 'admin']); + + $this->installEntitySchema('path_alias'); + } public function testCRUD() { // Prepare database table. $connection = Database::getConnection(); - $this->fixtures->createTables($connection); // Create Path object. - $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); + $aliasStorage = new AliasStorage($this->container->get('entity_type.manager')); - $aliases = $this->fixtures->sampleUrlAliases(); + $aliases = $this->sampleUrlAliases(); // Create a few aliases foreach ($aliases as $idx => $alias) { $aliasStorage->save($alias['source'], $alias['alias'], $alias['langcode']); - $result = $connection->query('SELECT * FROM {url_alias} WHERE source = :source AND alias= :alias AND langcode = :langcode', [':source' => $alias['source'], ':alias' => $alias['alias'], ':langcode' => $alias['langcode']]); + $result = $connection->query('SELECT * FROM {path_alias_field_data} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'], ':langcode' => $alias['langcode']]); $rows = $result->fetchAll(); $this->assertEqual(count($rows), 1, format_string('Created an entry for %alias.', ['%alias' => $alias['alias']])); // Cache the pid for further tests. - $aliases[$idx]['pid'] = $rows[0]->pid; + $aliases[$idx]['pid'] = $rows[0]->id; } // Load a few aliases @@ -55,7 +68,7 @@ public function testCRUD() { $this->assertEqual($alias['alias'], $fields['original']['alias']); - $result = $connection->query('SELECT pid FROM {url_alias} WHERE source = :source AND alias= :alias AND langcode = :langcode', [':source' => $alias['source'], ':alias' => $alias['alias'] . '_updated', ':langcode' => $alias['langcode']]); + $result = $connection->query('SELECT id FROM {path_alias_field_data} WHERE path = :path AND alias= :alias AND langcode = :langcode', [':path' => $alias['source'], ':alias' => $alias['alias'] . '_updated', ':langcode' => $alias['langcode']]); $pid = $result->fetchField(); $this->assertEqual($pid, $alias['pid'], format_string('Updated entry for pid %pid.', ['%pid' => $pid])); @@ -66,21 +79,47 @@ public function testCRUD() { $pid = $alias['pid']; $aliasStorage->delete(['pid' => $pid]); - $result = $connection->query('SELECT * FROM {url_alias} WHERE pid = :pid', [':pid' => $pid]); + $result = $connection->query('SELECT * FROM {path_alias_field_data} WHERE id = :id', [':id' => $pid]); $rows = $result->fetchAll(); $this->assertEqual(count($rows), 0, format_string('Deleted entry with pid %pid.', ['%pid' => $pid])); } } - public function testLookupPath() { - // Prepare database table. - $connection = Database::getConnection(); - $this->fixtures->createTables($connection); + /** + * Returns an array of URL aliases for testing. + * + * @return array of URL alias definitions. + */ + protected function sampleUrlAliases() { + return [ + [ + 'source' => '/node/1', + 'alias' => '/alias_for_node_1_en', + 'langcode' => 'en', + ], + [ + 'source' => '/node/2', + 'alias' => '/alias_for_node_2_en', + 'langcode' => 'en', + ], + [ + 'source' => '/node/1', + 'alias' => '/alias_for_node_1_fr', + 'langcode' => 'fr', + ], + [ + 'source' => '/node/1', + 'alias' => '/alias_for_node_1_und', + 'langcode' => 'und', + ], + ]; + } + public function testLookupPath() { // Create AliasManager and Path object. $aliasManager = $this->container->get('path.alias_manager'); - $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); + $aliasStorage = new AliasStorage($this->container->get('entity_type.manager')); // Test the situation where the source is the same for multiple aliases. // Start with a language-neutral alias, which we will override. @@ -156,14 +195,10 @@ public function testLookupPath() { * Tests the alias whitelist. */ public function testWhitelist() { - // Prepare database table. - $connection = Database::getConnection(); - $this->fixtures->createTables($connection); - $memoryCounterBackend = new MemoryCounterBackend(); // Create AliasManager and Path object. - $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); + $aliasStorage = new AliasStorage($this->container->get('entity_type.manager')); $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage); $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend); @@ -220,14 +255,10 @@ public function testWhitelist() { * Tests situation where the whitelist cache is deleted mid-request. */ public function testWhitelistCacheDeletionMidRequest() { - // Prepare database table. - $connection = Database::getConnection(); - $this->fixtures->createTables($connection); - $memoryCounterBackend = new MemoryCounterBackend(); // Create AliasManager and Path object. - $aliasStorage = new AliasStorage($connection, $this->container->get('module_handler')); + $aliasStorage = new AliasStorage($this->container->get('entity_type.manager')); $whitelist = new AliasWhitelist('path_alias_whitelist', $memoryCounterBackend, $this->container->get('lock'), $this->container->get('state'), $aliasStorage); $aliasManager = new AliasManager($aliasStorage, $whitelist, $this->container->get('language_manager'), $memoryCounterBackend); diff --git a/core/tests/Drupal/KernelTests/Core/Path/PathUnitTestBase.php b/core/tests/Drupal/KernelTests/Core/Path/PathUnitTestBase.php deleted file mode 100644 index 671dbc4..0000000 --- a/core/tests/Drupal/KernelTests/Core/Path/PathUnitTestBase.php +++ /dev/null @@ -1,33 +0,0 @@ -fixtures = new UrlAliasFixtures(); - // The alias whitelist expects that the menu path roots are set by a - // menu router rebuild. - \Drupal::state()->set('router.path_roots', ['user', 'admin']); - } - - protected function tearDown() { - $this->fixtures->dropTables(Database::getConnection()); - - parent::tearDown(); - } - -} diff --git a/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php b/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php index 34c6d58..ea23b6f 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php @@ -22,6 +22,15 @@ class ContentNegotiationRoutingTest extends KernelTestBase { /** * {@inheritdoc} */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('path_alias'); + } + + /** + * {@inheritdoc} + */ public function register(ContainerBuilder $container) { parent::register($container); diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php index 713eb3c..fe21802 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -88,6 +88,7 @@ protected function setUp() { $this->cache = new MemoryBackend(); $this->pathProcessor = \Drupal::service('path_processor_manager'); $this->cacheTagsInvalidator = \Drupal::service('cache_tags.invalidator'); + $this->installEntitySchema('path_alias'); } /** diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index dad4c97..dcca1e5 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -548,7 +548,7 @@ public function register(ContainerBuilder $container) { } if ($container->hasDefinition('path_processor_alias')) { - // Prevent the alias-based path processor, which requires a url_alias db + // Prevent the alias-based path processor, which requires a path_alias db // table, from being registered to the path processor manager. We do this // by removing the tags that the compiler pass looks for. This means the // url generator can safely be used within tests.