diff --git a/core/core.services.yml b/core/core.services.yml index ea97a9a..d9b1839 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1225,11 +1225,6 @@ services: - { name: path_processor_inbound, priority: 200 } - { name: path_processor_outbound, priority: 200 } arguments: ['@config.factory'] - path_processor_entity_uuid: - class: Drupal\Core\PathProcessor\PathProcessorEntityUuid - tags: - - { name: path_processor_inbound, priority: 50 } - arguments: ['@entity_type.manager'] route_processor_current: class: Drupal\Core\RouteProcessor\RouteProcessorCurrent arguments: ['@current_route_match'] diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManager.php b/core/lib/Drupal/Core/Entity/EntityTypeManager.php index ab7f645..a48e2e0 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeManager.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeManager.php @@ -83,6 +83,10 @@ public function processDefinition(&$definition, $plugin_id) { /** @var \Drupal\Core\Entity\EntityTypeInterface $definition */ parent::processDefinition($definition, $plugin_id); + // Add the UUID link template if applicable. + if ($definition->hasKey('uuid') && $definition->hasViewBuilderClass()) { + $definition->setLinkTemplate('uuid', "/{$plugin_id}/{{$plugin_id}}"); + } // All link templates must have a leading slash. foreach ((array) $definition->getLinkTemplates() as $link_relation_name => $link_template) { if ($link_template[0] != '/') { diff --git a/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php b/core/lib/Drupal/Core/Entity/Routing/DefaultHtmlRouteProvider.php index b1f6abd..1979974 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 so that the UUID pattern can 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 21bf078..cf3231db 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -154,11 +154,9 @@ public function checkNodeAccess(array $tree) { } $nids = $query->execute(); - foreach ($node_links as $nid => $links) { - foreach ($links as $key => $link) { - if (isset($nids[$nid])) { - $node_links[$nid][$key]->access = $access_result; - } + foreach ($nids as $nid) { + foreach ($node_links[$nid] as $key => $link) { + $node_links[$nid][$key]->access = $access_result; } } } @@ -179,7 +177,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/PathProcessor/PathProcessorEntityUuid.php b/core/lib/Drupal/Core/PathProcessor/PathProcessorEntityUuid.php deleted file mode 100644 index bbf5223..0000000 --- a/core/lib/Drupal/Core/PathProcessor/PathProcessorEntityUuid.php +++ /dev/null @@ -1,58 +0,0 @@ -entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public function processInbound($path, Request $request) { - $matches = []; - if (preg_match('/^\/([a-z_]+)\/(' . Uuid::VALID_PATTERN . ')$/i', $path, $matches)) { - $entity_type_id = $matches[1]; - $uuid = $matches[2]; - if ($this->entityTypeManager->hasDefinition($entity_type_id)) { - $storage = $this->entityTypeManager->getStorage($entity_type_id); - $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); - if ($entity_type->hasLinkTemplate('canonical') && $entities = $storage->loadByProperties(['uuid' => $uuid])) { - /* @var \Drupal\Core\Entity\EntityInterface $entity */ - $entity = reset($entities); - $path = '/' . ltrim($entity->toUrl()->getInternalPath(), '/'); - } - } - } - return $path; - } - -} diff --git a/core/lib/Drupal/Core/Url.php b/core/lib/Drupal/Core/Url.php index 6908bc8..d7e4831 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; @@ -346,7 +347,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 static::fromUri() for details. * @param string $uri @@ -361,7 +362,11 @@ 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."); + } + if (Uuid::isValid($entity_id)) { + // UUID instead of entity ID. + return new static("entity.$entity_type_id.uuid", [$entity_type_id => $entity_id], $options); } return new static("entity.$entity_type_id.canonical", [$entity_type_id => $entity_id], $options); diff --git a/core/tests/Drupal/KernelTests/Core/PathProcessor/PathProcessorEntityUuidTest.php b/core/tests/Drupal/KernelTests/Core/PathProcessor/PathProcessorEntityUuidTest.php deleted file mode 100644 index 3ad2b40..0000000 --- a/core/tests/Drupal/KernelTests/Core/PathProcessor/PathProcessorEntityUuidTest.php +++ /dev/null @@ -1,102 +0,0 @@ -installSchema('system', ['sequences']); - $this->installEntitySchema('node'); - $this->installEntitySchema('user'); - $this->installEntitySchema('contact_message'); - } - - /** - * Tests ::processInbound(). - * - * @covers ::processInbound - */ - public function testProcessInbound() { - $node_type = NodeType::create([ - 'type' => 'article', - 'name' => 'Article', - ]); - $node_type->save(); - $node = Node::create([ - 'type' => 'article', - 'title' => 'Some node', - 'status' => 1, - 'uid' => 1, - ]); - $node->save(); - - $user = User::create([ - 'name' => 'bob', - 'mail' => 'bob@example.com', - ]); - $user->save(); - - $message_type = ContactForm::create([ - 'id' => 'form', - 'label' => 'A form', - ]); - $message_type->save(); - - $message = Message::create([ - 'contact_form' => 'form', - ]); - $message->save(); - - $fake_uuid = '3FA3D516-FCE7-4788-BCCF-CE41B78F8F28'; - - /* @var \Drupal\Core\PathProcessor\PathProcessorEntityUuid $processor */ - $processor = $this->container->get('path_processor_entity_uuid'); - - $request = Request::create(''); - - // Non matching paths are ignored. - $this->assertEquals('/this-does-not-match', $processor->processInbound('/this-does-not-match', $request)); - // Node and user should be processed to their serial ID/canonical route. - $this->assertEquals('/user/' . $user->id(), $processor->processInbound('/user/' . $user->uuid(), $request)); - $this->assertEquals('/node/' . $node->id(), $processor->processInbound('/node/' . $node->uuid(), $request)); - // Taxonomy is not enable so the entity-type should not be processed. - $this->assertEquals('/taxonomy_term/' . $fake_uuid, $processor->processInbound('/taxonomy_term/' . $fake_uuid, $request)); - // This UUID doesn't exist for node, so should not be processed. - $this->assertEquals('/node/' . $fake_uuid, $processor->processInbound('/node/' . $fake_uuid, $request)); - // Contact messages don't have canonical links, so should not be processed. - $this->assertEquals('/contact_message/' . $message->uuid(), $processor->processInbound('/contact_message/' . $message->uuid(), $request)); - } - -} diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php index c2325d4..08ebd50 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeManagerTest.php @@ -99,6 +99,9 @@ protected function setUpEntityTypeDefinitions($definitions = []) { // Give the entity type a legitimate class to return. $entity_type->getClass()->willReturn($class); + // Don't allow uuid key. + $entity_type->hasKey('uuid')->willReturn(FALSE); + $definitions[$key] = $entity_type->reveal(); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php b/core/tests/Drupal/Tests/Core/Entity/Routing/DefaultHtmlRouteProviderTest.php index 7196652..6639a24 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/PathProcessor/PathProcessorEntityUuidTest.php b/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorEntityUuidTest.php deleted file mode 100644 index 9f70984..0000000 --- a/core/tests/Drupal/Tests/Core/PathProcessor/PathProcessorEntityUuidTest.php +++ /dev/null @@ -1,157 +0,0 @@ -getMock(EntityTypeManagerInterface::class); - } - $processor = new PathProcessorEntityUuid($entity_type_manager); - $this->assertEquals($expected, $processor->processInbound($inbound_path, Request::create($inbound_path))); - } - - /** - * Data provider for ::testProcessInbound(). - * - * @return array - * Test cases. - */ - public function providerProcessInbound() { - $user_uuid = '03C88CB8-76E1-4358-9BEC-EEF4233ED9F7'; - $node_uuid = '1E69BE45-DF30-40C6-B6AB-82E04084DC9E'; - $pony_uuid = 'EDCB122F-4851-41A5-8A9F-0DF94727E6B9'; - $contact_uuid = '72E91376-C52E-49EA-9F82-C7829463B874'; - $not_found_uuid = '3483E574-57D5-45F6-8D6E-3CFEAE605907'; - - // An entity-type with a canonical link. - $entity_type_with_link = $this->getMock(EntityTypeInterface::class); - $entity_type_with_link->expects($this->any()) - ->method('hasLinkTemplate') - ->willReturn(TRUE); - - // An entity-type without a canonical link. - $entity_type_without_link = $this->getMock(EntityTypeInterface::class); - $entity_type_without_link->expects($this->any()) - ->method('hasLinkTemplate') - ->willReturn(FALSE); - - // Entity-manager. - $entity_type_manager = $this->getMock(EntityTypeManagerInterface::class); - $entity_type_manager->expects($this->any()) - ->method('hasDefinition') - ->willReturnCallback(function($entity_type_id) { - return in_array($entity_type_id, ['user', 'node', 'contact'], TRUE); - }); - $entity_type_manager->expects($this->any()) - ->method('getDefinition') - ->willReturnCallback(function($entity_type_id) use ($entity_type_with_link, $entity_type_without_link) { - if (in_array($entity_type_id, ['user', 'node'], TRUE)) { - return $entity_type_with_link; - } - return $entity_type_without_link; - }); - - // Mock node. - $node_url = $this->getMockBuilder(Url::class) - ->disableOriginalConstructor() - ->getMock(); - $node_url->expects($this->any()) - ->method('getInternalPath') - ->willReturn('node/12'); - $node_mock = $this->getMock(NodeInterface::class); - $node_mock->expects($this->any()) - ->method('toUrl') - ->willReturn($node_url); - - // Mock user. - $user_url = $this->getMockBuilder(Url::class) - ->disableOriginalConstructor() - ->getMock(); - $user_url->expects($this->any()) - ->method('getInternalPath') - ->willReturn('user/1'); - $user_mock = $this->getMock(UserInterface::class); - $user_mock->expects($this->any()) - ->method('toUrl') - ->willReturn($user_url); - - // Mock node storage. - $node_storage = $this->getMockBuilder(NodeStorage::class) - ->disableOriginalConstructor() - ->getMock(); - $node_storage->expects($this->any()) - ->method('loadByProperties') - ->with(['uuid' => $node_uuid]) - ->willReturn([ - $node_mock, - ]); - - // Mock user storage. - $user_storage = $this->getMockBuilder(UserStorage::class) - ->disableOriginalConstructor() - ->getMock(); - $user_storage->expects($this->any()) - ->method('loadByProperties') - ->willReturnCallback(function($conditions) use ($user_mock, $user_uuid) { - if ($conditions['uuid'] == $user_uuid) { - return [$user_mock]; - } - return []; - }); - - // Mock entity-manager. - $entity_type_manager->expects($this->any()) - ->method('getStorage') - ->willReturnMap([ - ['user', $user_storage], - ['node', $node_storage], - ]); - return [ - 'irrelevant' => ['/this-does-not-match', '/this-does-not-match'], - 'user-valid' => ['/user/' . $user_uuid, '/user/1', $entity_type_manager], - 'node-valid' => ['/node/' . $node_uuid, '/node/12', $entity_type_manager], - 'invalid-entity-type' => [ - '/pony/' . $pony_uuid, - '/pony/' . $pony_uuid, - $entity_type_manager, - ], - 'no-such-uuid' => [ - '/user/' . $not_found_uuid, - '/user/' . $not_found_uuid, - $entity_type_manager, - ], - 'no-canonical-link' => [ - '/contact/' . $contact_uuid, - '/contact/' . $contact_uuid, - $entity_type_manager, - ], - ]; - } - -} diff --git a/core/tests/Drupal/Tests/Core/UrlTest.php b/core/tests/Drupal/Tests/Core/UrlTest.php index 1b8f080..b05ae5c 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, + ], ]; }