From 099ce08ac838449e7cb037ce4958aec09c0f0609 Mon Sep 17 00:00:00 2001 From: catch Date: Tue, 1 Oct 2019 11:30:19 +0100 Subject: [PATCH] Issue #2336597 by amateescu, slashrsm, jibran, Wim Leers, saki007ster, Berdir, catch, plach, Charlie ChX Negyesi, dawehner, gabesullice: Convert path aliases to full featured entities --- core/core.services.yml | 2 +- core/lib/Drupal/Core/Path/AliasStorage.php | 440 +++++++----------- .../lib/Drupal/Core/Path/Entity/PathAlias.php | 148 ++++++ .../Drupal/Core/Path/PathAliasInterface.php | 48 ++ .../lib/Drupal/Core/Path/PathAliasStorage.php | 50 ++ .../Core/Path/PathAliasStorageSchema.php | 27 ++ .../Core/Update/UpdateServiceProvider.php | 9 +- .../content_moderation/src/EntityTypeInfo.php | 7 +- .../tests/src/Functional/PathAliasTest.php | 112 +++++ .../tests/src/Functional/ResourceTestBase.php | 14 +- .../menu_link_content.install | 4 +- .../menu_link_content.module | 27 +- .../Kernel/PathAliasMenuLinkContentTest.php | 1 + .../Traits/CreateTestContentEntitiesTrait.php | 1 + .../tests/src/Functional/d6/Upgrade6Test.php | 1 + .../tests/src/Functional/d7/Upgrade7Test.php | 1 + core/modules/path/path.api.php | 15 +- .../tests/src/Functional/PathAliasTest.php | 6 +- .../Kernel/Migrate/d6/MigrateUrlAliasTest.php | 1 + .../Kernel/Migrate/d7/MigrateUrlAliasTest.php | 3 + .../path/tests/src/Kernel/PathItemTest.php | 1 + .../EntityResource/EntityResourceTestBase.php | 27 +- .../EntityResourcePermissionsUpdateTest.php | 22 +- .../modules/simpletest/src/KernelTestBase.php | 8 +- .../src/Tests/Path/UrlAliasFixtures.php | 96 ---- core/modules/system/system.install | 125 ++++- core/modules/system/system.module | 23 - ...nvert-path-aliases-to-entities-2336597.php | 46 ++ .../path_deprecated_test.info.yml | 6 + .../path_deprecated_test.module | 27 ++ .../modules/path_test/path_test.info.yml | 6 - .../tests/modules/path_test/path_test.module | 22 - .../Path/UrlAlterFunctionalTest.php | 4 +- .../Update/PathAliasToEntityUpdateTest.php | 103 ++++ .../src/Kernel/DeprecatedPathHooksTest.php | 76 +++ .../system/tests/src/Kernel/PathHooksTest.php | 17 +- .../Update/ExposedFilterBlocksUpdateTest.php | 9 +- .../Hal/PathAliasHalJsonAnonTest.php | 29 ++ .../Hal/PathAliasHalJsonBasicAuthTest.php | 34 ++ .../Hal/PathAliasHalJsonCookieTest.php | 34 ++ .../Hal/PathAliasHalJsonTestBase.php | 56 +++ .../Rest/PathAliasJsonAnonTest.php | 26 ++ .../Rest/PathAliasJsonBasicAuthTest.php | 36 ++ .../Rest/PathAliasJsonCookieTest.php | 31 ++ .../Rest/PathAliasResourceTestBase.php | 119 +++++ .../Rest/PathAliasXmlAnonTest.php | 28 ++ .../Rest/PathAliasXmlBasicAuthTest.php | 38 ++ .../Rest/PathAliasXmlCookieTest.php | 33 ++ .../KernelTests/Core/Command/DbDumpTest.php | 6 +- .../Core/Entity/CreateSampleEntityTest.php | 1 + .../Core/Path/AliasStorageTest.php | 1 + .../KernelTests/Core/Path/AliasTest.php | 78 +++- .../Core/Path/PathUnitTestBase.php | 33 -- .../Routing/ContentNegotiationRoutingTest.php | 9 + .../Core/Routing/RouteProviderTest.php | 1 + .../Drupal/KernelTests/KernelTestBase.php | 8 +- 56 files changed, 1579 insertions(+), 557 deletions(-) create mode 100644 core/lib/Drupal/Core/Path/Entity/PathAlias.php create mode 100644 core/lib/Drupal/Core/Path/PathAliasInterface.php create mode 100644 core/lib/Drupal/Core/Path/PathAliasStorage.php create mode 100644 core/lib/Drupal/Core/Path/PathAliasStorageSchema.php create mode 100644 core/modules/jsonapi/tests/src/Functional/PathAliasTest.php delete mode 100644 core/modules/system/src/Tests/Path/UrlAliasFixtures.php create mode 100644 core/modules/system/tests/fixtures/update/drupal-8.convert-path-aliases-to-entities-2336597.php create mode 100644 core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.info.yml create mode 100644 core/modules/system/tests/modules/path_deprecated_test/path_deprecated_test.module delete mode 100644 core/modules/system/tests/modules/path_test/path_test.info.yml delete mode 100644 core/modules/system/tests/modules/path_test/path_test.module create mode 100644 core/modules/system/tests/src/Functional/Update/PathAliasToEntityUpdateTest.php create mode 100644 core/modules/system/tests/src/Kernel/DeprecatedPathHooksTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonBasicAuthTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonCookieTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonTestBase.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonBasicAuthTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonCookieTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasResourceTestBase.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlBasicAuthTest.php create mode 100644 core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlCookieTest.php delete mode 100644 core/tests/Drupal/KernelTests/Core/Path/PathUnitTestBase.php diff --git a/core/core.services.yml b/core/core.services.yml index a5c1d499301..1cddf59cfed 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -955,7 +955,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 1aca63c6e01..7de344a781e 100644 --- a/core/lib/Drupal/Core/Path/AliasStorage.php +++ b/core/lib/Drupal/Core/Path/AliasStorage.php @@ -2,12 +2,12 @@ namespace Drupal\Core\Path; -use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\DatabaseException; +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\SelectInterface; +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 +21,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 +37,13 @@ class AliasStorage implements AliasStorageInterface { */ protected $moduleHandler; + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + /** * Constructs a Path CRUD object. * @@ -44,17 +51,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 +72,186 @@ class AliasStorage implements AliasStorageInterface { throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias)); } - $fields = [ - 'source' => $source, - 'alias' => $alias, - 'langcode' => $langcode, - ]; + 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, + ]; - // 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'; + $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(); + // Ignore access restrictions for this API. + $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); + elseif ($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); + $storage = $this->getPathAliasEntityStorage(); + $query = $storage->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); + elseif ($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(); + $storage->delete($storage->loadMultiple($result)); } /** * {@inheritdoc} */ public function preloadPathAlias($preloaded, $langcode) { - $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; $select = $this->connection->select(static::TABLE) - ->fields(static::TABLE, ['source', 'alias']); + ->fields(static::TABLE, ['path', 'alias']); if (!empty($preloaded)) { $conditions = new Condition('OR'); foreach ($preloaded as $preloaded_item) { - $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE'); + $conditions->condition('path', $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'); - } + $this->addLanguageFallback($select, $langcode); - $select->orderBy('pid', 'ASC'); - $select->condition('langcode', $langcode_list, 'IN'); - try { - return $select->execute()->fetchAllKeyed(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + // We 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. + $select->orderBy('id', 'ASC'); + + return $select->execute()->fetchAllKeyed(); } /** * {@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'); - } + ->condition('path', $this->connection->escapeLike($path), 'LIKE'); - $select->orderBy('pid', 'DESC'); - $select->condition('langcode', $langcode_list, 'IN'); - try { - return $select->execute()->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + $this->addLanguageFallback($select, $langcode); + + $select->orderBy('id', 'DESC'); + + return $select->execute()->fetchField(); } /** * {@inheritdoc} */ - public function lookupPathSource($path, $langcode) { - $alias = $this->connection->escapeLike($path); - $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; - + public function lookupPathSource($alias, $langcode) { // 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) { + ->fields(static::TABLE, ['path']) + ->condition('alias', $this->connection->escapeLike($alias), 'LIKE'); + + $this->addLanguageFallback($select, $langcode); + + $select->orderBy('id', 'DESC'); + + return $select->execute()->fetchField(); + } + + /** + * Adds path alias language fallback conditions to a select query object. + * + * @param \Drupal\Core\Database\Query\SelectInterface $query + * A Select query object. + * @param string $langcode + * Language code to search the path with. If there's no path defined for + * that language it will search paths without language. + */ + protected function addLanguageFallback(SelectInterface $query, $langcode) { + // 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. + $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED]; + if ($langcode === LanguageInterface::LANGCODE_NOT_SPECIFIED) { array_pop($langcode_list); } elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) { - $select->orderBy('langcode', 'DESC'); + $query->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; + $query->orderBy('langcode', 'ASC'); } + $query->condition('langcode', $langcode_list, 'IN'); } /** @@ -295,30 +263,19 @@ class AliasStorage implements AliasStorageInterface { ->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); - try { - return (bool) $query->execute()->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + + return (bool) $query->execute()->fetchField(); } /** * {@inheritdoc} */ public function languageAliasExists() { - try { - return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField(); - } - catch (\Exception $e) { - $this->catchException($e); - return FALSE; - } + return (bool) $this->connection->queryRange('SELECT 1 FROM {' . static::TABLE . '} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField(); } /** @@ -332,121 +289,42 @@ class AliasStorage implements AliasStorageInterface { // Replace wildcards with PDO wildcards. $query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE'); } - try { - return $query - ->fields(static::TABLE) - ->orderByHeader($header) - ->limit(50) - ->execute() - ->fetchAll(); - } - catch (\Exception $e) { - $this->catchException($e); - return []; - } + + $query->addField(static::TABLE, 'id', 'pid'); + $query->addField(static::TABLE, 'path', 'source'); + return $query + ->fields(static::TABLE, ['alias', 'langcode']) + ->orderByHeader($header) + ->limit(50) + ->execute() + ->fetchAll(); } /** * {@inheritdoc} */ public function pathHasMatchingAlias($initial_substring) { - $query = $this->connection->select(static::TABLE, 'u'); + $query = $this->connection->select(static::TABLE); $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 (bool) $query + ->condition('path', $this->connection->escapeLike($initial_substring) . '%', 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); } /** - * Check if the table exists and create it if not. + * Returns the path alias entity storage handler. + * + * We can not store it in the constructor because that leads to a circular + * dependency in the service container. + * + * @return \Drupal\Core\Entity\EntityStorageInterface + * The path alias entity storage. */ - 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. - * - * @param $e - * The exception. - * - * @throws \Exception - */ - protected function catchException(\Exception $e) { - if ($this->connection->schema()->tableExists(static::TABLE)) { - throw $e; - } - } - - /** - * 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'], - ], - ]; + protected function getPathAliasEntityStorage() { + return $this->entityTypeManager->getStorage('path_alias'); } } 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 00000000000..ce96d61a0a9 --- /dev/null +++ b/core/lib/Drupal/Core/Path/Entity/PathAlias.php @@ -0,0 +1,148 @@ +setLabel(new TranslatableMarkup('System path')) + ->setDescription(new TranslatableMarkup('The path that this alias belongs to.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE); + + $fields['alias'] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('Path alias')) + ->setDescription(new TranslatableMarkup('An alias used with this path.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE); + + $fields['langcode']->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($this->getAlias()), "\\/"); + $this->setAlias($alias); + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + $alias_manager = \Drupal::service('path.alias_manager'); + $alias_manager->cacheClear($this->getPath()); + if ($update) { + $alias_manager->cacheClear($this->original->getPath()); + } + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + + $alias_manager = \Drupal::service('path.alias_manager'); + foreach ($entities as $entity) { + $alias_manager->cacheClear($entity->getPath()); + } + } + + /** + * {@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 00000000000..73d2b4569f6 --- /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 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/PathAliasStorageSchema.php b/core/lib/Drupal/Core/Path/PathAliasStorageSchema.php new file mode 100644 index 00000000000..2d23c85e4fd --- /dev/null +++ b/core/lib/Drupal/Core/Path/PathAliasStorageSchema.php @@ -0,0 +1,27 @@ +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/Update/UpdateServiceProvider.php b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php index 7ac68e6b18e..e26b019470a 100644 --- a/core/lib/Drupal/Core/Update/UpdateServiceProvider.php +++ b/core/lib/Drupal/Core/Update/UpdateServiceProvider.php @@ -36,10 +36,11 @@ class UpdateServiceProvider implements ServiceProviderInterface, ServiceModifier * {@inheritdoc} */ public function alter(ContainerBuilder $container) { - // 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 during the database update process. + // The alias-based processor requires the path_alias entity schema to be + // installed, so we prevent it from being registered to the path processor + // manager. We do this by removing the tags that the compiler pass looks + // for. This means that the URL generator can safely be used during the + // database update process. if ($container->hasDefinition('path_processor_alias')) { $container->getDefinition('path_processor_alias') ->clearTag('path_processor_inbound') diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index f994a3ca47a..363ecaac919 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -129,8 +129,11 @@ class EntityTypeInfo implements ContainerInjectionInterface { */ public function entityTypeAlter(array &$entity_types) { foreach ($entity_types as $entity_type_id => $entity_type) { - // The ContentModerationState entity type should never be moderated. - if ($entity_type->isRevisionable() && !$entity_type->isInternal()) { + // Internal entity types should never be moderated, and the 'path_alias' + // entity type needs to be excluded for now. + // @todo Enable moderation for path aliases after they become publishable + // in https://www.drupal.org/project/drupal/issues/3007669. + if ($entity_type->isRevisionable() && !$entity_type->isInternal() && $entity_type_id !== 'path_alias') { $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type); } } 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 00000000000..873cb2abadd --- /dev/null +++ b/core/modules/jsonapi/tests/src/Functional/PathAliasTest.php @@ -0,0 +1,112 @@ +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' => static::$resourceTypeName, + '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' => static::$resourceTypeName, + '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 a173144f226..9513319a622 100644 --- a/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php @@ -2050,7 +2050,9 @@ abstract class ResourceTestBase extends BrowserTestBase { $doc = $this->getModifiedEntityForPostTesting(); $doc['data']['id'] = $uuid; $label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName; - $doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]]; + if (isset($label_field)) { + $doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]]; + } $request_options[RequestOptions::BODY] = Json::encode($doc); $response = $this->request('POST', $url, $request_options); @@ -2060,7 +2062,9 @@ abstract class ResourceTestBase extends BrowserTestBase { $doc = $this->getModifiedEntityForPostTesting(); $new_uuid = \Drupal::service('uuid')->generate(); $doc['data']['id'] = $new_uuid; - $doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]]; + if (isset($label_field)) { + $doc['data']['attributes'][$label_field] = [['value' => $this->randomMachineName()]]; + } $request_options[RequestOptions::BODY] = Json::encode($doc); $response = $this->request('POST', $url, $request_options); @@ -2094,7 +2098,9 @@ abstract class ResourceTestBase extends BrowserTestBase { $unparseable_request_body = '!{>}<'; $parseable_valid_request_body = Json::encode($this->getPatchDocument()); /* $parseable_valid_request_body_2 = Json::encode($this->getNormalizedPatchEntity()); */ - $parseable_invalid_request_body = Json::encode($this->makeNormalizationInvalid($this->getPatchDocument(), 'label')); + if ($this->entity->getEntityType()->hasKey('label')) { + $parseable_invalid_request_body = Json::encode($this->makeNormalizationInvalid($this->getPatchDocument(), 'label')); + } $parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep(['data' => ['attributes' => ['field_rest_test' => $this->randomString()]]], $this->getPatchDocument())); // The 'field_rest_test' field does not allow 'view' access, so does not end // up in the JSON:API document. Even when we explicitly add it to the JSON @@ -2340,7 +2346,7 @@ abstract class ResourceTestBase extends BrowserTestBase { // 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/menu_link_content/menu_link_content.install b/core/modules/menu_link_content/menu_link_content.install index 8a04d028599..8a881a56661 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 559405dfbe9..48dafd76d38 100644 --- a/core/modules/menu_link_content/menu_link_content.module +++ b/core/modules/menu_link_content/menu_link_content.module @@ -7,6 +7,7 @@ use Drupal\Core\Url; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Path\PathAliasInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\system\MenuInterface; @@ -50,10 +51,10 @@ 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(PathAliasInterface $path_alias) { + _menu_link_content_update_path_alias($path_alias->getAlias()); } /** @@ -75,23 +76,23 @@ 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(PathAliasInterface $path_alias) { + if ($path_alias->getAlias() != $path_alias->original->getAlias()) { + _menu_link_content_update_path_alias($path_alias->getAlias()); + _menu_link_content_update_path_alias($path_alias->original->getAlias()); } - elseif ($path['source'] != $path['original']['source']) { - _menu_link_content_update_path_alias($path['alias']); + elseif ($path_alias->getPath() != $path_alias->original->getPath()) { + _menu_link_content_update_path_alias($path_alias->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(PathAliasInterface $path_alias) { + _menu_link_content_update_path_alias($path_alias->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 104f356cf6a..c0cdba31b80 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 @@ class PathAliasMenuLinkContentTest extends KernelTestBase { $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 8896222f93f..3f1dc9b4b43 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 @@ trait CreateTestContentEntitiesTrait { $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 046b4cecfed..8100282ddc5 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 @@ -86,6 +86,7 @@ class Upgrade6Test extends MigrateUpgradeExecuteTestBase { '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 6c2bb395219..f769516fdc4 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 @@ -90,6 +90,7 @@ class Upgrade7Test extends MigrateUpgradeExecuteTestBase { 'menu' => 6, 'taxonomy_term' => 24, 'taxonomy_vocabulary' => 7, + 'path_alias' => 8, 'tour' => 5, 'user' => 4, 'user_role' => 3, diff --git a/core/modules/path/path.api.php b/core/modules/path/path.api.php index 5df539b52c1..2df3d2542b8 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/tests/src/Functional/PathAliasTest.php b/core/modules/path/tests/src/Functional/PathAliasTest.php index 41ec662b634..8e816bc245d 100644 --- a/core/modules/path/tests/src/Functional/PathAliasTest.php +++ b/core/modules/path/tests/src/Functional/PathAliasTest.php @@ -368,7 +368,11 @@ class PathAliasTest extends PathTestBase { * 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/Kernel/Migrate/d6/MigrateUrlAliasTest.php b/core/modules/path/tests/src/Kernel/Migrate/d6/MigrateUrlAliasTest.php index fd4ea59f061..db63c41104e 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 e0607b96d2a..b055e5cbf90 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 224a8420b3b..f001dc6bcfe 100644 --- a/core/modules/path/tests/src/Kernel/PathItemTest.php +++ b/core/modules/path/tests/src/Kernel/PathItemTest.php @@ -29,6 +29,7 @@ class PathItemTest extends KernelTestBase { $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 d4b111cf48f..da39d3b2cdf 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 @@ abstract class EntityResourceTestBase extends ResourceTestBase { // 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 @@ abstract class EntityResourceTestBase extends ResourceTestBase { // 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 @@ abstract class EntityResourceTestBase extends ResourceTestBase { $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 @@ abstract class EntityResourceTestBase extends ResourceTestBase { // 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 @@ abstract class EntityResourceTestBase extends ResourceTestBase { 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 739089a9886..e2b322ac5de 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 @@ class EntityResourcePermissionsUpdateTest extends UpdatePathTestBase { * 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 @@ class EntityResourcePermissionsUpdateTest extends UpdatePathTestBase { $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 caab5335b59..b212bc5961e 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -386,10 +386,10 @@ EOD; } if ($container->hasDefinition('path_processor_alias')) { - // Prevent the alias-based path processor, which requires a url_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. + // The alias-based processor requires the path_alias entity schema to be + // installed, so we prevent it from being registered to the path processor + // manager. We do this by removing the tags that the compiler pass looks + // for. This means that the URL generator can safely be used within tests. $definition = $container->getDefinition('path_processor_alias'); $definition->clearTag('path_processor_inbound')->clearTag('path_processor_outbound'); } 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 41fd896dfe1..00000000000 --- 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 43aa3e95888..69433cf8977 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -13,13 +13,16 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Extension\Extension; use Drupal\Core\Field\BaseFieldDefinition; 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\Site\Settings; use Drupal\Core\StreamWrapper\PrivateStream; use Drupal\Core\StreamWrapper\PublicStream; @@ -1229,11 +1232,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; } @@ -2359,3 +2357,118 @@ function system_update_8802() { ->save(TRUE); } } + +/** + * Install the 'path_alias' entity type. + */ +function system_update_8803() { + $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.'); + } +} + +/** + * Convert path aliases to entities. + */ +function system_update_8804(&$sandbox = NULL) { + // Bail out early if the entity type is not using the default storage class. + $storage = \Drupal::entityTypeManager()->getStorage('path_alias'); + if (!$storage instanceof PathAliasStorage) { + return; + } + + if (!isset($sandbox['current_id'])) { + // This must be the first run. Initialize the sandbox. + $sandbox['progress'] = 0; + $sandbox['current_id'] = 0; + } + + $database = \Drupal::database(); + $step_size = 200; + $url_aliases = $database->select('url_alias', 't') + ->condition('t.pid', $sandbox['current_id'], '>') + ->fields('t') + ->orderBy('pid', 'ASC') + ->range(0, $step_size) + ->execute() + ->fetchAll(); + + if ($url_aliases) { + /** @var \Drupal\Component\Uuid\UuidInterface $uuid */ + $uuid = \Drupal::service('uuid'); + + $base_table_insert = $database->insert('path_alias'); + $base_table_insert->fields(['id', 'revision_id', 'uuid', 'path', 'alias', 'langcode']); + $revision_table_insert = $database->insert('path_alias_revision'); + $revision_table_insert->fields(['id', 'revision_id', 'path', 'alias', 'langcode', 'revision_default']); + foreach ($url_aliases as $url_alias) { + $values = [ + 'id' => $url_alias->pid, + 'revision_id' => $url_alias->pid, + 'uuid' => $uuid->generate(), + 'path' => $url_alias->source, + 'alias' => $url_alias->alias, + 'langcode' => $url_alias->langcode, + ]; + $base_table_insert->values($values); + + unset($values['uuid']); + $values['revision_default'] = 1; + $revision_table_insert->values($values); + } + $base_table_insert->execute(); + $revision_table_insert->execute(); + + $sandbox['progress'] += count($url_aliases); + $last_url_alias = end($url_aliases); + $sandbox['current_id'] = $last_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; + } + else { + $sandbox['#finished'] = 1; + } + + if ($sandbox['#finished'] >= 1) { + // Keep a backup of the old 'url_alias' table if requested. + if (Settings::get('entity_update_backup', TRUE)) { + $old_table_name = 'old_' . substr(uniqid(), 0, 6) . '_url_alias'; + if (!$database->schema()->tableExists($old_table_name)) { + $database->schema()->renameTable('url_alias', $old_table_name); + } + } + else { + $database->schema()->dropTable('url_alias'); + } + + return t('Path aliases have been converted to entities.'); + } +} diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 9bedc79d605..b65838f376c 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1421,29 +1421,6 @@ function system_block_view_system_main_block_alter(array &$build, BlockPluginInt unset($build['#contextual_links']); } -/** - * Implements hook_path_update(). - */ -function system_path_update($path) { - $alias_manager = \Drupal::service('path.alias_manager'); - $alias_manager->cacheClear($path['source']); - $alias_manager->cacheClear($path['original']['source']); -} - -/** - * Implements hook_path_insert(). - */ -function system_path_insert($path) { - \Drupal::service('path.alias_manager')->cacheClear($path['source']); -} - -/** - * Implements hook_path_delete(). - */ -function system_path_delete($path) { - \Drupal::service('path.alias_manager')->cacheClear($path['source']); -} - /** * Implements hook_query_TAG_alter() for entity reference selection handlers. */ 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 00000000000..8f7cb770cec --- /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 00000000000..15b9357e74b --- /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 00000000000..30b9b91b7da --- /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 764d3892bc2..88e3d42e2f6 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. 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 00000000000..0677f061f94 --- /dev/null +++ b/core/modules/system/tests/src/Functional/Update/PathAliasToEntityUpdateTest.php @@ -0,0 +1,103 @@ +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_8803() + * @see system_update_8804() + */ + public function testConversionToEntities() { + $database = \Drupal::database(); + $schema = $database->schema(); + $this->assertTrue($schema->tableExists('url_alias')); + + $query = $database->select('url_alias'); + $query->addField('url_alias', 'pid', 'id'); + $query->addField('url_alias', 'source', 'path'); + $query->addField('url_alias', 'alias'); + $query->addField('url_alias', 'langcode'); + $original_records = $query->execute()->fetchAllAssoc('id'); + + // 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->assertCount($url_alias_count, $original_records); + + $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')); + + // Check that we have a backup of the old table. + $this->assertCount(1, $schema->findTables('old_%_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'); + + // Check that correct data was written in both the base and the revision + // tables. + $base_table_records = $database->select('path_alias') + ->fields('path_alias', ['id', 'path', 'alias', 'langcode']) + ->execute()->fetchAllAssoc('id'); + $this->assertEquals($original_records, $base_table_records); + + $revision_table_records = $database->select('path_alias_revision') + ->fields('path_alias_revision', ['id', 'path', 'alias', 'langcode']) + ->execute()->fetchAllAssoc('id'); + $this->assertEquals($original_records, $revision_table_records); + } + +} 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 00000000000..75f3ae4a7fe --- /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 892c7d1abd7..0c4638cbad2 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 @@ class PathHooksTest extends KernelTestBase { $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 @@ class PathHooksTest extends KernelTestBase { \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 a9a952670f3..8f985574bf0 100644 --- a/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php +++ b/core/modules/views/tests/src/Functional/Update/ExposedFilterBlocksUpdateTest.php @@ -46,7 +46,14 @@ class ExposedFilterBlocksUpdateTest extends UpdatePathTestBase { // the config schema checker ignore the block. static::$configSchemaCheckerExclusions[] = 'block.block.seven_secondary_local_tasks'; - $this->container->get('module_installer')->uninstall(['block']); + // We 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_update_8803() runs. + // Note that path alias processing is disabled during the regular database + // update process, so this only happens because we uninstall the Block + // module before running the updates. + // @see \Drupal\Core\Update\UpdateServiceProvider::alter() + $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 00000000000..5c8c16c6a89 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Hal/PathAliasHalJsonAnonTest.php @@ -0,0 +1,29 @@ +applyHalFieldNormalization($default_normalization); + return $normalization + [ + '_links' => [ + 'self' => [ + 'href' => '', + ], + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/path_alias/path_alias', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return parent::getNormalizedPostEntity() + [ + '_links' => [ + 'type' => [ + 'href' => $this->baseUrl . '/rest/type/path_alias/path_alias', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return [ + 'url.site', + 'user.permissions', + ]; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasJsonAnonTest.php new file mode 100644 index 00000000000..9febf9eaded --- /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', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return ['user.permissions']; + } + +} diff --git a/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php b/core/tests/Drupal/FunctionalTests/Rest/PathAliasXmlAnonTest.php new file mode 100644 index 00000000000..e0575a913ea --- /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 @@ class DbDumpTest extends KernelTestBase { $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 @@ class DbDumpTest extends KernelTestBase { '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 b5bcd487f2f..0efc69ef14b 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/CreateSampleEntityTest.php @@ -40,6 +40,7 @@ class CreateSampleEntityTest extends KernelTestBase { $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 d7b24b497f7..2e986d18b3e 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 0892edd09f2..bd26e1c6e0f 100644 --- a/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php +++ b/core/tests/Drupal/KernelTests/Core/Path/AliasTest.php @@ -4,39 +4,51 @@ namespace Drupal\KernelTests\Core\Path; 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 @@ class AliasTest extends PathUnitTestBase { $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 @@ class AliasTest extends PathUnitTestBase { $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 @@ class AliasTest extends PathUnitTestBase { * 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 @@ class AliasTest extends PathUnitTestBase { * 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 671dbc4792b..00000000000 --- 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 34c6d5852ae..ea23b6f8e65 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 0c73971122e..17196879228 100644 --- a/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php +++ b/core/tests/Drupal/KernelTests/Core/Routing/RouteProviderTest.php @@ -87,6 +87,7 @@ class RouteProviderTest extends KernelTestBase { $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 fd26d08a142..ad46657cf64 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -539,10 +539,10 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa } if ($container->hasDefinition('path_processor_alias')) { - // Prevent the alias-based path processor, which requires a url_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. + // The alias-based processor requires the path_alias entity schema to be + // installed, so we prevent it from being registered to the path processor + // manager. We do this by removing the tags that the compiler pass looks + // for. This means that the URL generator can safely be used within tests. $container->getDefinition('path_processor_alias') ->clearTag('path_processor_inbound') ->clearTag('path_processor_outbound');