core/core.services.yml | 6 +- core/includes/menu.inc | 5 + .../Core/Menu/DefaultMenuLinkTreeManipulators.php | 146 +++++++++++++ .../Core/Menu/DefaultMenuTreeManipulators.php | 161 -------------- core/lib/Drupal/Core/Menu/MenuLinkTree.php | 139 ++++-------- .../lib/Drupal/Core/Menu/MenuLinkTreeInterface.php | 17 +- .../Drupal/Core/Menu/MenuParentFormSelector.php | 7 +- core/modules/menu_ui/src/MenuForm.php | 13 +- .../system/src/Controller/SystemController.php | 33 ++- .../system/src/Plugin/Block/SystemMenuBlock.php | 5 + core/modules/system/src/SystemManager.php | 5 + core/modules/system/src/Tests/System/AdminTest.php | 19 +- core/modules/system/system.module | 20 +- .../toolbar/src/Tests/ToolbarAdminMenuTest.php | 7 +- core/modules/toolbar/toolbar.module | 74 ++++--- .../user/src/Tests/UserAccountLinksTests.php | 4 + .../Menu/DefautlMenuLinkTreeManipulatorsTest.php | 241 +++++++++++++++++++++ 17 files changed, 565 insertions(+), 337 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 19fc8d8..cde0c05 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -266,10 +266,10 @@ services: arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@module_handler'] menu.link_tree: class: Drupal\Core\Menu\MenuLinkTree - arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@access_manager', '@current_user', '@menu.active_trail'] + arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@access_manager', '@current_user', '@menu.active_trail', '@controller_resolver'] menu.default_tree_manipulators: - class: Drupal\Core\Menu\DefaultMenuTreeManipulators - arguments: ['@router.route_provider', '@access_manager', '@current_user'] + class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators + arguments: ['@access_manager', '@current_user'] menu.active_trail: class: Drupal\Core\Menu\MenuActiveTrail arguments: ['@plugin.manager.menu.link', '@request_stack', '@access_manager', '@current_user', '@config.factory'] diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 7ba61c2..ae269df 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -303,6 +303,11 @@ function menu_navigation_links($menu_name, $level = 0) { $menu_tree = \Drupal::service('menu.link_tree'); $parameters = $menu_tree->buildPageDataTreeParameters($menu_name, $level + 1); $tree = $menu_tree->build($menu_name, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $menu_tree->transform($tree, $manipulators); // Go down the active trail until the right level is reached. while ($level-- > 0 && $tree) { diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php new file mode 100644 index 0000000..2934a24 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/DefaultMenuLinkTreeManipulators.php @@ -0,0 +1,146 @@ +accessManager = $access_manager; + $this->account = $account; + } + + /** + * Tree manipulator that performs access checks. + * + * Removes menu items from the given menu tree whose links are inaccessible + * for the current user, sets the 'access' property to TRUE on tree elements + * that are accessible for the current user. + * + * Makes the resulting menu tree impossible to render cache, unless render + * caching per user is acceptable. + * + * @param array $tree + * The menu tree to manipulate. + * + * @return array + * The manipulated menu tree. + */ + public function checkAccess(array $tree) { + foreach ($tree as $key => $item) { + // Other menu tree manipulators may already have calculated access, do + // not overwrite the existing value in that case. + if (!isset($item['access'])) { + $tree[$key]['access'] = $this->menuLinkCheckAccess($item['link']); + } + if ($tree[$key]['access']) { + if ($tree[$key]['below']) { + $tree[$key]['below'] = $this->checkAccess($tree[$key]['below']); + } + } + else { + unset($tree[$key]); + } + } + return $tree; + } + + /** + * Check access for one menu link instance. + * + * @param \Drupal\Core\Menu\MenuLinkInterface $instance + * The menu link instance. + * + * @return bool + * TRUE if the current user can access the link, FALSE otherwise. + */ + protected function menuLinkCheckAccess(MenuLinkInterface $instance) { + // Use the definition here since that's a lot faster than creating a Url + // object that we don't need. + $definition = $instance->getPluginDefinition(); + // 'url' should only be populated for external links. + if (!empty($definition['url']) && empty($definition['route_name'])) { + $access = TRUE; + } + else { + $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account); + } + return $access; + } + + /** + * Tree manipulator that generates a unique index, and sorts by it. + * + * @param array $tree + * The menu tree to manipulate. + * + * @return array + * The manipulated menu tree. + */ + public function generateIndexAndSort(array $tree) { + $new_tree = array(); + foreach ($tree as $key => $v) { + if ($tree[$key]['below']) { + $tree[$key]['below'] = $this->generateIndexAndSort($tree[$key]['below']); + } + /** @var \Drupal\Core\Menu\MenuLinkInterface $instance */ + $instance = $tree[$key]['link']; + // The weights are made a uniform 5 digits by adding 50000 as an offset. + // After $this->menuLinkCheckAccess(), $instance->getTitle() has the + // localized or translated title. Adding the plugin id to the end of the + // index insures that it is unique. } + $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; + } + ksort($new_tree); + return $new_tree; + } + + /** + * Tree manipulator that flattens the tree to a single level. + * + * @param array $tree + * The menu tree to manipulate. + * + * @return array + * The manipulated menu tree. + */ + public function flatten($tree) { + foreach ($tree as $key => $item) { + if ($tree[$key]['below']) { + $tree[$key]['below'] = $this->flatten($tree[$key]['below']); + foreach ($tree[$key]['below'] as $child_key => $child_item) { + $tree[$child_key] = $child_item; + } + } + } + return $tree; + } + +} diff --git a/core/lib/Drupal/Core/Menu/DefaultMenuTreeManipulators.php b/core/lib/Drupal/Core/Menu/DefaultMenuTreeManipulators.php deleted file mode 100644 index c909ac7..0000000 --- a/core/lib/Drupal/Core/Menu/DefaultMenuTreeManipulators.php +++ /dev/null @@ -1,161 +0,0 @@ -routeProvider = $route_provider; - $this->accessManager = $access_manager; - $this->account = $account; - } - - /** - * Tree manipulator that performs access checks. - * - * Removes menu items from the given menu tree whose links are inaccessible - * for the current user, sets the 'access' property to TRUE on tree elements - * that are accessible for the current user. - * - * Makes the resulting menu tree impossible to render cache, unless render - * caching per user is acceptable. - * - * @param array $tree - * The menu tree to manipulate. - * - * @return array - * The manipulated menu tree. - */ - public function checkAccess(array $tree) { - $this->doTreeCheckAccess($tree); - return $tree; - } - - /** - * Helper function that recursively checks access for each item. - */ - protected function doTreeCheckAccess(&$tree) { - foreach ($tree as $key => $item) { - // Other menu tree manipulators may already have calculated access, do - // not overwrite the existing value in that case. - if (!isset($item['access'])) { - $tree[$key]['access'] = $this->menuLinkCheckAccess($item['link']); - } - if ($tree[$key]['access']) { - if ($tree[$key]['below']) { - $this->doTreeCheckAccess($tree[$key]['below']); - } - } - else { - unset($tree[$key]); - } - } - } - - /** - * Check access for one menu link instance. - * - * @param \Drupal\Core\Menu\MenuLinkInterface $instance - * The menu link instance. - * - * @return bool - * TRUE if the current user can access the link, FALSE otherwise. - */ - protected function menuLinkCheckAccess(MenuLinkInterface $instance) { - // Use the definition here since that's a lot faster than creating a Url - // object that we don't need. - $definition = $instance->getPluginDefinition(); - // 'url' should only be populated for external links. - if (!empty($definition['url']) && empty($definition['route_name'])) { - $access = TRUE; - } - else { - $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account); - } - return $access; - } - - /** - * Tree manipulator that generates a unique index, and sorts by it. - * - * @param array $tree - * The menu tree to manipulate. - * - * @return array - * The manipulated menu tree. - */ - public function generateIndexAndSort(array $tree) { - $new_tree = array(); - foreach ($tree as $key => $v) { - if ($tree[$key]['below']) { - $tree[$key]['below'] = $this->generateIndexAndSort($tree[$key]['below']); - } - /** @var \Drupal\Core\Menu\MenuLinkInterface $instance */ - $instance = $tree[$key]['link']; - // The weights are made a uniform 5 digits by adding 50000 as an offset. - // After $this->menuLinkCheckAccess(), $instance->getTitle() has the - // localized or translated title. Adding the plugin id to the end of the - // index insures that it is unique. } - $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; - } - ksort($new_tree); - return $new_tree; - } - - /** - * Tree manipulator that flattens the tree to a single level. - * - * @param array $tree - * The menu tree to manipulate. - * - * @return array - * The manipulated menu tree. - */ - public function flatten($tree) { - foreach ($tree as $key => $item) { - if ($tree[$key]['below']) { - $tree[$key]['below'] = $this->flatten($tree[$key]['below']); - foreach ($tree[$key]['below'] as $child_key => $child_item) { - $tree[$child_key] = $child_item; - } - } - } - return $tree; - } - -} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php index 6994452..1cbf233 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php @@ -7,9 +7,12 @@ namespace Drupal\Core\Menu; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Access\AccessManager; +use Drupal\Core\Controller\ControllerResolverInterface; use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\Session\AccountInterface; +use Symfony\Component\DependencyInjection\Tests\Compiler\InlineServiceDefinitionsPassTest; /** * Manages discovery, instantiation, and tree building of menu link plugins. @@ -61,6 +64,13 @@ class MenuLinkTree implements MenuLinkTreeInterface { protected $menuActiveTrail; /** + * The controller resolver. + * + * @var \Drupal\Core\Controller\ControllerResolverInterface + */ + protected $controllerResolver; + + /** * Constructs a \Drupal\Core\Menu\MenuLinkTree object. * * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage @@ -75,14 +85,17 @@ class MenuLinkTree implements MenuLinkTreeInterface { * The current user. * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail * The active menu trail service. + * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver + * The controller resolver. */ - public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, AccessManager $access_manager, AccountInterface $account, MenuActiveTrailInterface $menu_active_trail) { + public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, AccessManager $access_manager, AccountInterface $account, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) { $this->treeStorage = $tree_storage; $this->menuLinkManager = $menu_link_manager; $this->routeProvider = $route_provider; $this->accessManager = $access_manager; $this->account = $account; $this->menuActiveTrail = $menu_active_trail; + $this->controllerResolver = $controller_resolver; } /** @@ -135,6 +148,9 @@ public function render($tree) { $element['#title'] = $link->getTitle(); $element['#url'] = $link->getUrlObject(); $element['#below'] = $data['below'] ? $this->render($data['below']) : array(); + if (isset($data['options'])) { + $element['#url']->setOptions(NestedArray::mergeDeep($element['#url']->getOptions(), $data['options'])); + } $element['#original_link'] = $link; // Index using the link's unique ID. $build[$link->getPluginId()] = $element; @@ -215,37 +231,16 @@ public function buildAllDataTreeParameters($id = NULL, $max_depth = NULL) { /** * {@inheritdoc} */ - public function getChildLinks($id, $max_relative_depth = NULL) { - $links = array(); - $definitions = $this->treeStorage->loadAllChildLinks($id, $max_relative_depth); - foreach ($definitions as $id => $definition) { - $access = $this->menuLinkCheckAccess($definition); - if ($access) { - $links[$id] = $this->menuLinkManager->createInstance($definition['id']);; - } - } - return $links; - } - - /** - * {@inheritdoc} - */ public function buildSubtree($id, $max_relative_depth = NULL) { $data = $this->treeStorage->loadSubtreeData($id, $max_relative_depth); if ($data['subtree']) { - $subtree = $data['subtree']; - // Check access and instantiate. - $access = $this->menuLinkCheckAccess($subtree['definition']); - if ($access) { - $subtree['link'] = $this->menuLinkManager->createInstance($subtree['definition']['id']); - // Pre-load all the route objects in the tree for access checks. - if ($data['route_names']) { - $this->routeProvider->getRoutesByNames($data['route_names']); - } - $this->createInstances($subtree['below']); - $this->treeCheckAccess($subtree['below']); - return $subtree; + // Pre-load all the route objects in the subtree for access checks. + if ($data['route_names']) { + $this->routeProvider->getRoutesByNames($data['route_names']); } + $tree = array($data['subtree']['definition']['id'] => $data['subtree']); + $this->createInstances($tree); + return $tree; } return NULL; } @@ -261,19 +256,27 @@ public function build($menu_name, array $parameters = array()) { } $tree = $data['tree']; $this->createInstances($tree); - $this->treeCheckAccess($tree); return $tree; } /** - * Sorts the menu tree and recursively checks access for each item. - * - * @param array $tree - * The menu tree you wish to operate on. + * {@inheritdoc} */ - protected function treeCheckAccess(&$tree) { - $this->doTreeCheckAccess($tree); - $this->sortTree($tree); + public function transform(array $tree, array $manipulators) { + foreach ($manipulators as $manipulator) { + // Prepare the arguments for the menu tree manipulator callable; the first + // argument is always the menu link tree. + $args = isset($manipulator['args']) ? $manipulator['args'] : array(); + array_unshift($args, $tree); + + $callable = $manipulator['callable']; + if (strpos($callable, '::') === FALSE) { + $callable = $this->controllerResolver->getControllerFromDefinition($callable); + } + + $tree = call_user_func_array($callable, $args); + } + return $tree; } /** @@ -285,70 +288,8 @@ protected function createInstances(&$tree) { $this->createInstances($tree[$key]['below']); } $tree[$key]['link'] = $this->menuLinkManager->createInstance($tree[$key]['definition']['id']); + unset($tree[$key]['definition']); } } - /** - * Helper function that recursively checks access for each item. - */ - protected function doTreeCheckAccess(&$tree) { - foreach ($tree as $key => $v) { - $definition = $tree[$key]['definition']; - $tree[$key]['access'] = $this->menuLinkCheckAccess($definition); - if ($tree[$key]['access']) { - if ($tree[$key]['below']) { - $this->doTreeCheckAccess($tree[$key]['below']); - } - unset($tree[$key]['definition']); - } - else { - unset($tree[$key]); - } - } - } - - /** - * Sorts the menu tree and recursively using the weight and title. - * - * @param array $tree - * The menu tree you wish to operate on. - */ - protected function sortTree(&$tree) { - $new_tree = array(); - foreach ($tree as $key => $v) { - if ($tree[$key]['below']) { - $this->sortTree($tree[$key]['below']); - } - $instance = $tree[$key]['link']; - // The weights are made a uniform 5 digits by adding 50000 as an offset. - // After $this->menuLinkCheckAccess(), $instance->getTitle() has the - // localized or translated title. Adding the plugin id to the end of the - // index insures that it is unique. - $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; - } - // Sort siblings in the tree based on the weights and localized titles. - ksort($new_tree); - $tree = $new_tree; - } - - /** - * Check access for the item and create an instance if it is accessible. - * - * @param array $definition - * The menu link definition. - * - * @return \Drupal\Core\Menu\MenuLinkInterface|NULL - * A plugin instance or NULL if the current user can not access its route. - */ - protected function menuLinkCheckAccess(array $definition) { - // 'url' should only be populated for external links. - if (!empty($definition['url']) && empty($definition['route_name'])) { - $access = TRUE; - } - else { - $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account); - } - return $access; - } - } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php index 92c03d3..e3c2851 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php @@ -146,17 +146,16 @@ public function build($menu_name, array $parameters = array()); public function buildSubtree($id, $max_relative_depth = NULL); /** - * Loads all child links of a given menu link. + * Applies the given tree manipulators in order to a menu link tree. * - * @param string $id - * The menu link plugin ID. - * - * @param int $max_relative_depth - * If provided, limit the maximum relative depth of children retrieved. + * @param array $tree + * The menu tree to manipulate. + * @param array $manipulators + * The menu tree manipulators to apply. * - * @return \Drupal\Core\Menu\MenuLinkInterface[] - * An array of child links keyed by ID. + * @return array + * The manipulated menu tree. */ - public function getChildLinks($id, $max_relative_depth = NULL); + public function transform(array $tree, array $manipulators); } diff --git a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php index b0fd804..b6ee9d8 100644 --- a/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php +++ b/core/lib/Drupal/Core/Menu/MenuParentFormSelector.php @@ -14,7 +14,7 @@ class MenuParentFormSelector implements MenuParentFormSelectorInterface { /** - * The menu link tree storage. + * The menu link tree service. * * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ @@ -47,6 +47,11 @@ public function getParentSelectOptions($id = '', array $menus = NULL) { $parameters = $this->menuLinkTree->buildAllDataTreeParameters(NULL, $depth_limit); $tree = $this->menuLinkTree->build($menu_name, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuLinkTree->transform($tree, $manipulators); $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit); } return $options; diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index d73fca2..2512bbe 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -214,17 +214,22 @@ protected function buildOverviewForm(array &$form, array &$form_state) { $form_state += array('menu_overview_form_parents' => array()); $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css'); - // We indicate that a menu administrator is running the menu access check. - $this->getRequest()->attributes->set('_menu_admin', TRUE); $parameters = $this->menuTree->buildAllDataTreeParameters(); $tree = $this->menuTree->build($this->entity->id(), $parameters); + // We indicate that a menu administrator is running the menu access check. + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $this->getRequest()->attributes->set('_menu_admin', TRUE); + $tree = $this->menuTree->transform($tree, $manipulators); + $this->getRequest()->attributes->set('_menu_admin', FALSE); + $count = $this->countElements($tree); $delta = max($count, 50); - $this->getRequest()->attributes->set('_menu_admin', FALSE); - $form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta)); $form['#empty_text'] = $this->t('There are no menu links yet. Add link.', array('@link' => url('admin/structure/menu/manage/' . $this->entity->id() .'/add'))); diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index 09fc577..61fe270 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -7,12 +7,12 @@ namespace Drupal\system\Controller; -use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Component\Serialization\Json; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Theme\ThemeAccessCheck; use Drupal\system\SystemManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -58,6 +58,13 @@ class SystemController extends ControllerBase { protected $themeHandler; /** + * The menu link tree service. + * + * @var \Drupal\Core\Menu\MenuLinkTreeInterface + */ + protected $menuLinkTree; + + /** * Constructs a new SystemController. * * @param \Drupal\system\SystemManager $systemManager @@ -70,13 +77,16 @@ class SystemController extends ControllerBase { * The form builder. * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface + * The menu link tree service. */ - public function __construct(SystemManager $systemManager, QueryFactory $queryFactory, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler) { + public function __construct(SystemManager $systemManager, QueryFactory $queryFactory, ThemeAccessCheck $theme_access, FormBuilderInterface $form_builder, ThemeHandlerInterface $theme_handler, MenuLinkTreeInterface $menu_link_tree) { $this->systemManager = $systemManager; $this->queryFactory = $queryFactory; $this->themeAccess = $theme_access; $this->formBuilder = $form_builder; $this->themeHandler = $theme_handler; + $this->menuLinkTree = $menu_link_tree; } /** @@ -88,7 +98,8 @@ public static function create(ContainerInterface $container) { $container->get('entity.query'), $container->get('access_check.theme'), $container->get('form_builder'), - $container->get('theme_handler') + $container->get('theme_handler'), + $container->get('menu.link_tree') ); } @@ -106,15 +117,13 @@ public function overview($link_id) { if ($this->systemManager->checkRequirements() && $this->currentUser()->hasPermission('administer site configuration')) { drupal_set_message($this->t('One or more problems were detected with your Drupal installation. Check the status report for more information.', array('@status' => url('admin/reports/status'))), 'error'); } - /* @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ - $menu_tree = \Drupal::menuTree(); - //$system_link = $menu_tree->createInstance($link_id); // 'system.admin_config' - // Only find the children of this link. - //$parameters['expanded'][] = 'system.admin_config'; - //$parameters['conditions']['hidden'] = 0; - //$tree = $menu_tree->buildTree($system_link->getMenuName(), $parameters); - $top_tree = $menu_tree->buildSubtree($link_id, 1); - $tree = !empty($top_tree['below']) ? $top_tree['below'] : array(); + $top_tree = $this->menuLinkTree->buildSubtree($link_id, 1); + $tree = !empty($top_tree[$link_id]['below']) ? $top_tree[$link_id]['below'] : array(); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuLinkTree->transform($tree, $manipulators); $blocks = array(); // Load all menu links below it. foreach ($tree as $key => $item) { diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index 9716dd8..e6fdd7c 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -81,6 +81,11 @@ public function build() { $menu_name = $this->getDerivativeId(); $parameters = $this->menuTree->buildPageDataTreeParameters($menu_name); $tree = $this->menuTree->build($menu_name, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuTree->transform($tree, $manipulators); return $this->menuTree->render($tree); } diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 2e09e49..98b3071 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -219,6 +219,11 @@ public function getAdminBlock(MenuLinkInterface $instance) { $parameters['expanded'][] = $instance->getPluginId(); $parameters['conditions']['hidden'] = 0; $tree = $this->menuTree->build($instance->getMenuName(), $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuTree->transform($tree, $manipulators); foreach ($tree as $key => $item) { /** @var $link \Drupal\Core\Menu\MenuLinkInterface */ $link = $item['link']; diff --git a/core/modules/system/src/Tests/System/AdminTest.php b/core/modules/system/src/Tests/System/AdminTest.php index 04543db..3a15495 100644 --- a/core/modules/system/src/Tests/System/AdminTest.php +++ b/core/modules/system/src/Tests/System/AdminTest.php @@ -131,9 +131,24 @@ function testAdminPages() { protected function getTopLevelMenuLinks() { /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = $this->container->get('menu.link_tree'); + // The system.admin link is normally the parent of all top-level admin links. - $menu_items = $menu_tree->getChildLinks('system.admin', 1); - return $menu_items; + $link_id = 'system.admin'; + $top_tree = $menu_tree->buildSubtree($link_id, 1); + $tree = !empty($top_tree[$link_id]['below']) ? $top_tree[$link_id]['below'] : array(); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:flatten'), + ); + $tree = $menu_tree->transform($tree, $manipulators); + + // Transform the tree to a list of menu links. + $menu_links = array(); + foreach ($tree as $item) { + $menu_links[] = $item['link']; + } + + return $menu_links; } /** diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 1e6bffa..9fd85f4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1454,24 +1454,28 @@ function system_admin_compact_mode() { * An array of task links. */ function system_get_module_admin_tasks($module, array $info) { - $links = &drupal_static(__FUNCTION__); + $tree = &drupal_static(__FUNCTION__); /* @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); - if (!isset($links)) { - $links = array(); - /* @var \Drupal\Core\Menu\MenuLinkInterface $admin_link */ - $links = $menu_tree->getChildLinks('system.admin'); + if (!isset($tree)) { + $tree = $menu_tree->buildSubtree('system.admin'); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'menu.default_tree_manipulators:flatten'), + ); + $tree = $menu_tree->transform($tree, $manipulators); } $admin_tasks = array(); - $titles = array(); - foreach ($links as $id => $instance) { + foreach ($tree as $item) { + $instance = $item['link']; if ($instance->getProvider() != $module) { continue; } - $admin_tasks[$id] = array( + $admin_tasks[] = array( 'title' => $instance->getTitle(), 'description' => $instance->getDescription(), 'url' => $instance->getUrlObject(), diff --git a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php index 4f8d90c..627e164 100644 --- a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php +++ b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php @@ -133,14 +133,13 @@ function testModuleStatusChangeSubtreesHashCacheClear() { * Tests toolbar cache tags implementation. */ function testMenuLinkUpdateSubtreesHashCacheClear() { - $links = \Drupal::menuTree()->getChildLinks('system.admin', 2); - $links = array_values($links); - $link = array_shift($links); + // The ID of a (any) admin menu link. + $admin_menu_link_id = 'system.admin_config_development'; // Disable the link. $edit = array(); $edit['enabled'] = FALSE; - $this->drupalPostForm("admin/structure/menu/link/" . $link->getPluginId() . "/edit", $edit, t('Save')); + $this->drupalPostForm("admin/structure/menu/link/" . $admin_menu_link_id . "/edit", $edit, t('Save')); $this->assertResponse(200); $this->assertText('The menu link has been saved.'); diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 59753c5..c760871 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -348,13 +348,17 @@ function toolbar_toolbar() { $menu_tree = \Drupal::menuTree(); // Retrieve the administration menu from the menu tree manager. - $tree = $menu_tree->build('admin', array( + $parameters = array( 'expanded' => array('system.admin') - )); - + ); + $tree = $menu_tree->build('admin', $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'toolbar_menu_navigation_links'), + ); + $tree = $menu_tree->transform($tree, $manipulators); $render_tree = $menu_tree->render($tree); - // Add attributes to the links before rendering. - toolbar_menu_navigation_links($render_tree); $menu = array( '#heading' => t('Administration menu'), @@ -408,45 +412,41 @@ function toolbar_toolbar() { } /** - * Modifies a render array of links from a menu tree array. + * Tree manipulator that adds toolbar-specific attributes. * - * Adds route-based IDs and icon placeholders to the link attributes. + * @param array $tree + * The menu tree to manipulate. * - * @param array $render_tree - * A menu link tree render array. The contained url objects are modified. + * @return array + * The manipulated menu tree. */ -function toolbar_menu_navigation_links($render_tree) { - foreach (Element::children($render_tree, FALSE) as $key) { - $element = $render_tree[$key]; - // Configure sub-items. - if (!empty($element['#below'])) { - toolbar_menu_navigation_links($render_tree[$key]['#below']); +function toolbar_menu_navigation_links($tree) { + foreach ($tree as $key => $item) { + if (!empty($item['below'])) { + toolbar_menu_navigation_links($tree[$key]['below']); } - /** @var \Drupal\Core\Url $url */ - $url = $element['#url']; + // Make sure we have a path specific ID in place, so we can attach icons - // and behaviors to the items. + // and behaviors to the menu links. + $link = $item['link']; + $url = $link->getUrlObject(); if ($url->isExternal()) { - // This is an unusual case, so just get a distinct, safe string + // This is an unusual case, so just get a distinct, safe string. $id = substr(Crypt::hashBase64($url->getPath()), 0, 16); } else { $id = str_replace(array('.', '<', '>'), array('-', '', ''), $url->getRouteName()); } - $options = $url->getOptions(); - $options['attributes'] = array( - 'id' => 'toolbar-link-' . $id, - 'class' => array( - 'toolbar-icon', - ), - ); + // Get the non-localized title to make the icon class. - $definition = $element['#original_link']->getPluginDefinition(); - if (!empty($definition['title'])) { - $options['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(' ', '-', $definition['title'])); - } - $url->setOptions($options); + $definition = $link->getPluginDefinition(); + + $tree[$key]['options']['attributes']['id'] = 'toolbar-link-' . $id; + $tree[$key]['options']['attributes']['class'][] = 'toolbar-icon'; + $tree[$key]['options']['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(' ', '-', $definition['title'])); + $tree[$key]['options']['attributes']['title'] = String::checkPlain($link->getDescription()); } + return $tree; } /** @@ -455,15 +455,21 @@ function toolbar_menu_navigation_links($render_tree) { function toolbar_get_rendered_subtrees() { /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); - $top_tree = $menu_tree->buildSubtree('system.admin', 3); - $tree = !empty($top_tree['below']) ? $top_tree['below'] : array(); + $link_id = 'system.admin'; + $top_tree = $menu_tree->buildSubtree($link_id, 3); + $tree = !empty($top_tree[$link_id]['below']) ? $top_tree[$link_id]['below'] : array(); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'toolbar_menu_navigation_links'), + ); + $tree = $menu_tree->transform($tree, $manipulators); $subtrees = array(); foreach ($tree as $tree_item) { /** @var \Drupal\Core\Menu\MenuLinkInterface $item */ $item = $tree_item['link']; if ($tree_item['below']) { $subtree = $menu_tree->render($tree_item['below']); - toolbar_menu_navigation_links($subtree); $output = drupal_render($subtree); } else { diff --git a/core/modules/user/src/Tests/UserAccountLinksTests.php b/core/modules/user/src/Tests/UserAccountLinksTests.php index c472b4b..ee529e9 100644 --- a/core/modules/user/src/Tests/UserAccountLinksTests.php +++ b/core/modules/user/src/Tests/UserAccountLinksTests.php @@ -70,6 +70,10 @@ function testSecondaryMenu() { /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ $menu_tree = \Drupal::service('menu.link_tree'); $tree = $menu_tree->build('account'); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + ); + $tree = $menu_tree->transform($tree, $manipulators); $this->assertEqual(count($tree), 1, 'The secondary links menu contains only one menu link.'); $link = reset($tree); $link = $link['link']; diff --git a/core/tests/Drupal/Tests/Core/Menu/DefautlMenuLinkTreeManipulatorsTest.php b/core/tests/Drupal/Tests/Core/Menu/DefautlMenuLinkTreeManipulatorsTest.php new file mode 100644 index 0000000..c14ce7f --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/DefautlMenuLinkTreeManipulatorsTest.php @@ -0,0 +1,241 @@ + 'Tests \Drupal\Core\Menu\DefaultMenuLinkTreeManipulators', + 'description' => '', + 'group' => 'Menu', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->accessManager = $this->getMockBuilder('\Drupal\Core\Access\AccessManager') + ->disableOriginalConstructor()->getMock(); + $this->currentUser = $this->getMock('Drupal\Core\Session\AccountInterface'); + + $this->defaultMenuTreeManipulators = new DefaultMenuLinkTreeManipulators($this->accessManager, $this->currentUser); + } + + /** + * Mock a menu link. + * + * @param array $definition + * A menu link definition. + * + * @return \Drupal\Core\Menu\MenuLinkDefault + * The menu link instance. + */ + protected function mockLink(array $definition) { + $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $container->expects($this->any()) + ->method('get') + ->with('menu_link.static.overrides') + ->will($this->returnValue($this->getMock('\Drupal\Core\Menu\StaticMenuLinkOverridesInterface'))); + + $string_translation = $this->getMock('\Drupal\Core\StringTranslation\TranslationInterface'); + $string_translation->expects($this->any()) + ->method('translate') + ->will($this->returnArgument(0)); + + $defaults = array ( + 'menu_name' => 'admin', + 'route_name' => 'MUST BE PROVIDED', + 'route_parameters' => array(), + 'url' => '', + 'title' => 'MUST BE PROVIDED', + 'title_arguments' => array(), + 'title_context' => '', + 'description' => '', + 'parent' => 'MUST BE PROVIDED', + 'weight' => '0', + 'options' => array(), + 'expanded' => '0', + 'hidden' => '0', + 'discovered' => '1', + 'provider' => $this->randomName(), + 'metadata' => array(), + 'class' => 'Drupal\\Core\\Menu\\MenuLinkDefault', + 'form_class' => 'Drupal\\Core\\Menu\\Form\\MenuLinkDefaultForm', + 'id' => 'MUST BE PROVIDED', + ); + + $menu_link = MenuLinkDefault::create($container, array(), $definition['id'], $definition + $defaults); + $menu_link->setStringTranslation($string_translation); + + return $menu_link; + } + + /** + * This mocks a tree with the following structure: + * - 1 + * - 2 + * - 3 + * - 4 + * - 5 + * - 7 + * - 6 + * - 8 + * + * With link 6 being the only external link. + */ + protected function mockTree() { + $this->links = array( + 1 => $this->mockLink(array('id' => 'test.example1', 'route_name' => 'example1', 'title' => 'foo', 'parent' => '')), + 2 => $this->mockLink(array('id' => 'test.example2', 'route_name' => 'example2', 'title' => 'bar', 'parent' => 'test.example1', 'route_parameters' => array('foo' => 'bar'))), + 3 => $this->mockLink(array('id' => 'test.example3', 'route_name' => 'example3', 'title' => 'baz', 'parent' => 'test.example2', 'route_parameters' => array('baz' => 'qux'))), + 4 => $this->mockLink(array('id' => 'test.example4', 'route_name' => 'example4', 'title' => 'qux', 'parent' => 'test.example3')), + 5 => $this->mockLink(array('id' => 'test.example5', 'route_name' => 'example5', 'title' => 'foofoo', 'parent' => '')), + 6 => $this->mockLink(array('id' => 'test.example6', 'route_name' => '', 'url' => 'https://drupal.org/', 'title' => 'barbar', 'parent' => '')), + 7 => $this->mockLink(array('id' => 'test.example7', 'route_name' => 'example7', 'title' => 'bazbaz', 'parent' => '')), + 8 => $this->mockLink(array('id' => 'test.example8', 'route_name' => 'example8', 'title' => 'quxqux', 'parent' => '')), + ); + $this->originalTree = array(); + $this->originalTree[1] = array('link' => $this->links[1], 'below' => array()); + $this->originalTree[2] = array('link' => $this->links[2], 'below' => array( + 3 => array('link' => $this->links[3], 'below' => array( + 4 => array('link' => $this->links[4], 'below' => array()), + )), + )); + $this->originalTree[5] = array('link' => $this->links[5], 'below' => array( + 7 => array('link' => $this->links[7], 'below' => array()), + )); + $this->originalTree[6] = array('link' => $this->links[6], 'below' => array()); + $this->originalTree[8] = array('link' => $this->links[8], 'below' => array()); + } + + /** + * Tests the generateIndexAndSort() tree manipulator. + * + * @covers ::generateIndexAndSort + */ + public function testGenerateIndexAndSort() { + $this->mockTree(); + $tree = $this->originalTree; + $tree = $this->defaultMenuTreeManipulators->generateIndexAndSort($tree); + + // Validate that parent items #1, #2, #5 and #6 exist on the root level. + $this->assertEquals($this->links[1]->getPluginId(), $tree['50000 foo test.example1']['link']->getPluginId()); + $this->assertEquals($this->links[2]->getPluginId(), $tree['50000 bar test.example2']['link']->getPluginId()); + $this->assertEquals($this->links[5]->getPluginId(), $tree['50000 foofoo test.example5']['link']->getPluginId()); + $this->assertEquals($this->links[6]->getPluginId(), $tree['50000 barbar test.example6']['link']->getPluginId()); + $this->assertEquals($this->links[8]->getPluginId(), $tree['50000 quxqux test.example8']['link']->getPluginId()); + + // Validate that child item #4 exists at the correct location in the hierarchy. + $this->assertEquals($this->links[4]->getPluginId(), $tree['50000 bar test.example2']['below']['50000 baz test.example3']['below']['50000 qux test.example4']['link']->getPluginId()); + // Validate that child item #7 exists at the correct location in the hierarchy. + $this->assertEquals($this->links[7]->getPluginId(), $tree['50000 foofoo test.example5']['below']['50000 bazbaz test.example7']['link']->getPluginId()); + } + + /** + * Tests the checkAccess() tree manipulator. + * + * @covers ::checkAccess + */ + public function testCheckAccess() { + // Those menu links that are non-external will have their access checks + // performed. 8 routes, but 1 is external, 2 already have their 'access' + // property set, and 1 is a child if an inaccessible menu link, so only 4 + // calls will be made. + $this->accessManager->expects($this->exactly(4)) + ->method('checkNamedRoute') + ->will($this->returnValueMap(array( + array('example1', array(), $this->currentUser, NULL, FALSE), + array('example2', array('foo' => 'bar'), $this->currentUser, NULL, TRUE), + array('example3', array('baz' => 'qux'), $this->currentUser, NULL, FALSE), + array('example5', array(), $this->currentUser, NULL, TRUE), + ))); + + $this->mockTree(); + $this->originalTree[5]['below'][7]['access'] = TRUE; + $this->originalTree[8]['access'] = FALSE; + + $tree = $this->defaultMenuTreeManipulators->checkAccess($this->originalTree); + + // Menu link 1: route without parameters, access forbidden, hence removed. + $this->assertFalse(array_key_exists(1, $tree)); + // Menu link 2: route with parameters, access granted. + $item = $tree[2]; + $this->assertTrue(array_key_exists('access', $item)); + $this->assertTrue($item['access']); + // Menu link 3: route with parameters, access forbidden, hence removed, + // including its children. + $this->assertFalse(array_key_exists(3, $tree[2]['below'])); + // Menu link 4: child of menu link 3, which already is removed. + $this->assertSame(array(), $tree[2]['below']); + // Menu link 5: no route name, treated as external, hence access granted. + $item = $tree[5]; + $this->assertTrue(array_key_exists('access', $item)); + $this->assertTrue($item['access']); + // Menu link 6: external URL, hence access granted. + $item = $tree[6]; + $this->assertTrue(array_key_exists('access', $item)); + $this->assertTrue($item['access']); + // Menu link 7: 'access' already set. + $item = $tree[5]['below'][7]; + $this->assertTrue(array_key_exists('access', $item)); + $this->assertTrue($item['access']); + // Menu link 8: 'access' already set, to FALSE, hence removed. + $this->assertFalse(array_key_exists(8, $tree)); + } + + /** + * Tests the flatten() tree manipulator. + * + * @covers ::flatten + */ + public function testFlatten() { + $this->mockTree(); + $tree = $this->defaultMenuTreeManipulators->flatten($this->originalTree); + $this->assertEquals(array(1, 2, 5, 6, 8), array_keys($this->originalTree)); + $this->assertEquals(array(1, 2, 5, 6, 8, 3, 4, 7), array_keys($tree)); + } + +}