diff --git a/core/core.services.yml b/core/core.services.yml index f2b22a8780..c1a86ad382 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -953,7 +953,7 @@ services: - { name: event_subscriber } path.alias_storage: class: Drupal\Core\Path\AliasStorage - arguments: ['@database', '@module_handler'] + arguments: ['@database', '@module_handler', '@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 1aca63c6e0..9b80d24632 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -2,12 +2,10 @@ namespace Drupal\Core\Path; -use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\Database\Query\Condition; /** * Provides a class for CRUD operations on path aliases. @@ -21,7 +19,7 @@ class AliasStorage implements AliasStorageInterface { /** * The table for the url_alias storage. */ - const TABLE = 'url_alias'; + const TABLE = 'path_alias'; /** * The database connection. @@ -37,6 +35,20 @@ class AliasStorage implements AliasStorageInterface { */ protected $moduleHandler; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The storage handler for path_alias entities. + * + * @var \Drupal\Core\Path\PathAliasStorageInterface + */ + protected $pathAliasEntityStorage; + /** * Constructs a Path CRUD object. * @@ -44,17 +56,19 @@ class AliasStorage implements AliasStorageInterface { * 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) { + public function __construct(Connection $connection, ModuleHandlerInterface $module_handler, EntityTypeManagerInterface $entity_type_manager = NULL) { $this->connection = $connection; $this->moduleHandler = $module_handler; + $this->entityTypeManager = $entity_type_manager ?: \Drupal::entityTypeManager(); } /** * {@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,227 +77,125 @@ 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; - } - } - // 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'; + 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->get('langcode')->value, + ]; + + $path_alias->setPath($source); + $path_alias->setAlias($alias); + $path_alias->set('langcode', $langcode); } 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; - } - $query = $this->connection->update(static::TABLE) - ->fields($fields) - ->condition('pid', $pid); - $pid = $query->execute(); - $fields['pid'] = $pid; - $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->get('langcode')->value, + ]; + + 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(); + // API functions should be able to access all entities regardless of access + // restrictions. Those need to happen on a higher level. + $query->accessCheck(FALSE); 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->get('langcode')->value, + ]; } + + return FALSE; } /** * {@inheritdoc} */ public function delete($conditions) { - $path = $this->load($conditions); - $query = $this->connection->delete(static::TABLE); + $query = $this->getPathAliasEntityStorage()->getQuery(); + // API functions should be able to access all entities regardless of access + // restrictions. Those need to happen on a higher level. + $query->accessCheck(FALSE); 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); } /** @@ -295,7 +207,7 @@ public function aliasExists($alias, $langcode, $source = NULL) { ->condition('alias', $this->connection->escapeLike($alias), 'LIKE') ->condition('langcode', $langcode); if (!empty($source)) { - $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE'); + $query->condition('path', $this->connection->escapeLike($source), 'NOT LIKE'); } $query->addExpression('1'); $query->range(0, 1); @@ -313,7 +225,7 @@ public function aliasExists($alias, $langcode, $source = NULL) { */ 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(); + return (bool) $this->connection->queryRange('SELECT 1 FROM {' . static::TABLE . '} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField(); } catch (\Exception $e) { $this->catchException($e); @@ -350,103 +262,23 @@ public function getAliasesForAdminListing($header, $keys = NULL) { * {@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; - } + return $this->getPathAliasEntityStorage()->pathHasMatchingAlias($initial_substring); } /** - * 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 (DatabaseException $e) { - return TRUE; - } - return FALSE; - } - - /** - * Act on an exception when url_alias might be stale. - * - * 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. + * Returns the path alias entity storage handler. * - * @param $e - * The exception. + * We can not store it in the constructor because that leads to a circular + * dependency in the service container. * - * @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 0000000000..9f1c4b433f --- /dev/null +++ b/core/lib/Drupal/Core/Path/Entity/PathAlias.php @@ -0,0 +1,144 @@ +setLabel(new TranslatableMarkup('System path')) + ->setDescription(new TranslatableMarkup('The path that this alias belongs to.')) + ->setRequired(TRUE) + ->setDefaultValue('') + ->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('An alias used with this path.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->addPropertyConstraints('value', [ + 'Regex' => [ + 'pattern' => '/^\//i', + 'message' => new TranslatableMarkup('The alias path has to start with a slash.'), + ], + ]); + + // The language code of a path alias should not be able to change between + // revisions because path aliases are not translatable. + $fields['langcode'] + ->setRevisionable(FALSE) + ->setDefaultValue(LanguageInterface::LANGCODE_NOT_SPECIFIED); + + return $fields; + } + + /** + * {@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 = rtrim(trim(trim($this->getAlias()), ''), "\\/"); + $this->setAlias($alias); + } + + /** + * {@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 label() { + return $this->getAlias(); + } + + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + return ['route_match']; + } + +} diff --git a/core/lib/Drupal/Core/Path/PathAliasInterface.php b/core/lib/Drupal/Core/Path/PathAliasInterface.php new file mode 100644 index 0000000000..73d2b4569f --- /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()->invokeAllDeprecated("It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_{$hook}() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865.", 'path_' . $hook, [$values]); + } + } + + /** + * {@inheritdoc} + */ + public function preloadPathAlias($preloaded, $langcode) { + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + $select = $this->database->select($this->baseTable) + ->fields($this->baseTable, ['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) { + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->database->select($this->baseTable) + ->fields($this->baseTable, ['alias']) + ->condition('path', $this->database->escapeLike($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($alias, $langcode) { + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + + // See the queries above. Use LIKE for case-insensitive matching. + $select = $this->database->select($this->baseTable, 'pa'); + $select->addField('pa', 'path', 'source'); + $select->condition('alias', $this->database->escapeLike($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 pathHasMatchingAlias($initial_substring) { + $query = $this->database->select($this->baseTable); + $query->addExpression(1); + + return (bool) $query + ->condition('path', $this->database->escapeLike($initial_substring) . '%', 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function createWithSampleValues($bundle = FALSE, array $values = []) { + $entity = parent::createWithSampleValues($bundle, ['path' => '/'] + $values); + $entity->set('alias', '/' . $entity->get('alias')->value); + return $entity; + } + +} diff --git a/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php b/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php new file mode 100644 index 0000000000..4109d2522f --- /dev/null +++ b/core/lib/Drupal/Core/Path/PathAliasStorageInterface.php @@ -0,0 +1,71 @@ +storage->getBaseTable()]['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/Path/Plugin/Validation/Constraint/UniquePathAliasConstraint.php b/core/lib/Drupal/Core/Path/Plugin/Validation/Constraint/UniquePathAliasConstraint.php new file mode 100644 index 0000000000..40a25088dd --- /dev/null +++ b/core/lib/Drupal/Core/Path/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() + ->accessCheck(FALSE) + ->condition('alias', $alias, '=') + ->condition('langcode', $langcode, '='); + + if (!$entity->isNew()) { + $query->condition('id', $entity->id(), '<>'); + } + if ($path) { + $query->condition('path', $path, '<>'); + } + + if ($result = $query->range(0, 1)->execute()) { + $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/lib/Drupal/Core/Path/Plugin/Validation/Constraint/ValidPathConstraint.php b/core/lib/Drupal/Core/Path/Plugin/Validation/Constraint/ValidPathConstraint.php new file mode 100644 index 0000000000..5cb8373d08 --- /dev/null +++ b/core/lib/Drupal/Core/Path/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/jsonapi/tests/src/Functional/PathAliasTest.php b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php new file mode 100644 index 0000000000..b477144519 --- /dev/null +++ b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php @@ -0,0 +1,117 @@ +grantPermissionsToTestedRole(['administer url aliases']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $path_alias = PathAlias::create([ + 'alias' => '/frontpage1', + 'path' => '/', + 'langcode' => 'en', + ]); + $path_alias->save(); + return $path_alias; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedDocument() { + $self_url = Url::fromUri('base:/jsonapi/path_alias/path_alias/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl(); + return [ + 'jsonapi' => [ + 'meta' => [ + 'links' => [ + 'self' => ['href' => 'http://jsonapi.org/format/1.0/'], + ], + ], + 'version' => '1.0', + ], + 'links' => [ + 'self' => ['href' => $self_url], + ], + 'data' => [ + 'id' => $this->entity->uuid(), + 'type' => 'path_alias--path_alias', + 'links' => [ + 'self' => ['href' => $self_url], + ], + 'attributes' => [ + 'alias' => '/frontpage1', + 'path' => '/', + 'langcode' => 'en', + 'drupal_internal__id' => 1, + 'drupal_internal__revision_id' => 1, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getPostDocument() { + return [ + 'data' => [ + 'type' => 'path_alias--path_alias', + 'attributes' => [ + 'alias' => '/frontpage1', + 'path' => '/', + 'langcode' => 'en', + ], + ], + ]; + } + +} diff --git a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php index 6257adb4d8..bc9d82a72c 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -2339,7 +2339,7 @@ public function testPatchIndividual() { // Ensure that PATCHing an entity that is not the latest revision is // unsupported. - if (!$this->entity->getEntityType()->isRevisionable() || !$this->entity instanceof FieldableEntityInterface) { + if (!$this->entity->getEntityType()->isRevisionable() || !$this->entity->getEntityType()->hasHandlerClass('moderation') || !$this->entity instanceof FieldableEntityInterface) { return; } assert($this->entity instanceof RevisionableInterface); diff --git a/core/modules/locale/tests/src/Functional/LocalePathTest.php b/core/modules/locale/tests/src/Functional/LocalePathTest.php index 16cc26865e..074a82c47d 100644 --- a/core/modules/locale/tests/src/Functional/LocalePathTest.php +++ b/core/modules/locale/tests/src/Functional/LocalePathTest.php @@ -69,18 +69,18 @@ public function testPathLanguageConfiguration() { $path = 'admin/config/search/path/add'; $english_path = $this->randomMachineName(8); $edit = [ - 'source' => '/node/' . $node->id(), - 'alias' => '/' . $english_path, - 'langcode' => 'en', + 'path[0][value]' => '/node/' . $node->id(), + 'alias[0][value]' => '/' . $english_path, + 'langcode[0][value]' => 'en', ]; $this->drupalPostForm($path, $edit, t('Save')); // Create a path alias in new custom language. $custom_language_path = $this->randomMachineName(8); $edit = [ - 'source' => '/node/' . $node->id(), - 'alias' => '/' . $custom_language_path, - 'langcode' => $langcode, + 'path[0][value]' => '/node/' . $node->id(), + 'alias[0][value]' => '/' . $custom_language_path, + 'langcode[0][value]' => $langcode, ]; $this->drupalPostForm($path, $edit, t('Save')); @@ -97,17 +97,22 @@ public function testPathLanguageConfiguration() { // Check priority of language for alias by source path. $edit = [ - 'source' => '/node/' . $node->id(), - 'alias' => '/' . $custom_path, - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'path[0][value]' => '/node/' . $node->id(), + 'alias[0][value]' => '/' . $custom_path, + 'langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED, ]; - $this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']); + $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']); $lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), 'en'); $this->assertEqual('/' . $english_path, $lookup_path, 'English language alias has priority.'); // Same check for language 'xx'. $lookup_path = $this->container->get('path.alias_manager')->getAliasByPath('/node/' . $node->id(), $prefix); $this->assertEqual('/' . $custom_language_path, $lookup_path, 'Custom language alias has priority.'); - $this->container->get('path.alias_storage')->delete($edit); + $path_alias = [ + 'path' => $edit['path[0][value]'], + 'alias' => $edit['alias[0][value]'], + 'langcode' => $edit['langcode[0][value]'], + ]; + $this->container->get('path.alias_storage')->delete($path_alias); // Create language nodes to check priority of aliases. $first_node = $this->drupalCreateNode(['type' => 'page', 'promote' => 1, 'langcode' => 'en']); @@ -115,20 +120,20 @@ public function testPathLanguageConfiguration() { // Assign a custom path alias to the first node with the English language. $edit = [ - 'source' => '/node/' . $first_node->id(), - 'alias' => '/' . $custom_path, - 'langcode' => $first_node->language()->getId(), + 'path[0][value]' => '/node/' . $first_node->id(), + 'alias[0][value]' => '/' . $custom_path, + 'langcode[0][value]' => $first_node->language()->getId(), ]; - $this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']); + $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']); // Assign a custom path alias to second node with // LanguageInterface::LANGCODE_NOT_SPECIFIED. $edit = [ - 'source' => '/node/' . $second_node->id(), - 'alias' => '/' . $custom_path, - 'langcode' => $second_node->language()->getId(), + 'path[0][value]' => '/node/' . $second_node->id(), + 'alias[0][value]' => '/' . $custom_path, + 'langcode[0][value]' => $second_node->language()->getId(), ]; - $this->container->get('path.alias_storage')->save($edit['source'], $edit['alias'], $edit['langcode']); + $this->container->get('path.alias_storage')->save($edit['path[0][value]'], $edit['alias[0][value]'], $edit['langcode[0][value]']); // Test that both node titles link to our path alias. $this->drupalGet('admin/content'); diff --git a/core/modules/menu_link_content/menu_link_content.install b/core/modules/menu_link_content/menu_link_content.install index 8a04d02859..8a881a5666 100644 --- a/core/modules/menu_link_content/menu_link_content.install +++ b/core/modules/menu_link_content/menu_link_content.install @@ -9,8 +9,8 @@ * Implements hook_install(). */ function menu_link_content_install() { - // Add a higher weight so that menu_link_content_path_update() is called after - // system_path_update() clears the path alias cache. + // Add a higher weight so that menu_link_content_path_alias_update() is called + // after system_path_alias_update() clears the path alias cache. // @todo remove this when the cache clearing is moved to path module or if // caching is removed for path aliases due to // https://www.drupal.org/node/1965074 diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module index 559405dfbe..c2c53cac9a 100644 --- a/core/modules/menu_link_content/menu_link_content.module +++ b/core/modules/menu_link_content/menu_link_content.module @@ -50,10 +50,11 @@ function menu_link_content_menu_delete(MenuInterface $menu) { } /** - * Implements hook_path_insert(). + * Implements hook_ENTITY_TYPE_insert() for 'path_alias'. */ -function menu_link_content_path_insert($path) { - _menu_link_content_update_path_alias($path['alias']); +function menu_link_content_path_alias_insert(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + _menu_link_content_update_path_alias($entity->getAlias()); } /** @@ -75,23 +76,25 @@ function _menu_link_content_update_path_alias($path) { } /** - * Implements hook_path_update(). + * Implements hook_ENTITY_TYPE_update() for 'path_alias'. */ -function menu_link_content_path_update($path) { - if ($path['alias'] != $path['original']['alias']) { - _menu_link_content_update_path_alias($path['alias']); - _menu_link_content_update_path_alias($path['original']['alias']); +function menu_link_content_path_alias_update(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + if ($entity->getAlias() != $entity->original->getAlias()) { + _menu_link_content_update_path_alias($entity->getAlias()); + _menu_link_content_update_path_alias($entity->original->getAlias()); } - elseif ($path['source'] != $path['original']['source']) { - _menu_link_content_update_path_alias($path['alias']); + elseif ($entity->getPath() != $entity->original->getPath()) { + _menu_link_content_update_path_alias($entity->getAlias()); } } /** - * Implements hook_path_delete(). + * Implements hook_ENTITY_TYPE_delete() for 'path_alias'. */ -function menu_link_content_path_delete($path) { - _menu_link_content_update_path_alias($path['alias']); +function menu_link_content_path_alias_delete(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + _menu_link_content_update_path_alias($entity->getAlias()); } /** 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 104f356cf6..c0cdba31b8 100644 --- a/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php +++ b/core/modules/menu_link_content/tests/src/Kernel/PathAliasMenuLinkContentTest.php @@ -28,6 +28,7 @@ protected function setUp() { $this->installEntitySchema('user'); $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/tests/src/Traits/CreateTestContentEntitiesTrait.php b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php index 8896222f93..3f1dc9b4b4 100644 --- a/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php +++ b/core/modules/migrate_drupal/tests/src/Traits/CreateTestContentEntitiesTrait.php @@ -42,6 +42,7 @@ protected function installEntitySchemas() { $this->installEntitySchema('file'); $this->installEntitySchema('menu_link_content'); $this->installEntitySchema('node'); + $this->installEntitySchema('path_alias'); $this->installEntitySchema('taxonomy_term'); $this->installEntitySchema('user'); } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php index 046b4cecfe..0731f12aff 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/Upgrade6Test.php @@ -75,7 +75,7 @@ protected function getEntityCounts() { 'file' => 7, 'filter_format' => 7, 'image_style' => 5, - 'language_content_settings' => 14, + 'language_content_settings' => 15, 'node' => 18, // The 'book' module provides the 'book' node type, and the migration // creates 12 node types. @@ -86,6 +86,7 @@ protected function getEntityCounts() { 'shortcut_set' => 1, 'action' => 25, 'menu' => 8, + 'path_alias' => 8, 'taxonomy_term' => 15, 'taxonomy_vocabulary' => 7, 'tour' => 5, diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php index 4e7e16c4e4..75a8c1b94c 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/Upgrade7Test.php @@ -79,7 +79,7 @@ protected function getEntityCounts() { 'file' => 3, 'filter_format' => 7, 'image_style' => 6, - 'language_content_settings' => 17, + 'language_content_settings' => 18, 'node' => 6, 'node_type' => 6, 'rdf_mapping' => 8, @@ -90,6 +90,7 @@ protected function getEntityCounts() { 'menu' => 6, 'taxonomy_term' => 24, 'taxonomy_vocabulary' => 7, + 'path_alias' => 8, 'tour' => 5, 'user' => 4, 'user_role' => 3, 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 0000000000..e7c75a72a8 --- /dev/null +++ b/core/modules/path/config/optional/language.content_settings.path_alias.path_alias.yml @@ -0,0 +1,8 @@ +langcode: en +status: true +dependencies: { } +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.api.php b/core/modules/path/path.api.php index 5df539b52c..2df3d2542b 100644 --- a/core/modules/path/path.api.php +++ b/core/modules/path/path.api.php @@ -17,7 +17,10 @@ * The array structure is identical to that of the return value of * \Drupal\Core\Path\AliasStorageInterface::save(). * - * @see \Drupal\Core\Path\AliasStorageInterface::save() + * @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use + * hook_path_alias_insert() instead. + * + * @see https://www.drupal.org/node/3013865 */ function hook_path_insert($path) { \Drupal::database()->insert('mytable') @@ -35,7 +38,10 @@ function hook_path_insert($path) { * The array structure is identical to that of the return value of * \Drupal\Core\Path\AliasStorageInterface::save(). * - * @see \Drupal\Core\Path\AliasStorageInterface::save() + * @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use + * hook_path_alias_update() instead. + * + * @see https://www.drupal.org/node/3013865 */ function hook_path_update($path) { if ($path['alias'] != $path['original']['alias']) { @@ -53,7 +59,10 @@ function hook_path_update($path) { * The array structure is identical to that of the return value of * \Drupal\Core\Path\AliasStorageInterface::save(). * - * @see \Drupal\Core\Path\AliasStorageInterface::delete() + * @deprecated in drupal:8.8.0 and will be removed from drupal:9.0.0. Use + * hook_path_alias_delete() instead. + * + * @see https://www.drupal.org/node/3013865 */ function hook_path_delete($path) { \Drupal::database()->delete('mytable') diff --git a/core/modules/path/path.info.yml b/core/modules/path/path.info.yml index ba2c759c99..7862684aa8 100644 --- a/core/modules/path/path.info.yml +++ b/core/modules/path/path.info.yml @@ -4,4 +4,4 @@ description: 'Allows users to rename URLs.' package: Core version: VERSION core: 8.x -configure: path.admin_overview +configure: entity.path_alias.collection diff --git a/core/modules/path/path.links.action.yml b/core/modules/path/path.links.action.yml index 9c58984807..985482ec31 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 4f394a055f..549a4eb929 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 bc59857a4a..e9328fe87e 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 f344c74b6e..24d3f7304b 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -6,10 +6,15 @@ */ use Drupal\Core\Url; +use Drupal\Core\Entity\ContentEntityDeleteForm; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\path\PathAliasForm; /** * Implements hook_help(). @@ -23,20 +28,66 @@ function path_help($route_name, RouteMatchInterface $route_match) { $output .= '

' . t('Uses') . '

'; $output .= '
'; $output .= '
' . t('Creating aliases') . '
'; - $output .= '
' . t('If you create or edit a taxonomy term you can add an alias (for example music/jazz) in the field "URL alias". When creating or editing content you can add an alias (for example about-us/team) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page URL aliases. To add aliases a user needs the permission Create and edit URL aliases.', [':aliases' => Url::fromRoute('path.admin_overview')->toString(), ':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-path'])->toString()]) . '
'; + $output .= '
' . t('If you create or edit a taxonomy term you can add an alias (for example music/jazz) in the field "URL alias". When creating or editing content you can add an alias (for example about-us/team) under the section "URL path settings" in the field "URL alias". Aliases for any other path can be added through the page URL aliases. To add aliases a user needs the permission Create and edit URL aliases.', [':aliases' => Url::fromRoute('entity.path_alias.collection')->toString(), ':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-path'])->toString()]) . '
'; $output .= '
' . t('Managing aliases') . '
'; - $output .= '
' . t('The Path module provides a way to search and view a list of all aliases that are in use on your website. Aliases can be added, edited and deleted through this list.', [':aliases' => Url::fromRoute('path.admin_overview')->toString()]) . '
'; + $output .= '
' . t('The Path module provides a way to search and view a list of all aliases that are in use on your website. Aliases can be added, edited and deleted through this list.', [':aliases' => Url::fromRoute('entity.path_alias.collection')->toString()]) . '
'; $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.') . '

'; } } +/** + * Implements hook_entity_type_alter(). + */ +function path_entity_type_alter(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['path_alias']->setFormClass('default', PathAliasForm::class); + $entity_types['path_alias']->setFormClass('delete', ContentEntityDeleteForm::class); + $entity_types['path_alias']->setHandlerClass('route_provider', ['html' => AdminHtmlRouteProvider::class]); + $entity_types['path_alias']->setLinkTemplate('collection', '/admin/config/search/path'); + $entity_types['path_alias']->setLinkTemplate('add-form', '/admin/config/search/path/add'); + $entity_types['path_alias']->setLinkTemplate('edit-form', '/admin/config/search/path/edit/{path_alias}'); + $entity_types['path_alias']->setLinkTemplate('delete-form', '/admin/config/search/path/delete/{path_alias}'); +} + +/** + * Implements hook_entity_base_field_info_alter(). + */ +function path_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) { + /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ + if ($entity_type->id() === 'path_alias') { + $fields['langcode']->setDisplayOptions('form', [ + 'type' => 'language_select', + 'weight' => 0, + 'settings' => [ + 'include_locked' => FALSE, + ], + ]); + + $fields['path']->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => 5, + 'settings' => [ + 'size' => 45, + ], + ]); + + $fields['alias']->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => 10, + 'settings' => [ + 'size' => 45, + ], + ]); + } +} + /** * Implements hook_entity_base_field_info(). */ @@ -69,3 +120,33 @@ function path_entity_translation_create(ContentEntityInterface $translation) { } } } + +/** + * Implements hook_field_widget_form_alter(). + */ +function path_field_widget_form_alter(&$element, 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.post_update.php b/core/modules/path/path.post_update.php new file mode 100644 index 0000000000..b11ec8f935 --- /dev/null +++ b/core/modules/path/path.post_update.php @@ -0,0 +1,23 @@ +getEntityType('language_content_settings')) { + ContentLanguageSettings::loadByEntityTypeBundle('path_alias', 'path_alias') + ->setDefaultLangcode(LanguageInterface::LANGCODE_NOT_SPECIFIED) + ->setLanguageAlterable(TRUE) + ->trustData() + ->save(); + } +} diff --git a/core/modules/path/path.routing.yml b/core/modules/path/path.routing.yml index 96345415c1..a4005ce231 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 6db3db647a..3d73be6a77 100644 --- a/core/modules/path/src/Controller/PathController.php +++ b/core/modules/path/src/Controller/PathController.php @@ -85,12 +85,12 @@ public function adminOverview(Request $request) { $row = []; // @todo Should Path module store leading slashes? See // https://www.drupal.org/node/2430593. - $row['data']['alias'] = Link::fromTextAndUrl(Unicode::truncate($data->alias, 50, FALSE, TRUE), Url::fromUserInput($data->source, [ + $row['data']['alias'] = Link::fromTextAndUrl(Unicode::truncate($data->alias, 50, FALSE, TRUE), Url::fromUserInput($data->path, [ 'attributes' => ['title' => $data->alias], ]))->toString(); - $row['data']['source'] = Link::fromTextAndUrl(Unicode::truncate($data->source, 50, FALSE, TRUE), Url::fromUserInput($data->source, [ + $row['data']['source'] = Link::fromTextAndUrl(Unicode::truncate($data->path, 50, FALSE, TRUE), Url::fromUserInput($data->path, [ 'alias' => TRUE, - 'attributes' => ['title' => $data->source], + 'attributes' => ['title' => $data->path], ]))->toString(); if ($multilanguage) { $row['data']['language_name'] = $this->languageManager()->getLanguageName($data->langcode); @@ -99,11 +99,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->id], ['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->id], ['query' => $destination]), ]; $row['data']['operations'] = [ 'data' => [ @@ -114,7 +114,7 @@ public function adminOverview(Request $request) { // If the system path maps to a different URL alias, highlight this table // row to let the user know of old aliases. - if ($data->alias != $this->aliasManager->getAliasByPath($data->source, $data->langcode)) { + if ($data->alias != $this->aliasManager->getAliasByPath($data->path, $data->langcode)) { $row['class'] = ['warning']; } @@ -125,7 +125,7 @@ public function adminOverview(Request $request) { '#type' => 'table', '#header' => $header, '#rows' => $rows, - '#empty' => $this->t('No URL aliases available. Add URL alias.', [':link' => Url::fromRoute('path.admin_add')->toString()]), + '#empty' => $this->t('No URL aliases available. Add URL alias.', [':link' => Url::fromRoute('entity.path_alias.add_form')->toString()]), ]; $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 59db6f9c19..0000000000 --- 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 aac413f2d7..0000000000 --- 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 9638b57ab5..0000000000 --- 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/PathFilterForm.php b/core/modules/path/src/Form/PathFilterForm.php index 52b0ffbb94..93b5af8163 100644 --- a/core/modules/path/src/Form/PathFilterForm.php +++ b/core/modules/path/src/Form/PathFilterForm.php @@ -65,7 +65,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * Resets the filter selections. */ public function resetForm(array &$form, FormStateInterface $form_state) { - $form_state->setRedirect('path.admin_overview'); + $form_state->setRedirect('entity.path_alias.collection'); } } diff --git a/core/modules/path/src/Form/PathFormBase.php b/core/modules/path/src/Form/PathFormBase.php deleted file mode 100644 index 4b1cbd5912..0000000000 --- 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 0000000000..6a44309110 --- /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/tests/src/Functional/PathAdminTest.php b/core/modules/path/tests/src/Functional/PathAdminTest.php index a43a1f8de2..02a9762ffd 100644 --- a/core/modules/path/tests/src/Functional/PathAdminTest.php +++ b/core/modules/path/tests/src/Functional/PathAdminTest.php @@ -36,22 +36,22 @@ public function testPathFiltering() { // Create aliases. $alias1 = '/' . $this->randomMachineName(8); $edit = [ - 'source' => '/node/' . $node1->id(), - 'alias' => $alias1, + 'path[0][value]' => '/node/' . $node1->id(), + 'alias[0][value]' => $alias1, ]; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $alias2 = '/' . $this->randomMachineName(8); $edit = [ - 'source' => '/node/' . $node2->id(), - 'alias' => $alias2, + 'path[0][value]' => '/node/' . $node2->id(), + 'alias[0][value]' => $alias2, ]; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $alias3 = '/' . $this->randomMachineName(4) . '/' . $this->randomMachineName(4); $edit = [ - 'source' => '/node/' . $node3->id(), - 'alias' => $alias3, + 'path[0][value]' => '/node/' . $node3->id(), + 'alias[0][value]' => $alias3, ]; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); diff --git a/core/modules/path/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index 41ec662b63..13b52680d1 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 Database::getConnection()->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/Functional/PathLanguageUiTest.php b/core/modules/path/tests/src/Functional/PathLanguageUiTest.php index c76284515b..b2dc2b0759 100644 --- a/core/modules/path/tests/src/Functional/PathLanguageUiTest.php +++ b/core/modules/path/tests/src/Functional/PathLanguageUiTest.php @@ -42,8 +42,8 @@ protected function setUp() { public function testLanguageNeutralUrl() { $name = $this->randomMachineName(8); $edit = []; - $edit['source'] = '/admin/config/search/path'; - $edit['alias'] = '/' . $name; + $edit['path[0][value]'] = '/admin/config/search/path'; + $edit['alias[0][value]'] = '/' . $name; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->drupalGet($name); @@ -56,9 +56,9 @@ public function testLanguageNeutralUrl() { public function testDefaultLanguageUrl() { $name = $this->randomMachineName(8); $edit = []; - $edit['source'] = '/admin/config/search/path'; - $edit['alias'] = '/' . $name; - $edit['langcode'] = 'en'; + $edit['path[0][value]'] = '/admin/config/search/path'; + $edit['alias[0][value]'] = '/' . $name; + $edit['langcode[0][value]'] = 'en'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->drupalGet($name); @@ -71,9 +71,9 @@ public function testDefaultLanguageUrl() { public function testNonDefaultUrl() { $name = $this->randomMachineName(8); $edit = []; - $edit['source'] = '/admin/config/search/path'; - $edit['alias'] = '/' . $name; - $edit['langcode'] = 'fr'; + $edit['path[0][value]'] = '/admin/config/search/path'; + $edit['alias[0][value]'] = '/' . $name; + $edit['langcode[0][value]'] = 'fr'; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->drupalGet('fr/' . $name); @@ -90,14 +90,14 @@ public function testNotSpecifiedNode() { // Create a language-unspecific alias in the admin UI, ensure that is // displayed and the langcode is not changed when saving. $edit = [ - 'source' => '/node/' . $node->id(), - 'alias' => '/' . $this->getRandomGenerator()->word(8), - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'path[0][value]' => '/node/' . $node->id(), + 'alias[0][value]' => '/' . $this->getRandomGenerator()->word(8), + 'langcode[0][value]' => LanguageInterface::LANGCODE_NOT_SPECIFIED, ]; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->drupalGet($node->toUrl('edit-form')); - $this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias']); + $this->assertSession()->fieldValueEquals('path[0][alias]', $edit['alias[0][value]']); $this->drupalPostForm(NULL, [], t('Save')); $this->drupalGet('admin/config/search/path'); 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 fd4ea59f06..db63c41104 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 e0607b96d2..b055e5cbf9 100644 --- a/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php +++ b/core/modules/path/tests/src/Kernel/Migrate/d7/MigrateUrlAliasTest.php @@ -31,6 +31,9 @@ class MigrateUrlAliasTest extends MigrateDrupal7TestBase { 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/PathItemTest.php b/core/modules/path/tests/src/Kernel/PathItemTest.php index 224a8420b3..f001dc6bcf 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 d4b111cf48..da39d3b2cd 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -901,9 +901,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; @@ -988,7 +989,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); @@ -999,7 +1002,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); @@ -1130,9 +1135,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; @@ -1499,8 +1505,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 739089a988..e2b322ac5d 100644 --- a/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php +++ b/core/modules/rest/tests/src/Functional/Update/EntityResourcePermissionsUpdateTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\rest\Functional\Update; use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\rest\RestPermissions; /** * Tests that existing sites continue to use permissions for EntityResource. @@ -33,16 +34,17 @@ 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)); + + // We can not use the 'user.permissions' service here because some + // permissions include generated URLs inside their description, thus + // requiring the path alias system, which is not guaranteed to be working + // before running the database updates. + $rest_permissions_callback = \Drupal::service('controller_resolver')->getControllerFromDefinition(RestPermissions::class . '::permissions'); + $rest_permissions = array_keys(call_user_func($rest_permissions_callback)); + $this->assertEquals([], $rest_permissions); $this->runUpdates(); @@ -50,8 +52,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(RestPermissions::class . '::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 03d4784ad6..af1901565e 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -386,7 +386,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 41fd896dfe..0000000000 --- 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 018d5d003e..99ff7bba58 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -11,8 +11,12 @@ use Drupal\Component\Utility\OpCodeCache; use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\File\FileSystemInterface; -use Drupal\Core\Path\AliasStorage; +use Drupal\Core\Path\Entity\PathAlias; +use Drupal\Core\Path\PathAliasStorage; +use Drupal\Core\Path\PathAliasStorageSchema; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\Core\Database\Database; use Drupal\Core\Entity\ContentEntityTypeInterface; @@ -1240,11 +1244,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; } @@ -2357,3 +2356,32 @@ function system_update_8801() { ->save(TRUE); } } + +/** + * Install the 'path_alias' entity type. + */ +function system_update_8802() { + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + if (!$entity_definition_update_manager->getEntityType('path_alias')) { + $entity_type = new ContentEntityType([ + 'id' => 'path_alias', + 'class' => PathAlias::class, + 'label' => new TranslatableMarkup('Path alias'), + 'handlers' => [ + 'storage' => PathAliasStorage::class, + 'storage_schema' => PathAliasStorageSchema::class, + ], + 'base_table' => 'path_alias', + 'revision_table' => 'path_alias_revision', + 'entity_keys' => [ + 'id' => 'id', + 'revision' => 'revision_id', + 'langcode' => 'langcode', + 'uuid' => 'uuid', + ], + ]); + $entity_definition_update_manager->installEntityType($entity_type); + + return t('The "path_alias" entity type has been installed.'); + } +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index c7b2937684..280d767cae 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -11,6 +11,7 @@ use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Asset\AttachedAssetsInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Queue\QueueGarbageCollectionInterface; @@ -1416,26 +1417,37 @@ function system_block_view_system_main_block_alter(array &$build, BlockPluginInt } /** - * Implements hook_path_update(). + * Implements hook_entity_type_alter() for 'path_alias'. */ -function system_path_update($path) { - $alias_manager = \Drupal::service('path.alias_manager'); - $alias_manager->cacheClear($path['source']); - $alias_manager->cacheClear($path['original']['source']); +function system_entity_type_alter(array &$entity_types) { + // Modration for 'path_alias' entity is not supported. + $entity_types['path_alias']->setHandlerClass('moderation', ''); +} + +/** + * Implements hook_ENTITY_TYPE_insert() for 'path_alias'. + */ +function system_path_alias_insert(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + \Drupal::service('path.alias_manager')->cacheClear($entity->getPath()); } /** - * Implements hook_path_insert(). + * Implements hook_ENTITY_TYPE_update() for 'path_alias'. */ -function system_path_insert($path) { - \Drupal::service('path.alias_manager')->cacheClear($path['source']); +function system_path_alias_update(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + $alias_manager = \Drupal::service('path.alias_manager'); + $alias_manager->cacheClear($entity->getPath()); + $alias_manager->cacheClear($entity->original->getPath()); } /** - * Implements hook_path_delete(). + * Implements hook_ENTITY_TYPE_delete() for 'path_alias'. */ -function system_path_delete($path) { - \Drupal::service('path.alias_manager')->cacheClear($path['source']); +function system_path_alias_delete(EntityInterface $entity) { + /** @var \Drupal\Core\Path\PathAliasInterface $entity */ + \Drupal::service('path.alias_manager')->cacheClear($entity->getPath()); } /** diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index 5f6c9f7663..f313e5e8c6 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Site\Settings; /** * Re-save all configuration entities to recalculate dependencies. @@ -213,3 +214,57 @@ function system_post_update_clear_menu_cache() { function system_post_update_layout_plugin_schema_change() { // Empty post-update hook. } + +/** + * Convert path aliases to entities. + */ +function system_post_update_convert_path_aliases_to_entities(&$sandbox = NULL) { + $database = \Drupal::database(); + + if (!isset($sandbox['current_id'])) { + // This must be the first run. Initialize the sandbox. + $sandbox['progress'] = 0; + $sandbox['current_id'] = 0; + } + + $step_size = Settings::get('entity_update_batch_size', 50); + $url_aliases = $database->select('url_alias', 't') + ->condition('t.pid', $sandbox['current_id'], '>') + ->fields('t') + ->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, + 'revision_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_id'] = $url_alias->pid; + } + + // If we're not in maintenance mode, the number of path aliases could change + // at any time so make sure that we always use the latest record count. + $missing = $database->select('url_alias', 't') + ->condition('t.pid', $sandbox['current_id'], '>') + ->orderBy('pid', 'ASC') + ->countQuery() + ->execute() + ->fetchField(); + $sandbox['#finished'] = $missing ? $sandbox['progress'] / ($sandbox['progress'] + (int) $missing) : 1; + + 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/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php b/core/modules/system/tests/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php new file mode 100644 index 0000000000..8f7cb770ce --- /dev/null +++ b/core/modules/system/tests/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php @@ -0,0 +1,46 @@ +insert('url_alias') +->fields([ + 'pid', + 'source', + 'alias', + 'langcode', +]) +->values([ + 'pid' => '2', + 'source' => '/node/1', + 'alias' => '/test-article-new-alias', + 'langcode' => 'und', +]) +->values([ + 'pid' => '3', + 'source' => '/node/8', + 'alias' => '/test-alias-for-any-language', + 'langcode' => 'und', +]) +->values([ + 'pid' => '4', + 'source' => '/node/8', + 'alias' => '/test-alias-in-english', + 'langcode' => 'en', +]) +->values([ + 'pid' => '5', + 'source' => '/node/8', + 'alias' => '/test-alias-in-spanish', + 'langcode' => 'es', +]) +->execute(); diff --git a/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.info.yml b/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.info.yml new file mode 100644 index 0000000000..15b9357e74 --- /dev/null +++ b/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.info.yml @@ -0,0 +1,6 @@ +name: 'Path deprecated test' +type: module +description: 'Support module for testing deprecated functionality for path aliases.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.module b/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.module new file mode 100644 index 0000000000..30b9b91b7d --- /dev/null +++ b/core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.module @@ -0,0 +1,27 @@ +set('path_test.results', []); -} - -/** - * Implements hook_path_update(). - */ -function path_test_path_update($path) { - $results = \Drupal::state()->get('path_test.results') ?: []; - $results['hook_path_update'] = $path; - \Drupal::state()->set('path_test.results', $results); -} diff --git a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php index 764d3892bc..8a79fd7c4b 100644 --- a/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php +++ b/core/modules/system/tests/src/Functional/Path/UrlAlterFunctionalTest.php @@ -26,8 +26,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. @@ -50,7 +50,7 @@ public function testUrlAlter() { $this->assertUrlOutboundAlter("/user/$uid/test1", '/alias/test1'); // Test adding an alias via the UI. - $edit = ['source' => "/user/$uid/edit", 'alias' => '/alias/test2']; + $edit = ['path[0][value]' => "/user/$uid/edit", 'alias[0][value]' => '/alias/test2']; $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save')); $this->assertText(t('The alias has been saved.')); $this->drupalGet('alias/test2'); diff --git a/core/modules/system/tests/src/Functional/Update/PathAliasToEntityUpdateTest.php b/core/modules/system/tests/src/Functional/Update/PathAliasToEntityUpdateTest.php new file mode 100644 index 0000000000..d5d0a094f8 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/PathAliasToEntityUpdateTest.php @@ -0,0 +1,80 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../tests/fixtures/update/drupal-8.filled.standard.php.gz', + __DIR__ . '/../../../../tests/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php', + ]; + } + + /** + * Tests the conversion of path aliases to entities. + * + * @see system_update_8701() + * @see system_post_update_convert_path_aliases_to_entities() + */ + public function testConversionToEntities() { + $database = \Drupal::database(); + $schema = $database->schema(); + $this->assertTrue($schema->tableExists('url_alias')); + + // drupal-8.filled.standard.php.gz contains one URL alias and + // drupal-8.convert-path-aliases-to-entities-2336597.php adds another four. + $url_alias_count = 5; + + $this->runUpdates(); + + // Check that the 'path_alias' entity tables have been created and the + // 'url_alias' table has been deleted. + $this->assertTrue($schema->tableExists('path_alias')); + $this->assertTrue($schema->tableExists('path_alias_revision')); + $this->assertFalse($schema->tableExists('url_alias')); + + $path_alias_count = \Drupal::entityTypeManager()->getStorage('path_alias')->loadMultiple(); + $this->assertCount($url_alias_count, $path_alias_count); + + // Make sure that existing aliases still work. + $assert_session = $this->assertSession(); + $this->drupalGet('test-article'); + $assert_session->responseContains('/node/1'); + $assert_session->pageTextContains('Test Article - New title'); + + $this->drupalGet('test-article-new-alias'); + $assert_session->responseContains('/node/1'); + $assert_session->pageTextContains('Test Article - New title'); + + $this->drupalGet('test-alias-for-any-language'); + $assert_session->responseContains('/node/8'); + $assert_session->pageTextContains('Test title'); + + $this->drupalGet('test-alias-in-english'); + $assert_session->responseContains('/node/8'); + $assert_session->pageTextContains('Test title'); + + $spanish = \Drupal::languageManager()->getLanguage('es'); + + $this->drupalGet('test-alias-for-any-language', ['language' => $spanish]); + $assert_session->responseContains('/es/node/8'); + $assert_session->pageTextContains('Test title Spanish'); + + $this->drupalGet('test-alias-in-spanish', ['language' => $spanish]); + $assert_session->responseContains('/es/node/8'); + $assert_session->pageTextContains('Test title Spanish'); + } + +} diff --git a/core/modules/system/tests/src/Kernel/DeprecatedPathHooksTest.php b/core/modules/system/tests/src/Kernel/DeprecatedPathHooksTest.php new file mode 100644 index 0000000000..75f3ae4a7f --- /dev/null +++ b/core/modules/system/tests/src/Kernel/DeprecatedPathHooksTest.php @@ -0,0 +1,76 @@ +installEntitySchema('path_alias'); + } + + /** + * @covers ::save + * + * @expectedDeprecation The deprecated hook hook_path_insert() is implemented in these functions: path_deprecated_test_path_insert(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_insert() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865. + */ + public function testInsert() { + $source = '/' . $this->randomMachineName(); + $alias = '/' . $this->randomMachineName(); + + $alias_storage = \Drupal::service('path.alias_storage'); + $alias_storage->save($source, $alias); + } + + /** + * @covers ::save + * + * @expectedDeprecation The deprecated hook hook_path_update() is implemented in these functions: path_deprecated_test_path_update(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_update() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865. + */ + public function testUpdate() { + $source = '/' . $this->randomMachineName(); + $alias = '/' . $this->randomMachineName(); + + $alias_storage = \Drupal::service('path.alias_storage'); + $alias_storage->save($source, $alias); + + $new_source = '/' . $this->randomMachineName(); + $path = $alias_storage->load(['source' => $source]); + $alias_storage->save($new_source, $alias, LanguageInterface::LANGCODE_NOT_SPECIFIED, $path['pid']); + } + + /** + * @covers ::delete + * + * @expectedDeprecation The deprecated hook hook_path_delete() is implemented in these functions: path_deprecated_test_path_delete(). It will be removed before Drupal 9.0.0. Use hook_ENTITY_TYPE_delete() for the 'path_alias' entity type instead. See https://www.drupal.org/node/3013865. + */ + public function testDelete() { + $source = '/' . $this->randomMachineName(); + $alias = '/' . $this->randomMachineName(); + + $alias_storage = \Drupal::service('path.alias_storage'); + $alias_storage->save($source, $alias); + + $path = $alias_storage->load(['source' => $source]); + $alias_storage->delete(['pid' => $path['pid']]); + } + +} diff --git a/core/modules/system/tests/src/Kernel/PathHooksTest.php b/core/modules/system/tests/src/Kernel/PathHooksTest.php index 892c7d1abd..0c4638cbad 100644 --- a/core/modules/system/tests/src/Kernel/PathHooksTest.php +++ b/core/modules/system/tests/src/Kernel/PathHooksTest.php @@ -18,13 +18,22 @@ class PathHooksTest extends KernelTestBase { public static $modules = ['system']; /** - * Test system_path_*() correctly clears caches. + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('path_alias'); + } + + /** + * Test system_path_alias_*() correctly clears caches. */ public function testPathHooks() { $source = '/' . $this->randomMachineName(); $alias = '/' . $this->randomMachineName(); - // Check system_path_insert(); + // Check system_path_alias_insert(); $alias_manager = $this->prophesize(AliasManagerInterface::class); $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1); $alias_manager->cacheClear($source)->shouldBeCalledTimes(1); @@ -35,7 +44,7 @@ public function testPathHooks() { $new_source = '/' . $this->randomMachineName(); $path = $alias_storage->load(['source' => $source]); - // Check system_path_update(); + // Check system_path_alias_update(); $alias_manager = $this->prophesize(AliasManagerInterface::class); $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(2); $alias_manager->cacheClear($source)->shouldBeCalledTimes(1); @@ -43,7 +52,7 @@ public function testPathHooks() { \Drupal::getContainer()->set('path.alias_manager', $alias_manager->reveal()); $alias_storage->save($new_source, $alias, LanguageInterface::LANGCODE_NOT_SPECIFIED, $path['pid']); - // Check system_path_delete(); + // Check system_path_alias_delete(); $alias_manager = $this->prophesize(AliasManagerInterface::class); $alias_manager->cacheClear(Argument::any())->shouldBeCalledTimes(1); $alias_manager->cacheClear($new_source)->shouldBeCalledTimes(1); diff --git a/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php b/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php index a9a952670f..f13ab32f85 100644 --- a/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php +++ b/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php @@ -46,7 +46,11 @@ public function testViewsPostUpdateExposedFilterBlocksWithoutBlock() { // the config schema checker ignore the block. static::$configSchemaCheckerExclusions[] = 'block.block.seven_secondary_local_tasks'; - $this->container->get('module_installer')->uninstall(['block']); + // We also need to uninstall the menu_link_content module because + // menu_link_content_entity_predelete() invokes alias processing and we + // don't have a working path alias system until + // system_post_update_convert_path_aliases_to_entities() runs. + $this->container->get('module_installer')->uninstall(['menu_link_content', 'block']); $this->runUpdates(); } diff --git a/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php b/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php new file mode 100644 index 0000000000..5c8c16c6a8 --- /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', + ], + ], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php new file mode 100644 index 0000000000..9febf9eade --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php @@ -0,0 +1,26 @@ +grantPermissionsToTestedRole(['administer url aliases']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $path_alias = PathAlias::create([ + 'path' => '/', + 'alias' => '/frontpage1', + ]); + $path_alias->save(); + return $path_alias; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'id' => [ + [ + 'value' => 1, + ], + ], + 'revision_id' => [ + [ + 'value' => 1, + ], + ], + 'langcode' => [ + [ + 'value' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ], + ], + 'path' => [ + [ + 'value' => '/', + ], + ], + 'alias' => [ + [ + 'value' => '/frontpage1', + ], + ], + 'uuid' => [ + [ + 'value' => $this->entity->uuid(), + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'path' => [ + [ + 'value' => '/', + ], + ], + 'alias' => [ + [ + 'value' => '/frontpage1', + ], + ], + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php new file mode 100644 index 0000000000..e0575a913e --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php @@ -0,0 +1,28 @@ +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'). @@ -134,7 +135,8 @@ protected function setUp() { 'menu_link_content_field_revision', 'sequences', 'sessions', - 'url_alias', + 'path_alias', + 'path_alias_revision', 'user__roles', 'users', 'users_field_data', diff --git a/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php b/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php index b5bcd487f2..0efc69ef14 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php @@ -40,6 +40,7 @@ protected function setUp() { $this->installEntitySchema('file'); $this->installEntitySchema('comment'); $this->installEntitySchema('comment_type'); + $this->installEntitySchema('path_alias'); $this->installEntitySchema('taxonomy_vocabulary'); $this->installEntitySchema('taxonomy_term'); $this->entityTypeManager = $this->container->get('entity_type.manager'); diff --git a/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php b/core/tests/Drupal/KernelTests/Core/Path/AliasStorageTest.php index d7b24b497f..2e986d18b3 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 0892edd09f..bd26e1c6e0 100644 --- a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php +++ b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php @@ -4,39 +4,51 @@ use Drupal\Component\Render\FormattableMarkup; use Drupal\Core\Cache\MemoryCounterBackend; -use Drupal\Core\Path\AliasStorage; 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 = $this->container->get('path.alias_storage'); - $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} 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, new FormattableMarkup('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 @@ -56,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} 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'], new FormattableMarkup('Updated entry for pid %pid.', ['%pid' => $pid])); @@ -67,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} WHERE id = :id', [':id' => $pid]); $rows = $result->fetchAll(); $this->assertEqual(count($rows), 0, new FormattableMarkup('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 = $this->container->get('path.alias_storage'); // Test the situation where the source is the same for multiple aliases. // Start with a language-neutral alias, which we will override. @@ -157,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 = $this->container->get('path.alias_storage'); $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); @@ -221,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 = $this->container->get('path.alias_storage'); $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 671dbc4792..0000000000 --- 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 34c6d5852a..ea23b6f8e6 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/ContentNegotiationRoutingTest.php @@ -19,6 +19,15 @@ class ContentNegotiationRoutingTest extends KernelTestBase { */ public static $modules = ['conneg_test']; + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('path_alias'); + } + /** * {@inheritdoc} */ diff --git a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php index 0c73971122..1719687922 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -87,6 +87,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 e2d1c17f3b..9f3c8109b7 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -539,7 +539,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.