diff --git a/core/core.services.yml b/core/core.services.yml index afccb31..3ae58c3 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1202,6 +1202,11 @@ services: arguments: ['@current_route_match'] tags: - { name: route_processor_outbound, priority: 200 } + route_processor_entity_uuid: + class: Drupal\Core\RouteProcessor\RouteProcessorEntityUUID + arguments: ['@entity.manager'] + tags: + - { name: route_processor_outbound, priority: 100 } path_processor_alias: class: Drupal\Core\PathProcessor\PathProcessorAlias tags: diff --git a/core/lib/Drupal/Core/Entity/EntityTypeManager.php b/core/lib/Drupal/Core/Entity/EntityTypeManager.php index 25f9cff..3a24e0e 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeManager.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeManager.php @@ -177,20 +177,25 @@ public function getListBuilder($entity_type) { * {@inheritdoc} */ public function getFormObject($entity_type, $operation) { - if (!$class = $this->getDefinition($entity_type, TRUE)->getFormClass($operation)) { - throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a "%s" form class.', $entity_type, $operation)); - } + if (!isset($this->handlers['form'][$operation][$entity_type])) { + if (!$class = $this->getDefinition($entity_type, TRUE)->getFormClass($operation)) { + throw new InvalidPluginDefinitionException($entity_type, sprintf('The "%s" entity type did not specify a "%s" form class.', $entity_type, $operation)); + } - $form_object = $this->classResolver->getInstanceFromDefinition($class); + $form_object = $this->classResolver->getInstanceFromDefinition($class); + + $form_object + ->setStringTranslation($this->stringTranslation) + ->setModuleHandler($this->moduleHandler) + ->setEntityTypeManager($this) + ->setOperation($operation) + // The entity manager cannot be injected due to a circular dependency. + // @todo Remove this set call in https://www.drupal.org/node/2603542. + ->setEntityManager(\Drupal::entityManager()); + $this->handlers['form'][$operation][$entity_type] = $form_object; + } - return $form_object - ->setStringTranslation($this->stringTranslation) - ->setModuleHandler($this->moduleHandler) - ->setEntityTypeManager($this) - ->setOperation($operation) - // The entity manager cannot be injected due to a circular dependency. - // @todo Remove this set call in https://www.drupal.org/node/2603542. - ->setEntityManager(\Drupal::entityManager()); + return $this->handlers['form'][$operation][$entity_type]; } /** diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php index 557f2d9..fd046a1 100644 --- a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -140,7 +140,10 @@ public function checkNodeAccess(array $tree) { $nids = array_keys($node_links); $query = $this->queryFactory->get('node'); - $query->condition('nid', $nids, 'IN'); + $group = $query->orConditionGroup() + ->condition('nid', $nids, 'IN') + ->condition('uuid', $nids, '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 @@ -156,9 +159,11 @@ public function checkNodeAccess(array $tree) { } $nids = $query->execute(); - foreach ($nids as $nid) { - foreach ($node_links[$nid] as $key => $link) { - $node_links[$nid][$key]->access = $access_result; + foreach ($node_links as $nid => $links) { + foreach ($links as $key => $link) { + if (isset($nids[$nid])) { + $node_links[$nid][$key]->access = $access_result; + } } } } @@ -179,8 +184,8 @@ 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') { - $nid = $element->link->getRouteParameters()['node']; + if (($url = $element->link->getUrlObject()) && !$url->isExternal() && $url->getRouteName() == 'entity.node.canonical') { + $nid = $element->link->getUrlObject()->getRouteParameters()['node']; $node_links[$nid][$key] = $element; // Deny access by default. checkNodeAccess() will re-add it. $element->access = AccessResult::neutral(); diff --git a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php index e1aa189..b8d584d 100644 --- a/core/lib/Drupal/Core/ParamConverter/EntityConverter.php +++ b/core/lib/Drupal/Core/ParamConverter/EntityConverter.php @@ -7,6 +7,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; @@ -67,6 +68,12 @@ 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/RouteProcessor/RouteProcessorEntityUUID.php b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorEntityUUID.php new file mode 100644 index 0000000..3c5ef78 --- /dev/null +++ b/core/lib/Drupal/Core/RouteProcessor/RouteProcessorEntityUUID.php @@ -0,0 +1,51 @@ +entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) { + if (strpos($route_name, 'entity.') === 0 && list(, $entity_type, $operation) = explode('.', $route_name)) { + if (isset($parameters[$entity_type]) && strlen($parameters[$entity_type]) === 32) { + if ($entities = $this->entityManager->getStorage($entity_type)->loadByProperties(['uuid' => $parameters[$entity_type]])) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = reset($entities); + $parameters[$entity_type] = $entity->id(); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Theme/Registry.php b/core/lib/Drupal/Core/Theme/Registry.php index 3498991..e2a89a9 100644 --- a/core/lib/Drupal/Core/Theme/Registry.php +++ b/core/lib/Drupal/Core/Theme/Registry.php @@ -40,8 +40,7 @@ class Registry implements DestructableInterface { * The complete theme registry. * * @var array - * An array of theme registries, keyed by the theme name. Each registry is - * an associative array keyed by theme hook names, whose values are + * An associative array keyed by theme hook names, whose values are * associative arrays containing the aggregated hook definition: * - type: The type of the extension the original theme hook originates * from; e.g., 'module' for theme hook 'node' of Node module. @@ -80,7 +79,7 @@ class Registry implements DestructableInterface { * - process: An array of theme variable process callbacks to invoke * before invoking the actual theme function or template. */ - protected $registry = []; + protected $registry; /** * The cache backend to use for the complete theme registry data. @@ -97,11 +96,11 @@ class Registry implements DestructableInterface { protected $moduleHandler; /** - * An array of incomplete, runtime theme registries, keyed by theme name. + * The incomplete, runtime theme registry. * - * @var \Drupal\Core\Utility\ThemeRegistry[] + * @var \Drupal\Core\Utility\ThemeRegistry */ - protected $runtimeRegistry = []; + protected $runtimeRegistry; /** * Stores whether the registry was already initialized. @@ -210,20 +209,20 @@ protected function init($theme_name = NULL) { */ public function get() { $this->init($this->themeName); - if (isset($this->registry[$this->theme->getName()])) { - return $this->registry[$this->theme->getName()]; + if (isset($this->registry)) { + return $this->registry; } if ($cache = $this->cache->get('theme_registry:' . $this->theme->getName())) { - $this->registry[$this->theme->getName()] = $cache->data; + $this->registry = $cache->data; } else { - $this->build(); + $this->registry = $this->build(); // Only persist it if all modules are loaded to ensure it is complete. if ($this->moduleHandler->isLoaded()) { $this->setCache(); } } - return $this->registry[$this->theme->getName()]; + return $this->registry; } /** @@ -236,17 +235,17 @@ public function get() { */ public function getRuntime() { $this->init($this->themeName); - if (!isset($this->runtimeRegistry[$this->theme->getName()])) { - $this->runtimeRegistry[$this->theme->getName()] = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->cache, $this->lock, array('theme_registry'), $this->moduleHandler->isLoaded()); + if (!isset($this->runtimeRegistry)) { + $this->runtimeRegistry = new ThemeRegistry('theme_registry:runtime:' . $this->theme->getName(), $this->cache, $this->lock, array('theme_registry'), $this->moduleHandler->isLoaded()); } - return $this->runtimeRegistry[$this->theme->getName()]; + return $this->runtimeRegistry; } /** * Persists the theme registry in the cache backend. */ protected function setCache() { - $this->cache->set('theme_registry:' . $this->theme->getName(), $this->registry[$this->theme->getName()], Cache::PERMANENT, array('theme_registry')); + $this->cache->set('theme_registry:' . $this->theme->getName(), $this->registry, Cache::PERMANENT, array('theme_registry')); } /** @@ -360,9 +359,9 @@ protected function build() { unset($cache[$hook]['preprocess functions']); } } - $this->registry[$this->theme->getName()] = $cache; + $this->registry = $cache; - return $this->registry[$this->theme->getName()]; + return $this->registry; } /** @@ -720,12 +719,12 @@ protected function postProcessExtension(array &$cache, ActiveTheme $theme) { */ public function reset() { // Reset the runtime registry. - foreach ($this->runtimeRegistry as $runtime_registry) { - $runtime_registry->clear(); + if (isset($this->runtimeRegistry) && $this->runtimeRegistry instanceof ThemeRegistry) { + $this->runtimeRegistry->clear(); } - $this->runtimeRegistry = []; + $this->runtimeRegistry = NULL; - $this->registry = []; + $this->registry = NULL; Cache::invalidateTags(array('theme_registry')); return $this; } @@ -734,8 +733,8 @@ public function reset() { * {@inheritdoc} */ public function destruct() { - foreach ($this->runtimeRegistry as $runtime_registry) { - $runtime_registry->destruct(); + if (isset($this->runtimeRegistry)) { + $this->runtimeRegistry->destruct(); } } diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index 7c01f6f..2c78f7b 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -194,8 +194,12 @@ 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', '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); @@ -205,8 +209,12 @@ 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', '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 d88911a..53ee016 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -13,6 +13,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; @@ -74,7 +75,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()); } @@ -226,6 +227,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); } @@ -471,6 +480,18 @@ 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())); } /** diff --git a/core/modules/system/tests/themes/test_stable/test_stable.theme b/core/modules/system/tests/themes/test_stable/test_stable.theme deleted file mode 100644 index 8498d89..0000000 --- a/core/modules/system/tests/themes/test_stable/test_stable.theme +++ /dev/null @@ -1,13 +0,0 @@ - '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)); } @@ -108,6 +114,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/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index 1243118..e0c20cd 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -86,7 +86,8 @@ protected function setUp() { * Tests getting the theme registry defined by a module. */ public function testGetRegistryForModule() { - $test_theme = new ActiveTheme([ + $this->setupTheme('test_theme'); + $this->registry->setTheme(new ActiveTheme([ 'name' => 'test_theme', 'path' => 'core/modules/system/tests/themes/test_theme/test_theme.info.yml', 'engine' => 'twig', @@ -97,29 +98,11 @@ public function testGetRegistryForModule() { 'libraries' => [], 'extension' => '.twig', 'base_themes' => [], - ]); + ])); - $test_stable = new ActiveTheme([ - 'name' => 'test_stable', - 'path' => 'core/modules/system/tests/themes/test_stable/test_stable.info.yml', - 'engine' => 'twig', - 'owner' => 'twig', - 'stylesheets_remove' => [], - 'libraries_override' => [], - 'libraries_extend' => [], - 'libraries' => [], - 'extension' => '.twig', - 'base_themes' => [], - ]); - - $this->themeManager->expects($this->exactly(2)) - ->method('getActiveTheme') - ->willReturnOnConsecutiveCalls($test_theme, $test_stable); - - // Include the module and theme files so that hook_theme can be called. + // Include the module so that hook_theme can be called. include_once $this->root . '/core/modules/system/tests/modules/theme_test/theme_test.module'; - include_once $this->root . '/core/modules/system/tests/themes/test_stable/test_stable.theme'; - $this->moduleHandler->expects($this->exactly(2)) + $this->moduleHandler->expects($this->once()) ->method('getImplementations') ->with('theme') ->will($this->returnValue(array('theme_test'))); @@ -143,24 +126,16 @@ public function testGetRegistryForModule() { $this->assertArrayHasKey('theme_test_function_template_override', $registry); $this->assertArrayNotHasKey('test_theme_not_existing_function', $registry); - $this->assertFalse(in_array('test_stable_preprocess_theme_test_render_element', $registry['theme_test_render_element']['preprocess functions'])); $info = $registry['theme_test_function_suggestions']; $this->assertEquals('module', $info['type']); $this->assertEquals('core/modules/system/tests/modules/theme_test', $info['theme path']); $this->assertEquals('theme_theme_test_function_suggestions', $info['function']); $this->assertEquals(array(), $info['variables']); - - // The second call will initialize with the second theme. Ensure that this - // returns a different object and the discovery for the second theme's - // preprocess function worked. - $other_registry = $this->registry->get(); - $this->assertNotSame($registry, $other_registry); - $this->assertTrue(in_array('test_stable_preprocess_theme_test_render_element', $other_registry['theme_test_render_element']['preprocess functions'])); } - protected function setupTheme() { - $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization); + protected function setupTheme($theme_name = NULL) { + $this->registry = new TestRegistry($this->root, $this->cache, $this->lock, $this->moduleHandler, $this->themeHandler, $this->themeInitialization, $theme_name); $this->registry->setThemeManager($this->themeManager); } @@ -168,10 +143,23 @@ protected function setupTheme() { class TestRegistry extends Registry { + public function setTheme(ActiveTheme $theme) { + $this->theme = $theme; + } + + protected function init($theme_name = NULL) { + } + protected function getPath($module) { if ($module == 'theme_test') { return 'core/modules/system/tests/modules/theme_test'; } } + protected function listThemes() { + } + + protected function initializeTheme() { + } + }