diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index 2c6dbc1..b3c865e 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -315,6 +315,9 @@ protected function urlRouteParameters($rel) { if ($rel === 'revision' && $this instanceof RevisionableInterface) { $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 1ac54bb..d24035b 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; @@ -24,6 +25,7 @@ * - edit-form * - delete-form * - collection + * - uuid * * @see \Drupal\Core\Entity\Routing\AdminHtmlRouteProvider. * @@ -84,6 +86,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); } @@ -235,6 +243,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->hasLinkTemplate('uuid')) { + $entity_type_id = $entity_type->id(); + $route = new Route($entity_type->getLinkTemplate('uuid')); + $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..de086e6 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -132,10 +132,36 @@ 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); + $nids = array_filter($node_identifiers, function ($value) { + return is_int($value) || ctype_digit((string) $value); + }); + $uuids = array_diff($node_identifiers, $nids); - $query = $this->queryFactory->get('node'); - $query->condition('nid', $nids, 'IN'); + // Create a query that will retrieve node IDs and UUIDs. + $query = $this->queryFactory->getAggregate('node'); + if (!empty($nids) && !empty($uuids)) { + $query + ->groupBy('nid') + ->groupBy('uuid'); + $group = $query + ->orConditionGroup() + ->condition('nid', $nids, 'IN') + ->condition('uuid', $uuids, 'IN'); + $query->condition($group); + $result_keys = ['nid', 'uuid']; + } + elseif (!empty($nids)) { + $query->groupBy('nid'); + $query->condition('nid', $nids, 'IN'); + $result_keys = ['nid']; + } + else { + $query->groupBy('uuid'); + $query->condition('uuid', $uuids, 'IN'); + $result_keys = ['uuid']; + } // 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 +176,15 @@ public function checkNodeAccess(array $tree) { $query->condition('status', NODE_PUBLISHED); } - $nids = $query->execute(); - foreach ($nids as $nid) { - foreach ($node_links[$nid] as $key => $link) { - $node_links[$nid][$key]->access = $access_result; + // Attach the access result to the menu tree for the use nodes the user is + // allowed to access. + foreach ($query->execute() as $result) { + foreach ($result_keys as $key) { + if (isset($node_links[$result[$key]])) { + foreach ($node_links[$result[$key]] as $node_link_key => $link) { + $node_links[$result[$key]][$node_link_key]->access = $access_result; + } + } } } } @@ -174,7 +205,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..157d79d 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -2,13 +2,14 @@ namespace Drupal\Core\ParamConverter; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\TypedData\TranslatableInterface; use Symfony\Component\Routing\Route; /** - * Parameter converter for upcasting entity IDs to full objects. + * Parameter converter for upcasting entity IDs or UUIDs to full objects. * * This is useful in cases where the dynamic elements of the path can't be * auto-determined; for example, if your path refers to multiple of the same @@ -57,18 +58,28 @@ 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); + $entity = NULL; if ($storage = $this->entityManager->getStorage($entity_type_id)) { - $entity = $storage->load($value); + // Load by UUID or ID depending on $value. + if (is_int($value) || ctype_digit((string) $value)) { + $entity = $storage->load($value); + } + elseif (Uuid::isValid($value)) { + $entities = $storage->loadByProperties(['uuid' => $value]); + $entity = ($entities) ? reset($entities) : NULL; + } // If the entity type is translatable, ensure we return the proper // translation object for the current context. if ($entity instanceof EntityInterface && $entity instanceof TranslatableInterface) { $entity = $this->entityManager->getTranslationFromContext($entity, NULL, array('operation' => 'entity_upcast')); } - return $entity; } + return $entity; } /** diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 6a58319..6360965 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; @@ -325,7 +326,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 either a UUID or the entity ID. * @param array $options * An array of options, see \Drupal\Core\Url::fromUri() for details. * @param string $uri @@ -340,10 +341,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/{uuid} 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 834a55e..e997e68 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 d642247..d829580 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/MenuNodeTest.php b/core/modules/menu_ui/src/Tests/MenuNodeTest.php index 3fc8423..07a8a43 100644 --- a/core/modules/menu_ui/src/Tests/MenuNodeTest.php +++ b/core/modules/menu_ui/src/Tests/MenuNodeTest.php @@ -2,7 +2,8 @@ namespace Drupal\menu_ui\Tests; -use Drupal\simpletest\WebTestBase; +use Drupal\Core\Url; +use Drupal\node\Entity\NodeType; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\node\Entity\Node; @@ -12,7 +13,7 @@ * * @group menu_ui */ -class MenuNodeTest extends WebTestBase { +class MenuNodeTest extends MenuWebTestBase { /** * An editor user. @@ -338,4 +339,40 @@ function testMultilingualMenuNodeFormWidget() { $this->assertFieldById('edit-menu-title', $translated_node_title); } + /** + * Tests adding links to nodes using the /node/{uuid} format. + */ + public function testNodeUuidLink() { + /* @var \Drupal\node\NodeTypeInterface $type */ + $type = NodeType::load('page'); + // Enable the main menu for this node type.. + $menu_name = 'main'; + $type->setThirdPartySetting('menu_ui', 'available_menus', [$menu_name]); + $type->save(); + + // Test links using node/{uuid}. + $node6 = $this->drupalCreateNode(array('type' => 'page')); + $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' => 'page')); + $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/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php index f640cc7..016cca7 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -69,7 +69,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 +221,7 @@ function addCustomMenu() { // Enable the block. $block = $this->drupalPlaceBlock('system_menu_block:' . $menu_name); $this->blockPlacements[$menu_name] = $block->id(); + return Menu::load($menu_name); } @@ -583,55 +584,6 @@ public function testBlockContextualLinks() { } /** - * Adds a menu link using the UI. - * - * @param string $parent - * Optional parent menu link id. - * @param string $path - * The path to enter on the form. Defaults to the front page. - * @param string $menu_name - * Menu name. Defaults to 'tools'. - * @param bool $expanded - * Whether or not this menu link is expanded. Setting this to TRUE should - * test whether it works when we do the authenticatedUser tests. Defaults - * to FALSE. - * @param string $weight - * Menu weight. Defaults to 0. - * - * @return \Drupal\menu_link_content\Entity\MenuLinkContent - * A menu link entity. - */ - function addMenuLink($parent = '', $path = '/', $menu_name = 'tools', $expanded = FALSE, $weight = '0') { - // View add menu link page. - $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); - $this->assertResponse(200); - - $title = '!link_' . $this->randomMachineName(16); - $edit = array( - 'link[0][uri]' => $path, - 'title[0][value]' => $title, - 'description[0][value]' => '', - 'enabled[value]' => 1, - 'expanded[value]' => $expanded, - 'menu_parent' => $menu_name . ':' . $parent, - 'weight[0][value]' => $weight, - ); - - // Add menu link. - $this->drupalPostForm(NULL, $edit, t('Save')); - $this->assertResponse(200); - $this->assertText('The menu link has been saved.'); - - $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $title)); - - $menu_link = reset($menu_links); - $this->assertTrue($menu_link, 'Menu link was found in database.'); - $this->assertMenuLink($menu_link->getPluginId(), array('menu_name' => $menu_name, 'children' => array(), 'parent' => $parent)); - - return $menu_link; - } - - /** * Attempts to add menu link with invalid path or no access permission. */ function addInvalidMenuLink() { @@ -688,45 +640,6 @@ function checkInvalidParentMenuLinks() { } /** - * Verifies a menu link using the UI. - * - * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item - * Menu link. - * @param object $item_node - * Menu link content node. - * @param \Drupal\menu_link_content\Entity\MenuLinkContent $parent - * Parent menu link. - * @param object $parent_node - * Parent menu link content node. - */ - function verifyMenuLink(MenuLinkContent $item, $item_node, MenuLinkContent $parent = NULL, $parent_node = NULL) { - // View home page. - $this->drupalGet(''); - $this->assertResponse(200); - - // Verify parent menu link. - if (isset($parent)) { - // Verify menu link. - $title = $parent->getTitle(); - $this->assertLink($title, 0, 'Parent menu link was displayed'); - - // Verify menu link link. - $this->clickLink($title); - $title = $parent_node->label(); - $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Parent menu link link target was correct'); - } - - // Verify menu link. - $title = $item->getTitle(); - $this->assertLink($title, 0, 'Menu link was displayed'); - - // Verify menu link link. - $this->clickLink($title); - $title = $item_node->label(); - $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Menu link link target was correct'); - } - - /** * Changes the parent of a menu link using the UI. * * @param \Drupal\menu_link_content\MenuLinkContentInterface $item diff --git a/core/modules/menu_ui/src/Tests/MenuWebTestBase.php b/core/modules/menu_ui/src/Tests/MenuWebTestBase.php index c08fc14..e45068e 100644 --- a/core/modules/menu_ui/src/Tests/MenuWebTestBase.php +++ b/core/modules/menu_ui/src/Tests/MenuWebTestBase.php @@ -2,6 +2,7 @@ namespace Drupal\menu_ui\Tests; +use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\simpletest\WebTestBase; /** @@ -74,4 +75,97 @@ function assertMenuLink($menu_plugin_id, array $expected_item) { } } + /** + * Adds a menu link using the UI. + * + * @param string $parent + * Optional parent menu link id. + * @param string $path + * The path to enter on the form. Defaults to the front page. + * @param string $menu_name + * Menu name. Defaults to 'tools'. + * @param bool $expanded + * Whether or not this menu link is expanded. Setting this to TRUE should + * test whether it works when we do the authenticatedUser tests. Defaults + * to FALSE. + * @param string $weight + * Menu weight. Defaults to 0. + * + * @return \Drupal\menu_link_content\Entity\MenuLinkContent + * A menu link entity. + */ + public function addMenuLink($parent = '', $path = '/', $menu_name = 'tools', $expanded = FALSE, $weight = '0') { + // View add menu link page. + $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); + $this->assertResponse(200); + + $title = '!link_' . $this->randomMachineName(16); + $edit = array( + 'link[0][uri]' => $path, + 'title[0][value]' => $title, + 'description[0][value]' => '', + 'enabled[value]' => 1, + 'expanded[value]' => $expanded, + 'menu_parent' => $menu_name . ':' . $parent, + 'weight[0][value]' => $weight, + ); + + // Add menu link. + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertText('The menu link has been saved.'); + + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $title)); + + $menu_link = reset($menu_links); + $this->assertTrue($menu_link, 'Menu link was found in database.'); + $this->assertMenuLink($menu_link->getPluginId(), [ + 'menu_name' => $menu_name, + 'children' => [], + 'parent' => $parent, + ]); + + return $menu_link; + } + + + /** + * Verifies a menu link using the UI. + * + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item + * Menu link. + * @param object $item_node + * Menu link content node. + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $parent + * Parent menu link. + * @param object $parent_node + * Parent menu link content node. + */ + public function verifyMenuLink(MenuLinkContent $item, $item_node, MenuLinkContent $parent = NULL, $parent_node = NULL) { + // View home page. + $this->drupalGet(''); + $this->assertResponse(200); + + // Verify parent menu link. + if (isset($parent)) { + // Verify menu link. + $title = $parent->getTitle(); + $this->assertLink($title, 0, 'Parent menu link was displayed'); + + // Verify menu link link. + $this->clickLink($title); + $title = $parent_node->label(); + $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Parent menu link link target was correct'); + } + + // Verify menu link. + $title = $item->getTitle(); + $this->assertLink($title, 0, 'Menu link was displayed'); + + // Verify menu link link. + $this->clickLink($title); + $title = $item_node->label(); + $this->assertTitle(t("@title | Drupal", array('@title' => $title)), 'Menu link link target was correct'); + } + } diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index bdb8050..ecbcaaf 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/node/src/Tests/NodeAccessMenuLinkTest.php b/core/modules/node/src/Tests/NodeAccessMenuLinkTest.php index c1bc0eb..d10208b 100644 --- a/core/modules/node/src/Tests/NodeAccessMenuLinkTest.php +++ b/core/modules/node/src/Tests/NodeAccessMenuLinkTest.php @@ -44,29 +44,57 @@ protected function setUp() { */ function testNodeAccessMenuLink() { - $menu_link_title = $this->randomString(); + $nid_menu_link_title = $this->randomString(); $this->drupalLogin($this->contentAdminUser); + $node_title = $this->randomString(); $edit = [ - 'title[0][value]' => $this->randomString(), + 'title[0][value]' => $node_title, 'body[0][value]' => $this->randomString(), 'menu[enabled]' => 1, - 'menu[title]' => $menu_link_title, + 'menu[title]' => $nid_menu_link_title, ]; $this->drupalPostForm('node/add/page', $edit, t('Save')); - $this->assertLink($menu_link_title); + + // Create another node to link by UUID. + $node = $this->createNode(); + $uuid_menu_link_title = '!link_' . $this->randomMachineName(16); + $edit = array( + 'link[0][uri]' => 'entity:node/' . $node->uuid(), + 'title[0][value]' => $uuid_menu_link_title, + 'description[0][value]' => '', + 'enabled[value]' => 1, + 'menu_parent' => 'main:', + ); + // Add menu link. + $this->drupalPostForm('admin/structure/menu/manage/main/add', $edit, t('Save')); + + // Ensure you can add a child link to both node menu links. + // @see \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators::checkNodeAccess() + $this->drupalGet('admin/structure/menu/manage/main/add'); + $elements = $this->xpath('//select["edit-menu-parent"]//option[contains(text(), :title)]', [':title' => $uuid_menu_link_title]); + $this->assertTrue(is_array($elements) && isset($elements[0]), 'Node menu item using UUID can be a parent'); + $elements = $this->xpath('//select["edit-menu-parent"]//option[contains(text(), :title)]', [':title' => $nid_menu_link_title]); + $this->assertTrue(is_array($elements) && isset($elements[0]), 'Node menu item using node ID can be a parent'); + + // Ensure the menu links appear for a user with the correct permissions. + $this->drupalGet(''); + $this->assertLink($nid_menu_link_title); + $this->assertLink($uuid_menu_link_title); // Ensure anonymous users without "access content" permission do not see // this menu link. $this->drupalLogout(); $this->drupalGet(''); - $this->assertNoLink($menu_link_title); + $this->assertNoLink($nid_menu_link_title); + $this->assertNoLink($uuid_menu_link_title); // Ensure anonymous users with "access content" permission see this menu // link. $this->config('user.role.' . RoleInterface::ANONYMOUS_ID)->set('permissions', array('access content'))->save(); $this->drupalGet(''); - $this->assertLink($menu_link_title); + $this->assertLink($nid_menu_link_title); + $this->assertLink($uuid_menu_link_title); } } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index ed181f2..615eec1 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1647,3 +1647,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 entity definitions. + */ +function system_update_8200() { + // 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 5fbc408..f7426c2 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 2e41e2d..6e70bbf 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 030b9a9..1a4b5d7 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/modules/views/src/Plugin/views/argument_validator/Entity.php b/core/modules/views/src/Plugin/views/argument_validator/Entity.php index 53e29d7..c3df9ca 100644 --- a/core/modules/views/src/Plugin/views/argument_validator/Entity.php +++ b/core/modules/views/src/Plugin/views/argument_validator/Entity.php @@ -2,6 +2,7 @@ namespace Drupal\views\Plugin\views\argument_validator; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -168,8 +169,27 @@ public function validateArgument($argument) { else { return FALSE; } + $uuids = array_filter($ids, function ($value) { + return Uuid::isValid($value); + }); + $ids = array_diff($ids, $uuids); + + $storage = $this->entityManager->getStorage($entity_type); + if ($uuids) { + $entities = $storage->loadByProperties(['uuid' => $uuids]); + if (count($entities) !== count($uuids)) { + // Missing one or more UUIDs, break and return false. + return FALSE; + } + + foreach ($entities as $entity) { + if (!$this->validateEntity($entity)) { + return FALSE; + } + } + } + $entities = $storage->loadMultiple($ids); - $entities = $this->entityManager->getStorage($entity_type)->loadMultiple($ids); // Validate each id => entity. If any fails break out and return false. foreach ($ids as $id) { // There is no entity for this ID. diff --git a/core/modules/views/tests/src/Unit/Plugin/argument_validator/EntityTest.php b/core/modules/views/tests/src/Unit/Plugin/argument_validator/EntityTest.php index f9ca7d8..3afebf3 100644 --- a/core/modules/views/tests/src/Unit/Plugin/argument_validator/EntityTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/argument_validator/EntityTest.php @@ -88,6 +88,13 @@ protected function setUp() { ->method('loadMultiple') ->will($this->returnValueMap($value_map)); + $uuid_value_map = [ + [['uuid' => ['fb0920d9-7a0d-42c5-8d11-b31d9ebfae6b']], [1 => $mock_entity]], + ]; + $storage->expects($this->any()) + ->method('loadByProperties') + ->will($this->returnValueMap($uuid_value_map)); + $this->entityManager->expects($this->any()) ->method('getStorage') ->with('entity_test') @@ -122,7 +129,9 @@ public function testValidateArgumentNoAccess() { $this->assertFalse($this->argumentValidator->validateArgument('')); $this->assertTrue($this->argumentValidator->validateArgument(1)); - $this->assertTrue($this->argumentValidator->validateArgument(2)); + $this->assertTrue($this->argumentValidator->validateArgument(1)); + $this->assertTrue($this->argumentValidator->validateArgument('fb0920d9-7a0d-42c5-8d11-b31d9ebfae6b')); + $this->assertFalse($this->argumentValidator->validateArgument('298109c5-c931-4367-af2b-356008905ed1')); $this->assertFalse($this->argumentValidator->validateArgument('1,2')); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php index a6675ec..0b128aa 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; @@ -299,6 +300,59 @@ public function providerTestGetCollectionRoute() { } /** + * @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->hasLinkTemplate('uuid')->willReturn(FALSE); + $data['no_uuid_link_template'] = [$entity_type2->reveal()]; + + $entity_type4 = $this->getEntityType($entity_type2); + $entity_type4->hasViewBuilderClass()->willReturn(TRUE); + $entity_type4->id()->willReturn('the_entity_type_id'); + $entity_type4->getKey('uuid')->willReturn(TRUE); + $entity_type4->hasLinkTemplate('uuid')->willReturn(TRUE); + $entity_type4->getLinkTemplate('uuid')->willReturn('/the_entity_type_id/{the_entity_type_id}'); + $entity_type4->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_type4->reveal(), $route]; + return $data; + } + + /** * @covers ::getEntityTypeIdKeyType */ public function testGetEntityTypeIdKeyType() { @@ -362,5 +416,8 @@ public function getCanonicalRoute(EntityTypeInterface $entity_type) { public function getCollectionRoute(EntityTypeInterface $entity_type) { return parent::getCollectionRoute($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..156d490 100644 --- a/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php +++ b/core/tests/Drupal/Tests/Core/Menu/DefaultMenuLinkTreeManipulatorsTest.php @@ -5,6 +5,8 @@ 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\Entity\Query\QueryAggregateInterface; use Drupal\Core\Menu\DefaultMenuLinkTreeManipulators; use Drupal\Core\Menu\MenuLinkTreeElement; use Drupal\Tests\UnitTestCase; @@ -73,7 +75,6 @@ protected function setUp() { $this->queryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory') ->disableOriginalConstructor() ->getMock(); - $this->defaultMenuTreeManipulators = new DefaultMenuLinkTreeManipulators($this->accessManager, $this->currentUser, $this->queryFactory); $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); @@ -263,44 +264,70 @@ public function testFlatten() { } /** - * Tests the optimized node access checking. + * Tests the optimized node access checking with both node IDs or UUIDs. * * @covers ::checkNodeAccess * @covers ::collectNodeLinks * @covers ::checkAccess */ - public function testCheckNodeAccess() { + public function testCheckNodeAccessUuidAndNid() { $links = array( 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))), - 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')), + 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' => 'node.5', 'route_name' => 'entity.node.uuid', 'title' => 'qux', 'parent' => 'node.3', 'route_parameters' => array('node' => '7910d537-04be-450a-8200-b53f49ea6b30'))), + 6 => MenuLinkMock::create(array('id' => 'test.1', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => '')), + 7 => MenuLinkMock::create(array('id' => 'test.2', 'route_name' => 'test_route', 'title' => 'qux', 'parent' => 'test.1')), ); $tree = array(); $tree[1] = new MenuLinkTreeElement($links[1], FALSE, 1, FALSE, array()); $tree[2] = new MenuLinkTreeElement($links[2], TRUE, 1, FALSE, array( 3 => new MenuLinkTreeElement($links[3], TRUE, 2, FALSE, array( 4 => new MenuLinkTreeElement($links[4], FALSE, 3, FALSE, array()), + 5 => new MenuLinkTreeElement($links[5], FALSE, 3, FALSE, array()), )), )); - $tree[5] = new MenuLinkTreeElement($links[5], TRUE, 1, FALSE, array( - 6 => new MenuLinkTreeElement($links[6], FALSE, 2, FALSE, array()), + $tree[6] = new MenuLinkTreeElement($links[6], TRUE, 1, FALSE, array( + 7 => new MenuLinkTreeElement($links[7], FALSE, 2, FALSE, array()), )); - $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); + $query = $this->getMock(QueryAggregateInterface::class); + $condition = $this->getMock(ConditionInterface::class); $query->expects($this->at(0)) - ->method('condition') - ->with('nid', array(1, 2, 3, 4)); + ->method('groupBy') + ->with('nid') + ->willReturn($query); $query->expects($this->at(1)) + ->method('groupBy') + ->with('uuid') + ->willReturn($query); + $query->expects($this->once()) + ->method('orConditionGroup') + ->willReturn($condition); + $condition->expects($this->at(0)) + ->method('condition') + ->with('nid', [1, 2, 3, 4]) + ->willReturn($condition); + $condition->expects($this->at(1)) + ->method('condition') + ->with('uuid', [4 => '7910d537-04be-450a-8200-b53f49ea6b30']) + ->willReturn($condition); + $query->expects($this->at(3)) + ->method('condition') + ->with($condition); + $query->expects($this->at(4)) ->method('condition') ->with('status', NODE_PUBLISHED); $query->expects($this->once()) ->method('execute') - ->willReturn(array(1, 2, 4)); + ->willReturn([ + ['nid' => 1, 'uuid' => '9d7316d5-2614-4679-bc00-ec677f248e4e'], + ['nid' => 2, 'uuid' => '2f347698-e682-442b-9d00-f756bf6bec6d'], + ['nid' => 4, 'uuid' => '7910d537-04be-450a-8200-b53f49ea6b30'], + ]); $this->queryFactory->expects($this->once()) - ->method('get') + ->method('getAggregate') ->with('node') ->willReturn($query); @@ -313,8 +340,8 @@ public function testCheckNodeAccess() { $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access); $this->assertEquals($node_access_result, $tree[2]->subtree[3]->subtree[4]->access); // Ensure that other routes than entity.node.canonical are set as well. - $this->assertNull($tree[5]->access); - $this->assertNull($tree[5]->subtree[6]->access); + $this->assertNull($tree[6]->access); + $this->assertNull($tree[6]->subtree[7]->access); // On top of the node access checking now run the ordinary route based // access checkers. @@ -333,8 +360,69 @@ public function testCheckNodeAccess() { $this->assertEquals($node_access_result, $tree[1]->access); $this->assertEquals($node_access_result, $tree[2]->access); $this->assertEquals(AccessResult::neutral(), $tree[2]->subtree[3]->access); - $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $tree[5]->access); - $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), $tree[5]->subtree[6]->access); + $this->assertEquals(AccessResult::allowed()->cachePerPermissions(), $tree[6]->access); + $this->assertEquals(AccessResult::neutral()->cachePerPermissions(), $tree[6]->subtree[7]->access); + } + + /** + * Tests the optimized node access checking with only node IDs or UUIDs. + * + * @covers ::checkNodeAccess + * @covers ::collectNodeLinks + * @covers ::checkAccess + * + * @dataProvider providerCheckNodeAccessNidOrUuidOnly + */ + public function testCheckNodeAccessNidOrUuidOnly($key, array $values) { + $links = [ + 1 => MenuLinkMock::create(['id' => 'node.1', 'route_name' => 'entity.node.canonical', 'title' => 'foo', 'parent' => '', 'route_parameters' => ['node' => $values[0]]]), + 2 => MenuLinkMock::create(['id' => 'node.2', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => ['node' => $values[1]]]), + 3 => MenuLinkMock::create(['id' => 'node.3', 'route_name' => 'entity.node.canonical', 'title' => 'bar', 'parent' => '', 'route_parameters' => ['node' => $values[2]]]), + ]; + $tree = []; + $tree[1] = new MenuLinkTreeElement($links[1], FALSE, 1, FALSE, []); + $tree[2] = new MenuLinkTreeElement($links[2], TRUE, 1, FALSE, []); + $tree[3] = new MenuLinkTreeElement($links[3], TRUE, 1, FALSE, []); + + $query = $this->getMock(QueryAggregateInterface::class); + $query->expects($this->at(0)) + ->method('groupBy') + ->with($key) + ->willReturn($query); + $query->expects($this->at(1)) + ->method('condition') + ->with($key, $values, 'IN'); + $query->expects($this->at(2)) + ->method('condition') + ->with('status', NODE_PUBLISHED); + $query->expects($this->once()) + ->method('execute') + ->willReturn([ + [$key => $values[0]], + [$key => $values[1]], + ]); + $this->queryFactory->expects($this->once()) + ->method('getAggregate') + ->with('node') + ->willReturn($query); + + $node_access_result = AccessResult::allowed()->cachePerPermissions()->addCacheContexts(['user.node_grants:view']); + + $tree = $this->defaultMenuTreeManipulators->checkNodeAccess($tree); + $this->assertEquals($node_access_result, $tree[1]->access); + $this->assertEquals($node_access_result, $tree[2]->access); + // Ensure that access denied is set. + $this->assertEquals(AccessResult::neutral(), $tree[3]->access); + } + + /** + * Data provider for self::testCheckNodeAccessNidOrUuidOnly(). + */ + public function providerCheckNodeAccessNidOrUuidOnly() { + return [ + ['nid', [1, 2, 3]], + ['uuid', ['9d7316d5-2614-4679-bc00-ec677f248e4e', '2f347698-e682-442b-9d00-f756bf6bec6d', '7910d537-04be-450a-8200-b53f49ea6b30']], + ]; } } 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 2808a20..0376b8d 100644 --- a/core/tests/Drupal/Tests/Core/UrlTest.php +++ b/core/tests/Drupal/Tests/Core/UrlTest.php @@ -582,6 +582,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, + ], ]; }