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));
+ }
+
+}