diff --git a/core/includes/menu.inc b/core/includes/menu.inc index b00965f1240..987d9d0561a 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -321,594 +321,6 @@ function _menu_link_translate(&$item) { } } -/** - * Renders a menu tree based on the current path. - * - * @param $menu_name - * The name of the menu. - * - * @return - * A structured array representing the specified menu on the current page, to - * be rendered by drupal_render(). - */ -function menu_tree($menu_name) { - $menu_output = &drupal_static(__FUNCTION__, array()); - - if (!isset($menu_output[$menu_name])) { - $tree = menu_tree_page_data($menu_name); - $menu_output[$menu_name] = menu_tree_output($tree); - } - return $menu_output[$menu_name]; -} - -/** - * Returns an output structure for rendering a menu tree. - * - * The menu item's LI element is given one of the following classes: - * - expanded: The menu item is showing its submenu. - * - collapsed: The menu item has a submenu which is not shown. - * - leaf: The menu item has no submenu. - * - * @param $tree - * A data structure representing the tree as returned from menu_tree_data. - * - * @return - * A structured array to be rendered by drupal_render(). - */ -function menu_tree_output($tree) { - $build = array(); - $items = array(); - - // Pull out just the menu links we are going to render so that we - // get an accurate count for the first/last classes. - foreach ($tree as $data) { - if ($data['link']['access'] && !$data['link']['hidden']) { - $items[] = $data; - } - } - - foreach ($items as $data) { - $class = array(); - // Set a class for the
  • -tag. Since $data['below'] may contain local - // tasks, only set 'expanded' class if the link also has children within - // the current menu. - if ($data['link']['has_children'] && $data['below']) { - $class[] = 'expanded'; - } - elseif ($data['link']['has_children']) { - $class[] = 'collapsed'; - } - else { - $class[] = 'leaf'; - } - // Set a class if the link is in the active trail. - if ($data['link']['in_active_trail']) { - $class[] = 'active-trail'; - $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; - } - - // Allow menu-specific theme overrides. - $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'); - $element['#attributes']['class'] = $class; - $element['#title'] = $data['link']['title']; - // @todo Use route name and parameters to generate the link path, unless - // it is external. - $element['#href'] = $data['link']['link_path']; - $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); - $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below']; - $element['#original_link'] = $data['link']; - // Index using the link's unique mlid. - $build[$data['link']['mlid']] = $element; - } - if ($build) { - // Make sure drupal_render() does not re-order the links. - $build['#sorted'] = TRUE; - // Add the theme wrapper for outer markup. - // Allow menu-specific theme overrides. - $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_'); - // Set cache tag. - $menu_name = $data['link']['menu_name']; - $build['#cache']['tags']['menu'][$menu_name] = $menu_name; - } - - return $build; -} - -/** - * Gets the data structure representing a named menu tree. - * - * Since this can be the full tree including hidden items, the data returned - * may be used for generating an an admin interface or a select. - * - * @param $menu_name - * The named menu links to return - * @param $link - * A fully loaded menu link, or NULL. If a link is supplied, only the - * path to root will be included in the returned tree - as if this link - * represented the current page in a visible menu. - * @param $max_depth - * Optional maximum depth of links to retrieve. Typically useful if only one - * or two levels of a sub tree are needed in conjunction with a non-NULL - * $link, in which case $max_depth should be greater than $link['depth']. - * - * @return - * An tree of menu links in an array, in the order they should be rendered. - */ -function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { - $tree = &drupal_static(__FUNCTION__, array()); - $language_interface = \Drupal::languageManager()->getCurrentLanguage(); - - // Use $mlid as a flag for whether the data being loaded is for the whole tree. - $mlid = isset($link['mlid']) ? $link['mlid'] : 0; - // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth. - $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth; - - if (!isset($tree[$cid])) { - // If the static variable doesn't have the data, check {cache_data}. - $cache = \Drupal::cache('data')->get($cid); - if ($cache && isset($cache->data)) { - // If the cache entry exists, it contains the parameters for - // menu_build_tree(). - $tree_parameters = $cache->data; - } - // If the tree data was not in the cache, build $tree_parameters. - if (!isset($tree_parameters)) { - $tree_parameters = array( - 'min_depth' => 1, - 'max_depth' => $max_depth, - ); - if ($mlid) { - // The tree is for a single item, so we need to match the values in its - // p columns and 0 (the top level) with the plid values of other links. - $parents = array(0); - for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { - if (!empty($link["p$i"])) { - $parents[] = $link["p$i"]; - } - } - $tree_parameters['expanded'] = $parents; - $tree_parameters['active_trail'] = $parents; - $tree_parameters['active_trail'][] = $mlid; - } - - // Cache the tree building parameters using the page-specific cid. - \Drupal::cache('data')->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name)); - } - - // Build the tree using the parameters; the resulting tree will be cached - // by _menu_build_tree()). - $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); - } - - return $tree[$cid]; -} - -/** - * Sets the path for determining the active trail of the specified menu tree. - * - * This path will also affect the breadcrumbs under some circumstances. - * Breadcrumbs are built using the preferred link returned by - * menu_link_get_preferred(). If the preferred link is inside one of the menus - * specified in calls to menu_tree_set_path(), the preferred link will be - * overridden by the corresponding path returned by menu_tree_get_path(). - * - * Setting this path does not affect the main content; for that use - * menu_set_active_item() instead. - * - * @param $menu_name - * The name of the affected menu tree. - * @param $path - * The path to use when finding the active trail. - */ -function menu_tree_set_path($menu_name, $path = NULL) { - $paths = &drupal_static(__FUNCTION__); - if (isset($path)) { - $paths[$menu_name] = $path; - } - return isset($paths[$menu_name]) ? $paths[$menu_name] : NULL; -} - -/** - * Gets the path for determining the active trail of the specified menu tree. - * - * @param $menu_name - * The menu name of the requested tree. - * - * @return - * A string containing the path. If no path has been specified with - * menu_tree_set_path(), NULL is returned. - */ -function menu_tree_get_path($menu_name) { - return menu_tree_set_path($menu_name); -} - -/** - * Gets the data structure for a named menu tree, based on the current page. - * - * The tree order is maintained by storing each parent in an individual - * field, see http://drupal.org/node/141866 for more. - * - * @param $menu_name - * The named menu links to return. - * @param $max_depth - * (optional) The maximum depth of links to retrieve. - * @param $only_active_trail - * (optional) Whether to only return the links in the active trail (TRUE) - * instead of all links on every level of the menu link tree (FALSE). Defaults - * to FALSE. - * - * @return - * An array of menu links, in the order they should be rendered. The array - * is a list of associative arrays -- these have two keys, link and below. - * link is a menu item, ready for theming as a link. Below represents the - * submenu below the link if there is one, and it is a subtree that has the - * same structure described for the top-level array. - */ -function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = FALSE) { - $tree = &drupal_static(__FUNCTION__, array()); - - $language_interface = \Drupal::languageManager()->getCurrentLanguage(); - - // Check if the active trail has been overridden for this menu tree. - $active_path = menu_tree_get_path($menu_name); - // Load the request corresponding to the current page. - $request = \Drupal::request(); - $system_path = NULL; - if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { - // @todo https://drupal.org/node/2068471 is adding support so we can tell - // if this is called on a 404/403 page. - $system_path = $request->attributes->get('_system_path'); - $page_not_403 = 1; - } - if (isset($system_path)) { - if (isset($max_depth)) { - $max_depth = min($max_depth, MENU_MAX_DEPTH); - } - // Generate a cache ID (cid) specific for this page. - $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth; - // If we are asked for the active trail only, and $menu_name has not been - // built and cached for this page yet, then this likely means that it - // won't be built anymore, as this function is invoked from - // template_preprocess_page(). So in order to not build a giant menu tree - // that needs to be checked for access on all levels, we simply check - // whether we have the menu already in cache, or otherwise, build a minimum - // tree containing the active trail only. - // @see menu_set_active_trail() - if (!isset($tree[$cid]) && $only_active_trail) { - $cid .= ':trail'; - } - - if (!isset($tree[$cid])) { - // If the static variable doesn't have the data, check {cache_data}. - $cache = \Drupal::cache('data')->get($cid); - if ($cache && isset($cache->data)) { - // If the cache entry exists, it contains the parameters for - // menu_build_tree(). - $tree_parameters = $cache->data; - } - // If the tree data was not in the cache, build $tree_parameters. - if (!isset($tree_parameters)) { - $tree_parameters = array( - 'min_depth' => 1, - 'max_depth' => $max_depth, - ); - // Parent mlids; used both as key and value to ensure uniqueness. - // We always want all the top-level links with plid == 0. - $active_trail = array(0 => 0); - - // If this page is accessible to the current user, build the tree - // parameters accordingly. - if ($page_not_403) { - // Find a menu link corresponding to the current path. If $active_path - // is NULL, let menu_link_get_preferred() determine the path. - if ($active_link = menu_link_get_preferred($active_path, $menu_name)) { - // The active link may only be taken into account to build the - // active trail, if it resides in the requested menu. Otherwise, - // we'd needlessly re-run _menu_build_tree() queries for every menu - // on every page. - if ($active_link['menu_name'] == $menu_name) { - // Use all the coordinates, except the last one because there - // can be no child beyond the last column. - for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { - if ($active_link['p' . $i]) { - $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; - } - } - // If we are asked to build links for the active trail only, skip - // the entire 'expanded' handling. - if ($only_active_trail) { - $tree_parameters['only_active_trail'] = TRUE; - } - } - } - $parents = $active_trail; - - $expanded = \Drupal::state()->get('menu_expanded'); - // Check whether the current menu has any links set to be expanded. - if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) { - // Collect all the links set to be expanded, and then add all of - // their children to the list as well. - do { - $query = \Drupal::entityQuery('menu_link') - ->condition('menu_name', $menu_name) - ->condition('expanded', 1) - ->condition('has_children', 1) - ->condition('plid', $parents, 'IN') - ->condition('mlid', $parents, 'NOT IN'); - $result = $query->execute(); - $parents += $result; - } while (!empty($result)); - } - $tree_parameters['expanded'] = $parents; - $tree_parameters['active_trail'] = $active_trail; - } - // If access is denied, we only show top-level links in menus. - else { - $tree_parameters['expanded'] = $active_trail; - $tree_parameters['active_trail'] = $active_trail; - } - // Cache the tree building parameters using the page-specific cid. - \Drupal::cache('data')->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name)); - } - - // Build the tree using the parameters; the resulting tree will be cached - // by _menu_build_tree(). - $tree[$cid] = menu_build_tree($menu_name, $tree_parameters); - } - return $tree[$cid]; - } - - return array(); -} - -/** - * Builds a menu tree, translates links, and checks access. - * - * @param $menu_name - * The name of the menu. - * @param $parameters - * (optional) An associative array of build parameters. Possible keys: - * - expanded: An array of parent link ids to return only menu links that are - * children of one of the plids in this list. If empty, the whole menu tree - * is built, unless 'only_active_trail' is TRUE. - * - active_trail: An array of mlids, representing the coordinates of the - * currently active menu link. - * - only_active_trail: Whether to only return links that are in the active - * trail. This option is ignored, if 'expanded' is non-empty. - * - min_depth: The minimum depth of menu links in the resulting tree. - * Defaults to 1, which is the default to build a whole tree for a menu - * (excluding menu container itself). - * - max_depth: The maximum depth of menu links in the resulting tree. - * - conditions: An associative array of custom database select query - * condition key/value pairs; see _menu_build_tree() for the actual query. - * - * @return - * A fully built menu tree. - */ -function menu_build_tree($menu_name, array $parameters = array()) { - // Build the menu tree. - $data = _menu_build_tree($menu_name, $parameters); - // Check access for the current user to each item in the tree. - menu_tree_check_access($data['tree'], $data['node_links']); - return $data['tree']; -} - -/** - * Builds a menu tree. - * - * This function may be used build the data for a menu tree only, for example - * to further massage the data manually before further processing happens. - * menu_tree_check_access() needs to be invoked afterwards. - * - * @see menu_build_tree() - */ -function _menu_build_tree($menu_name, array $parameters = array()) { - // Static cache of already built menu trees. - $trees = &drupal_static(__FUNCTION__, array()); - $language_interface = \Drupal::languageManager()->getCurrentLanguage(); - - // Build the cache id; sort parents to prevent duplicate storage and remove - // default parameter values. - if (isset($parameters['expanded'])) { - sort($parameters['expanded']); - } - $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters)); - - // If we do not have this tree in the static cache, check {cache_data}. - if (!isset($trees[$tree_cid])) { - $cache = \Drupal::cache('data')->get($tree_cid); - if ($cache && isset($cache->data)) { - $trees[$tree_cid] = $cache->data; - } - } - - if (!isset($trees[$tree_cid])) { - $query = \Drupal::entityQuery('menu_link'); - for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { - $query->sort('p' . $i, 'ASC'); - } - $query->condition('menu_name', $menu_name); - if (!empty($parameters['expanded'])) { - $query->condition('plid', $parameters['expanded'], 'IN'); - } - elseif (!empty($parameters['only_active_trail'])) { - $query->condition('mlid', $parameters['active_trail'], 'IN'); - } - $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); - if ($min_depth != 1) { - $query->condition('depth', $min_depth, '>='); - } - if (isset($parameters['max_depth'])) { - $query->condition('depth', $parameters['max_depth'], '<='); - } - // Add custom query conditions, if any were passed. - if (isset($parameters['conditions'])) { - foreach ($parameters['conditions'] as $column => $value) { - $query->condition($column, $value); - } - } - - // Build an ordered array of links using the query result object. - $links = array(); - if ($result = $query->execute()) { - $links = menu_link_load_multiple($result); - } - $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); - $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); - $data['node_links'] = array(); - menu_tree_collect_node_links($data['tree'], $data['node_links']); - - // Cache the data, if it is not already in the cache. - \Drupal::cache('data')->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); - $trees[$tree_cid] = $data; - } - - return $trees[$tree_cid]; -} - -/** - * Collects node links from a given menu tree recursively. - * - * @param $tree - * The menu tree you wish to collect node links from. - * @param $node_links - * An array in which to store the collected node links. - */ -function menu_tree_collect_node_links(&$tree, &$node_links) { - foreach ($tree as $key => $v) { - if ($tree[$key]['link']['router_path'] == 'node/%') { - $nid = substr($tree[$key]['link']['link_path'], 5); - if (is_numeric($nid)) { - $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link']; - $tree[$key]['link']['access'] = FALSE; - } - } - if ($tree[$key]['below']) { - menu_tree_collect_node_links($tree[$key]['below'], $node_links); - } - } -} - -/** - * Checks access and performs dynamic operations for each link in the tree. - * - * @param $tree - * The menu tree you wish to operate on. - * @param $node_links - * A collection of node link references generated from $tree by - * menu_tree_collect_node_links(). - */ -function menu_tree_check_access(&$tree, $node_links = array()) { - if ($node_links) { - $nids = array_keys($node_links); - $select = db_select('node_field_data', 'n'); - $select->addField('n', 'nid'); - // @todo This should be actually filtering on the desired node status field - // language and just fall back to the default language. - $select->condition('n.status', 1); - - $select->condition('n.nid', $nids, 'IN'); - $select->addTag('node_access'); - $nids = $select->execute()->fetchCol(); - foreach ($nids as $nid) { - foreach ($node_links[$nid] as $mlid => $link) { - $node_links[$nid][$mlid]['access'] = TRUE; - } - } - } - _menu_tree_check_access($tree); -} - -/** - * Sorts the menu tree and recursively checks access for each item. - */ -function _menu_tree_check_access(&$tree) { - $new_tree = array(); - foreach ($tree as $key => $v) { - $item = &$tree[$key]['link']; - _menu_link_translate($item); - if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { - if ($tree[$key]['below']) { - _menu_tree_check_access($tree[$key]['below']); - } - // The weights are made a uniform 5 digits by adding 50000 as an offset. - // After _menu_link_translate(), $item['title'] has the localized link title. - // Adding the mlid to the end of the index insures that it is unique. - $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key]; - } - } - // Sort siblings in the tree based on the weights and localized titles. - ksort($new_tree); - $tree = $new_tree; -} - -/** - * Sorts and returns the built data representing a menu tree. - * - * @param $links - * A flat array of menu links that are part of the menu. Each array element - * is an associative array of information about the menu link, containing the - * fields from the {menu_links} table, and optionally additional information - * from the {menu_router} table, if the menu item appears in both tables. - * This array must be ordered depth-first. See _menu_build_tree() for a sample - * query. - * @param $parents - * An array of the menu link ID values that are in the path from the current - * page to the root of the menu tree. - * @param $depth - * The minimum depth to include in the returned menu tree. - * - * @return - * An array of menu links in the form of a tree. Each item in the tree is an - * associative array containing: - * - link: The menu link item from $links, with additional element - * 'in_active_trail' (TRUE if the link ID was in $parents). - * - below: An array containing the sub-tree of this item, where each element - * is a tree item array with 'link' and 'below' elements. This array will be - * empty if the menu item has no items in its sub-tree having a depth - * greater than or equal to $depth. - */ -function menu_tree_data(array $links, array $parents = array(), $depth = 1) { - // Reverse the array so we can use the more efficient array_pop() function. - $links = array_reverse($links); - return _menu_tree_data($links, $parents, $depth); -} - -/** - * Builds the data representing a menu tree. - * - * The function is a bit complex because the rendering of a link depends on - * the next menu link. - */ -function _menu_tree_data(&$links, $parents, $depth) { - $tree = array(); - while ($item = array_pop($links)) { - // We need to determine if we're on the path to root so we can later build - // the correct active trail. - $item['in_active_trail'] = in_array($item['mlid'], $parents); - // Add the current link to the tree. - $tree[$item['mlid']] = array( - 'link' => $item, - 'below' => array(), - ); - // Look ahead to the next link, but leave it on the array so it's available - // to other recursive function calls if we return or build a sub-tree. - $next = end($links); - // Check whether the next link is the first in a new sub-tree. - if ($next && $next['depth'] > $depth) { - // Recursively call _menu_tree_data to build the sub-tree. - $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']); - // Fetch next link after filling the sub-tree. - $next = end($links); - } - // Determine if we should exit the loop and return. - if (!$next || $next['depth'] < $depth) { - break; - } - } - return $tree; -} - /** * Implements template_preprocess_HOOK() for theme_menu_tree(). */ @@ -1112,7 +524,9 @@ function menu_navigation_links($menu_name, $level = 0) { } // Get the menu hierarchy for the current page. - $tree = menu_tree_page_data($menu_name, $level + 1); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); + $tree = $menu_tree->buildPageData($menu_name, $level + 1); // Go down the active trail until the right level is reached. while ($level-- > 0 && $tree) { @@ -1361,6 +775,9 @@ function menu_set_active_item($path) { function menu_set_active_trail($new_trail = NULL) { $trail = &drupal_static(__FUNCTION__); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); + if (isset($new_trail)) { $trail = $new_trail; } @@ -1384,7 +801,7 @@ function menu_set_active_trail($new_trail = NULL) { // Pass TRUE for $only_active_trail to make menu_tree_page_data() build // a stripped down menu tree containing the active trail only, in case // the given menu has not been built in this request yet. - $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE); + $tree = $menu_tree->buildPageData($preferred_link['menu_name'], NULL, TRUE); list($key, $curr) = each($tree); } // There is no link for the current path. diff --git a/core/modules/menu/lib/Drupal/menu/MenuFormController.php b/core/modules/menu/lib/Drupal/menu/MenuFormController.php index d6720a7a408..fd3f12368dc 100644 --- a/core/modules/menu/lib/Drupal/menu/MenuFormController.php +++ b/core/modules/menu/lib/Drupal/menu/MenuFormController.php @@ -12,6 +12,7 @@ use Drupal\Core\Entity\EntityFormController; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Language\Language; use Drupal\menu_link\MenuLinkStorageControllerInterface; +use Drupal\menu_link\MenuTreeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -33,6 +34,13 @@ class MenuFormController extends EntityFormController { */ protected $menuLinkStorage; + /** + * The menu tree service. + * + * @var \Drupal\menu_link\MenuTreeInterface + */ + protected $menuTree; + /** * The overview tree form. * @@ -47,10 +55,13 @@ class MenuFormController extends EntityFormController { * The factory for entity queries. * @param \Drupal\menu_link\MenuLinkStorageControllerInterface $menu_link_storage * The menu link storage controller. + * @param \Drupal\menu_link\MenuTreeInterface $menu_tree + * The menu tree service. */ - public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageControllerInterface $menu_link_storage) { + public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageControllerInterface $menu_link_storage, MenuTreeInterface $menu_tree) { $this->entityQueryFactory = $entity_query_factory; $this->menuLinkStorage = $menu_link_storage; + $this->menuTree = $menu_tree; } /** @@ -59,7 +70,8 @@ class MenuFormController extends EntityFormController { public static function create(ContainerInterface $container) { return new static( $container->get('entity.query'), - $container->get('entity.manager')->getStorageController('menu_link') + $container->get('entity.manager')->getStorageController('menu_link'), + $container->get('menu_link.tree') ); } @@ -256,13 +268,9 @@ class MenuFormController extends EntityFormController { } $delta = max(count($links), 50); - $tree = menu_tree_data($links); - $node_links = array(); - menu_tree_collect_node_links($tree, $node_links); - // We indicate that a menu administrator is running the menu access check. $this->getRequest()->attributes->set('_menu_admin', TRUE); - menu_tree_check_access($tree, $node_links); + $tree = $this->menuTree->buildTreeData($links); $this->getRequest()->attributes->set('_menu_admin', FALSE); $form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta)); diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index bf900790f47..245e88860c6 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -263,10 +263,13 @@ function _menu_get_options($menus, $available_menus, $item) { $limit = _menu_parent_depth_limit($item); } + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); + $options = array(); foreach ($menus as $menu_name => $title) { if (isset($available_menus[$menu_name])) { - $tree = menu_tree_all_data($menu_name, NULL); + $tree = $menu_tree->buildAllData($menu_name, NULL); $options[$menu_name . ':0'] = '<' . $title . '>'; _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit); } diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php new file mode 100644 index 00000000000..b0a69f60868 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTree.php @@ -0,0 +1,606 @@ +database = $database; + $this->cache = $cache_backend; + $this->languageManager = $language_manager; + $this->requestStack = $request_stack; + $this->menuLinkStorage = $entity_manager->getStorageController('menu_link'); + $this->queryFactory = $entity_query_factory; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function buildAllData($menu_name, $link = NULL, $max_depth = NULL) { + $language_interface = $this->languageManager->getCurrentLanguage(); + + // Use $mlid as a flag for whether the data being loaded is for the whole + // tree. + $mlid = isset($link['mlid']) ? $link['mlid'] : 0; + // Generate a cache ID (cid) specific for this $menu_name, $link, $language, + // and depth. + $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->id . ':' . (int) $max_depth; + + if (!isset($this->menuFullTrees[$cid])) { + // If the static variable doesn't have the data, check {cache_menu}. + $cache = $this->cache->get($cid); + if ($cache && $cache->data) { + // If the cache entry exists, it contains the parameters for + // menu_build_tree(). + $tree_parameters = $cache->data; + } + // If the tree data was not in the cache, build $tree_parameters. + if (!isset($tree_parameters)) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + if ($mlid) { + // The tree is for a single item, so we need to match the values in + // its p columns and 0 (the top level) with the plid values of other + // links. + $parents = array(0); + for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { + if (!empty($link["p$i"])) { + $parents[] = $link["p$i"]; + } + } + $tree_parameters['expanded'] = $parents; + $tree_parameters['active_trail'] = $parents; + $tree_parameters['active_trail'][] = $mlid; + } + + // Cache the tree building parameters using the page-specific cid. + $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name)); + } + + // Build the tree using the parameters; the resulting tree will be cached + // by $this->doBuildTree()). + $this->menuFullTrees[$cid] = $this->buildTree($menu_name, $tree_parameters); + } + + return $this->menuFullTrees[$cid]; + } + + /** + * {@inheritdoc} + */ + public function buildPageData($menu_name, $max_depth = NULL, $only_active_trail = FALSE) { + $language_interface = $this->languageManager->getCurrentLanguage(); + + // Check if the active trail has been overridden for this menu tree. + $active_path = $this->getPath($menu_name); + // Load the request corresponding to the current page. + $request = $this->requestStack->getCurrentRequest(); + $system_path = NULL; + if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { + // @todo https://drupal.org/node/2068471 is adding support so we can tell + // if this is called on a 404/403 page. + $system_path = $request->attributes->get('_system_path'); + $page_not_403 = 1; + } + if (isset($system_path)) { + if (isset($max_depth)) { + $max_depth = min($max_depth, MENU_MAX_DEPTH); + } + // Generate a cache ID (cid) specific for this page. + $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_not_403 . ':' . (int) $max_depth; + // If we are asked for the active trail only, and $menu_name has not been + // built and cached for this page yet, then this likely means that it + // won't be built anymore, as this function is invoked from + // template_preprocess_page(). So in order to not build a giant menu tree + // that needs to be checked for access on all levels, we simply check + // whether we have the menu already in cache, or otherwise, build a + // minimum tree containing the active trail only. + // @see menu_set_active_trail() + if (!isset($this->menuPageTrees[$cid]) && $only_active_trail) { + $cid .= ':trail'; + } + + if (!isset($this->menuPageTrees[$cid])) { + // If the static variable doesn't have the data, check {cache_menu}. + $cache = $this->cache->get($cid); + if ($cache && $cache->data) { + // If the cache entry exists, it contains the parameters for + // menu_build_tree(). + $tree_parameters = $cache->data; + } + // If the tree data was not in the cache, build $tree_parameters. + if (!isset($tree_parameters)) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + // Parent mlids; used both as key and value to ensure uniqueness. + // We always want all the top-level links with plid == 0. + $active_trail = array(0 => 0); + + // If this page is accessible to the current user, build the tree + // parameters accordingly. + if ($page_not_403) { + // Find a menu link corresponding to the current path. If + // $active_path is NULL, let menu_link_get_preferred() determine + // the path. + if ($active_link = $this->menuLinkGetPreferred($menu_name, $active_path)) { + // The active link may only be taken into account to build the + // active trail, if it resides in the requested menu. + // Otherwise, we'd needlessly re-run _menu_build_tree() queries + // for every menu on every page. + if ($active_link['menu_name'] == $menu_name) { + // Use all the coordinates, except the last one because + // there can be no child beyond the last column. + for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { + if ($active_link['p' . $i]) { + $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; + } + } + // If we are asked to build links for the active trail only,skip + // the entire 'expanded' handling. + if ($only_active_trail) { + $tree_parameters['only_active_trail'] = TRUE; + } + } + } + $parents = $active_trail; + + $expanded = $this->state->get('menu_expanded'); + // Check whether the current menu has any links set to be expanded. + if (!$only_active_trail && $expanded && in_array($menu_name, $expanded)) { + // Collect all the links set to be expanded, and then add all of + // their children to the list as well. + do { + $query = $this->queryFactory->get('menu_link') + ->condition('menu_name', $menu_name) + ->condition('expanded', 1) + ->condition('has_children', 1) + ->condition('plid', $parents, 'IN') + ->condition('mlid', $parents, 'NOT IN'); + $result = $query->execute(); + $parents += $result; + } while (!empty($result)); + } + $tree_parameters['expanded'] = $parents; + $tree_parameters['active_trail'] = $active_trail; + } + // If access is denied, we only show top-level links in menus. + else { + $tree_parameters['expanded'] = $active_trail; + $tree_parameters['active_trail'] = $active_trail; + } + // Cache the tree building parameters using the page-specific cid. + $this->cache->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name)); + } + + // Build the tree using the parameters; the resulting tree will be + // cached by $tihs->buildTree(). + $this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters); + } + return $this->menuPageTrees[$cid]; + } + + return array(); + } + + /** + * {@inheritdoc} + */ + public function setPath($menu_name, $path = NULL) { + if (isset($path)) { + $this->trailPaths[$menu_name] = $path; + } + } + + /** + * {@inheritdoc} + */ + public function getPath($menu_name) { + return isset($this->trailPaths[$menu_name]) ? $this->trailPaths[$menu_name] : NULL; + } + + /** + * {@inheritdoc} + */ + public function renderMenu($menu_name) { + + if (!isset($this->menuOutput[$menu_name])) { + $tree = $this->buildPageData($menu_name); + $this->menuOutput[$menu_name] = $this->renderTree($tree); + } + return $this->menuOutput[$menu_name]; + } + + /** + * {@inheritdoc} + */ + public function renderTree($tree) { + $build = array(); + $items = array(); + $menu_name = $tree ? end($tree)['link']['menu_name'] : ''; + + // Pull out just the menu links we are going to render so that we + // get an accurate count for the first/last classes. + foreach ($tree as $data) { + if ($data['link']['access'] && !$data['link']['hidden']) { + $items[] = $data; + } + } + + foreach ($items as $data) { + $class = array(); + // Set a class for the
  • -tag. Since $data['below'] may contain local + // tasks, only set 'expanded' class if the link also has children within + // the current menu. + if ($data['link']['has_children'] && $data['below']) { + $class[] = 'expanded'; + } + elseif ($data['link']['has_children']) { + $class[] = 'collapsed'; + } + else { + $class[] = 'leaf'; + } + // Set a class if the link is in the active trail. + if ($data['link']['in_active_trail']) { + $class[] = 'active-trail'; + $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; + } + + // Allow menu-specific theme overrides. + $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'); + $element['#attributes']['class'] = $class; + $element['#title'] = $data['link']['title']; + // @todo Use route name and parameters to generate the link path, unless + // it is external. + $element['#href'] = $data['link']['link_path']; + $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); + $element['#below'] = $data['below'] ? $this->renderTree($data['below']) : $data['below']; + $element['#original_link'] = $data['link']; + // Index using the link's unique mlid. + $build[$data['link']['mlid']] = $element; + } + if ($build) { + // Make sure drupal_render() does not re-order the links. + $build['#sorted'] = TRUE; + // Add the theme wrapper for outer markup. + // Allow menu-specific theme overrides. + $build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_'); + // Set cache tag. + $menu_name = $data['link']['menu_name']; + $build['#cache']['tags']['menu'][$menu_name] = $menu_name; + } + + return $build; + } + + /** + * {@inheritdoc} + */ + public function buildTree($menu_name, array $parameters = array()) { + // Build the menu tree. + $tree = $this->doBuildTree($menu_name, $parameters); + // Check access for the current user to each item in the tree. + $this->checkAccess($tree); + return $tree; + } + + /** + * Builds a menu tree. + * + * This function may be used build the data for a menu tree only, for example + * to further massage the data manually before further processing happens. + * menu_tree_check_access() needs to be invoked afterwards. + * + * @param string $menu_name + * The name of the menu. + * @param array $parameters + * The parameters passed into static::buildTree() + * + * @see static::buildTree() + */ + protected function doBuildTree($menu_name, array $parameters = array()) { + $language_interface = $this->languageManager->getCurrentLanguage(); + + // Build the cache id; sort parents to prevent duplicate storage and remove + // default parameter values. + if (isset($parameters['expanded'])) { + sort($parameters['expanded']); + } + $tree_cid = 'links:' . $menu_name . ':tree-data:' . $language_interface->id . ':' . hash('sha256', serialize($parameters)); + + // If we do not have this tree in the static cache, check {cache_menu}. + if (!isset($this->menuTree[$tree_cid])) { + $cache = $this->cache->get($tree_cid); + if ($cache && $cache->data) { + $this->menuFullTrees[$tree_cid] = $cache->data; + } + } + + if (!isset($this->menuTree[$tree_cid])) { + $query = $this->queryFactory->get('menu_link'); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $query->sort('p' . $i, 'ASC'); + } + $query->condition('menu_name', $menu_name); + if (!empty($parameters['expanded'])) { + $query->condition('plid', $parameters['expanded'], 'IN'); + } + elseif (!empty($parameters['only_active_trail'])) { + $query->condition('mlid', $parameters['active_trail'], 'IN'); + } + $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); + if ($min_depth != 1) { + $query->condition('depth', $min_depth, '>='); + } + if (isset($parameters['max_depth'])) { + $query->condition('depth', $parameters['max_depth'], '<='); + } + // Add custom query conditions, if any were passed. + if (isset($parameters['conditions'])) { + foreach ($parameters['conditions'] as $column => $value) { + $query->condition($column, $value); + } + } + + // Build an ordered array of links using the query result object. + $links = array(); + if ($result = $query->execute()) { + $links = $this->menuLinkStorage->loadMultiple($result); + } + $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); + $tree = $this->doBuildTreeData($links, $active_trail, $min_depth); + + // Cache the data, if it is not already in the cache. + $this->cache->set($tree_cid, $tree, Cache::PERMANENT, array('menu' => $menu_name)); + $this->menuTree[$tree_cid] = $tree; + } + + return $this->menuTree[$tree_cid]; + } + + /** + * Sorts the menu tree and recursively checks access for each item. + * + * @param array $tree + * The menu tree you wish to operate on. + */ + protected function checkAccess(&$tree) { + $new_tree = array(); + foreach ($tree as $key => $v) { + $item = &$tree[$key]['link']; + $this->menuLinkTranslate($item); + if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { + if ($tree[$key]['below']) { + $this->checkAccess($tree[$key]['below']); + } + // The weights are made a uniform 5 digits by adding 50000 as an offset. + // After _menu_link_translate(), $item['title'] has the localized link + // title. Adding the mlid to the end of the index insures that it is + // unique. + $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key]; + } + } + // Sort siblings in the tree based on the weights and localized titles. + ksort($new_tree); + $tree = $new_tree; + } + + /** + * {@inheritdoc} + */ + public function buildTreeData(array $links, array $parents = array(), $depth = 1) { + $tree = $this->doBuildTreeData($links, $parents, $depth); + $this->checkAccess($tree); + return $tree; + } + + /** + * Prepares the data for calling $this->treeDataRecursive(). + */ + protected function doBuildTreeData(array $links, array $parents = array(), $depth = 1) { + // Reverse the array so we can use the more efficient array_pop() function. + $links = array_reverse($links); + return $this->treeDataRecursive($links, $parents, $depth); + } + + /** + * Builds the data representing a menu tree. + * + * The function is a bit complex because the rendering of a link depends on + * the next menu link. + * + * @param array $links + * A flat array of menu links that are part of the menu. Each array element + * is an associative array of information about the menu link, containing + * the fields from the {menu_links} table, and optionally additional + * information from the {menu_router} table, if the menu item appears in + * both tables. This array must be ordered depth-first. + * See _menu_build_tree() for a sample query. + * @param array $parents + * An array of the menu link ID values that are in the path from the current + * page to the root of the menu tree. + * @param int $depth + * The minimum depth to include in the returned menu tree. + * + * @return array + */ + protected function treeDataRecursive(&$links, $parents, $depth) { + $tree = array(); + while ($item = array_pop($links)) { + // We need to determine if we're on the path to root so we can later build + // the correct active trail. + $item['in_active_trail'] = in_array($item['mlid'], $parents); + // Add the current link to the tree. + $tree[$item['mlid']] = array( + 'link' => $item, + 'below' => array(), + ); + // Look ahead to the next link, but leave it on the array so it's + // available to other recursive function calls if we return or build a + // sub-tree. + $next = end($links); + // Check whether the next link is the first in a new sub-tree. + if ($next && $next['depth'] > $depth) { + // Recursively call doBuildTreeData to build the sub-tree. + $tree[$item['mlid']]['below'] = $this->treeDataRecursive($links, $parents, $next['depth']); + // Fetch next link after filling the sub-tree. + $next = end($links); + } + // Determine if we should exit the loop and return. + if (!$next || $next['depth'] < $depth) { + break; + } + } + return $tree; + } + + /** + * Wraps menu_link_get_preferred(). + */ + protected function menuLinkGetPreferred($menu_name, $active_path) { + return menu_link_get_preferred($active_path, $menu_name); + } + + /** + * Wraps _menu_link_translate(). + */ + protected function menuLinkTranslate(&$item) { + _menu_link_translate($item); + } + +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php new file mode 100644 index 00000000000..e4cd83de3f0 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuTreeInterface.php @@ -0,0 +1,174 @@ + 'main-menu', + 'mlid' => 1, + 'title' => 'Example 1', + 'route_name' => 'example1', + 'link_path' => 'example1', + 'access' => 1, + 'hidden' => FALSE, + 'has_children' => FALSE, + 'in_active_trail' => TRUE, + 'localized_options' => array('attributes' => array('title' => '')), + 'weight' => 0, + ); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Tests \Drupal\menu_link\MenuTree', + 'description' => '', + 'group' => 'Menu', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); + $this->requestStack = new RequestStack(); + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->entityQueryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory') + ->disableOriginalConstructor() + ->getMock(); + $this->state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface'); + + $this->menuTree = new TestMenuTree($this->connection, $this->cacheBackend, $this->languageManager, $this->requestStack, $this->entityManager, $this->entityQueryFactory, $this->state); + } + + /** + * Tests active paths. + * + * @covers ::setPath + * @covers ::getPath + */ + public function testActivePaths() { + $this->assertNull($this->menuTree->getPath('test_menu1')); + + $this->menuTree->setPath('test_menu1', 'example_path1'); + $this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1')); + $this->assertNull($this->menuTree->getPath('test_menu2')); + + $this->menuTree->setPath('test_menu2', 'example_path2'); + $this->assertEquals('example_path1', $this->menuTree->getPath('test_menu1')); + $this->assertEquals('example_path2', $this->menuTree->getPath('test_menu2')); + } + + /** + * Tests buildTreeData with a single level. + * + * @covers ::buildTreeData + * @covers ::doBuildTreeData + */ + public function testBuildTreeDataWithSingleLevel() { + $items = array(); + $items[] = array( + 'mlid' => 1, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example1', + 'access' => TRUE, + ); + $items[] = array( + 'mlid' => 2, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example2', + 'access' => TRUE, + ); + + $result = $this->menuTree->buildTreeData($items, array(), 1); + + $this->assertCount(2, $result); + $result1 = array_shift($result); + $this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']); + $result2 = array_shift($result); + $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']); + } + + /** + * Tests buildTreeData with a single level and one item being active. + * + * @covers ::buildTreeData + * @covers ::doBuildTreeData + */ + public function testBuildTreeDataWithSingleLevelAndActiveItem() { + $items = array(); + $items[] = array( + 'mlid' => 1, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example1', + 'access' => TRUE, + ); + $items[] = array( + 'mlid' => 2, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example2', + 'access' => TRUE, + ); + + $result = $this->menuTree->buildTreeData($items, array(1), 1); + + $this->assertCount(2, $result); + $result1 = array_shift($result); + $this->assertEquals($items[0] + array('in_active_trail' => TRUE), $result1['link']); + $result2 = array_shift($result); + $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']); + } + + /** + * Tests buildTreeData with a single level and none item being active. + * + * @covers ::buildTreeData + * @covers ::doBuildTreeData + */ + public function testBuildTreeDataWithSingleLevelAndNoActiveItem() { + $items = array(); + $items[] = array( + 'mlid' => 1, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example1', + 'access' => TRUE, + ); + $items[] = array( + 'mlid' => 2, + 'depth' => 1, + 'weight' => 0, + 'title' => '', + 'route_name' => 'example2', + 'access' => TRUE, + ); + + $result = $this->menuTree->buildTreeData($items, array(3), 1); + + $this->assertCount(2, $result); + $result1 = array_shift($result); + $this->assertEquals($items[0] + array('in_active_trail' => FALSE), $result1['link']); + $result2 = array_shift($result); + $this->assertEquals($items[1] + array('in_active_trail' => FALSE), $result2['link']); + } + + /** + * Tests buildTreeData with a more complex example. + * + * @covers ::buildTreeData + * @covers ::doBuildTreeData + */ + public function testBuildTreeWithComplexData() { + $items = array( + 1 => array('mlid' => 1, 'depth' => 1, 'route_name' => 'example1', 'access' => TRUE, 'weight' => 0, 'title' => ''), + 2 => array('mlid' => 2, 'depth' => 1, 'route_name' => 'example2', 'access' => TRUE, 'weight' => 0, 'title' => ''), + 3 => array('mlid' => 3, 'depth' => 2, 'route_name' => 'example3', 'access' => TRUE, 'weight' => 0, 'title' => ''), + 4 => array('mlid' => 4, 'depth' => 3, 'route_name' => 'example4', 'access' => TRUE, 'weight' => 0, 'title' => ''), + 5 => array('mlid' => 5, 'depth' => 1, 'route_name' => 'example5', 'access' => TRUE, 'weight' => 0, 'title' => ''), + ); + + $tree = $this->menuTree->buildTreeData($items); + + // Validate that parent items #1, #2, and #5 exist on the root level. + $this->assertEquals($items[1]['mlid'], $tree['50000 1']['link']['mlid']); + $this->assertEquals($items[2]['mlid'], $tree['50000 2']['link']['mlid']); + $this->assertEquals($items[5]['mlid'], $tree['50000 5']['link']['mlid']); + + // Validate that child item #4 exists at the correct location in the hierarchy. + $this->assertEquals($items[4]['mlid'], $tree['50000 2']['below']['50000 3']['below']['50000 4']['link']['mlid']); + } + + /** + * Tests the output with a single level. + * + * @covers ::output + */ + public function testOutputWithSingleLevel() { + $tree = array( + '1' => array( + 'link' => array('mlid' => 1) + $this->defaultMenuLink, + 'below' => array(), + ), + '2' => array( + 'link' => array('mlid' => 2) + $this->defaultMenuLink, + 'below' => array(), + ), + ); + + $output = $this->menuTree->renderTree($tree); + + // Validate that the - in main-menu is changed into an underscore + $this->assertEquals($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link'); + $this->assertEquals($output['2']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link'); + $this->assertEquals($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper'); + } + + /** + * Tests the output method with a complex example. + * + * @covers ::output + */ + public function testOutputWithComplexData() { + $tree = array( + '1'=> array( + 'link' => array('mlid' => 1, 'has_children' => 1, 'title' => 'Item 1', 'link_path' => 'a') + $this->defaultMenuLink, + 'below' => array( + '2' => array('link' => array('mlid' => 2, 'title' => 'Item 2', 'link_path' => 'a/b') + $this->defaultMenuLink, + 'below' => array( + '3' => array('link' => array('mlid' => 3, 'title' => 'Item 3', 'in_active_trail' => 0, 'link_path' => 'a/b/c') + $this->defaultMenuLink, + 'below' => array()), + '4' => array('link' => array('mlid' => 4, 'title' => 'Item 4', 'in_active_trail' => 0, 'link_path' => 'a/b/d') + $this->defaultMenuLink, + 'below' => array()) + ) + ) + ) + ), + '5' => array('link' => array('mlid' => 5, 'hidden' => 1, 'title' => 'Item 5', 'link_path' => 'e') + $this->defaultMenuLink, 'below' => array()), + '6' => array('link' => array('mlid' => 6, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f') + $this->defaultMenuLink, 'below' => array()), + '7' => array('link' => array('mlid' => 7, 'title' => 'Item 7', 'in_active_trail' => 0, 'link_path' => 'g') + $this->defaultMenuLink, 'below' => array()) + ); + + $output = $this->menuTree->renderTree($tree); + + // Looking for child items in the data + $this->assertEquals( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item'); + $this->assertTrue(in_array('active-trail', $output['1']['#below']['2']['#attributes']['class']), 'Checking the active trail class'); + // Validate that the hidden and no access items are missing + $this->assertFalse(isset($output['5']), 'Hidden item should be missing'); + $this->assertFalse(isset($output['6']), 'False access should be missing'); + // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are + // skipped and 7 still included. + $this->assertTrue(isset($output['7']), 'Item after hidden items is present'); + } + + /** + * Tests menu tree access check with a single level. + * + * @covers ::checkAccess + */ + public function testCheckAccessWithSingleLevel() { + $items = array( + array('mlid' => 1, 'route_name' => 'menu_test_1', 'depth' => 1, 'link_path' => 'menu_test/test_1', 'in_active_trail' => FALSE) + $this->defaultMenuLink, + array('mlid' => 2, 'route_name' => 'menu_test_2', 'depth' => 1, 'link_path' => 'menu_test/test_2', 'in_active_trail' => FALSE) + $this->defaultMenuLink, + ); + + // Register a menuLinkTranslate to mock the access. + $this->menuTree->menuLinkTranslateCallable = function(&$item) { + $item['access'] = $item['mlid'] == 1; + }; + + // Build the menu tree and check access for all of the items. + $tree = $this->menuTree->buildTreeData($items); + + $this->assertCount(1, $tree); + $item = reset($tree); + $this->assertEquals($items[0], $item['link']); + } + +} + +class TestMenuTree extends MenuTree { + + /** + * An alternative callable used for menuLinkTranslate. + * @var callable + */ + public $menuLinkTranslateCallable; + + /** + * {@inheritdoc} + */ + protected function menuLinkTranslate(&$item) { + if (isset($this->menuLinkTranslateCallable)) { + call_user_func_array($this->menuLinkTranslateCallable, array(&$item)); + } + } + + /** + * {@inheritdoc} + */ + protected function menuLinkGetPreferred($menu_name, $active_path) { + } + +} diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index 8cd6333355e..c3d41972517 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -304,8 +304,6 @@ function shortcut_valid_link($path) { * * @return \Drupal\shortcut\ShortcutInterface[] * An array of shortcut links, in the format returned by the menu system. - * - * @see menu_tree() */ function shortcut_renderable_links($shortcut_set = NULL) { $shortcut_links = array(); diff --git a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php index 15cb1d8a148..32a84d7cffa 100644 --- a/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/lib/Drupal/system/Plugin/Block/SystemMenuBlock.php @@ -9,6 +9,10 @@ namespace Drupal\system\Plugin\Block; use Drupal\Component\Utility\NestedArray; use Drupal\block\BlockBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\menu_link\MenuTreeInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + /** * Provides a generic Menu block. @@ -20,14 +24,50 @@ use Drupal\block\BlockBase; * derivative = "Drupal\system\Plugin\Derivative\SystemMenuBlock" * ) */ -class SystemMenuBlock extends BlockBase { +class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { + + /** + * The menu tree. + * + * @var \Drupal\menu_link\MenuTreeInterface + */ + protected $menuTree; + + /** + * Constructs a new SystemMenuBlock. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\menu_link\MenuTreeInterface $menu_tree + * The menu tree. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, MenuTreeInterface $menu_tree) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->menuTree = $menu_tree; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, array $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('menu_link.tree') + ); + } /** * {@inheritdoc} */ public function build() { $menu = $this->getDerivativeId(); - return menu_tree($menu); + return $this->menuTree->renderMenu($menu); } /** diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php deleted file mode 100644 index 974208f5d0d..00000000000 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeAccessTest.php +++ /dev/null @@ -1,116 +0,0 @@ - 'Menu tree access', - 'description' => 'Tests the access check for menu tree using both menu links and route items.', - 'group' => 'Menu', - ); - } - - /** - * Overrides \Drupal\simpletest\DrupalUnitTestBase::containerBuild(). - */ - public function containerBuild(ContainerBuilder $container) { - parent::containerBuild($container); - - $route_collection = $this->getTestRouteCollection(); - - $container->register('router.route_provider', 'Drupal\system\Tests\Routing\MockRouteProvider') - ->addArgument($route_collection); - } - - /** - * Generates the test route collection. - * - * @return \Symfony\Component\Routing\RouteCollection - * Returns the test route collection. - */ - protected function getTestRouteCollection() { - if (!isset($this->routeCollection)) { - $route_collection = new RouteCollection(); - $route_collection->add('menu_test_1', new Route('/menu_test/test_1', - array( - '_controller' => '\Drupal\menu_test\TestController::test' - ), - array( - '_access' => 'TRUE' - ) - )); - $route_collection->add('menu_test_2', new Route('/menu_test/test_2', - array( - '_controller' => '\Drupal\menu_test\TestController::test' - ), - array( - '_access' => 'FALSE' - ) - )); - $this->routeCollection = $route_collection; - } - - return $this->routeCollection; - } - - /** - * Tests access check for menu links with a route item. - */ - public function testRouteItemMenuLinksAccess() { - // Add the access checkers to the route items. - $this->container->get('access_manager')->setChecks($this->getTestRouteCollection()); - - // Setup the links with the route items. - $this->links = array( - new MenuLink(array('mlid' => 1, 'route_name' => 'menu_test_1', 'depth' => 1, 'link_path' => 'menu_test/test_1'), 'menu_link'), - new MenuLink(array('mlid' => 2, 'route_name' => 'menu_test_2', 'depth' => 1, 'link_path' => 'menu_test/test_2'), 'menu_link'), - ); - - // Build the menu tree and check access for all of the items. - $tree = menu_tree_data($this->links); - menu_tree_check_access($tree); - - $this->assertEqual(count($tree), 1, 'Ensure that just one menu link got access.'); - $item = reset($tree); - $this->assertEqual($this->links[0], $item['link'], 'Ensure that the right link got access'); - } - -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php deleted file mode 100644 index 8b6c4a136db..00000000000 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php +++ /dev/null @@ -1,68 +0,0 @@ - 'Menu tree generation', - 'description' => 'Tests recursive menu tree generation functions.', - 'group' => 'Menu', - ); - } - - /** - * Validate the generation of a proper menu tree hierarchy. - */ - public function testMenuTreeData() { - $this->links = array( - 1 => new MenuLink(array('mlid' => 1, 'depth' => 1), 'menu_link'), - 2 => new MenuLink(array('mlid' => 2, 'depth' => 1), 'menu_link'), - 3 => new MenuLink(array('mlid' => 3, 'depth' => 2), 'menu_link'), - 4 => new MenuLink(array('mlid' => 4, 'depth' => 3), 'menu_link'), - 5 => new MenuLink(array('mlid' => 5, 'depth' => 1), 'menu_link'), - ); - - $tree = menu_tree_data($this->links); - - // Validate that parent items #1, #2, and #5 exist on the root level. - $this->assertSameLink($this->links[1], $tree[1]['link'], 'Parent item #1 exists.'); - $this->assertSameLink($this->links[2], $tree[2]['link'], 'Parent item #2 exists.'); - $this->assertSameLink($this->links[5], $tree[5]['link'], 'Parent item #5 exists.'); - - // Validate that child item #4 exists at the correct location in the hierarchy. - $this->assertSameLink($this->links[4], $tree[2]['below'][3]['below'][4]['link'], 'Child item #4 exists in the hierarchy.'); - } - - /** - * Check that two menu links are the same by comparing the mlid. - * - * @param $link1 - * A menu link item. - * @param $link2 - * A menu link item. - * @param $message - * The message to display along with the assertion. - * @return - * TRUE if the assertion succeeded, FALSE otherwise. - */ - protected function assertSameLink($link1, $link2, $message = '') { - return $this->assert($link1['mlid'] == $link2['mlid'], $message ?: 'First link is identical to second link'); - } -} diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php deleted file mode 100644 index bc80cb584f2..00000000000 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php +++ /dev/null @@ -1,77 +0,0 @@ - 'Menu tree output', - 'description' => 'Tests menu tree output functions.', - 'group' => 'Menu', - ); - } - - function setUp() { - parent::setUp(); - - $this->installSchema('system', array('router')); - } - - /** - * Validate the generation of a proper menu tree output. - */ - function testMenuTreeData() { - $storage_controller = $this->container->get('entity.manager')->getStorageController('menu_link'); - // @todo Prettify this tree buildup code, it's very hard to read. - $this->tree_data = array( - '1'=> array( - 'link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 1, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access' => 1, 'link_path' => 'a', 'localized_options' => array('attributes' => array('title' =>'')))), - 'below' => array( - '2' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 2, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access' => 1, 'link_path' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')))), - 'below' => array( - '3' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 3, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')))), - 'below' => array() ), - '4' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 4, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')))), - 'below' => array() ) - ) - ) - ) - ), - '5' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 5, 'hidden' => 1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'e', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()), - '6' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 6, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'link_path' => 'f', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()), - '7' => array('link' => $storage_controller->create(array('menu_name' => 'main-menu', 'mlid' => 7, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access' => 1, 'link_path' => 'g', 'localized_options' => array('attributes' => array('title' =>'')))), 'below' => array()) - ); - - $output = menu_tree_output($this->tree_data); - - // Validate that the - in main-menu is changed into an underscore - $this->assertEqual($output['1']['#theme'], 'menu_link__main_menu', 'Hyphen is changed to an underscore on menu_link'); - $this->assertEqual($output['#theme_wrappers'][0], 'menu_tree__main_menu', 'Hyphen is changed to an underscore on menu_tree wrapper'); - // Looking for child items in the data - $this->assertEqual( $output['1']['#below']['2']['#href'], 'a/b', 'Checking the href on a child item'); - $this->assertTrue( in_array('active-trail',$output['1']['#below']['2']['#attributes']['class']) , 'Checking the active trail class'); - // Validate that the hidden and no access items are missing - $this->assertFalse( isset($output['5']), 'Hidden item should be missing'); - $this->assertFalse( isset($output['6']), 'False access should be missing'); - // Item 7 is after a couple hidden items. Just to make sure that 5 and 6 are skipped and 7 still included - $this->assertTrue( isset($output['7']), 'Item after hidden items is present'); - } -} diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module index 4d31718cb31..3f34a1eb8ec 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -223,8 +223,10 @@ function menu_test_callback() { */ function menu_test_menu_trail_callback() { $menu_path = \Drupal::state()->get('menu_test.menu_tree_set_path') ?: array(); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); if (!empty($menu_path)) { - menu_tree_set_path($menu_path['menu_name'], $menu_path['path']); + $menu_tree->setPath($menu_path['menu_name'], $menu_path['path']); } return 'This is menu_test_menu_trail_callback().'; } diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index feadc08cf2f..29773f1fc2c 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -358,6 +358,9 @@ function toolbar_toolbar() { // Add attributes to the links before rendering. toolbar_menu_navigation_links($tree); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); + $menu = array( '#heading' => t('Administration menu'), 'toolbar_administration' => array( @@ -365,7 +368,7 @@ function toolbar_toolbar() { '#attributes' => array( 'class' => array('toolbar-menu-administration'), ), - 'administration_menu' => menu_tree_output($tree), + 'administration_menu' => $menu_tree->renderTree($tree), ), ); @@ -415,6 +418,8 @@ function toolbar_toolbar() { */ function toolbar_get_menu_tree() { $tree = array(); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); $query = \Drupal::entityQuery('menu_link') ->condition('menu_name', 'admin') ->condition('module', 'system') @@ -422,7 +427,7 @@ function toolbar_get_menu_tree() { $result = $query->execute(); if (!empty($result)) { $admin_link = menu_link_load(reset($result)); - $tree = menu_build_tree('admin', array( + $tree = $menu_tree->buildTree('admin', array( 'expanded' => array($admin_link['mlid']), 'min_depth' => $admin_link['depth'] + 1, 'max_depth' => $admin_link['depth'] + 1, @@ -465,6 +470,8 @@ function toolbar_menu_navigation_links(&$tree) { */ function toolbar_get_rendered_subtrees() { $subtrees = array(); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); $tree = toolbar_get_menu_tree(); foreach ($tree as $tree_item) { $item = $tree_item['link']; @@ -476,9 +483,9 @@ function toolbar_get_rendered_subtrees() { $query->condition('p' . $i, $item['p' . $i]); } $parents = $query->execute(); - $subtree = menu_build_tree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1)); + $subtree = $menu_tree->buildTree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1)); toolbar_menu_navigation_links($subtree); - $subtree = menu_tree_output($subtree); + $subtree = $menu_tree->renderTree($subtree); $subtree = drupal_render($subtree); } else { diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php b/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php index bbe5a71ed18..c9dcd302df7 100644 --- a/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php +++ b/core/modules/user/lib/Drupal/user/Tests/UserAccountLinksTests.php @@ -67,7 +67,9 @@ class UserAccountLinksTests extends WebTestBase { $this->drupalGet(''); // For a logged-out user, expect no secondary links. - $tree = menu_build_tree('account'); + /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu_link.tree'); + $tree = $menu_tree->buildTree('account'); $this->assertEqual(count($tree), 1, 'The secondary links menu contains only one menu link.'); $link = reset($tree); $link = $link['link'];