diff --git a/core/modules/menu_ui/src/Hook/MenuUiHooks.php b/core/modules/menu_ui/src/Hook/MenuUiHooks.php index ac7c7602f3be8edd09f97598777659bbbab33983..9b32ffa182476332a0516980346f9d09bad7f223 100644 --- a/core/modules/menu_ui/src/Hook/MenuUiHooks.php +++ b/core/modules/menu_ui/src/Hook/MenuUiHooks.php @@ -16,6 +16,9 @@ use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Hook\Attribute\Hook; +use Drupal\menu_link_content\Entity\MenuLinkContent; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Hook implementations for menu_ui. @@ -24,6 +27,16 @@ class MenuUiHooks { use StringTranslationTrait; + /** + * Constructs a new MenuUiHooks object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager. + */ + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + ) {} + /** * Implements hook_help(). */ @@ -86,6 +99,27 @@ public function blockViewSystemMenuBlockAlter(array &$build, BlockPluginInterfac } } + /** + * Check if user is allowed to use the menu link subform. + * + * @param array $defaults + * An array that contains default values for the menu link form. + * + * @see menu_ui_get_menu_link_defaults() + */ + protected function getMenuLinkContentAccess(array $defaults): AccessResultInterface { + if (!empty($defaults['entity_id'])) { + $entity = MenuLinkContent::load($defaults['entity_id']); + + // The form can be used to edit or delete the menu link. + return $entity->access('update', NULL, TRUE)->andIf($entity->access('delete', NULL, TRUE)); + } + else { + // If the node has no corresponding menu link, users needs to permission to create one. + return $this->entityTypeManager->getAccessControlHandler('menu_link_content')->createAccess(NULL, NULL, [], TRUE); + } + } + /** * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm. * @@ -128,7 +162,7 @@ public function formNodeFormAlter(&$form, FormStateInterface $form_state) : void $form['menu'] = [ '#type' => 'details', '#title' => $this->t('Menu settings'), - '#access' => \Drupal::currentUser()->hasPermission('administer menu'), + '#access' => $this->getMenuLinkContentAccess($defaults), '#open' => (bool) $defaults['id'], '#group' => 'advanced', '#attached' => [ diff --git a/core/modules/menu_ui/tests/modules/menu_link_access_test/menu_link_access_test.info.yml b/core/modules/menu_ui/tests/modules/menu_link_access_test/menu_link_access_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..36ba82e667713b87941c2c348979e4c70503d0e0 --- /dev/null +++ b/core/modules/menu_ui/tests/modules/menu_link_access_test/menu_link_access_test.info.yml @@ -0,0 +1,4 @@ +name: 'Tests menu link access' +type: module +package: Testing +version: VERSION diff --git a/core/modules/menu_ui/tests/modules/menu_link_access_test/src/Hook/MenuLinkAccessTestHooks.php b/core/modules/menu_ui/tests/modules/menu_link_access_test/src/Hook/MenuLinkAccessTestHooks.php new file mode 100644 index 0000000000000000000000000000000000000000..586d2a793691dfb94efdee6a489bb86b6742cf61 --- /dev/null +++ b/core/modules/menu_ui/tests/modules/menu_link_access_test/src/Hook/MenuLinkAccessTestHooks.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\menu_link_access_test\Hook; + +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Access\AccessResultInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Hook\Attribute\Hook; + +/** + * Hook implementations for menu_link_access_test. + */ +class MenuLinkAccessTestHooks { + + /** + * Implements hook_ENTITY_TYPE_access(). + */ + #[Hook('menu_link_content_access')] + public function entityTestAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResultInterface { + if (in_array($operation, ['update', 'delete'])) { + return AccessResult::forbidden(); + } + + return AccessResult::neutral(); + } + + /** + * Implements hook_ENTITY_TYPE_create_access(). + */ + #[Hook('menu_link_content_create_access')] + public function entityTestCreateAccess(AccountInterface $account, $context, $entity_bundle): AccessResultInterface { + return AccessResult::forbidden(); + } + +} diff --git a/core/modules/menu_ui/tests/src/Functional/MenuUiNodeAccessTest.php b/core/modules/menu_ui/tests/src/Functional/MenuUiNodeAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..93acdc88754f03ecbc16ef90ec955d32ea8ced67 --- /dev/null +++ b/core/modules/menu_ui/tests/src/Functional/MenuUiNodeAccessTest.php @@ -0,0 +1,88 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\menu_ui\Functional; + +use Drupal\menu_link_content\Entity\MenuLinkContent; +use Drupal\node\Entity\Node; +use Drupal\Tests\BrowserTestBase; + +/** + * Edit a node when you don't have permission to add or edit menu links. + * + * @group menu_ui + */ +class MenuUiNodeAccessTest extends BrowserTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'menu_ui', + 'test_page_test', + 'node', + 'menu_link_access_test', + ]; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + } + + /** + * Tests menu link create access is enforced. + */ + public function testMenuLinkCreateAccess(): void { + $this->drupalLogin($this->drupalCreateUser([ + 'administer menu', + 'edit any page content', + ])); + $node = Node::create([ + 'type' => 'page', + 'title' => $this->randomMachineName(), + 'uid' => $this->rootUser->id(), + 'status' => 1, + ]); + $node->save(); + + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->elementNotExists('css', 'input[name="menu[title]"]'); + } + + /** + * Tests menu link edit/delete access is enforced. + */ + public function testMenuLinkEditAccess(): void { + $this->drupalLogin($this->drupalCreateUser([ + 'administer menu', + 'edit any page content', + ])); + $mainLinkTitle = $this->randomMachineName(); + $node = Node::create([ + 'type' => 'page', + 'title' => $this->randomMachineName(), + 'uid' => $this->rootUser->id(), + 'status' => 1, + ]); + $node->save(); + MenuLinkContent::create([ + 'link' => [['uri' => 'entity:node/' . $node->id()]], + 'title' => $mainLinkTitle, + 'menu_name' => 'main', + ])->save(); + + $this->drupalGet('node/' . $node->id() . '/edit'); + $this->assertSession()->elementNotExists('css', 'input[name="menu[title]"]'); + } + +} diff --git a/core/modules/menu_ui/tests/src/Functional/MenuUiNodeTest.php b/core/modules/menu_ui/tests/src/Functional/MenuUiNodeTest.php index 69b11f376d03e41b650b65f42fb0f62060a1f04a..0025ae08718c9e0673ba495b99551b5151db0794 100644 --- a/core/modules/menu_ui/tests/src/Functional/MenuUiNodeTest.php +++ b/core/modules/menu_ui/tests/src/Functional/MenuUiNodeTest.php @@ -191,7 +191,9 @@ public function testMenuNodeFormWidget(): void { $this->drupalGet('test-page'); $this->assertSession()->linkNotExists($node_title, 'Found no menu link with the node unpublished'); // Assert that the link exists if published. - $edit['status[value]'] = TRUE; + $edit = [ + 'status[value]' => TRUE, + ]; $this->drupalGet('node/' . $node->id() . '/edit'); $this->submitForm($edit, 'Save'); $this->drupalGet('test-page'); diff --git a/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php b/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php index f87a2639b877248c6616d7daeb8204e1ae1dfc6f..f3f6d51e512e12cd973a0a4440d0dc44f68ce9d2 100644 --- a/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php +++ b/core/modules/menu_ui/tests/src/Kernel/MenuBlockTest.php @@ -71,7 +71,7 @@ public function testOperationLinks(): void { ]); // Test when user does have "administer menu" permission. - $menuUiEntityOperation = new MenuUiHooks(); + $menuUiEntityOperation = new MenuUiHooks(\Drupal::entityTypeManager()); $this->assertEquals([ 'menu-edit' => [ 'title' => 'Edit menu',