core/core.services.yml | 2 +- core/lib/Drupal/Core/Menu/MenuActiveTrail.php | 171 ++++----------------- .../Drupal/Core/Menu/MenuActiveTrailInterface.php | 43 ++---- .../system/src/Plugin/Block/SystemMenuBlock.php | 32 +--- core/modules/system/src/SystemManager.php | 6 +- core/modules/toolbar/toolbar.module | 69 +++++---- .../Drupal/Tests/Core/Menu/MenuActiveTrailTest.php | 160 +++++++++++++++++++ 7 files changed, 254 insertions(+), 229 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index cde0c05..6d32ed7 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -272,7 +272,7 @@ services: 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'] + arguments: ['@plugin.manager.menu.link', '@request_stack'] menu.parent_form_selector: class: Drupal\Core\Menu\MenuParentFormSelector arguments: ['@menu.link_tree', '@entity.manager'] diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php index 25a7284..d3df147 100644 --- a/core/lib/Drupal/Core/Menu/MenuActiveTrail.php +++ b/core/lib/Drupal/Core/Menu/MenuActiveTrail.php @@ -19,25 +19,11 @@ class MenuActiveTrail implements MenuActiveTrailInterface { /** - * The menu link tree storage. + * The menu link plugin manager. * - * @var \Drupal\Core\Menu\MenuTreeStorageInterface + * @var \Drupal\Core\Menu\MenuLinkManagerInterface */ - protected $treeStorage; - - /** - * The access manager. - * - * @var \Drupal\Core\Access\AccessManager - */ - protected $accessManager; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $account; + protected $menuLinkManager; /** * The request stack. @@ -47,47 +33,16 @@ class MenuActiveTrail implements MenuActiveTrailInterface { protected $requestStack; /** - * The configuration factory. - * - * @var \Drupal\Core\Config\ConfigFactoryInterface - */ - protected $configFactory; - - /** - * Stores the preferred menu link keyed by route_name + parameters. - * - * @var array - */ - protected $preferredLinks = array(); - - /** - * Stores the active menu names. - * - * @var array - */ - protected $activeMenus = array(); - - /** * Constructs a \Drupal\Core\Menu\MenuActiveTrail object. * * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager * The menu link plugin manager. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * A request object for the controller resolver and finding the preferred - * menu and link for the current page. - * @param \Drupal\Core\Access\AccessManager $access_manager - * The access manager. - * @param \Drupal\Core\Session\AccountInterface $account - * The current user. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * Configuration factory. + * A request object for the controller resolver and finding the active link. */ - public function __construct(MenuLinkManagerInterface $menu_link_manager, RequestStack $request_stack, AccessManager $access_manager, AccountInterface $account, ConfigFactoryInterface $config_factory) { + public function __construct(MenuLinkManagerInterface $menu_link_manager, RequestStack $request_stack) { $this->menuLinkManager = $menu_link_manager; $this->requestStack = $request_stack; - $this->accessManager = $access_manager; - $this->account = $account; - $this->configFactory = $config_factory; } /** @@ -98,112 +53,52 @@ public function getActiveTrailIds($menu_name) { // We always want all the top-level links with parent == ''. $active_trail = array('' => ''); - $request = $this->requestStack->getCurrentRequest(); - - if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { - $route_parameters = $request->attributes->get('_raw_variables')->all(); - $page_is_403 = $request->attributes->get('_exception_statuscode') == 403; - // Find a menu link corresponding to the current path. If - // $active_path is NULL, let $this->menuLinkGetPreferred() determine the - // path. - if (!$page_is_403) { - $active_link = $this->menuLinkGetPreferred($route_name, $route_parameters, $menu_name); - if ($active_link && $active_link->getMenuName() == $menu_name) { - $active_trail += $this->menuLinkManager->getParentIds($active_link->getPluginId()); - } - } + // If a link in the given menu indeed matches the route, then use it to + // complete the active trail. + if ($active_link = $this->getActiveLink($menu_name)) { + $active_trail = $this->menuLinkManager->getParentIds($active_link->getPluginId()) + $active_trail; } + return $active_trail; } /** * {@inheritdoc} */ - public function menuLinkGetPreferred($route_name = NULL, array $route_parameters = array(), $selected_menu = NULL) { - if (!isset($route_name)) { - $request = $this->requestStack->getCurrentRequest(); + public function getActiveTrailCacheKey($menu_name) { + return 'menu_trail.' . implode('|', $this->getActiveTrailIds($menu_name)); + } - $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); - $route_parameters = $request->attributes->get('_raw_variables')->all(); - } + /** + * {@inheritdoc} + */ + public function getActiveLink($menu_name = NULL) { + $request = $this->requestStack->getCurrentRequest(); - $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account); - if (!$access) { + // If the current request is inaccessible, then we cannot find the active + // link, because if it existed, the current user wouldn't be able to see it. + if ($request->attributes->get('_exception_statuscode') == 403) { return NULL; } - asort($route_parameters); - $route_key = $route_name . serialize($route_parameters); - if (empty($selected_menu)) { - // Use an illegal menu name as the key for the preferred menu link. - $selected_menu = '%'; - } + if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { + $route_parameters = $request->attributes->get('_raw_variables')->all(); - if (!isset($this->preferredLinks[$route_key])) { - // Retrieve a list of menu names, ordered by preference. - $menu_names = $this->getActiveMenuNames(); - // Put the selected menu at the front of the list. - array_unshift($menu_names, $selected_menu); - // If this menu name is not fond later, we want to just get NULL. - $this->preferredLinks[$route_key][$selected_menu] = NULL; - - // Only load non-hidden links. - $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters); - // Sort candidates by menu name. - $candidates = array(); - foreach ($links as $candidate) { - $candidates[$candidate->getMenuName()] = $candidate; - $menu_names[] = $candidate->getMenuName(); + // Load links matching this route. + $links = $this->menuLinkManager->loadLinksByRoute($route_name, $route_parameters, $menu_name); + if (empty($links)) { + return NULL; } - foreach ($menu_names as $menu_name) { - if (isset($candidates[$menu_name]) && !isset($this->preferredLinks[$route_key][$menu_name])) { - $instance = $candidates[$menu_name]; - $this->preferredLinks[$route_key][$menu_name] = $instance; - if (!isset($this->preferredLinks[$route_key]['%'])) { - $this->preferredLinks[$route_key]['%'] = $instance; - } - } - } - - } - return isset($this->preferredLinks[$route_key][$selected_menu]) ? $this->preferredLinks[$route_key][$selected_menu] : NULL; - } - /** - * {@inheritdoc} - */ - public function getActiveMenuNames() { - return $this->activeMenus; - } + // Select one of the matching links: pick the first, after sorting by key. + // @todo Improve this; add MenuTreeStorage::loadByRoute(), which should allow for more control over which link is selected when there are multiple matches. + ksort($links); + $active_link = reset($links); - /** - * {@inheritdoc} - */ - public function setActiveMenuNames(array $menu_names) { - - if (isset($menu_names) && is_array($menu_names)) { - $this->activeMenus = $menu_names; - } - elseif (!isset($this->activeMenus)) { - $config = $this->configFactory->get('system.menu'); - $this->activeMenus = $config->get('active_menus_default') ?: array_keys($this->listSystemMenus()); + return $active_link; } - } - /** - * Returns an array containing the names of system-defined (default) menus. - */ - protected function listSystemMenus() { - // For simplicity and performance, this is simply a hard-coded list copied - // from menu_list_system_menus() which is simply the list of all Menu config - // entities that are shipped with system module. - return array( - 'tools' => 'Tools', - 'admin' => 'Administration', - 'account' => 'User account menu', - 'main' => 'Main navigation', - 'footer' => 'Footer menu', - ); + return NULL; } } diff --git a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php index 2f09fac..50995ba 100644 --- a/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php +++ b/core/lib/Drupal/Core/Menu/MenuActiveTrailInterface.php @@ -27,43 +27,28 @@ public function getActiveTrailIds($menu_name); /** - * Gets the active menus for the current page. + * Gets the active trail cache key of the specified menu tree. * - * The active menu for the page determines the active trail. - * - * @return array - * An array of menu machine names, in order of preference. The - * 'system.menu:active_menus_default' config item may be used to set a menu - * order different from the default order, or to prevent a particular menu - * from being used at all in the active trail. - */ - public function getActiveMenuNames(); - - /** - * Sets the active menu for the current page. - * - * This overrides for the current page the preferred list of menus returned - * by getActiveMenuNames(). The active menu for the page determines the active - * trail. + * @param string $menu_name + * The menu name of the requested tree. * - * @param array $menu_names - * The menu names to use as active for the current page. + * @return string + * The cache key that uniquely identifies the active trail of the menu tree. */ - public function setActiveMenuNames(array $menu_names); + public function getActiveTrailCacheKey($menu_name); /** * Fetches a menu link which matches the route name, parameters and menu name. * - * @param string $route_name - * (optional) The route name, defaults to NULL. - * @param array $route_parameters - * (optional) the route parameters, defaults to an empty array. - * @param string|NULL $selected_menu - * (optional) The menu to use to find preferred links, defaults to NULL. + * @param string|NULL $menu_name + * (optional) The menu within which to find the active link. If omitted, all + * menus will be searched. * - * @return \Drupal\Core\Menu\MenuLinkInterface - * The prepared menu link for the given route name, parameters and menu. + * @return \Drupal\Core\Menu\MenuLinkInterface|NULL + * The menu link for the given route name, parameters and menu, or NULL if + * the current user cannot access the current page (i.e. we have a 403 + * response). */ - public function menuLinkGetPreferred($route_name = NULL, array $route_parameters = array(), $selected_menu = NULL); + public function getActiveLink($menu_name = NULL); } diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index a6e5f25..d87fec0 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -28,7 +28,7 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { /** - * The menu link tree manager. + * The menu link tree service. * * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ @@ -51,7 +51,7 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterfa * @param array $plugin_definition * The plugin implementation definition. * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree - * The menu tree. + * The menu tree service. * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail * The active menu trail service. */ @@ -92,27 +92,10 @@ public function build() { /** * {@inheritdoc} */ - public function defaultConfiguration() { - // Modify the default max age for menu blocks: modifications made to menus, - // menu links and menu blocks will automatically invalidate corresponding - // cache tags, therefore allowing us to cache menu blocks forever. This is - // only not the case if there are user-specific or dynamic alterations (e.g. - // hook_node_access()), but in that: - // 1) it is possible to set a different max age for individual blocks, since - // this is just the default value. - // 2) modules can modify caching by implementing hook_block_view_alter() - return array('cache' => array('max_age' => \Drupal\Core\Cache\Cache::PERMANENT)); - } - - /** - * {@inheritdoc} - */ public function getCacheKeys() { // Add a key for the active menu trail. $menu = $this->getDerivativeId(); - $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu); - $active_trail_key = 'trail.' . implode('|', $active_trail); - return array_merge(parent::getCacheKeys(), array($active_trail_key)); + return array_merge(parent::getCacheKeys(), array($this->menuActiveTrail->getActiveTrailCacheKey($menu))); } /** @@ -127,13 +110,4 @@ public function getCacheTags() { return NestedArray::mergeDeep(parent::getCacheTags(), $tags); } - /** - * {@inheritdoc} - */ - protected function getRequiredCacheContexts() { - // Menu blocks must be cached per role: different roles may have access to - // different menu links. - return array('cache_context.user.roles', 'cache_context.url', 'cache_context.language'); - } - } diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 6d9b482..5d54692 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -185,11 +185,7 @@ public function getMaxSeverity(&$requirements) { * A render array suitable for drupal_render. */ public function getBlockContents() { - $request = $this->requestStack->getCurrentRequest(); - $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); - $route_parameters = $request->attributes->get('_raw_variables')->all(); - // @todo - use loadLinksByRoute() instead. - $instance = $this->menuActiveTrail->menuLinkGetPreferred($route_name, $route_parameters); + $instance = $this->menuActiveTrail->getActiveLink(); if ($instance && $content = $this->getAdminBlock($instance)) { $output = array( '#theme' => 'admin_block_content', diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 850e272..065d88e 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -346,38 +346,13 @@ function toolbar_toolbar() { '#weight' => -20, ); - $menu_tree = \Drupal::menuTree(); - // Retrieve the administration menu from the menu tree manager. - $parameters = array( - 'expanded' => array('system.admin') - ); - $tree = $menu_tree->buildTree('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); - - $menu = array( - '#heading' => t('Administration menu'), - 'toolbar_administration' => array( - '#type' => 'container', - '#attributes' => array( - 'class' => array('toolbar-menu-administration'), - ), - 'administration_menu' => $render_tree, - ), - ); - // To conserve bandwidth, we only include the top-level links in the HTML. // The subtrees are fetched through a JSONP script that is generated at the // toolbar_subtrees route. We provide the JavaScript requesting that JSONP // script here with the hash parameter that is needed for that route. // @see toolbar_subtrees_jsonp() $langcode = \Drupal::languageManager()->getCurrentLanguage()->id; - $menu['toolbar_administration']['#attached']['js'][] = array( + $subtrees_attached['js'][] = array( 'type' => 'setting', 'data' => array('toolbar' => array( 'subtreesHash' => _toolbar_get_subtrees_hash($langcode), @@ -404,7 +379,19 @@ function toolbar_toolbar() { 'data-drupal-subtrees' => '', ), ), - 'tray' => $menu, + 'tray' => array( + '#heading' => t('Administration menu'), + '#attached' => $subtrees_attached, + 'toolbar_administration' => array( + '#pre_render' => array( + 'toolbar_prerender_toolbar_administration_tray', + ), + '#type' => 'container', + '#attributes' => array( + 'class' => array('toolbar-menu-administration'), + ), + ), + ), '#weight' => -15, ); @@ -412,6 +399,34 @@ function toolbar_toolbar() { } /** + * #pre_render callback to render the toolbar's administration tray. + * + * @param array $element + * A renderable array. + * + * @return array + * The updated renderable array + * + * @see drupal_render() + */ +function toolbar_prerender_toolbar_administration_tray(array $element) { + $menu_tree = \Drupal::menuTree(); + // Retrieve the administration menu from the menu tree manager. + $parameters = array( + 'expanded' => array('system.admin') + ); + $tree = $menu_tree->buildTree('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); + $element['administration_menu'] = $menu_tree->render($tree); + return $element; +} + +/** * Tree manipulator that adds toolbar-specific attributes. * * @param array $tree diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php new file mode 100644 index 0000000..abeab39 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Menu/MenuActiveTrailTest.php @@ -0,0 +1,160 @@ + 'Tests \Drupal\Core\Menu\MenuActiveTrail', + 'description' => '', + 'group' => 'Menu', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->requestStack = new RequestStack(); + $this->menuLinkManager = $this->getMock('Drupal\Core\Menu\MenuLinkManagerInterface'); + + $this->menuActiveTrail = new MenuActiveTrail($this->menuLinkManager, $this->requestStack); + } + + /** + * Provides test data for all test methods. + */ + public function provider() { + $data = array(); + + $request = (new Request()); + $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'baby_llama'); + $request->attributes->set('_raw_variables', new ParameterBag(array())); + + $link_1 = MenuLinkMock::create(array('id' => 'baby_llama_link_1', 'route_name' => 'baby_llama', 'title' => 'Baby llama', 'parent' => 'mama_llama_link')); + $link_2 = MenuLinkMock::create(array('id' => 'baby_llama_link_2', 'route_name' => 'baby_llama', 'title' => 'Baby llama', 'parent' => 'papa_llama_link')); + + // @see \Drupal\Core\Menu\MenuLinkManagerInterface::getParentIds() + $link_1_parent_ids = array('baby_llama_link_1', 'mama_llama_link', ''); + $empty_active_trail = array(''); + + $link_1__active_trail_cache_key = 'menu_trail.baby_llama_link_1|mama_llama_link|'; + $empty_active_trail_cache_key = 'menu_trail.'; + + // No active link is returned when zero links match the current route. + $data[] = array($request, array(), $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key); + + // The first (and only) matching link is returned when one link matches the + // current route. + $data[] = array($request, array('baby_llama_link_1' => $link_1), $this->randomName(), $link_1, $link_1_parent_ids, $link_1__active_trail_cache_key); + + // The first of multiple matching links is returned when multiple links + // match the current route, where "first" is determined by sorting by key. + $data[] = array($request, array('baby_llama_link_1' => $link_1, 'baby_llama_link_2' => $link_2), $this->randomName(), $link_1, $link_1_parent_ids, $link_1__active_trail_cache_key); + $data[] = array($request, array('baby_llama_link_2' => $link_2, 'baby_llama_link_1' => $link_1), $this->randomName(), $link_1, $link_1_parent_ids, $link_1__active_trail_cache_key); + + // No active link is returned in case of a 403. + $request = (new Request()); + $request->attributes->set('_exception_statuscode', 403); + $data[] = array($request, FALSE, $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key); + + // No active link is returned when the route name is missing. + $request = (new Request()); + $data[] = array($request, FALSE, $this->randomName(), NULL, $empty_active_trail, $empty_active_trail_cache_key); + + return $data; + } + + /** + * Tests getActiveLink(). + * + * @covers ::getActiveLink() + * @dataProvider provider + */ + public function testGetActiveLink(Request $request, $links, $menu_name, $expected_link) { + $this->requestStack->push($request); + if ($links !== FALSE) { + $this->menuLinkManager->expects($this->exactly(2)) + ->method('loadLinksbyRoute') + ->will($this->returnValue($links)); + } + // Test with menu name. + $this->assertSame($expected_link, $this->menuActiveTrail->getActiveLink($menu_name)); + // Test without menu name. + $this->assertSame($expected_link, $this->menuActiveTrail->getActiveLink()); + } + + /** + * Tests getActiveTrailIds(). + * + * @covers ::getActiveTrailIds() + * @covers ::getActiveTrailCacheKey() + * @dataProvider provider + */ + public function testGetActiveTrailIds(Request $request, $links, $menu_name, $expected_link, $expected_trail, $expected_cache_key) { + $expected_trail_ids = array_combine($expected_trail, $expected_trail); + + $this->requestStack->push($request); + if ($links !== FALSE) { + $this->menuLinkManager->expects($this->exactly(2)) + ->method('loadLinksbyRoute') + ->will($this->returnValue($links)); + if ($expected_link !== NULL) { + $this->menuLinkManager->expects($this->exactly(2)) + ->method('getParentIds') + ->will($this->returnValueMap(array( + array($expected_link->getPluginId(), $expected_trail_ids), + ))); + } + } + + $this->assertSame($expected_trail_ids, $this->menuActiveTrail->getActiveTrailIds($menu_name)); + $this->assertSame($expected_cache_key, $this->menuActiveTrail->getActiveTrailCacheKey($menu_name)); + } + +}