diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 6e0b4d8..ef25bb1 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -309,6 +309,9 @@ protected function urlRouteParameters($rel) { if ($rel === 'revision') { $uri_route_parameters[$this->getEntityTypeId() . '_revision'] = $this->getRevisionId(); } + if ($rel === 'uuid') { + $uri_route_parameters[$this->getEntityTypeId()] = $this->uuid(); + } return $uri_route_parameters; } diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php index b1f6abd..2873563 100644 --- a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php +++ b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Entity\Routing; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\Controller\EntityController; use Drupal\Core\Entity\EntityFieldManagerInterface; @@ -23,6 +24,7 @@ * - add-form * - edit-form * - delete-form + * - uuid * * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider. * @@ -83,6 +85,12 @@ public function getRoutes(EntityTypeInterface $entity_type) { $collection->add("entity.{$entity_type_id}.add_form", $add_form_route); } + // This goes before canonical because the UUID pattern must be tested before + // non-integer entity IDs. + if ($uuid_route = $this->getUuidRoute($entity_type)) { + $collection->add("entity.{$entity_type_id}.uuid", $uuid_route); + } + if ($canonical_route = $this->getCanonicalRoute($entity_type)) { $collection->add("entity.{$entity_type_id}.canonical", $canonical_route); } @@ -230,6 +238,34 @@ protected function getCanonicalRoute(EntityTypeInterface $entity_type) { } /** + * Gets the UUID route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function getUuidRoute(EntityTypeInterface $entity_type) { + if ($entity_type->getKey('uuid') && $entity_type->hasViewBuilderClass()) { + $entity_type_id = $entity_type->id(); + $route = new Route("/{$entity_type_id}/{{$entity_type_id}}"); + $route + ->addDefaults([ + '_entity_view' => "{$entity_type_id}.full", + '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title', + ]) + ->setRequirement('_entity_access', "{$entity_type_id}.view") + ->setOption('parameters', [ + $entity_type_id => ['type' => 'entity:' . $entity_type_id], + ]) + // Set requirement for UUID pattern. + ->setRequirement($entity_type_id, '^' . Uuid::VALID_PATTERN . '$'); + return $route; + } + } + + /** * Gets the edit-form route. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php index 43c4468..8cb6795 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -132,10 +132,14 @@ public function checkNodeAccess(array $tree) { $node_links = array(); $this->collectNodeLinks($tree, $node_links); if ($node_links) { - $nids = array_keys($node_links); + // These could be serial node IDs or UUIDs. + $node_identifiers = array_keys($node_links); $query = $this->queryFactory->get('node'); - $query->condition('nid', $nids, 'IN'); + $group = $query->orConditionGroup() + ->condition('nid', $node_identifiers, 'IN') + ->condition('uuid', $node_identifiers, 'IN'); + $query->condition($group); // Allows admins to view all nodes, by both disabling node_access // query rewrite as well as not checking for the node status. The @@ -150,10 +154,13 @@ public function checkNodeAccess(array $tree) { $query->condition('status', NODE_PUBLISHED); } - $nids = $query->execute(); + // Cast to an array so we can loop, even if there are no results. + $nids = (array) $query->execute(); foreach ($nids as $nid) { - foreach ($node_links[$nid] as $key => $link) { - $node_links[$nid][$key]->access = $access_result; + if (isset($node_links[$nid])) { + foreach ($node_links[$nid] as $key => $link) { + $node_links[$nid][$key]->access = $access_result; + } } } } @@ -174,7 +181,7 @@ public function checkNodeAccess(array $tree) { */ protected function collectNodeLinks(array &$tree, array &$node_links) { foreach ($tree as $key => &$element) { - if ($element->link->getRouteName() == 'entity.node.canonical') { + if (in_array($element->link->getRouteName(), ['entity.node.canonical', 'entity.node.uuid'], TRUE)) { $nid = $element->link->getRouteParameters()['node']; $node_links[$nid][$key] = $element; // Deny access by default. checkNodeAccess() will re-add it. diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index 573cd49..855356b7 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -2,6 +2,7 @@ namespace Drupal\Core\ParamConverter; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\TypedData\TranslatableInterface; @@ -57,11 +58,19 @@ public function __construct(EntityManagerInterface $entity_manager) { /** * {@inheritdoc} + * + * The value here can be either a serial entity ID, or the entity UUID. */ public function convert($value, $definition, $name, array $defaults) { $entity_type_id = $this->getEntityTypeFromDefaults($definition, $name, $defaults); if ($storage = $this->entityManager->getStorage($entity_type_id)) { $entity = $storage->load($value); + // If there is no entity loadable by ID, try to load by UUID. + if (!$entity && Uuid::isValid($value)) { + if ($entities = $storage->loadByProperties(['uuid' => $value])) { + $entity = reset($entities); + } + } // If the entity type is translatable, ensure we return the proper // translation object for the current context. if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index bf978bf..9b8b911 100644 --- a/core/lib/Drupal/Core/Url.php +++ b/core/lib/Drupal/Core/Url.php @@ -3,6 +3,7 @@ namespace Drupal\Core; use Drupal\Component\Utility\UrlHelper; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\UrlGeneratorInterface; @@ -321,7 +322,7 @@ public static function fromUri($uri, $options = []) { * * @param array $uri_parts * Parts from an URI of the form entity:{entity_type}/{entity_id} as from - * parse_url(). + * parse_url(). Note that {entity_id} can be both a UUID and a serial ID. * @param array $options * An array of options, see \Drupal\Core\Url::fromUri() for details. * @param string $uri @@ -336,10 +337,15 @@ public static function fromUri($uri, $options = []) { protected static function fromEntityUri(array $uri_parts, array $options, $uri) { list($entity_type_id, $entity_id) = explode('/', $uri_parts['path'], 2); if ($uri_parts['scheme'] != 'entity' || $entity_id === '') { - throw new \InvalidArgumentException("The entity URI '$uri' is invalid. You must specify the entity id in the URL. e.g., entity:node/1 for loading the canonical path to node entity with id 1."); + throw new \InvalidArgumentException("The entity URI '$uri' is invalid. You must specify the entity id in the URL. e.g., entity:node/1 or entity:node/d44a0040-2844-4cca-b0b5-20c6c96c4d8c for loading the canonical path to node entity with id 1."); + } + $route_name = "entity.$entity_type_id.canonical"; + if (Uuid::isValid($entity_id)) { + // UUID instead of entity ID. + $route_name = "entity.$entity_type_id.uuid"; } - return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options); + return new static($route_name, [$entity_type_id => $entity_id], $options); } /** diff --git a/core/modules/aggregator/src/Entity/Feed.php b/core/modules/aggregator/src/Entity/Feed.php index 724a875..87b1cf1 100644 --- a/core/modules/aggregator/src/Entity/Feed.php +++ b/core/modules/aggregator/src/Entity/Feed.php @@ -31,6 +31,7 @@ * }, * links = { * "canonical" = "/aggregator/sources/{aggregator_feed}", + * "uuid" = "/aggregator/sources/{aggregator_feed}", * "edit-form" = "/aggregator/sources/{aggregator_feed}/configure", * "delete-form" = "/aggregator/sources/{aggregator_feed}/delete", * }, diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index 99b61d0..29c6bfd 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -201,8 +201,13 @@ function menu_ui_get_menu_link_defaults(NodeInterface $node) { // Give priority to the default menu $type_menus = $node_type->getThirdPartySetting('menu_ui', 'available_menus', array('main')); if (in_array($menu_name, $type_menus)) { - $query = \Drupal::entityQuery('menu_link_content') - ->condition('link.uri', 'node/' . $node->id()) + $query = \Drupal::entityQuery('menu_link_content'); + $group = $query->orConditionGroup() + ->condition('link.uri', 'entity:node/' . $node->id()) + ->condition('link.uri', 'entity:node/' . $node->uuid()) + ->condition('link.uri', 'internal:/node/' . $node->id()) + ->condition('link.uri', 'internal:/node/' . $node->uuid()); + $query->condition($group) ->condition('menu_name', $menu_name) ->sort('id', 'ASC') ->range(0, 1); @@ -212,8 +217,13 @@ function menu_ui_get_menu_link_defaults(NodeInterface $node) { } // Check all allowed menus if a link does not exist in the default menu. if (!$id && !empty($type_menus)) { - $query = \Drupal::entityQuery('menu_link_content') + $query = \Drupal::entityQuery('menu_link_content'); + $group = $query->orConditionGroup() ->condition('link.uri', 'entity:node/' . $node->id()) + ->condition('link.uri', 'entity:node/' . $node->uuid()) + ->condition('link.uri', 'internal:/node/' . $node->id()) + ->condition('link.uri', 'internal:/node/' . $node->uuid()); + $query->condition($group) ->condition('menu_name', array_values($type_menus), 'IN') ->sort('id', 'ASC') ->range(0, 1); diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php index f640cc7..1ce52a5 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Url; use Drupal\menu_link_content\Entity\MenuLinkContent; +use Drupal\node\Entity\NodeType; use Drupal\system\Entity\Menu; use Drupal\node\Entity\Node; @@ -69,7 +70,7 @@ protected function setUp() { $this->drupalCreateContentType(array('type' => 'article', 'name' => 'Article')); // Create users. - $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer blocks', 'administer menu', 'create article content')); + $this->adminUser = $this->drupalCreateUser(array('access administration pages', 'administer blocks', 'administer menu', 'create article content', 'edit any article content')); $this->authenticatedUser = $this->drupalCreateUser(array()); } @@ -221,6 +222,14 @@ function addCustomMenu() { // Enable the block. $block = $this->drupalPlaceBlock('system_menu_block:' . $menu_name); $this->blockPlacements[$menu_name] = $block->id(); + + // Make this menu available for node-edit forms. + /* @var \Drupal\node\NodeTypeInterface $type */ + $type = NodeType::load('article'); + $node_menus = $type->getThirdPartySetting('menu_ui', 'available_menus', array('main')); + $node_menus[] = $menu_name; + $type->setThirdPartySetting('menu_ui', 'available_menus', $node_menus); + $type->save(); return Menu::load($menu_name); } @@ -466,6 +475,29 @@ function doMenuTests() { // Save menu links for later tests. $this->items[] = $item1; $this->items[] = $item2; + + // Test links using node/{uuid}. + $node6 = $this->drupalCreateNode(array('type' => 'article')); + $uuid_link = $this->addMenuLink('', '/node/' . $node6->uuid(), $menu_name); + $this->verifyMenuLink($uuid_link, $node6); + $this->drupalGet($node6->url('edit-form')); + $this->assertFieldByName('menu[title]', $uuid_link->label()); + $this->drupalPostForm(NULL, [], t('Save')); + \Drupal::entityManager()->getStorage('menu_link_content')->resetCache([$uuid_link->id()]); + /** @var \Drupal\menu_link_content\MenuLinkContentInterface $uuid_link */ + $uuid_link = MenuLinkContent::load($uuid_link->id()); + $this->assertEqual($uuid_link->getUrlObject(), Url::fromUri('internal:/node/' . $node6->uuid())); + // Test with entity:node/{uuid}. + $node7 = $this->drupalCreateNode(array('type' => 'article')); + $uuid_link = $this->addMenuLink('', 'entity:node/' . $node7->uuid(), $menu_name); + $this->verifyMenuLink($uuid_link, $node7); + $this->drupalGet($node7->url('edit-form')); + $this->assertFieldByName('menu[title]', $uuid_link->label()); + $this->drupalPostForm(NULL, [], t('Save')); + \Drupal::entityManager()->getStorage('menu_link_content')->resetCache([$uuid_link->id()]); + /** @var \Drupal\menu_link_content\MenuLinkContentInterface $uuid_link */ + $uuid_link = MenuLinkContent::load($uuid_link->id()); + $this->assertEqual($uuid_link->getUrlObject(), Url::fromUri('entity:node/' . $node7->uuid())); } /** diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index 51794be..fd6ead8 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -63,6 +63,7 @@ * permission_granularity = "bundle", * links = { * "canonical" = "/node/{node}", + * "uuid" = "/node/{node}", * "delete-form" = "/node/{node}/delete", * "edit-form" = "/node/{node}/edit", * "version-history" = "/node/{node}/revisions", diff --git a/core/modules/node/src/Entity/NodeRouteProvider.php b/core/modules/node/src/Entity/NodeRouteProvider.php index 0803f1e..b46b09d 100644 --- a/core/modules/node/src/Entity/NodeRouteProvider.php +++ b/core/modules/node/src/Entity/NodeRouteProvider.php @@ -2,6 +2,7 @@ namespace Drupal\node\Entity; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; use Symfony\Component\Routing\Route; @@ -17,6 +18,17 @@ class NodeRouteProvider implements EntityRouteProviderInterface { */ public function getRoutes( EntityTypeInterface $entity_type) { $route_collection = new RouteCollection(); + + $route = (new Route("/node/{node}")) + ->addDefaults([ + '_controller' => '\Drupal\node\Controller\NodeViewController::view', + '_title_callback' => '\Drupal\node\Controller\NodeViewController::title', + ]) + // Set requirement for UUID pattern. + ->setRequirement('node', '^' . Uuid::VALID_PATTERN . '$') + ->setRequirement('_entity_access', 'node.view'); + $route_collection->add('entity.node.uuid', $route); + $route = (new Route('/node/{node}')) ->addDefaults([ '_controller' => '\Drupal\node\Controller\NodeViewController::view', diff --git a/core/modules/system/system.install b/core/modules/system/system.install index c716891..000a4cf 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1652,3 +1652,19 @@ function system_update_8014() { /** * @} End of "addtogroup updates-8.0.0-rc". */ + +/** + * @addtogroup updates-8.2.0 + * @{ + */ + +/** + * The simple presence of this update function clears cached field definitions. + */ +function system_update_8015() { + // Many core entity-types now have a UUID link template and route. +} + +/** + * @} End of "addtogroup updates-8.2.0". + */ diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index a603f53..38ac4ca 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -42,6 +42,7 @@ * }, * links = { * "canonical" = "/entity_test/{entity_test}", + * "uuid" = "/entity_test/{entity_test}", * "add-form" = "/entity_test/add", * "edit-form" = "/entity_test/manage/{entity_test}/edit", * "delete-form" = "/entity_test/delete/entity_test/{entity_test}", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php index f7fe8e6..629a6b5 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestBaseFieldDisplay.php @@ -33,6 +33,7 @@ * }, * links = { * "canonical" = "/entity_test_base_field_display/{entity_test_base_field_display}/edit", + * "uuid" = "/entity_test_base_field_display/{entity_test_base_field_display}/edit", * "add-form" = "/entity_test_base_field_display/add", * "edit-form" = "/entity_test_base_field_display/manage/{entity_test_base_field_display}", * "delete-form" = "/entity_test/delete/entity_test_base_field_display/{entity_test_base_field_display}/edit", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php index cbe7ba4..3a11384 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php @@ -36,6 +36,7 @@ * "add-page" = "/entity_test_mul/add", * "add-form" = "/entity_test_mul/add/{type}", * "canonical" = "/entity_test_mul/manage/{entity_test_mul}", + * "uuid" = "/entity_test_mul/manage/{entity_test_mul}", * "edit-form" = "/entity_test_mul/manage/{entity_test_mul}/edit", * "delete-form" = "/entity_test/delete/entity_test_mul/{entity_test_mul}", * }, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php index 4ae6652..f4b3d52 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php @@ -39,6 +39,7 @@ * links = { * "add-form" = "/entity_test_mul_changed/add", * "canonical" = "/entity_test_mul_changed/manage/{entity_test_mul_changed}", + * "uuid" = "/entity_test_mul_changed/manage/{entity_test_mul_changed}", * "edit-form" = "/entity_test_mul_changed/manage/{entity_test_mul_changed}/edit", * "delete-form" = "/entity_test/delete/entity_test_mul_changed/{entity_test_mul_changed}", * }, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulLangcodeKey.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulLangcodeKey.php index 94948a3..adc4357 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulLangcodeKey.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulLangcodeKey.php @@ -36,6 +36,7 @@ * links = { * "add-form" = "/entity_test_mul_langcode_key/add", * "canonical" = "/entity_test_mul_langcode_key/manage/{entity_test_mul_langcode_key}", + * "uuid" = "/entity_test_mul_langcode_key/manage/{entity_test_mul_langcode_key}", * "edit-form" = "/entity_test_mul_langcode_key/manage/{entity_test_mul_langcode_key}/edit", * "delete-form" = "/entity_test/delete/entity_test_mul_langcode_key/{entity_test_mul_langcode_key}", * }, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php index 0ee28bf..74e987f 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php @@ -38,6 +38,7 @@ * links = { * "add-form" = "/entity_test_mulrev/add", * "canonical" = "/entity_test_mulrev/manage/{entity_test_mulrev}", + * "uuid" = "/entity_test_mulrev/manage/{entity_test_mulrev}", * "delete-form" = "/entity_test/delete/entity_test_mulrev/{entity_test_mulrev}", * "edit-form" = "/entity_test_mulrev/manage/{entity_test_mulrev}/edit", * "revision" = "/entity_test_mulrev/{entity_test_mulrev}/revision/{entity_test_mulrev_revision}/view", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php index 3913075..500b1e3 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRevChanged.php @@ -40,6 +40,7 @@ * links = { * "add-form" = "/entity_test_mulrev_changed/add", * "canonical" = "/entity_test_mulrev_changed/manage/{entity_test_mulrev_changed}", + * "uuid" = "/entity_test_mulrev_changed/manage/{entity_test_mulrev_changed}", * "delete-form" = "/entity_test/delete/entity_test_mulrev_changed/{entity_test_mulrev_changed}", * "edit-form" = "/entity_test_mulrev_changed/manage/{entity_test_mulrev_changed}/edit", * "revision" = "/entity_test_mulrev_changed/{entity_test_mulrev_changed}/revision/{entity_test_mulrev_changed_revision}/view", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php index b299d90..5c8a86f 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php @@ -39,6 +39,7 @@ * links = { * "add-form" = "/entity_test_rev/add", * "canonical" = "/entity_test_rev/manage/{entity_test_rev}", + * "uuid" = "/entity_test_rev/manage/{entity_test_rev}", * "delete-form" = "/entity_test/delete/entity_test_rev/{entity_test_rev}", * "edit-form" = "/entity_test_rev/manage/{entity_test_rev}/edit", * "revision" = "/entity_test_rev/{entity_test_rev}/revision/{entity_test_rev_revision}/view", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php index a63abad..752013c 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php @@ -29,6 +29,7 @@ * }, * links = { * "canonical" = "/entity_test_string_id/manage/{entity_test_string_id}", + * "uuid" = "/entity_test_string_id/manage/{entity_test_string_id}", * "add-form" = "/entity_test_string_id/add", * "edit-form" = "/entity_test_string_id/manage/{entity_test_string_id}", * }, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php index c668bd3..7ef8e3a 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithBundle.php @@ -37,6 +37,7 @@ * bundle_entity_type = "entity_test_bundle", * links = { * "canonical" = "/entity_test_with_bundle/{entity_test_with_bundle}", + * "uuid" = "/entity_test_with_bundle/{entity_test_with_bundle}", * "add-page" = "/entity_test_with_bundle/add", * "add-form" = "/entity_test_with_bundle/add/{entity_test_bundle}", * "edit-form" = "/entity_test_with_bundle/{entity_test_with_bundle}/edit", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php index 4f4f4f1..465e663 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestWithRevisionLog.php @@ -37,6 +37,7 @@ * }, * links = { * "canonical" = "/entity_test_revlog/manage/{entity_test_revlog}", + * "uuid" = "/entity_test_revlog/manage/{entity_test_revlog}", * "delete-form" = "/entity_test/delete/entity_test_revlog/{entity_test_revlog}", * "edit-form" = "/entity_test_revlog/manage/{entity_test_revlog}/edit", * "revision" = "/entity_test_revlog/{entity_test_revlog}/revision/{entity_test_revlog_revision}/view", diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 65386479..0449e00 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -44,6 +44,7 @@ * common_reference_target = TRUE, * links = { * "canonical" = "/taxonomy/term/{taxonomy_term}", + * "uuid" = "/taxonomy/term/{taxonomy_term}", * "delete-form" = "/taxonomy/term/{taxonomy_term}/delete", * "edit-form" = "/taxonomy/term/{taxonomy_term}/edit", * }, diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml index 8a3bd1a..4957fa9 100644 --- a/core/modules/taxonomy/taxonomy.routing.yml +++ b/core/modules/taxonomy/taxonomy.routing.yml @@ -76,6 +76,16 @@ entity.taxonomy_vocabulary.overview_form: requirements: _entity_access: 'taxonomy_vocabulary.view' +entity.taxonomy_term.uuid: + path: '/taxonomy/term/{taxonomy_term}' + defaults: + _entity_view: 'taxonomy_term.full' + _title: 'Taxonomy term' + _title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle' + requirements: + _entity_access: 'taxonomy_term.view' + taxonomy_term: '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}' + entity.taxonomy_term.canonical: path: '/taxonomy/term/{taxonomy_term}' defaults: diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php index 4896bad..68598e8 100644 --- a/core/modules/user/src/Entity/User.php +++ b/core/modules/user/src/Entity/User.php @@ -48,6 +48,7 @@ * }, * links = { * "canonical" = "/user/{user}", + * "uuid" = "/user/{user}", * "edit-form" = "/user/{user}/edit", * "cancel-form" = "/user/{user}/cancel", * "collection" = "/admin/people", diff --git a/core/modules/user/src/Entity/UserRouteProvider.php b/core/modules/user/src/Entity/UserRouteProvider.php index d1e9671..14543bb 100644 --- a/core/modules/user/src/Entity/UserRouteProvider.php +++ b/core/modules/user/src/Entity/UserRouteProvider.php @@ -2,6 +2,7 @@ namespace Drupal\user\Entity; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; use Symfony\Component\Routing\Route; @@ -17,6 +18,17 @@ class UserRouteProvider implements EntityRouteProviderInterface { */ public function getRoutes(EntityTypeInterface $entity_type) { $route_collection = new RouteCollection(); + + $route = (new Route("/user/{user}")) + ->addDefaults([ + '_entity_view' => 'user.full', + '_title_callback' => 'Drupal\user\Controller\UserController::userTitle', + ]) + // Set requirement for UUID pattern. + ->setRequirement('user', '^' . Uuid::VALID_PATTERN . '$') + ->setRequirement('_entity_access', 'user.view'); + $route_collection->add('entity.user.uuid', $route); + $route = (new Route('/user/{user}')) ->setDefaults([ '_entity_view' => 'user.full', diff --git a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php index 7196652..e3c1478 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Entity\Routing; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -253,6 +254,52 @@ public function providerTestGetCanonicalRoute() { } /** + * @covers ::getUuidRoute + * @dataProvider providerTestGetUuidRoute + */ + public function testGetUuidRoute(EntityTypeInterface $entity_type, Route $expected = NULL) { + $route = $this->routeProvider->getUuidRoute($entity_type); + $this->assertEquals($expected, $route); + } + + public function providerTestGetUuidRoute() { + $data = []; + + $entity_type1 = $this->getEntityType(); + $entity_type1->getKey('uuid')->willReturn(FALSE); + $data['no_canonical_link_template'] = [$entity_type1->reveal()]; + + $entity_type2 = $this->getEntityType();; + $entity_type2->getKey('uuid')->willReturn(TRUE); + $entity_type2->hasViewBuilderClass()->willReturn(FALSE); + $data['no_view_builder'] = [$entity_type2->reveal()]; + + $entity_type3 = $this->getEntityType($entity_type2); + $entity_type3->hasViewBuilderClass()->willReturn(TRUE); + $entity_type3->id()->willReturn('the_entity_type_id'); + $entity_type3->getKey('uuid')->willReturn(TRUE); + $entity_type3->isSubclassOf(FieldableEntityInterface::class)->willReturn(FALSE); + $route = (new Route('/the_entity_type_id/{the_entity_type_id}')) + ->setDefaults([ + '_entity_view' => 'the_entity_type_id.full', + '_title_callback' => '\Drupal\Core\Entity\Controller\EntityController::title', + ]) + ->setRequirements([ + '_entity_access' => 'the_entity_type_id.view', + 'the_entity_type_id' => '^' . Uuid::VALID_PATTERN . '$', + ]) + ->setOptions([ + 'parameters' => [ + 'the_entity_type_id' => [ + 'type' => 'entity:the_entity_type_id', + ], + ], + ]); + $data['has_uuid_route'] = [$entity_type3->reveal(), $route]; + return $data; + } + + /** * @covers ::getEntityTypeIdKeyType */ public function testGetEntityTypeIdKeyType() { @@ -313,5 +360,8 @@ public function getAddFormRoute(EntityTypeInterface $entity_type) { public function getCanonicalRoute(EntityTypeInterface $entity_type) { return parent::getCanonicalRoute($entity_type); } + public function getUuidRoute(EntityTypeInterface $entity_type) { + return parent::getUuidRoute($entity_type); + } } diff --git a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php index 2000fe6..24aa65f 100644 --- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\DependencyInjection\Container; +use Drupal\Core\Entity\Query\ConditionInterface; use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators; use Drupal\Core\Menu\MenuLinkTreeElement; use Drupal\Tests\UnitTestCase; @@ -274,7 +275,7 @@ public function testCheckNodeAccess() { 1 => MenuLinkMock::create(array('id' => 'node.1', 'route_name' => 'entity.node.canonical', 'title' => 'foo', 'parent' => '', 'route_parameters' => array('node' => 1))), 2 => MenuLinkMock::create(array('id' => 'node.2', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => array('node' => 2))), 3 => MenuLinkMock::create(array('id' => 'node.3', 'route_name' => 'entity.node.canonical', 'title' => 'baz', 'parent' => 'node.2', 'route_parameters' => array('node' => 3))), - 4 => MenuLinkMock::create(array('id' => 'node.4', 'route_name' => 'entity.node.canonical', 'title' => 'qux', 'parent' => 'node.3', 'route_parameters' => array('node' => 4))), + 4 => MenuLinkMock::create(array('id' => 'node.4', 'route_name' => 'entity.node.uuid', 'title' => 'qux', 'parent' => 'node.3', 'route_parameters' => array('node' => 4))), 5 => MenuLinkMock::create(array('id' => 'test.1', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => '')), 6 => MenuLinkMock::create(array('id' => 'test.2', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => 'test.1')), ); @@ -290,11 +291,23 @@ public function testCheckNodeAccess() { )); $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + $condition = $this->getMock(ConditionInterface::class); $query->expects($this->at(0)) + ->method('orConditionGroup') + ->willReturn($condition); + $condition->expects($this->at(0)) ->method('condition') - ->with('nid', array(1, 2, 3, 4)); + ->with('nid', array(1, 2, 3, 4)) + ->willReturn($condition); + $condition->expects($this->at(1)) + ->method('condition') + ->with('uuid', array(1, 2, 3, 4)) + ->willReturn($condition); $query->expects($this->at(1)) ->method('condition') + ->with($condition); + $query->expects($this->at(2)) + ->method('condition') ->with('status', NODE_PUBLISHED); $query->expects($this->once()) ->method('execute') diff --git a/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php index ebafab0..7b6fcc8 100644 --- a/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php +++ b/core/tests/Drupal/Tests/Core/ParamConverter/EntityConverterTest.php @@ -89,6 +89,12 @@ public function testConvert($value, array $definition, array $defaults, $expecte ['valid_id', (object) ['id' => 'valid_id']], ['invalid_id', NULL], ]); + $entity_storage->expects($this->any()) + ->method('loadByProperties') + ->willReturnMap([ + [['uuid' => 'invalid_id'], NULL], + [['uuid' => $value], [(object) ['uuid' => $value, 'id' => 'valid_id']]], + ]); $this->assertEquals($expected_result, $this->entityConverter->convert($value, $definition, 'foo', $defaults)); } @@ -104,6 +110,8 @@ public function providerTestConvert() { $data[] = ['invalid_id', ['type' => 'entity:entity_test'], ['foo' => 'invalid_id'], NULL]; // Entity type placeholder. $data[] = ['valid_id', ['type' => 'entity:{entity_type}'], ['foo' => 'valid_id', 'entity_type' => 'entity_test'], (object) ['id' => 'valid_id']]; + // UUID. + $data[] = ['1c5217f4-553c-40d8-8389-a3cc3529d79c', ['type' => 'entity:entity_test'], ['foo' => '1c5217f4-553c-40d8-8389-a3cc3529d79c'], (object) ['uuid' => '1c5217f4-553c-40d8-8389-a3cc3529d79c', 'id' => 'valid_id']]; return $data; } diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index 2571093..6e37a6d 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -570,6 +570,14 @@ public function providerTestEntityUris() { ['page' => '1', 'foo' => 'yes', 'focus' => 'no'], 'top', ], + [ + 'entity:test_entity/d44a0040-2844-4cca-b0b5-20c6c96c4d8c', + ['fragment' => ''], + 'entity.test_entity.uuid', + ['test_entity' => 'd44a0040-2844-4cca-b0b5-20c6c96c4d8c'], + NULL, + NULL, + ], ]; }