diff --git a/core/core.services.yml b/core/core.services.yml index 562d64f..fd9028c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -277,7 +277,7 @@ 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', '@menu.active_trail', '@controller_resolver'] + arguments: ['@menu.tree_storage', '@plugin.manager.menu.link', '@router.route_provider', '@menu.active_trail', '@controller_resolver', '@cache.menu', '@current_route_match'] menu.default_tree_manipulators: class: Drupal\Core\Menu\DefaultMenuLinkTreeManipulators arguments: ['@access_manager', '@current_user'] @@ -443,7 +443,12 @@ services: arguments: ['@router.dumper', '@lock', '@event_dispatcher', '@module_handler', '@controller_resolver', '@state'] router.rebuild_subscriber: class: Drupal\Core\EventSubscriber\RouterRebuildSubscriber - arguments: ['@router.builder', '@lock'] + arguments: ['@router.builder'] + tags: + - { name: event_subscriber } + menu.rebuild_subscriber: + class: Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber + arguments: ['@lock', '@plugin.manager.menu.link'] tags: - { name: event_subscriber } path.alias_storage: diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 8f1ce73..bd38752 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -153,8 +153,8 @@ * - weight: Lower (negative) numbers come before higher (positive) numbers, * for menu items with the same parent. * - * Menu items from other modules can be altered using - * hook_menu_link_defaults_alter(). + * Discovered menu links from other modules can be altered using + * hook_menu_links_discovered_alter(). * * @todo Derivatives will probably be defined for these; when they are, add * documentation here. @@ -256,135 +256,98 @@ */ /** - * The maximum depth of a menu links tree - matches the number of p columns. + * @section Rendering menus + * Once you have created menus (that contain menu links), you want to render + * them. Drupal provides a block (Drupal\system\Plugin\Block\SystemMenuBlock) to + * do so. + * + * However, perhaps you have more advanced needs and you're not satisfied with + * what the menu blocks offer you. If that's the case, you'll want to: + * - Instantiate \Drupal\Core\Menu\MenuTreeParameters, and set its values to + * match your needs. Alternatively, you can use + * MenuLinkTree::getCurrentRouteMenuTreeParameters() to get a typical + * default set of parameters, and then customize them to suit your needs. + * - Call \Drupal\Core\MenuLinkTree::load() with your menu link tree parameters, + * this will return a menu link tree. + * - Pass the menu tree to \Drupal\Core\Menu\MenuLinkTree::transform() to apply + * menu link tree manipulators that transform the tree. You will almost always + * want to apply access checking. The manipulators that you will typically + * need can be found in \Drupal\Core\Menu\DefaultMenuTreeManipulators. + * - Potentially write a custom menu tree manipulator, see + * \Drupal\Core\Menu\DefaultMenuTreeManipulators for examples. This is only + * necessary if you want to do things like adding extra metadata to rendered + * links to display icons next to them. + * - Pass the menu tree to \Drupal\Core\Menu\MenuLinkTree::build(), this will + * build a renderable array. + * + * Combined, that would look like this: + * @code + * $menu_tree = \Drupal::menuTree(); + * $menu_name = 'my_menu'; * - * @todo Move this constant to MenuLinkStorage along with all the tree - * functionality. - */ -const MENU_MAX_DEPTH = 9; - -/** - * Reserved key to identify the most specific menu link for a given path. + * // Build the typical default set of menu tree parameters. + * $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name); * - * The value of this constant is a hash of the constant name. We use the hash - * so that the reserved key is over 32 characters in length and will not - * collide with allowed menu names: - * @code - * sha1('MENU_PREFERRED_LINK') = 1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91 - * @endcode + * // Load the tree based on this set of parameters. + * $tree = $menu_tree->load($menu_name, $parameters); * - * @see menu_link_get_preferred() - */ -const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'; - -/** - * Localizes a menu link title using t() if possible. + * // Transform the tree using the manipulators you want. + * $manipulators = array( + * // Only show links that are accessible for the current user. + * array('callable' => 'menu.default_tree_manipulators:checkAccess'), + * // Use the default sorting of menu links. + * array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + * ); + * $tree = $menu_tree->transform($tree, $manipulators); * - * Translate the title and description to allow storage of English title - * strings in the database, yet display of them in the language required - * by the current user. + * // Finally, build a renderable array from the transformed tree. + * $menu = $menu_tree->build($tree); * - * @param $item - * A menu link entity. + * $menu_html = drupal_render($menu); + * @endcode */ -function _menu_item_localize(&$item) { - // Allow default menu links to be translated. - $item['localized_options'] = $item['options']; - // All 'class' attributes are assumed to be an array during rendering, but - // links stored in the database may use an old string value. - // @todo In order to remove this code we need to implement a database update - // including unserializing all existing link options and running this code - // on them, as well as adding validation to menu_link_save(). - if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) { - $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']); - } - // If the menu link is defined in code and not customized, we can use t(). - if (!empty($item['machine_name']) && !$item['customized']) { - // @todo Figure out a proper way to support translations of menu links, see - // https://drupal.org/node/2193777. - $item['title'] = t($item['link_title']); - } - else { - $item['title'] = $item['link_title']; - } -} /** - * Provides menu link unserializing, access control, and argument handling. - * - * @param array $item - * The passed in item has the following keys: - * - access: (optional) Becomes TRUE if the item is accessible, FALSE - * otherwise. If the key is not set, the access manager is used to - * determine the access. - * - options: (required) Is unserialized and copied to $item['localized_options']. - * - link_title: (required) The title of the menu link. - * - route_name: (required) The route name of the menu link. - * - route_parameters: (required) The unserialized route parameters of the menu link. - * The passed in item is changed by the following keys: - * - href: The actual path to the link. This path is generated from the - * link_path of the menu link entity. - * - title: The title of the link. This title is generated from the - * link_title of the menu link entity. + * Implements template_preprocess_HOOK() for theme_menu_tree(). */ -function _menu_link_translate(&$item) { - if (!is_array($item['options'])) { - $item['options'] = (array) unserialize($item['options']); - } - $item['localized_options'] = $item['options']; - $item['title'] = $item['link_title']; - if ($item['external'] || empty($item['route_name'])) { - $item['access'] = 1; - $item['href'] = $item['link_path']; - $item['route_parameters'] = array(); - // Set to NULL so that drupal_pre_render_link() is certain to skip it. - $item['route_name'] = NULL; - } - else { - $item['href'] = NULL; - if (!is_array($item['route_parameters'])) { - $item['route_parameters'] = (array) unserialize($item['route_parameters']); - } - // menu_tree_check_access() may set this ahead of time for links to nodes. - if (!isset($item['access'])) { - $item['access'] = \Drupal::getContainer()->get('access_manager')->checkNamedRoute($item['route_name'], $item['route_parameters'], \Drupal::currentUser()); +function template_preprocess_menu_tree(&$variables) { + if (isset($variables['tree']['#heading'])) { + $variables['heading'] = $variables['tree']['#heading']; + $heading = &$variables['heading']; + // Convert a string heading into an array, using a H2 tag by default. + if (is_string($heading)) { + $heading = array('text' => $heading); } - // For performance, don't localize a link the user can't access. - if ($item['access']) { - _menu_item_localize($item); + // Merge in default array properties into $heading. + $heading += array( + 'level' => 'h2', + 'attributes' => array(), + ); + // @todo Remove backwards compatibility for $heading['class']. + // https://www.drupal.org/node/2310341 + if (isset($heading['class'])) { + $heading['attributes']['class'] = $heading['class']; } + // Convert the attributes array into an Attribute object. + $heading['attributes'] = new Attribute($heading['attributes']); + $heading['text'] = String::checkPlain($heading['text']); } - // Allow other customizations - e.g. adding a page-specific query string to the - // options array. For performance reasons we only invoke this hook if the link - // has the 'alter' flag set in the options array. - if (!empty($item['options']['alter'])) { - \Drupal::moduleHandler()->alter('translated_menu_link', $item, $map); + if (isset($variables['tree']['#attributes'])) { + $variables['attributes'] = new Attribute($variables['tree']['#attributes']); } -} + else { + $variables['attributes'] = new Attribute(); + } + if (!isset($variables['attributes']['class'])) { + $variables['attributes']['class'] = array(); + } + $variables['attributes']['class'][] = 'menu'; -/** - * Implements template_preprocess_HOOK() for theme_menu_tree(). - */ -function template_preprocess_menu_tree(&$variables) { $variables['tree'] = $variables['tree']['#children']; } /** - * Returns HTML for a wrapper for a menu sub-tree. - * - * @param $variables - * An associative array containing: - * - tree: An HTML string containing the tree's items. - * - * @see template_preprocess_menu_tree() - * @ingroup themeable - */ -function theme_menu_tree($variables) { - return ''; -} - -/** * Returns HTML for a menu link and submenu. * * @param $variables @@ -400,8 +363,10 @@ function theme_menu_link(array $variables) { if ($element['#below']) { $sub_menu = drupal_render($element['#below']); } - $element['#localized_options']['set_active_class'] = TRUE; - $output = l($element['#title'], $element['#href'], $element['#localized_options']); + /** @var \Drupal\Core\Url $url */ + $url = $element['#url']; + $url->setOption('set_active_class', TRUE); + $output = \Drupal::linkGenerator()->generateFromUrl($element['#title'], $url); return '' . $output . $sub_menu . "\n"; } @@ -541,56 +506,28 @@ function _menu_get_links_source($name, $default) { } /** - * Returns an array of links for a navigation menu. + * Builds a renderable array for a navigation menu. * - * @param $menu_name + * @param string $menu_name * The name of the menu. - * @param $level + * @param int $level * Optional, the depth of the menu to be returned. * - * @return - * An array of links of the specified menu and level. + * @return array + * A renderable array. */ function menu_navigation_links($menu_name, $level = 0) { - // Don't even bother querying the menu table if no menu is specified. - if (empty($menu_name)) { - return array(); - } - - // Get the menu hierarchy for the current page. - /** @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) { - // Loop through the current level's items until we find one that is in trail. - while ($item = array_shift($tree)) { - if ($item['link']['in_active_trail']) { - // If the item is in the active trail, we continue in the subtree. - $tree = empty($item['below']) ? array() : $item['below']; - break; - } - } - } - - // Create a single level of links. - $links = array(); - foreach ($tree as $item) { - if (!$item['link']['hidden']) { - $class = ''; - $l = $item['link']['localized_options']; - $l['href'] = $item['link']['link_path']; - $l['title'] = $item['link']['title']; - if ($item['link']['in_active_trail']) { - $class = ' active-trail'; - $l['attributes']['class'][] = 'active-trail'; - } - // Keyed with the unique mlid to generate classes in links.html.twig. - $links['menu-' . $item['link']['mlid'] . $class] = $l; - } - } - return $links; + $menu_tree = \Drupal::menuTree(); + $parameters = $menu_tree->getCurrentRouteMenuTreeParameters($menu_name); + $parameters->setMaxDepth($level + 1); + $tree = $menu_tree->load($menu_name, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'menu.default_tree_manipulators:extractSubtreeOfActiveTrail', 'args' => array($level)), + ); + $tree = $menu_tree->transform($tree, $manipulators); + return $menu_tree->build($tree); } /** @@ -734,355 +671,13 @@ function theme_menu_local_tasks(&$variables) { } /** - * Sets (or gets) the active menu for the current page. - * - * The active menu for the page determines the active trail. - * - * @return - * An array of menu machine names, in order of preference. The - * 'system.menu:active_menus_default' config item may be used to assert a menu - * order different from the order of creation, or to prevent a particular menu - * from being used at all in the active trail. - */ -function menu_set_active_menu_names($menu_names = NULL) { - $active = &drupal_static(__FUNCTION__); - - if (isset($menu_names) && is_array($menu_names)) { - $active = $menu_names; - } - elseif (!isset($active)) { - $config = \Drupal::config('system.menu'); - $active = $config->get('active_menus_default') ?: array_keys(menu_list_system_menus()); - } - return $active; -} - -/** - * Gets the active menu for the current page. - */ -function menu_get_active_menu_names() { - return menu_set_active_menu_names(); -} - -/** - * Looks up the preferred menu link for a given system path. - * - * @param $path - * The path; for example, 'node/5'. The function will find the corresponding - * menu link ('node/5' if it exists, or fallback to 'node/%'). - * @param $selected_menu - * The name of a menu used to restrict the search for a preferred menu link. - * If not specified, all the menus returned by menu_get_active_menu_names() - * will be used. - * - * @return - * A fully translated menu link, or FALSE if no matching menu link was - * found. The most specific menu link ('node/5' preferred over 'node/%') in - * the most preferred menu (as defined by menu_get_active_menu_names()) is - * returned. - */ -function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { - $preferred_links = &drupal_static(__FUNCTION__); - - if (!isset($path)) { - $path = current_path(); - } - - if (empty($selected_menu)) { - // Use an illegal menu name as the key for the preferred menu link. - $selected_menu = MENU_PREFERRED_LINK; - } - - if (!isset($preferred_links[$path])) { - // Look for the correct menu link by building a list of candidate paths, - // which are ordered by priority (translated hrefs are preferred over - // untranslated paths). Afterwards, the most relevant path is picked from - // the menus, ordered by menu preference. - $path_candidates = array(); - // 1. The current item href. - // @todo simplify this code and convert to using route names. - // @see https://drupal.org/node/2154949 - $path_candidates[$path] = $path; - - // Retrieve a list of menu names, ordered by preference. - $menu_names = menu_get_active_menu_names(); - // Put the selected menu at the front of the list. - array_unshift($menu_names, $selected_menu); - - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path_candidates)); - - // Sort candidates by link path and menu name. - $candidates = array(); - foreach ($menu_links as $candidate) { - $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate; - // Add any menus not already in the menu name search list. - if (!in_array($candidate['menu_name'], $menu_names)) { - $menu_names[] = $candidate['menu_name']; - } - } - - // Store the most specific link for each menu. Also save the most specific - // link of the most preferred menu in $preferred_link. - foreach ($path_candidates as $link_path) { - if (isset($candidates[$link_path])) { - foreach ($menu_names as $menu_name) { - if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) { - $candidate_item = $candidates[$link_path][$menu_name]; - $candidate_item['access'] = \Drupal::service('access_manager')->checkNamedRoute($candidate_item['route_name'], $candidate_item['route_parameters'], \Drupal::currentUser()); - if ($candidate_item['access']) { - _menu_item_localize($candidate_item); - $preferred_links[$path][$menu_name] = $candidate_item; - if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) { - // Store the most specific link. - $preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item; - } - } - } - } - } - } - } - - return isset($preferred_links[$path][$selected_menu]) ? $preferred_links[$path][$selected_menu] : FALSE; -} - -/** * Clears all cached menu data. * * This should be called any time broad changes * might have been made to the router items or menu links. */ function menu_cache_clear_all() { - \Drupal::cache('data')->deleteAll(); - menu_reset_static_cache(); -} - -/** - * Resets the menu system static cache. - */ -function menu_reset_static_cache() { - \Drupal::entityManager() - ->getStorage('menu_link')->resetCache(); - drupal_static_reset('menu_link_get_preferred'); -} - -/** - * Saves menu links recursively for menu_links_rebuild_defaults(). - */ -function _menu_link_save_recursive($controller, $machine_name, &$children, &$links) { - $menu_link = $links[$machine_name]; - if ($menu_link->isNew() || !$menu_link->customized) { - if (!isset($menu_link->plid) && !empty($menu_link->parent) && !empty($links[$menu_link->parent])) { - $parent = $links[$menu_link->parent]; - - if (empty($menu_link->menu_name) || $parent->menu_name == $menu_link->menu_name) { - $menu_link->plid = $parent->id(); - $menu_link->menu_name = $parent->menu_name; - } - } - $controller->save($menu_link); - } - if (!empty($children[$machine_name])) { - foreach ($children[$machine_name] as $next_name) { - _menu_link_save_recursive($controller, $next_name, $children, $links); - } - } - // Remove processed link names so we can find stragglers. - unset($children[$machine_name]); -} - -/** - * Builds menu links for the items returned from the menu_link.static service. - */ -function menu_link_rebuild_defaults() { - // Ensure that all configuration used to build the menu items are loaded - // without overrides. - $old_state = \Drupal::configFactory()->getOverrideState(); - \Drupal::configFactory()->setOverrideState(FALSE); - $module_handler = \Drupal::moduleHandler(); - if (!$module_handler->moduleExists('menu_link')) { - // The Menu link module may not be available during install, so rebuild - // when possible. - return; - } - /** @var \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage */ - $menu_link_storage = \Drupal::entityManager() - ->getStorage('menu_link'); - $links = array(); - $children = array(); - $top_links = array(); - $all_links = \Drupal::service('menu_link.static')->getLinks(); - if ($all_links) { - foreach ($all_links as $machine_name => $link) { - // For performance reasons, do a straight query now and convert to a menu - // link entity later. - // @todo revisit before release. - $existing_item = db_select('menu_links') - ->fields('menu_links') - ->condition('machine_name', $machine_name) - ->execute()->fetchObject(); - if ($existing_item) { - $existing_item->options = unserialize($existing_item->options); - $existing_item->route_parameters = unserialize($existing_item->route_parameters); - $link['mlid'] = $existing_item->mlid; - $link['plid'] = $existing_item->plid; - $link['uuid'] = $existing_item->uuid; - $link['customized'] = $existing_item->customized; - $link['updated'] = $existing_item->updated; - $menu_link = $menu_link_storage->createFromDefaultLink($link); - // @todo Do not create a new entity in order to update it, see - // https://drupal.org/node/2241865 - $menu_link->setOriginalId($existing_item->mlid); - - // Convert the existing item to a typed object. - /** @var \Drupal\menu_link\MenuLinkInterface $existing_item */ - $existing_item = $menu_link_storage->create(get_object_vars($existing_item)); - - if (!$existing_item->customized) { - // A change in the default menu links may move the link to a - // different menu or parent. - if (!empty($link['menu_name']) && ($link['menu_name'] != $existing_item->menu_name)) { - $menu_link->plid = NULL; - $menu_link->menu_name = $link['menu_name']; - } - elseif (!empty($link['parent'])) { - $menu_link->plid = NULL; - } - - $menu_link->original = $existing_item; - } - } - else { - if (empty($link['route_name']) && empty($link['link_path'])) { - \Drupal::logger('menu_link')->error('Menu_link %machine_name does neither provide a route_name nor a link_path, so it got skipped.', array('%machine_name' => $machine_name)); - continue; - } - $menu_link = $menu_link_storage->createFromDefaultLink($link); - } - if (!empty($link['parent'])) { - $children[$link['parent']][$machine_name] = $machine_name; - $menu_link->parent = $link['parent']; - if (empty($link['menu_name'])) { - // Reset the default menu name so it is populated from the parent. - $menu_link->menu_name = NULL; - } - } - else { - // A top level link - we need them to root our tree. - $top_links[$machine_name] = $machine_name; - $menu_link->plid = 0; - } - $links[$machine_name] = $menu_link; - } - } - foreach ($top_links as $machine_name) { - _menu_link_save_recursive($menu_link_storage, $machine_name, $children, $links); - } - // Handle any children we didn't find starting from top-level links. - foreach ($children as $orphan_links) { - foreach ($orphan_links as $machine_name) { - // Force it to the top level. - $links[$machine_name]->plid = 0; - _menu_link_save_recursive($menu_link_storage, $machine_name, $children, $links); - } - } - - // Find any item whose default menu link no longer exists. - if ($all_links) { - $query = \Drupal::entityQuery('menu_link') - ->condition('machine_name', array_keys($all_links), 'NOT IN') - ->exists('machine_name') - ->condition('external', 0) - ->condition('updated', 0) - ->condition('customized', 0) - ->sort('depth', 'DESC'); - $result = $query->execute(); - } - else { - $result = array(); - } - - // Remove all such items. Starting from those with the greatest depth will - // minimize the amount of re-parenting done by the menu link controller. - if ($result) { - menu_link_delete_multiple($result, TRUE); - } - \Drupal::configFactory()->setOverrideState($old_state); -} - -/** - * Returns an array containing all links for a menu. - * - * @param $menu_name - * The name of the menu whose links should be returned. - * - * @return - * An array of menu links. - */ -function menu_load_links($menu_name) { - $links = array(); - - $query = \Drupal::entityQuery('menu_link') - ->condition('menu_name', $menu_name) - // Order by weight so as to be helpful for menus that are only one level - // deep. - ->sort('weight'); - $result = $query->execute(); - - if (!empty($result)) { - $links = menu_link_load_multiple($result); - } - - return $links; -} - -/** - * Deletes all links for a menu. - * - * @param $menu_name - * The name of the menu whose links will be deleted. - */ -function menu_delete_links($menu_name) { - $links = menu_load_links($menu_name); - menu_link_delete_multiple(array_keys($links), FALSE, TRUE); -} - -/** - * Updates the expanded menu item state at most twice per page load. - */ -function _menu_update_expanded_menus() { - $expanded_menus_updated = &drupal_static(__FUNCTION__, 0); - - // Update the expanded menu item state, but at most twice, including at - // the end of the page load when there are multiple links saved or deleted. - if ($expanded_menus_updated == 0) { - // Keep track of which menus have expanded items. - _menu_set_expanded_menus(); - $expanded_menus_updated = 1; - } - elseif ($expanded_menus_updated == 1) { - // Keep track of which menus have expanded items. - drupal_register_shutdown_function('_menu_set_expanded_menus'); - $expanded_menus_updated = 2; - } -} - -/** - * Updates a list of menus with expanded items. - */ -function _menu_set_expanded_menus() { - $names = array(); - $result = Drupal::entityQueryAggregate('menu_link') - ->condition('expanded', 0, '<>') - ->groupBy('menu_name') - ->execute(); - - // Flatten the resulting array. - foreach($result as $k => $v) { - $names[$k] = $v['menu_name']; - } - - \Drupal::state()->set('menu_expanded', $names); + \Drupal::cache('menu')->invalidateAll(); } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 248c795..1a20acb 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2126,27 +2126,17 @@ function template_preprocess_page(&$variables) { // Pass the main menu and secondary menu to the template as render arrays. if (!empty($variables['main_menu'])) { - $variables['main_menu'] = array( - '#theme' =>'links__system_main_menu', - '#links' => $variables['main_menu'], - '#heading' => array( - 'text' => t('Main menu'), - 'class' => array('visually-hidden'), - 'attributes' => array('id' => 'links__system_main_menu'), - ), - '#set_active_class' => TRUE, + $variables['main_menu']['#heading'] = array( + 'text' => t('Main menu'), + 'class' => array('visually-hidden'), + 'attributes' => array('id' => 'links__system_main_menu'), ); } if (!empty($variables['secondary_menu'])) { - $variables['secondary_menu'] = array( - '#theme' =>'links__system_secondary_menu', - '#links' => $variables['secondary_menu'], - '#heading' => array( - 'text' => t('Secondary menu'), - 'class' => array('visually-hidden'), - 'attributes' => array('id' => 'links__system_secondary_menu'), - ), - '#set_active_class' => TRUE, + $variables['secondary_menu']['#heading'] = array( + 'text' => t('Secondary menu'), + 'class' => array('visually-hidden'), + 'attributes' => array('id' => 'links__system_secondary_menu'), ); } @@ -2632,6 +2622,7 @@ function drupal_common_theme() { ), 'menu_tree' => array( 'render element' => 'tree', + 'template' => 'menu-tree', ), 'menu_local_task' => array( 'render element' => 'element', diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 21443e5..e20af77 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -647,4 +647,14 @@ public static function logger($channel) { return static::$container->get('logger.factory')->get($channel); } + /** + * Returns the menu tree. + * + * @return \Drupal\Core\Menu\MenuLinkTreeInterface + * The menu tree. + */ + public static function menuTree() { + return static::$container->get('menu.link_tree'); + } + } diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php index facca39..23ce020 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -255,10 +255,7 @@ protected function getTableMapping($table, $entity_type_id) { $mapping = $storage->getTableMapping()->getAllColumns($table); } else { - // @todo Stop calling drupal_get_schema() once menu links are converted - // to the Entity Field API. See https://drupal.org/node/1842858. - $schema = drupal_get_schema($table); - $mapping = array_keys($schema['fields']); + return FALSE; } return array_flip($mapping); } diff --git a/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php similarity index 59% copy from core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php copy to core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php index 87fdb87..4b0b41f 100644 --- a/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/MenuRouterRebuildSubscriber.php @@ -2,24 +2,22 @@ /** * @file - * Contains \Drupal\Core\EventSubscriber\RouterRebuildSubscriber. + * Contains \Drupal\Core\EventSubscriber\MenuRouterRebuildSubscriber. */ namespace Drupal\Core\EventSubscriber; use Drupal\Core\Cache\Cache; use Drupal\Core\Lock\LockBackendInterface; -use Drupal\Core\Routing\RouteBuilderInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Routing\RoutingEvents; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\HttpKernel\Event\PostResponseEvent; -use Symfony\Component\HttpKernel\KernelEvents; /** * Rebuilds the default menu links and runs menu-specific code if necessary. */ -class RouterRebuildSubscriber implements EventSubscriberInterface { +class MenuRouterRebuildSubscriber implements EventSubscriberInterface { /** * @var \Drupal\Core\Routing\RouteBuilderInterface @@ -32,26 +30,23 @@ class RouterRebuildSubscriber implements EventSubscriberInterface { protected $lock; /** - * Constructs the RouterRebuildSubscriber object. + * The menu link plugin manager. * - * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder - * The route builder. - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * The lock backend. + * @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager */ - public function __construct(RouteBuilderInterface $route_builder, LockBackendInterface $lock) { - $this->routeBuilder = $route_builder; - $this->lock = $lock; - } + protected $menuLinkManager; /** - * Rebuilds routers if necessary. + * Constructs the MenuRouterRebuildSubscriber object. * - * @param \Symfony\Component\HttpKernel\Event\PostResponseEvent $event - * The event object. + * @param \Drupal\Core\Lock\LockBackendInterface $lock + * The lock backend. + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager + * The menu link plugin manager. */ - public function onKernelTerminate(PostResponseEvent $event) { - $this->routeBuilder->rebuildIfNeeded(); + public function __construct(LockBackendInterface $lock, MenuLinkManagerInterface $menu_link_manager) { + $this->lock = $lock; + $this->menuLinkManager = $menu_link_manager; } /** @@ -73,11 +68,9 @@ protected function menuLinksRebuild() { $transaction = db_transaction(); try { // Ensure the menu links are up to date. - menu_link_rebuild_defaults(); - // Clear the menu cache. - menu_cache_clear_all(); - // Track which menu items are expanded. - _menu_update_expanded_menus(); + $this->menuLinkManager->rebuild(); + // Ignore any database replicas temporarily. + db_ignore_replica(); } catch (\Exception $e) { $transaction->rollback(); @@ -95,13 +88,9 @@ protected function menuLinksRebuild() { } /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. + * {@inheritdoc} */ static function getSubscribedEvents() { - $events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200); $events[RoutingEvents::FINISHED][] = array('onRouterRebuild', 200); return $events; } diff --git a/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php index 87fdb87..584b141 100644 --- a/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php +++ b/core/lib/Drupal/Core/EventSubscriber/RouterRebuildSubscriber.php @@ -17,7 +17,7 @@ use Symfony\Component\HttpKernel\KernelEvents; /** - * Rebuilds the default menu links and runs menu-specific code if necessary. + * Rebuilds the router if needed at the end of the request. */ class RouterRebuildSubscriber implements EventSubscriberInterface { @@ -27,21 +27,13 @@ class RouterRebuildSubscriber implements EventSubscriberInterface { protected $routeBuilder; /** - * @var \Drupal\Core\Lock\LockBackendInterface - */ - protected $lock; - - /** * Constructs the RouterRebuildSubscriber object. * * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder * The route builder. - * @param \Drupal\Core\Lock\LockBackendInterface $lock - * The lock backend. */ - public function __construct(RouteBuilderInterface $route_builder, LockBackendInterface $lock) { + public function __construct(RouteBuilderInterface $route_builder) { $this->routeBuilder = $route_builder; - $this->lock = $lock; } /** @@ -55,54 +47,10 @@ public function onKernelTerminate(PostResponseEvent $event) { } /** - * Rebuilds the menu links and deletes the local_task cache tag. - * - * @param \Symfony\Component\EventDispatcher\Event $event - * The event object. - */ - public function onRouterRebuild(Event $event) { - $this->menuLinksRebuild(); - Cache::deleteTags(array('local_task' => 1)); - } - - /** - * Perform menu-specific rebuilding. - */ - protected function menuLinksRebuild() { - if ($this->lock->acquire(__FUNCTION__)) { - $transaction = db_transaction(); - try { - // Ensure the menu links are up to date. - menu_link_rebuild_defaults(); - // Clear the menu cache. - menu_cache_clear_all(); - // Track which menu items are expanded. - _menu_update_expanded_menus(); - } - catch (\Exception $e) { - $transaction->rollback(); - watchdog_exception('menu', $e); - } - - $this->lock->release(__FUNCTION__); - } - else { - // Wait for another request that is already doing this work. - // We choose to block here since otherwise the router item may not - // be available during routing resulting in a 404. - $this->lock->wait(__FUNCTION__); - } - } - - /** - * Registers the methods in this class that should be listeners. - * - * @return array - * An array of event listener definitions. + * {@inheritdoc} */ static function getSubscribedEvents() { $events[KernelEvents::TERMINATE][] = array('onKernelTerminate', 200); - $events[RoutingEvents::FINISHED][] = array('onRouterRebuild', 200); return $events; } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkBase.php b/core/lib/Drupal/Core/Menu/MenuLinkBase.php index 43cb49a..125bbf0 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkBase.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkBase.php @@ -40,22 +40,6 @@ public function getWeight() { /** * {@inheritdoc} */ - public function getTitle() { - // Subclasses may pull in the request or specific attributes as parameters. - $options = array(); - if (!empty($this->pluginDefinition['title_context'])) { - $options['context'] = $this->pluginDefinition['title_context']; - } - $args = array(); - if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) { - $args = (array) $title_arguments; - } - return $this->t($this->pluginDefinition['title'], $args, $options); - } - - /** - * {@inheritdoc} - */ public function getMenuName() { return $this->pluginDefinition['menu_name']; } @@ -112,16 +96,6 @@ public function isDeletable() { /** * {@inheritdoc} */ - public function getDescription() { - if ($this->pluginDefinition['description']) { - return $this->t($this->pluginDefinition['description']); - } - return ''; - } - - /** - * {@inheritdoc} - */ public function getOptions() { return $this->pluginDefinition['options'] ?: array(); } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php index cf14df7..5d0f8e2 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php @@ -66,6 +66,32 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} */ + public function getTitle() { + // Subclasses may pull in the request or specific attributes as parameters. + $options = array(); + if (!empty($this->pluginDefinition['title_context'])) { + $options['context'] = $this->pluginDefinition['title_context']; + } + $args = array(); + if (isset($this->pluginDefinition['title_arguments']) && $title_arguments = $this->pluginDefinition['title_arguments']) { + $args = (array) $title_arguments; + } + return $this->t($this->pluginDefinition['title'], $args, $options); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + if ($this->pluginDefinition['description']) { + return $this->t($this->pluginDefinition['description']); + } + return ''; + } + + /** + * {@inheritdoc} + */ public function isResettable() { // The link can be reset if it has an override. return (bool) $this->staticOverride->loadOverride($this->getPluginId()); @@ -77,11 +103,13 @@ public function isResettable() { public function updateLink(array $new_definition_values, $persist) { // Filter the list of updates to only those that are allowed. $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed); + // Update the definition. + $this->pluginDefinition = $overrides + $this->getPluginDefinition(); if ($persist) { + // Always save the menu name as an override to avoid defaulting to tools. + $overrides['menu_name'] = $this->pluginDefinition['menu_name']; $this->staticOverride->saveOverride($this->getPluginId(), $overrides); } - // Update the definition. - $this->pluginDefinition = $overrides + $this->getPluginDefinition(); return $this->pluginDefinition; } diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTree.php b/core/lib/Drupal/Core/Menu/MenuLinkTree.php index 8d09c2c..e5d54fe 100644 --- a/core/lib/Drupal/Core/Menu/MenuLinkTree.php +++ b/core/lib/Drupal/Core/Menu/MenuLinkTree.php @@ -8,7 +8,9 @@ namespace Drupal\Core\Menu; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Controller\ControllerResolverInterface; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Routing\RouteProviderInterface; /** @@ -45,6 +47,30 @@ class MenuLinkTree implements MenuLinkTreeInterface { protected $controllerResolver; /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The current route match. + * + * @var \Drupal\Core\Routing\RouteMatchInterface + */ + protected $routeMatch; + + /** + * Stores the cached current route parameters by menu and current route match. + * + * @todo Remove this non-static caching in + * https://www.drupal.org/node/1805054. + * + * @var \Drupal\Core\Menu\MenuTreeParameters[] + */ + protected $cachedCurrentRouteParameters; + + /** * Constructs a \Drupal\Core\Menu\MenuLinkTree object. * * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage @@ -57,31 +83,53 @@ class MenuLinkTree implements MenuLinkTreeInterface { * The active menu trail service. * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver * The controller resolver. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The current route match. */ - public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver) { + public function __construct(MenuTreeStorageInterface $tree_storage, MenuLinkManagerInterface $menu_link_manager, RouteProviderInterface $route_provider, MenuActiveTrailInterface $menu_active_trail, ControllerResolverInterface $controller_resolver, CacheBackendInterface $cache, RouteMatchInterface $route_match) { $this->treeStorage = $tree_storage; $this->menuLinkManager = $menu_link_manager; $this->routeProvider = $route_provider; $this->menuActiveTrail = $menu_active_trail; $this->controllerResolver = $controller_resolver; + // @todo Remove these two in https://www.drupal.org/node/1805054. + $this->cache = $cache; + $this->routeMatch = $route_match; } /** * {@inheritdoc} */ public function getCurrentRouteMenuTreeParameters($menu_name) { - $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name); - - $parameters = new MenuTreeParameters(); - $parameters->setActiveTrail($active_trail) - // We want links in the active trail to be expanded. - ->addExpandedParents($active_trail) - // We marked the links in the active trail to be expanded, but we also - // want their descendants that have the "expanded" flag enabled to be - // expanded. - ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail)); - - return $parameters; + $route_parameters = $this->routeMatch->getRawParameters()->all(); + ksort($route_parameters); + $cid = 'current-route-parameters:' . $menu_name . ':route:' . $this->routeMatch->getRouteName() . ':route_parameters:' . serialize($route_parameters); + + if (!isset($this->cachedCurrentRouteParameters[$menu_name])) { + $cache = $this->cache->get($cid); + if ($cache && $cache->data) { + $parameters = $cache->data; + } + else { + $active_trail = $this->menuActiveTrail->getActiveTrailIds($menu_name); + + $parameters = new MenuTreeParameters(); + $parameters->setActiveTrail($active_trail) + // We want links in the active trail to be expanded. + ->addExpandedParents($active_trail) + // We marked the links in the active trail to be expanded, but we also + // want their descendants that have the "expanded" flag enabled to be + // expanded. + ->addExpandedParents($this->treeStorage->getExpanded($menu_name, $active_trail)); + + $this->cache->set($cid, $parameters, CacheBackendInterface::CACHE_PERMANENT, array('menu' => $menu_name)); + } + $this->cachedCurrentRouteParameters[$menu_name] = $parameters; + } + + return $this->cachedCurrentRouteParameters[$menu_name]; } /** diff --git a/core/lib/Drupal/Core/Utility/LinkGenerator.php b/core/lib/Drupal/Core/Utility/LinkGenerator.php index 4912a05..d3093ac 100644 --- a/core/lib/Drupal/Core/Utility/LinkGenerator.php +++ b/core/lib/Drupal/Core/Utility/LinkGenerator.php @@ -99,7 +99,9 @@ public function generateFromUrl($text, Url $url) { // drupal.active-link library know the path in a standardized manner. if (!isset($variables['options']['attributes']['data-drupal-link-system-path'])) { // @todo System path is deprecated - use the route name and parameters. - $variables['options']['attributes']['data-drupal-link-system-path'] = $url->getInternalPath(); + $system_path = $url->getInternalPath(); + // Special case for the front page. + $variables['options']['attributes']['data-drupal-link-system-path'] = $system_path == '' ? '' : $system_path; } } diff --git a/core/modules/book/book.admin.inc b/core/modules/book/book.admin.inc index 9a55869..6a3edc1 100644 --- a/core/modules/book/book.admin.inc +++ b/core/modules/book/book.admin.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\book\BookManager; use Drupal\Core\Render\Element; /** @@ -87,7 +88,7 @@ function theme_book_admin_table($variables) { 'subgroup' => 'book-pid', 'source' => 'book-nid', 'hidden' => TRUE, - 'limit' => MENU_MAX_DEPTH - 2, + 'limit' => BookManager::BOOK_MAX_DEPTH - 2, ), array( 'action' => 'order', diff --git a/core/modules/book/book.module b/core/modules/book/book.module index ba00049..1177ebe 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -5,6 +5,7 @@ * Allows users to create and organize related content in an outline. */ +use Drupal\book\BookManager; use Drupal\book\BookManagerInterface; use Drupal\Component\Utility\String; use Drupal\Core\Entity\EntityInterface; @@ -123,7 +124,7 @@ function book_node_links_alter(array &$node_links, NodeInterface $node, array &$ if ($context['view_mode'] == 'full' && node_is_page($node)) { $child_type = \Drupal::config('book.settings')->get('child_type'); $access_controller = \Drupal::entityManager()->getAccessController('node'); - if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_controller->createAccess($child_type) && $node->isPublished() && $node->book['depth'] < MENU_MAX_DEPTH) { + if (($account->hasPermission('add content to books') || $account->hasPermission('administer book outlines')) && $access_controller->createAccess($child_type) && $node->isPublished() && $node->book['depth'] < BookManager::BOOK_MAX_DEPTH) { $links['book_add_child'] = array( 'title' => t('Add child page'), 'href' => 'node/add/' . $child_type, diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 4fbd597..21144a5 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -203,9 +203,9 @@ function content_translation_entity_operation_alter(array &$operations, \Drupal\ } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_discovered_alter(). */ -function content_translation_menu_link_defaults_alter(array &$links) { +function content_translation_menu_links_discovered_alter(array &$links) { // Clarify where translation settings are located. $links['language.content_settings_page']['title'] = 'Content language and translation'; $links['language.content_settings_page']['description'] = 'Configure language and translation support for content.'; diff --git a/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php b/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php index 556e419..76fa6be 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php @@ -44,7 +44,7 @@ function setUp() { * Tests that the settings UI works as expected. */ function testSettingsUI() { - // Check for the content_translation_menu_link_defaults_alter() changes. + // Check for the content_translation_menu_links_discovered_alter() changes. $this->drupalGet('admin/config'); $this->assertLink('Content language and translation'); $this->assertText('Configure language and translation support for content.'); diff --git a/core/modules/dblog/dblog.module b/core/modules/dblog/dblog.module index d6e20ff..b206879 100644 --- a/core/modules/dblog/dblog.module +++ b/core/modules/dblog/dblog.module @@ -37,9 +37,9 @@ function dblog_help($route_name, RouteMatchInterface $route_match) { } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_discovered_alter(). */ -function dblog_menu_link_defaults_alter(&$links) { +function dblog_menu_links_discovered_alter(&$links) { if (\Drupal::moduleHandler()->moduleExists('search')) { $links['dblog.search'] = array( 'title' => 'Top search phrases', diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 5aa7929..40c1eb7 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -40,12 +40,12 @@ function editor_help($route_name, RouteMatchInterface $route_match) { } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_discovered_alter(). * * Rewrites the menu entries for filter module that relate to the configuration * of text editors. */ -function editor_menu_link_defaults_alter(array &$links) { +function editor_menu_links_discovered_alter(array &$links) { $links['filter.admin_overview']['title'] = 'Text formats and editors'; $links['filter.admin_overview']['description'] = 'Configure how user-contributed content is filtered and formatted, as well as the text editor user interface (WYSIWYGs or toolbars).'; } diff --git a/core/modules/entity/src/Controller/EntityDisplayModeController.php b/core/modules/entity/src/Controller/EntityDisplayModeController.php index f0db942..af31044 100644 --- a/core/modules/entity/src/Controller/EntityDisplayModeController.php +++ b/core/modules/entity/src/Controller/EntityDisplayModeController.php @@ -8,6 +8,7 @@ namespace Drupal\entity\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Url; /** * Provides methods for entity display mode routes. @@ -26,7 +27,7 @@ public function viewModeTypeSelection() { if ($entity_type->isFieldable() && $entity_type->hasViewBuilderClass()) { $entity_types[$entity_type_id] = array( 'title' => $entity_type->getLabel(), - 'link_path' => 'admin/structure/display-modes/view/add/' . $entity_type_id, + 'url' => new Url('entity.view_mode_add_type', array('entity_type_id' => $entity_type_id)), 'localized_options' => array(), ); } @@ -49,7 +50,7 @@ public function formModeTypeSelection() { if ($entity_type->isFieldable() && $entity_type->hasFormClasses()) { $entity_types[$entity_type_id] = array( 'title' => $entity_type->getLabel(), - 'link_path' => 'admin/structure/display-modes/form/add/' . $entity_type_id, + 'url' => new Url('entity.form_mode_add_type', array('entity_type_id' => $entity_type_id)), 'localized_options' => array(), ); } diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php index 26a3160..437c3a8 100644 --- a/core/modules/help/src/Controller/HelpController.php +++ b/core/modules/help/src/Controller/HelpController.php @@ -125,8 +125,7 @@ public function helpPage($name) { if (!empty($admin_tasks)) { $links = array(); foreach ($admin_tasks as $task) { - $link = $task['localized_options']; - $link['href'] = $task['link_path']; + $link = $task['url']->toArray(); $link['title'] = $task['title']; $links[] = $link; } diff --git a/core/modules/language/src/Tests/LanguageConfigSchemaTest.php b/core/modules/language/src/Tests/LanguageConfigSchemaTest.php index e4862bc..28fcad0 100644 --- a/core/modules/language/src/Tests/LanguageConfigSchemaTest.php +++ b/core/modules/language/src/Tests/LanguageConfigSchemaTest.php @@ -24,7 +24,7 @@ class LanguageConfigSchemaTest extends WebTestBase { * * @var array */ - public static $modules = array('language'); + public static $modules = array('language', 'menu_link_content'); /** * A user with administrative permissions. @@ -55,8 +55,8 @@ function testValidLanguageConfigSchema() { $settings_path = 'admin/config/regional/content-language'; // Enable translation for menu link. - $edit['entity_types[menu_link]'] = TRUE; - $edit['settings[menu_link][menu_link][settings][language][language_show]'] = TRUE; + $edit['entity_types[menu_link_content]'] = TRUE; + $edit['settings[menu_link_content][menu_link_content][settings][language][language_show]'] = TRUE; // Enable translation for user. $edit['entity_types[user]'] = TRUE; @@ -67,7 +67,7 @@ function testValidLanguageConfigSchema() { $config_data = \Drupal::config('language.settings')->get(); // Make sure configuration saved correctly. - $this->assertTrue($config_data['entities']['menu_link']['menu_link']['language']['default_configuration']['language_show']); + $this->assertTrue($config_data['entities']['menu_link_content']['menu_link_content']['language']['default_configuration']['language_show']); $this->assertConfigSchema(\Drupal::service('config.typed'), 'language.settings', $config_data); } diff --git a/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php index b25d639..4f4c3d5 100644 --- a/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php +++ b/core/modules/locale/src/Tests/LocaleLocaleLookupTest.php @@ -22,7 +22,7 @@ class LocaleLocaleLookupTest extends WebTestBase { * * @var array */ - public static $modules = array('locale', 'menu_link'); + public static $modules = array('locale'); /** * Tests hasTranslation(). diff --git a/core/modules/menu_link/menu_link.api.php b/core/modules/menu_link/menu_link.api.php deleted file mode 100644 index 4702436..0000000 --- a/core/modules/menu_link/menu_link.api.php +++ /dev/null @@ -1,42 +0,0 @@ - 1, - * @endcode - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - * - * @see hook_menu_link_alter() - */ -function hook_translated_menu_link_alter(\Drupal\menu_link\Entity\MenuLink &$menu_link, $map) { - if ($menu_link->href == 'devel/cache/clear') { - $menu_link->localized_options['query'] = drupal_get_destination(); - } -} - -/** - * @} End of "addtogroup hooks". - */ diff --git a/core/modules/menu_link/menu_link.info.yml b/core/modules/menu_link/menu_link.info.yml deleted file mode 100644 index bc8e62b..0000000 --- a/core/modules/menu_link/menu_link.info.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Menu Link -type: module -description: Provides menu links. -package: Core -version: VERSION -core: 8.x -# @todo Menu links functionality has been moved from system.module and menu.inc -# to this module, so make it required until everything is moved over. -required: TRUE diff --git a/core/modules/menu_link/menu_link.install b/core/modules/menu_link/menu_link.install deleted file mode 100644 index c789d34..0000000 --- a/core/modules/menu_link/menu_link.install +++ /dev/null @@ -1,221 +0,0 @@ - 'Contains the individual links within a menu.', - 'fields' => array( - 'menu_name' => array( - 'description' => "The menu name. All links with the same menu name (such as 'tools') are part of the same menu.", - 'type' => 'varchar', - 'length' => 32, - 'not null' => TRUE, - 'default' => '', - ), - 'mlid' => array( - 'description' => 'The menu link ID (mlid) is the integer primary key.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'uuid' => array( - 'description' => 'Unique Key: Universally unique identifier for this entity.', - 'type' => 'varchar', - 'length' => 128, - 'not null' => FALSE, - ), - 'machine_name' => array( - 'description' => 'Unique machine name: Optional human-readable ID for this link.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ), - 'plid' => array( - 'description' => 'The parent link ID (plid) is the mlid of the link above in the hierarchy, or zero if the link is at the top level in its menu.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'link_path' => array( - 'description' => 'The Drupal path or external path this link points to.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'langcode' => array( - 'description' => 'The {language}.langcode of this link.', - 'type' => 'varchar', - 'length' => 12, - 'not null' => TRUE, - 'default' => '', - ), - 'link_title' => array( - 'description' => 'The text displayed for the link.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'options' => array( - 'description' => 'A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.', - 'type' => 'blob', - 'not null' => FALSE, - 'serialize' => TRUE, - ), - 'module' => array( - 'description' => 'The name of the module that generated this link.', - 'type' => 'varchar', - 'length' => DRUPAL_EXTENSION_NAME_MAX_LENGTH, - 'not null' => TRUE, - 'default' => 'system', - ), - 'hidden' => array( - 'description' => 'A flag for whether the link should be rendered in menus. (1 = a disabled menu item that may be shown on admin screens, -1 = a menu callback, 0 = a normal, visible link)', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'external' => array( - 'description' => 'A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'has_children' => array( - 'description' => 'Flag indicating whether any links have this link as a parent (1 = children exist, 0 = no children).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'expanded' => array( - 'description' => 'Flag for whether this link should be rendered as expanded in menus - expanded links always have their child links displayed, instead of only when the link is in the active trail (1 = expanded, 0 = not expanded)', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'weight' => array( - 'description' => 'Link weight among links in the same menu at the same depth.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - ), - 'depth' => array( - 'description' => 'The depth relative to the top level. A link with plid == 0 will have depth == 1.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'customized' => array( - 'description' => 'A flag to indicate that the user has manually created or edited the link (1 = customized, 0 = not customized).', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'p1' => array( - 'description' => 'The first mlid in the materialized path. If N = depth, then pN must equal the mlid. If depth > 1 then p(N-1) must equal the plid. All pX where X > depth must equal zero. The columns p1 .. p9 are also called the parents.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p2' => array( - 'description' => 'The second mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p3' => array( - 'description' => 'The third mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p4' => array( - 'description' => 'The fourth mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p5' => array( - 'description' => 'The fifth mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p6' => array( - 'description' => 'The sixth mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p7' => array( - 'description' => 'The seventh mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p8' => array( - 'description' => 'The eighth mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'p9' => array( - 'description' => 'The ninth mlid in the materialized path. See p1.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ), - 'updated' => array( - 'description' => 'Flag that indicates that this link was generated during the update from Drupal 5.', - 'type' => 'int', - 'not null' => TRUE, - 'default' => 0, - 'size' => 'small', - ), - 'route_name' => array( - 'description' => 'The machine name of a defined Symfony Route this menu item represents.', - 'type' => 'varchar', - 'length' => 255, - ), - 'route_parameters' => array( - 'description' => 'Serialized array of route parameters of this menu link.', - 'type' => 'blob', - 'size' => 'big', - 'not null' => FALSE, - 'serialize' => TRUE, - ), - ), - 'indexes' => array( - 'path_menu' => array(array('link_path', 128), 'menu_name'), - 'menu_plid_expand_child' => array('menu_name', 'plid', 'expanded', 'has_children'), - 'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'), - ), - 'primary key' => array('mlid'), - ); - - return $schema; -} diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module deleted file mode 100644 index 8a2ce38..0000000 --- a/core/modules/menu_link/menu_link.module +++ /dev/null @@ -1,225 +0,0 @@ -' . t('About') . ''; - $output .= '

' . t('The Menu Link module allows users to create menu links. It is required by the Menu UI module, which provides an interface for managing menus. See the Menu UI module help page for more information.', array('!menu-help' => \Drupal::url('help.page', array('name' => 'menu_ui')))) . '

'; - return $output; - } -} - -/** - * Entity URI callback. - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - */ -function menu_link_uri(MenuLink $menu_link) { - return new Url($menu_link->route_name, $menu_link->route_parameters); -} - -/** - * Loads a menu link entity. - * - * This function should never be called from within node_load() or any other - * function used as a menu object load function since an infinite recursion may - * occur. - * - * @param int $mlid - * The menu link ID. - * @param bool $reset - * (optional) Whether to reset the menu_link_load_multiple() cache. - * - * @return \Drupal\menu_link\Entity\MenuLink|null - * A menu link entity, or NULL if there is no entity with the given ID. - * - * @deprecated in Drupal 8.x, will be removed before Drupal 9.0. - * Use \Drupal\menu_link\Entity\MenuLink::load(). - */ -function menu_link_load($mlid = NULL, $reset = FALSE) { - if ($reset) { - \Drupal::entityManager()->getStorage('menu_link')->resetCache(array($mlid)); - } - return MenuLink::load($mlid); -} - -/** - * Loads menu link entities from the database. - * - * @param array $mlids - * (optional) An array of entity IDs. If omitted, all entities are loaded. - * @param bool $reset - * (optional) Whether to reset the internal cache. - * - * @return array<\Drupal\menu_link\Entity\MenuLink> - * An array of menu link entities indexed by entity IDs. - * - * @see menu_link_load() - * @see entity_load_multiple() - * - * @deprecated in Drupal 8.x, will be removed before Drupal 9.0. - * Use \Drupal\menu_link\Entity\MenuLink::loadMultiple(). - */ -function menu_link_load_multiple(array $mlids = NULL, $reset = FALSE) { - if ($reset) { - \Drupal::entityManager()->getStorage('menu_link')->resetCache($mlids); - } - return MenuLink::loadMultiple($mlids); -} - -/** - * Deletes a menu link. - * - * @param int $mlid - * The menu link ID. - * - * @see menu_link_delete_multiple() - */ -function menu_link_delete($mlid) { - menu_link_delete_multiple(array($mlid)); -} - -/** - * Deletes multiple menu links. - * - * @param array $mlids - * An array of menu link IDs. - * @param bool $force - * (optional) Forces deletion. Internal use only, setting to TRUE is - * discouraged. Defaults to FALSE. - * @param bool $prevent_reparenting - * (optional) Disables the re-parenting logic from the deletion process. - * Defaults to FALSE. - */ -function menu_link_delete_multiple(array $mlids, $force = FALSE, $prevent_reparenting = FALSE) { - if (!$mlids) { - // If no IDs or invalid IDs were passed, do nothing. - return; - } - - $controller = \Drupal::entityManager() - ->getStorage('menu_link'); - if (!$force) { - $entity_query = \Drupal::entityQuery('menu_link'); - $group = $entity_query->orConditionGroup() - ->condition('module', 'system', '<>') - ->condition('updated', 0, '<>'); - - $entity_query->condition('mlid', $mlids, 'IN'); - $entity_query->condition($group); - - $result = $entity_query->execute(); - $entities = $controller->loadMultiple($result); - } - else { - $entities = $controller->loadMultiple($mlids); - } - $controller->setPreventReparenting($prevent_reparenting); - $controller->delete($entities); -} - -/** - * Saves a menu link. - * - * After calling this function, rebuild the menu cache using - * menu_cache_clear_all(). - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * The menu link entity to be saved. - * - * @return int|bool - * Returns SAVED_NEW or SAVED_UPDATED if the save operation succeeded, or - * FALSE if it failed. - */ -function menu_link_save(MenuLink $menu_link) { - return $menu_link->save(); -} - -/** - * Inserts, updates, enables, disables, or deletes an uncustomized menu link. - * - * @param string $module - * The name of the module that owns the link. - * @param string $op - * Operation to perform: insert, update, enable, disable, or delete. - * @param string $link_path - * The path this link points to. - * @param string $link_title - * (optional) Title of the link to insert or new title to update the link to. - * Unused for delete. Defaults to NULL. - * - * @return integer|null - * The insert op returns the mlid of the new item. Others op return NULL. - */ -function menu_link_maintain($module, $op, $link_path, $link_title = NULL) { - $menu_link_controller = \Drupal::entityManager() - ->getStorage('menu_link'); - switch ($op) { - case 'insert': - $menu_link = entity_create('menu_link', array( - 'link_title' => $link_title, - 'link_path' => $link_path, - 'module' => $module,) - ); - return $menu_link->save(); - - case 'update': - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0)); - foreach ($menu_links as $menu_link) { - $menu_link->original = clone $menu_link; - if (isset($link_title)) { - $menu_link->link_title = $link_title; - } - $menu_link_controller->save($menu_link); - } - break; - - case 'enable': - case 'disable': - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path, 'module' => $module, 'customized' => 0)); - foreach ($menu_links as $menu_link) { - $menu_link->original = clone $menu_link; - $menu_link->hidden = ($op == 'disable' ? 1 : 0); - $menu_link->customized = 1; - if (isset($link_title)) { - $menu_link->link_title = $link_title; - } - $menu_link_controller->save($menu_link); - } - break; - - case 'delete': - $result = \Drupal::entityQuery('menu_link')->condition('link_path', $link_path)->execute(); - if (!empty($result)) { - menu_link_delete_multiple($result); - } - break; - } -} - -/** - * Implements hook_system_breadcrumb_alter(). - */ -function menu_link_system_breadcrumb_alter(array &$breadcrumb, RouteMatchInterface $route_match, array $context) { - // Custom breadcrumb behavior for editing menu links, we append a link to - // the menu in which the link is found. - if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link')) { - if (($menu_link instanceof MenuLinkInterface) && !$menu_link->isNew()) { - // Add a link to the menu admin screen. - $menu = entity_load('menu', $menu_link->menu_name); - $breadcrumb[] = Drupal::l($menu->label(), 'menu_ui.menu_edit', array('menu' => $menu->id)); - } - } -} diff --git a/core/modules/menu_link/menu_link.services.yml b/core/modules/menu_link/menu_link.services.yml deleted file mode 100644 index 88f5037..0000000 --- a/core/modules/menu_link/menu_link.services.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - menu_link.tree: - class: Drupal\menu_link\MenuTree - arguments: ['@database', '@cache.data', '@language_manager', '@request_stack', '@entity.manager', '@entity.query', '@state'] - menu_link.static: - class: Drupal\menu_link\StaticMenuLinks - arguments: ['@module_handler'] diff --git a/core/modules/menu_link/src/Entity/MenuLink.php b/core/modules/menu_link/src/Entity/MenuLink.php deleted file mode 100644 index e2215fd..0000000 --- a/core/modules/menu_link/src/Entity/MenuLink.php +++ /dev/null @@ -1,680 +0,0 @@ -newRevision = $value; - } - /** - * {@inheritdoc} - */ - public function isNewRevision() { - return $this->newRevision || ($this->getEntityType()->hasKey('revision') && !$this->getRevisionId()); - } - - /** - * {@inheritdoc} - */ - public function getRevisionId() { - return NULL; - } - - /** - * {@inheritdoc} - */ - public function isTranslatable() { - // @todo Inject the entity manager and retrieve bundle info from it. - $bundles = entity_get_bundles($this->entityTypeId); - return !empty($bundles[$this->bundle()]['translatable']); - } - - /** - * {@inheritdoc} - */ - public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { - } - - /** - * Overrides Entity::id(). - */ - public function id() { - return $this->mlid; - } - - /** - * {@inheritdoc} - */ - public function bundle() { - return $this->bundle; - } - - /** - * Overrides Entity::createDuplicate(). - */ - public function createDuplicate() { - $duplicate = parent::createDuplicate(); - $duplicate->plid = NULL; - return $duplicate; - } - - /** - * {@inheritdoc} - */ - public function getRoute() { - if (!$this->route_name) { - return NULL; - } - if (!($this->routeObject instanceof Route)) { - $route_provider = \Drupal::service('router.route_provider'); - $this->routeObject = $route_provider->getRouteByName($this->route_name); - } - return $this->routeObject; - } - - /** - * {@inheritdoc} - */ - public function setRouteObject(Route $route) { - $this->routeObject = $route; - } - - /** - * {@inheritdoc} - */ - public function reset() { - // To reset the link to its original values, we need to retrieve its - // definition from the menu_link.static service. Otherwise, for example, - // the link's menu would not be reset, because properties like the original - // 'menu_name' are not stored anywhere else. Since resetting a link happens - // rarely and this is a one-time operation, retrieving the full set of - // default menu links does little harm. - $all_links = \Drupal::service('menu_link.static')->getLinks(); - $original = $all_links[$this->machine_name]; - $original['machine_name'] = $this->machine_name; - /** @var \Drupal\menu_link\MenuLinkStorageInterface $storage */ - $storage = \Drupal::entityManager()->getStorage($this->entityTypeId); - // @todo Do not create a new entity in order to update it, see - // https://drupal.org/node/2241865 - $new_link = $storage->createFromDefaultLink($original); - $new_link->setOriginalId($this->id()); - // Allow the menu to be determined by the parent - if (!empty($new_link['parent']) && !empty($all_links[$new_link['parent']])) { - // Walk up the tree to find the menu name. - $parent = $all_links[$new_link['parent']]; - $existing_parent = db_select('menu_links') - ->fields('menu_links') - ->condition('machine_name', $parent['machine_name']) - ->execute()->fetchAssoc(); - if ($existing_parent) { - /** @var \Drupal\Core\Entity\EntityInterface $existing_parent */ - $existing_parent = $storage->create($existing_parent); - $new_link->menu_name = $existing_parent->menu_name; - $new_link->plid = $existing_parent->id(); - } - } - // Merge existing menu link's ID and 'has_children' property. - foreach (array('mlid', 'has_children') as $key) { - $new_link->{$key} = $this->{$key}; - } - $new_link->save(); - return $new_link; - } - - /** - * Implements ArrayAccess::offsetExists(). - */ - public function offsetExists($offset) { - return isset($this->{$offset}); - } - - /** - * Implements ArrayAccess::offsetGet(). - */ - public function &offsetGet($offset) { - return $this->{$offset}; - } - - /** - * Implements ArrayAccess::offsetSet(). - */ - public function offsetSet($offset, $value) { - $this->{$offset} = $value; - } - - /** - * Implements ArrayAccess::offsetUnset(). - */ - public function offsetUnset($offset) { - unset($this->{$offset}); - } - - /** - * {@inheritdoc} - */ - public static function preDelete(EntityStorageInterface $storage, array $entities) { - parent::preDelete($storage, $entities); - - // Nothing to do if we don't want to reparent children. - if ($storage->getPreventReparenting()) { - return; - } - - foreach ($entities as $entity) { - // Children get re-attached to the item's parent. - if ($entity->has_children) { - $children = $storage->loadByProperties(array('plid' => $entity->plid)); - foreach ($children as $child) { - $child->plid = $entity->plid; - $storage->save($child); - } - } - } - } - - /** - * {@inheritdoc} - */ - public static function postDelete(EntityStorageInterface $storage, array $entities) { - parent::postDelete($storage, $entities); - - // Update the has_children status of the parent. - foreach ($entities as $entity) { - if (!$storage->getPreventReparenting()) { - $storage->updateParentalStatus($entity); - } - } - - // Also clear the menu system static caches. - menu_reset_static_cache(); - _menu_update_expanded_menus(); - } - - /** - * {@inheritdoc} - */ - public function preSave(EntityStorageInterface $storage) { - parent::preSave($storage); - - // This is the easiest way to handle the unique internal path '', - // since a path marked as external does not need to match a route. - $this->external = (UrlHelper::isExternal($this->link_path) || $this->link_path == '') ? 1 : 0; - - // Try to find a parent link. If found, assign it and derive its menu. - $parent = $this->findParent($storage); - if ($parent) { - $this->plid = $parent->id(); - $this->menu_name = $parent->menu_name; - } - // If no corresponding parent link was found, move the link to the top-level. - else { - $this->plid = 0; - } - - // Directly fill parents for top-level links. - if ($this->plid == 0) { - $this->p1 = $this->id(); - for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { - $parent_property = "p$i"; - $this->{$parent_property} = 0; - } - $this->depth = 1; - } - // Otherwise, ensure that this link's depth is not beyond the maximum depth - // and fill parents based on the parent link. - else { - if ($this->has_children && $this->original) { - $limit = MENU_MAX_DEPTH - $storage->findChildrenRelativeDepth($this->original) - 1; - } - else { - $limit = MENU_MAX_DEPTH - 1; - } - if ($parent->depth > $limit) { - return FALSE; - } - $this->depth = $parent->depth + 1; - $this->setParents($parent); - } - - // Need to check both plid and menu_name, since plid can be 0 in any menu. - if (isset($this->original) && ($this->plid != $this->original->plid || $this->menu_name != $this->original->menu_name)) { - $storage->moveChildren($this); - } - - // Find the route_name. - if (!$this->external && !isset($this->route_name)) { - $url = Url::createFromPath($this->link_path); - $this->route_name = $url->getRouteName(); - $this->route_parameters = $url->getRouteParameters(); - } - elseif (empty($this->link_path)) { - $this->link_path = \Drupal::urlGenerator()->getPathFromRoute($this->route_name, $this->route_parameters); - } - } - - /** - * {@inheritdoc} - */ - public function postSave(EntityStorageInterface $storage, $update = TRUE) { - parent::postSave($storage, $update); - - // Check the has_children status of the parent. - $storage->updateParentalStatus($this); - - - // Entity::postSave() calls Entity::invalidateTagsOnSave(), which only - // handles the regular cases. The MenuLink entity has two special cases. - $cache_tags = array(); - // Case 1: a newly created menu link is *also* added to a menu, so we must - // invalidate the associated menu's cache tag. - if (!$update) { - $cache_tags = $this->getCacheTag(); - } - // Case 2: a menu link may be moved from one menu to another; the original - // menu's cache tag must also be invalidated. - if (isset($this->original) && $this->menu_name != $this->original->menu_name) { - $cache_tags = NestedArray::mergeDeep($cache_tags, $this->original->getCacheTag()); - } - Cache::invalidateTags($cache_tags); - - // Also clear the menu system static caches. - menu_reset_static_cache(); - - // Track which menu items are expanded. - _menu_update_expanded_menus(); - } - - /** - * {@inheritdoc} - */ - public static function postLoad(EntityStorageInterface $storage, array &$entities) { - parent::postLoad($storage, $entities); - - $routes = array(); - foreach ($entities as $menu_link) { - $menu_link->options = unserialize($menu_link->options); - $menu_link->route_parameters = unserialize($menu_link->route_parameters); - - // By default use the menu_name as type. - $menu_link->bundle = $menu_link->menu_name; - - // For all links that have an associated route, load the route object now - // and save it on the object. That way we avoid a select N+1 problem later. - if ($menu_link->route_name) { - $routes[$menu_link->id()] = $menu_link->route_name; - } - } - - // Now mass-load any routes needed and associate them. - if ($routes) { - $route_objects = \Drupal::service('router.route_provider')->getRoutesByNames($routes); - foreach ($routes as $entity_id => $route) { - // Not all stored routes will be valid on load. - if (isset($route_objects[$route])) { - $entities[$entity_id]->setRouteObject($route_objects[$route]); - } - } - } - } - - /** - * {@inheritdoc} - */ - protected function setParents(MenuLinkInterface $parent) { - $i = 1; - while ($i < $this->depth) { - $p = 'p' . $i++; - $this->{$p} = $parent->{$p}; - } - $p = 'p' . $i++; - // The parent (p1 - p9) corresponding to the depth always equals the mlid. - $this->{$p} = $this->id(); - while ($i <= MENU_MAX_DEPTH) { - $p = 'p' . $i++; - $this->{$p} = 0; - } - } - - /** - * {@inheritdoc} - */ - protected function findParent(EntityStorageInterface $storage) { - $parent = FALSE; - - // This item is explicitly top-level, skip the rest of the parenting. - if (isset($this->plid) && empty($this->plid)) { - return $parent; - } - - // If we have a parent link ID, try to use that. - $candidates = array(); - if (isset($this->plid)) { - $candidates[] = $this->plid; - } - - // Else, if we have a link hierarchy try to find a valid parent in there. - if (!empty($this->depth) && $this->depth > 1) { - for ($depth = $this->depth - 1; $depth >= 1; $depth--) { - $parent_property = "p$depth"; - $candidates[] = $this->$parent_property; - } - } - - foreach ($candidates as $mlid) { - $parent = $storage->load($mlid); - if ($parent) { - break; - } - } - return $parent; - } - - /** - * Builds and returns the renderable array for this menu link. - * - * @return array - * A renderable array representing the content of the link. - */ - public function build() { - $build = array( - '#type' => 'link', - '#title' => $this->title, - '#href' => $this->href, - '#route_name' => $this->route_name ? $this->route_name : NULL, - '#route_parameters' => $this->route_parameters, - '#options' => !empty($this->localized_options) ? $this->localized_options : array(), - ); - return $build; - } - - /** - * {@inheritdoc} - */ - public function getCacheTag() { - return entity_load('menu', $this->menu_name)->getCacheTag(); - } - - /** - * {@inheritdoc} - */ - public function getListCacheTags() { - return entity_load('menu', $this->menu_name)->getListCacheTags(); - } - -} diff --git a/core/modules/menu_link/src/MenuLinkAccessController.php b/core/modules/menu_link/src/MenuLinkAccessController.php deleted file mode 100644 index c6e6b91..0000000 --- a/core/modules/menu_link/src/MenuLinkAccessController.php +++ /dev/null @@ -1,41 +0,0 @@ -hasPermission('administer menu'); - if ($access) { - switch ($operation) { - case 'reset': - // Reset allowed for items defined via hook_menu() and customized. - return !empty($entity->machine_name) && $entity->customized; - - case 'delete': - // Only items created by the Menu UI module can be deleted. - return $entity->module == 'menu_ui' || $entity->updated == 1; - - } - } - return $access; - } - -} diff --git a/core/modules/menu_link/src/MenuLinkForm.php b/core/modules/menu_link/src/MenuLinkForm.php deleted file mode 100644 index f5cbca1..0000000 --- a/core/modules/menu_link/src/MenuLinkForm.php +++ /dev/null @@ -1,311 +0,0 @@ -menuLinkStorage = $menu_link_storage; - $this->pathAliasManager = $path_alias_manager; - $this->urlGenerator = $url_generator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity.manager')->getStorage('menu_link'), - $container->get('path.alias_manager'), - $container->get('url_generator') - ); - } - - /** - * Overrides EntityForm::form(). - */ - public function form(array $form, array &$form_state) { - $menu_link = $this->entity; - // Since menu_link_load() no longer returns a translated and access checked - // item, do it here instead. - _menu_link_translate($menu_link); - - $form['link_title'] = array( - '#type' => 'textfield', - '#title' => t('Menu link title'), - '#default_value' => $menu_link->link_title, - '#description' => t('The text to be used for this link in the menu.'), - '#required' => TRUE, - ); - foreach (array('link_path', 'mlid', 'module', 'has_children', 'options') as $key) { - $form[$key] = array('#type' => 'value', '#value' => $menu_link->{$key}); - } - // Any item created or edited via this interface is considered "customized". - $form['customized'] = array('#type' => 'value', '#value' => 1); - - // We are not using url() when constructing this path because it would add - // $base_path. - $path = $menu_link->link_path; - if (isset($menu_link->options['query'])) { - $path .= '?' . $this->urlGenerator->httpBuildQuery($menu_link->options['query']); - } - if (isset($menu_link->options['fragment'])) { - $path .= '#' . $menu_link->options['fragment']; - } - if ($menu_link->module == 'menu_ui') { - $form['link_path'] = array( - '#type' => 'textfield', - '#title' => t('Path'), - '#maxlength' => 255, - '#default_value' => $path, - '#description' => t('The path for this menu link. This can be an internal Drupal path such as %add-node or an external URL such as %drupal. Enter %front to link to the front page.', array('%front' => '', '%add-node' => 'node/add', '%drupal' => 'http://drupal.org')), - '#required' => TRUE, - ); - } - else { - $form['_path'] = array( - '#type' => 'item', - '#title' => t('Path'), - '#description' => l($menu_link->link_title, $menu_link->href, $menu_link->options), - ); - } - - $form['description'] = array( - '#type' => 'textarea', - '#title' => t('Description'), - '#default_value' => isset($menu_link->options['attributes']['title']) ? $menu_link->options['attributes']['title'] : '', - '#rows' => 1, - '#description' => t('Shown when hovering over the menu link.'), - ); - $form['enabled'] = array( - '#type' => 'checkbox', - '#title' => t('Enabled'), - '#default_value' => !$menu_link->hidden, - '#description' => t('Menu links that are not enabled will not be listed in any menu.'), - ); - $form['expanded'] = array( - '#type' => 'checkbox', - '#title' => t('Show as expanded'), - '#default_value' => $menu_link->expanded, - '#description' => t('If selected and this menu link has children, the menu will always appear expanded.'), - ); - - // Generate a list of possible parents (not including this link or descendants). - $options = menu_ui_parent_options(menu_ui_get_menus(), $menu_link); - $default = $menu_link->menu_name . ':' . $menu_link->plid; - if (!isset($options[$default])) { - $default = 'tools:0'; - } - $form['parent'] = array( - '#type' => 'select', - '#title' => t('Parent link'), - '#default_value' => $default, - '#options' => $options, - '#description' => t('The maximum depth for a link and all its children is fixed at !maxdepth. Some menu links may not be available as parents if selecting them would exceed this limit.', array('!maxdepth' => MENU_MAX_DEPTH)), - '#attributes' => array('class' => array('menu-title-select')), - ); - - // Get number of items in menu so the weight selector is sized appropriately. - $delta = $this->menuLinkStorage->countMenuLinks($menu_link->menu_name); - $form['weight'] = array( - '#type' => 'weight', - '#title' => t('Weight'), - // Old hardcoded value. - '#delta' => max($delta, 50), - '#default_value' => $menu_link->weight, - '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'), - ); - - // Language module allows to configure the menu link language independently - // of the menu language. It also allows to optionally show the language - // selector on the menu link form so that the language of each menu link can - // be configured individually. - if ($this->moduleHandler->moduleExists('language')) { - $language_configuration = language_get_default_configuration('menu_link', $menu_link->bundle()); - $default_langcode = ($menu_link->isNew() ? $language_configuration['langcode'] : $menu_link->langcode); - $language_show = $language_configuration['language_show']; - } - // Without Language module menu links inherit the menu language and no - // language selector is shown. - else { - $default_langcode = ($menu_link->isNew() ? entity_load('menu', $menu_link->menu_name)->language()->getId() : $menu_link->langcode); - $language_show = FALSE; - } - - $form['langcode'] = array( - '#type' => 'language_select', - '#title' => t('Language'), - '#languages' => LanguageInterface::STATE_ALL, - '#default_value' => $default_langcode, - '#access' => $language_show, - ); - - return parent::form($form, $form_state, $menu_link); - } - - /** - * Overrides EntityForm::actions(). - */ - protected function actions(array $form, array &$form_state) { - $element = parent::actions($form, $form_state); - $element['submit']['#button_type'] = 'primary'; - return $element; - } - - /** - * Overrides EntityForm::validate(). - */ - public function validate(array $form, array &$form_state) { - $menu_link = $this->buildEntity($form, $form_state); - - $normal_path = $this->pathAliasManager->getPathByAlias($menu_link->link_path); - if ($menu_link->link_path != $normal_path) { - drupal_set_message(t('The menu system stores system paths only, but will use the URL alias for display. %link_path has been stored as %normal_path', array('%link_path' => $menu_link->link_path, '%normal_path' => $normal_path))); - $menu_link->link_path = $normal_path; - $form_state['values']['link_path'] = $normal_path; - } - if (!UrlHelper::isExternal($menu_link->link_path)) { - $parsed_link = parse_url($menu_link->link_path); - if (isset($parsed_link['query'])) { - $menu_link->options['query'] = array(); - parse_str($parsed_link['query'], $menu_link->options['query']); - } - else { - // Use unset() rather than setting to empty string - // to avoid redundant serialized data being stored. - unset($menu_link->options['query']); - } - if (isset($parsed_link['fragment'])) { - $menu_link->options['fragment'] = $parsed_link['fragment']; - } - else { - unset($menu_link->options['fragment']); - } - if (isset($parsed_link['path']) && $menu_link->link_path != $parsed_link['path']) { - $menu_link->link_path = $parsed_link['path']; - } - } - if (!trim($menu_link->link_path) || !drupal_valid_path($menu_link->link_path, TRUE)) { - $this->setFormError('link_path', $form_state, $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $menu_link->link_path))); - } - - parent::validate($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function buildEntity(array $form, array &$form_state) { - // @todo: Remove this when menu links are converted to content entities in - // http://drupal.org/node/1842858. - $entity = clone $this->entity; - // If you submit a form, the form state comes from caching, which forces - // the controller to be the one before caching. Ensure to have the - // controller of the current request. - $form_state['controller'] = $this; - - // Copy top-level form values to entity properties, without changing - // existing entity properties that are not being edited by - // this form. - foreach ($form_state['values'] as $key => $value) { - $entity->$key = $value; - } - - // Invoke all specified builders for copying form values to entity properties. - if (isset($form['#entity_builders'])) { - foreach ($form['#entity_builders'] as $function) { - call_user_func_array($function, array($entity->getEntityTypeId(), $entity, &$form, &$form_state)); - } - } - - return $entity; - } - - /** - * Overrides EntityForm::submit(). - */ - public function submit(array $form, array &$form_state) { - // Build the menu link object from the submitted values. - $menu_link = parent::submit($form, $form_state); - - // The value of "hidden" is the opposite of the value supplied by the - // "enabled" checkbox. - $menu_link->hidden = (int) !$menu_link->enabled; - unset($menu_link->enabled); - - $menu_link->options['attributes']['title'] = $menu_link->description; - list($menu_link->menu_name, $menu_link->plid) = explode(':', $menu_link->parent); - - return $menu_link; - } - - /** - * Overrides EntityForm::save(). - */ - public function save(array $form, array &$form_state) { - $menu_link = $this->entity; - - $saved = $menu_link->save(); - - if ($saved) { - drupal_set_message(t('The menu link has been saved.')); - $form_state['redirect_route'] = array( - 'route_name' => 'menu_ui.menu_edit', - 'route_parameters' => array( - 'menu' => $menu_link->menu_name, - ), - ); - } - else { - drupal_set_message(t('There was an error saving the menu link.'), 'error'); - $form_state['rebuild'] = TRUE; - } - } - -} diff --git a/core/modules/menu_link/src/MenuLinkInterface.php b/core/modules/menu_link/src/MenuLinkInterface.php deleted file mode 100644 index fdfab67..0000000 --- a/core/modules/menu_link/src/MenuLinkInterface.php +++ /dev/null @@ -1,45 +0,0 @@ -database->startTransaction(); - try { - // Load the stored entity, if any. - if (!$entity->isNew() && !isset($entity->original)) { - $id = $entity->id(); - if ($entity->getOriginalId() !== NULL) { - $id = $entity->getOriginalId(); - } - $entity->original = $this->loadUnchanged($id); - } - - if ($entity->isNew()) { - $entity->mlid = $this->database->insert($this->entityType->getBaseTable())->fields(array('menu_name' => $entity->menu_name))->execute(); - $entity->enforceIsNew(); - } - - // Unlike the save() method from EntityDatabaseStorage, we invoke the - // 'presave' hook first because we want to allow modules to alter the - // entity before all the logic from our preSave() method. - $this->invokeHook('presave', $entity); - $entity->preSave($this); - - // If every value in $entity->original is the same in the $entity, there - // is no reason to run the update queries or clear the caches. We use - // array_intersect_key() with the $entity as the first parameter because - // $entity may have additional keys left over from building a router entry. - // The intersect removes the extra keys, allowing a meaningful comparison. - if ($entity->isNew() || (array_intersect_key(get_object_vars($entity), get_object_vars($entity->original)) != get_object_vars($entity->original))) { - $return = drupal_write_record($this->entityType->getBaseTable(), $entity, $this->idKey); - - if ($return) { - if (!$entity->isNew()) { - $this->resetCache(array($entity->{$this->idKey})); - $entity->postSave($this, TRUE); - $this->invokeHook('update', $entity); - } - else { - $return = SAVED_NEW; - $this->resetCache(); - - $entity->enforceIsNew(FALSE); - $entity->postSave($this, FALSE); - $this->invokeHook('insert', $entity); - } - } - } - - // Ignore replica server temporarily. - db_ignore_replica(); - unset($entity->original); - - return $return; - } - catch (\Exception $e) { - $transaction->rollback(); - watchdog_exception($this->entityTypeId, $e); - throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); - } - } - - /** - * {@inheritdoc} - */ - public function setPreventReparenting($value = FALSE) { - $this->preventReparenting = $value; - } - - /** - * {@inheritdoc} - */ - public function getPreventReparenting() { - return $this->preventReparenting; - } - - /** - * {@inheritdoc} - */ - public function loadUpdatedCustomized(array $router_paths) { - $query = parent::buildQuery(NULL); - $query - ->condition(db_or() - ->condition('updated', 1) - ->condition(db_and() - ->condition('router_path', $router_paths, 'NOT IN') - ->condition('external', 0) - ->condition('customized', 1) - ) - ); - $query_result = $query->execute(); - - // We provide the necessary arguments for PDO to create objects of the - // specified entity class. - // @see \Drupal\Core\Entity\EntityInterface::__construct() - $query_result->setFetchMode(\PDO::FETCH_CLASS, $this->entityClass, array(array(), $this->entityTypeId)); - - return $query_result->fetchAllAssoc($this->idKey); - } - - /** - * {@inheritdoc} - */ - public function loadModuleAdminTasks() { - // @todo - this code will move out of the menu link entity, so we are doing - // a straight SQL query for expediency. - $result = $this->database->select('menu_links'); - $result->condition('machine_name', 'system.admin'); - $result->addField('menu_links', 'mlid'); - $plid = $result->execute()->fetchField(); - - $query = $this->database->select('menu_links', 'base', array('fetch' => \PDO::FETCH_ASSOC)); - $query->fields('base'); - $query - ->condition('base.hidden', 0, '>=') - ->condition('base.module', '', '>') - ->condition('base.machine_name', '', '>') - ->condition('base.p1', $plid); - $entities = $query->execute()->fetchAll(); - - return $entities; - } - - /** - * {@inheritdoc} - */ - public function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) { - // If plid == 0, there is nothing to update. - if ($entity->plid) { - // Check if at least one visible child exists in the table. - $query = $this->getQuery(); - $query - ->condition('menu_name', $entity->menu_name) - ->condition('hidden', 0) - ->condition('plid', $entity->plid) - ->count(); - - if ($exclude) { - $query->condition('mlid', $entity->id(), '<>'); - } - - $parent_has_children = ((bool) $query->execute()) ? 1 : 0; - $this->database->update('menu_links') - ->fields(array('has_children' => $parent_has_children)) - ->condition('mlid', $entity->plid) - ->execute(); - } - } - - /** - * {@inheritdoc} - */ - public function findChildrenRelativeDepth(EntityInterface $entity) { - // @todo Since all we need is a specific field from the base table, does it - // make sense to convert to EFQ? - $query = $this->database->select('menu_links'); - $query->addField('menu_links', 'depth'); - $query->condition('menu_name', $entity->menu_name); - $query->orderBy('depth', 'DESC'); - $query->range(0, 1); - - $i = 1; - $p = 'p1'; - while ($i <= MENU_MAX_DEPTH && $entity->{$p}) { - $query->condition($p, $entity->{$p}); - $p = 'p' . ++$i; - } - - $max_depth = $query->execute()->fetchField(); - - return ($max_depth > $entity->depth) ? $max_depth - $entity->depth : 0; - } - - /** - * {@inheritdoc} - */ - public function moveChildren(EntityInterface $entity) { - $query = $this->database->update($this->entityType->getBaseTable()); - - $query->fields(array('menu_name' => $entity->menu_name)); - - $p = 'p1'; - $expressions = array(); - for ($i = 1; $i <= $entity->depth; $p = 'p' . ++$i) { - $expressions[] = array($p, ":p_$i", array(":p_$i" => $entity->{$p})); - } - $j = $entity->original->depth + 1; - while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) { - $expressions[] = array('p' . $i++, 'p' . $j++, array()); - } - while ($i <= MENU_MAX_DEPTH) { - $expressions[] = array('p' . $i++, 0, array()); - } - - $shift = $entity->depth - $entity->original->depth; - if ($shift > 0) { - // The order of expressions must be reversed so the new values don't - // overwrite the old ones before they can be used because "Single-table - // UPDATE assignments are generally evaluated from left to right" - // @see http://dev.mysql.com/doc/refman/5.0/en/update.html - $expressions = array_reverse($expressions); - } - foreach ($expressions as $expression) { - $query->expression($expression[0], $expression[1], $expression[2]); - } - - $query->expression('depth', 'depth + :depth', array(':depth' => $shift)); - $query->condition('menu_name', $entity->original->menu_name); - $p = 'p1'; - for ($i = 1; $i <= MENU_MAX_DEPTH && $entity->original->{$p}; $p = 'p' . ++$i) { - $query->condition($p, $entity->original->{$p}); - } - - $query->execute(); - - // Check the has_children status of the parent, while excluding this item. - $this->updateParentalStatus($entity->original, TRUE); - } - - /** - * {@inheritdoc} - */ - public function countMenuLinks($menu_name) { - $query = $this->getQuery(); - $query - ->condition('menu_name', $menu_name) - ->count(); - return $query->execute(); - } - - /** - * {@inheritdoc} - */ - public function getParentFromHierarchy(EntityInterface $entity) { - $parent_path = $entity->link_path; - do { - $parent = FALSE; - $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); - - $query = $this->getQuery(); - $query - ->condition('mlid', $entity->id(), '<>') - ->condition('module', 'system') - // We always respect the link's 'menu_name'; inheritance for router - // items is ensured in _menu_router_build(). - ->condition('menu_name', $entity->menu_name) - ->condition('link_path', $parent_path); - - $result = $query->execute(); - // Only valid if we get a unique result. - if (count($result) == 1) { - $parent = $this->load(reset($result)); - } - } while ($parent === FALSE && $parent_path); - - return $parent; - } - - /** - * {@inheritdoc} - */ - public function createFromDefaultLink(array $item) { - // Suggested items are disabled by default. - $item += array( - 'hidden' => 0, - 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), - ); - return $this->create($item); - } - -} diff --git a/core/modules/menu_link/src/MenuLinkStorageInterface.php b/core/modules/menu_link/src/MenuLinkStorageInterface.php deleted file mode 100644 index 3d3fd5d..0000000 --- a/core/modules/menu_link/src/MenuLinkStorageInterface.php +++ /dev/null @@ -1,110 +0,0 @@ -database = $database; - $this->cache = $cache_backend; - $this->languageManager = $language_manager; - $this->requestStack = $request_stack; - $this->menuLinkStorage = $entity_manager->getStorage('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(); - - // 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. - 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, - ); - $active_trail = $this->getActiveTrailIds($menu_name); - - // If this page is accessible to the current user, build the tree - // parameters accordingly. - if ($page_not_403) { - // The active trail contains more than only array(0 => 0). - if (count($active_trail) > 1) { - // 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 getActiveTrailIds($menu_name) { - // 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); - - $request = $this->requestStack->getCurrentRequest(); - - 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. - // Check if the active trail has been overridden for this menu tree. - $active_path = $this->getPath($menu_name); - // 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)) { - 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]; - } - } - } - } - } - return $active_trail; - } - - /** - * {@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. - * MenuTree::checkAccess() 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->menuTree[$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/src/MenuTreeInterface.php b/core/modules/menu_link/src/MenuTreeInterface.php deleted file mode 100644 index 418f602..0000000 --- a/core/modules/menu_link/src/MenuTreeInterface.php +++ /dev/null @@ -1,182 +0,0 @@ -moduleHandler = $module_handler; - } - - /** - * Gets the menu links defined in YAML files. - * - * @return array - * An array of default menu links. - */ - public function getLinks() { - $discovery = $this->getDiscovery(); - foreach ($discovery->findAll() as $module => $menu_links) { - foreach ($menu_links as $machine_name => $menu_link) { - $all_links[$machine_name] = $menu_link; - $all_links[$machine_name]['machine_name'] = $machine_name; - $all_links[$machine_name]['module'] = $module; - } - } - - $this->moduleHandler->alter('menu_link_defaults', $all_links); - foreach ($all_links as $machine_name => $menu_link) { - // Set the machine_name to the menu links added dynamically. - if (!isset($menu_link['machine_name'])) { - $all_links[$machine_name]['machine_name'] = $machine_name; - } - // Change the key to match the DB column for now. - $all_links[$machine_name]['link_title'] = $all_links[$machine_name]['title']; - unset($all_links[$machine_name]['title']); - } - - return $all_links; - } - - /** - * Creates a YAML discovery for menu links. - * - * @return \Drupal\Component\Discovery\YamlDiscovery - * An YAML discovery instance. - */ - protected function getDiscovery() { - return new YamlDiscovery('links.menu', $this->moduleHandler->getModuleDirectories()); - } - -} - diff --git a/core/modules/menu_link/tests/src/MenuTreeTest.php b/core/modules/menu_link/tests/src/MenuTreeTest.php deleted file mode 100644 index 747335a..0000000 --- a/core/modules/menu_link/tests/src/MenuTreeTest.php +++ /dev/null @@ -1,539 +0,0 @@ - '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} - */ - 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\State\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 getActiveTrailIds(). - * - * @covers ::getActiveTrailIds() - */ - public function testGetActiveTrailIds() { - $menu_link = array( - 'mlid' => 10, - 'route_name' => 'example1', - 'p1' => 3, - 'p2' => 2, - 'p3' => 1, - 'p4' => 4, - 'p5' => 9, - 'p6' => 5, - 'p7' => 6, - 'p8' => 7, - 'p9' => 8, - 'menu_name' => 'test_menu' - ); - $this->menuTree->setPreferredMenuLink('test_menu', 'test/path', $menu_link); - $request = (new Request()); - $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'test_route'); - $this->requestStack->push($request); - $this->menuTree->setPath('test_menu', 'test/path'); - - $trail = $this->menuTree->getActiveTrailIds('test_menu'); - $this->assertEquals(array(0 => 0, 3 => 3, 2 => 2, 1 => 1, 4 => 4, 9 => 9, 5 => 5, 6 => 6, 7 => 7), $trail); - } - - /** - * Tests getActiveTrailIds() without preferred link. - * - * @covers ::getActiveTrailIds() - */ - public function testGetActiveTrailIdsWithoutPreferredLink() { - $request = (new Request()); - $request->attributes->set(RouteObjectInterface::ROUTE_NAME, 'test_route'); - $this->requestStack->push($request); - $this->menuTree->setPath('test_menu', 'test/path'); - - $trail = $this->menuTree->getActiveTrailIds('test_menu'); - $this->assertEquals(array(0 => 0), $trail); - } - - - /** - * Tests buildTree with simple menu_name and no parameters. - */ - public function testBuildTreeWithoutParameters() { - $language = new Language(array('id' => 'en')); - $this->languageManager->expects($this->any()) - ->method('getCurrentLanguage') - ->will($this->returnValue($language)); - - // Setup query and the query result. - $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface'); - $this->entityQueryFactory->expects($this->once()) - ->method('get') - ->with('menu_link') - ->will($this->returnValue($query)); - $query->expects($this->once()) - ->method('condition') - ->with('menu_name', 'test_menu'); - $query->expects($this->once()) - ->method('execute') - ->will($this->returnValue(array(1, 2, 3))); - - $storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface'); - $base = array( - 'access' => TRUE, - 'weight' => 0, - 'title' => 'title', - ); - $menu_link = $base + array( - 'mlid' => 1, - 'p1' => 3, - 'p2' => 2, - 'p3' => 1, - ); - $links[1] = $menu_link; - $menu_link = $base + array( - 'mlid' => 3, - 'p1' => 3, - 'depth' => 1, - ); - $links[3] = $menu_link; - $menu_link = $base + array( - 'mlid' => 2, - 'p1' => 3, - 'p2' => 2, - 'depth' => 2, - ); - $links[2] = $menu_link; - $storage->expects($this->once()) - ->method('loadMultiple') - ->with(array(1, 2, 3)) - ->will($this->returnValue($links)); - $this->menuTree->setStorage($storage); - - // Ensure that static/non static caching works. - // First setup no working caching. - $this->cacheBackend->expects($this->at(0)) - ->method('get') - ->with('links:test_menu:tree-data:en:35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3') - ->will($this->returnValue(FALSE)); - $this->cacheBackend->expects($this->at(1)) - ->method('set') - ->with('links:test_menu:tree-data:en:35786c7117b4e38d0f169239752ce71158266ae2f6e4aa230fbbb87bd699c0e3', $this->anything(), Cache::PERMANENT, array('menu' => 'test_menu')); - - // Ensure that the static caching triggered. - $this->cacheBackend->expects($this->exactly(1)) - ->method('get'); - - $this->menuTree->buildTree('test_menu'); - $this->menuTree->buildTree('test_menu'); - } - - /** - * Tests the output with a single level. - * - * @covers ::renderTree - */ - 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 ::renderTree - */ - 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; - - /** - * Stores the preferred menu link per menu and path. - * - * @var array - */ - protected $preferredMenuLink; - - /** - * {@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) { - return isset($this->preferredMenuLink[$menu_name][$active_path]) ? $this->preferredMenuLink[$menu_name][$active_path] : NULL; - } - - /** - * Set the storage. - * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The menu link storage. - */ - public function setStorage(EntityStorageInterface $storage) { - $this->menuLinkStorage = $storage; - } - - /** - * Sets the preferred menu link. - * - * @param string $menu_name - * The menu name. - * @param string $active_path - * The active path. - * @param array $menu_link - * The preferred menu link. - */ - public function setPreferredMenuLink($menu_name, $active_path, $menu_link) { - $this->preferredMenuLink[$menu_name][$active_path] = $menu_link; - } - -} - -} - -namespace { - if (!defined('MENU_MAX_DEPTH')) { - define('MENU_MAX_DEPTH', 9); - } -} diff --git a/core/modules/menu_link_content/menu_link_content.install b/core/modules/menu_link_content/menu_link_content.install new file mode 100644 index 0000000..31fa93f --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.install @@ -0,0 +1,27 @@ +groupBy('uuid'); + $result = $query->execute(); + $uuids = array(); + foreach ($result as $row) { + $uuids[] = $row['uuid']; + } + + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + foreach ($uuids as $uuid) { + // Manually build the plugin ID, and remove it from the menu tree. + $menu_link_manager->removeDefinition("menu_link_content:$uuid", FALSE); + } +} diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module index 25dfb26..6d33dce 100644 --- a/core/modules/menu_link_content/menu_link_content.module +++ b/core/modules/menu_link_content/menu_link_content.module @@ -5,9 +5,30 @@ * Allows administrators to create custom menu links. */ +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\system\MenuInterface; /** + * Implements hook_help(). + */ +function menu_link_content_help($route_name, RouteMatchInterface $route_match) { + switch ($route_name) { + case 'help.page.menu_link_content': + $output = ''; + $output .= '

    ' . t('About') . '

    '; + $output .= '

    ' . t('The Custom Menu Links module allows users to create menu links. These links can be translated if multiple languages are used for the site.'); + if (\Drupal::moduleHandler()->moduleExists('menu_ui')) { + $output .= ' ' . t('It is required by the Menu UI module, which provides an interface for managing menus and menu links. See the Menu UI module help page for more information.', array('!menu-help' => \Drupal::url('help.page', array('name' => 'menu_ui')))); + } + else { + $output .= ' ' . t('If you enable the Menu UI module, it provides an interface for managing menus and menu links.'); + } + $output .= '

    '; + return $output; + } +} + +/** * Implements hook_menu_delete(). */ function menu_link_content_menu_delete(MenuInterface $menu) { diff --git a/core/modules/menu_link_content/menu_link_content.routing.yml b/core/modules/menu_link_content/menu_link_content.routing.yml index f96b22d..cebc134 100644 --- a/core/modules/menu_link_content/menu_link_content.routing.yml +++ b/core/modules/menu_link_content/menu_link_content.routing.yml @@ -11,6 +11,8 @@ menu_link_content.link_edit: defaults: _entity_form: 'menu_link_content.default' _title: 'Edit menu link' + options: + _admin_route: TRUE requirements: _entity_access: 'menu_link_content.update' diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php index efbc76c..54f174a 100644 --- a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php @@ -144,13 +144,6 @@ public function setMenuLinkInstance(MenuLinkInterface $menu_link) { * {@inheritdoc} */ public function buildConfigurationForm(array $form, array &$form_state) { - return $this->buildEditForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function buildEditForm(array &$form, array &$form_state) { $this->setOperation('default'); $this->init($form_state); @@ -167,26 +160,10 @@ public function validateConfigurationForm(array &$form, array &$form_state) { /** * {@inheritdoc} */ - public function validateEditForm(array &$form, array &$form_state) { - $this->doValidate($form, $form_state); - } - - /** - * {@inheritdoc} - */ public function submitConfigurationForm(array &$form, array &$form_state) { - return $this->submitEditForm($form, $form_state); - } - - /** - * {@inheritdoc} - */ - public function submitEditForm(array &$form, array &$form_state) { // Remove button and internal Form API values from submitted values. - form_state_values_clean($form_state); - $this->entity = $this->buildEntity($form, $form_state); - $this->entity->save(); - return $form_state; + parent::submit($form, $form_state); + $this->save($form, $form_state); } /** diff --git a/core/modules/menu_ui/menu_ui.admin.inc b/core/modules/menu_ui/menu_ui.admin.inc index 0d7a0fd..f23093f 100644 --- a/core/modules/menu_ui/menu_ui.admin.inc +++ b/core/modules/menu_ui/menu_ui.admin.inc @@ -5,15 +5,19 @@ * Administrative page callbacks for Menu UI module. */ +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Render\Element; /** * Returns HTML for the menu overview form into a table. * - * @param $variables + * @param array $variables * An associative array containing: * - form: A render element representing the form. * + * @return string + * The themed HTML. + * * @ingroup themeable */ function theme_menu_overview_form($variables) { @@ -27,27 +31,28 @@ function theme_menu_overview_form($variables) { ); $rows = array(); - foreach (Element::children($form) as $mlid) { - if (isset($form[$mlid]['hidden'])) { - $element = &$form[$mlid]; + foreach (Element::children($form) as $id) { + if (isset($form[$id]['#item'])) { + $element = &$form[$id]; // Add special classes to be used for tabledrag.js. - $element['plid']['#attributes']['class'] = array('menu-plid'); - $element['mlid']['#attributes']['class'] = array('menu-mlid'); + $element['parent']['#attributes']['class'] = array('menu-parent'); + $element['id']['#attributes']['class'] = array('menu-id'); $element['weight']['#attributes']['class'] = array('menu-weight'); - // Change the parent field to a hidden. This allows any value but hides the field. - $element['plid']['#type'] = 'hidden'; + // Change the parent field to a hidden. This allows any value but hides + // the field. + $element['parent']['#type'] = 'hidden'; $indent = array( '#theme' => 'indentation', - '#size' => $element['#item']['depth'] - 1, + '#size' => $element['#item']->depth - 1, ); $row = array(); - $row[] = drupal_render($indent) . drupal_render($element['title']); - $row[] = array('data' => drupal_render($element['hidden']), 'class' => array('checkbox', 'menu-enabled')); - $row[] = drupal_render($element['weight']) . drupal_render($element['plid']) . drupal_render($element['mlid']); + $row[] = SafeMarkup::set(drupal_render($indent) . drupal_render($element['title'])); + $row[] = array('data' => drupal_render($element['enabled']), 'class' => array('checkbox', 'menu-enabled')); + $row[] = SafeMarkup::set(drupal_render($element['weight']) . drupal_render($element['parent']) . drupal_render($element['id'])); $row[] = drupal_render($element['operations']); $row = array_merge(array('data' => $row), $element['#attributes']); @@ -71,11 +76,11 @@ function theme_menu_overview_form($variables) { array( 'action' => 'match', 'relationship' => 'parent', - 'group' => 'menu-plid', - 'subgroup' => 'menu-plid', - 'source' => 'menu-mlid', + 'group' => 'menu-parent', + 'subgroup' => 'menu-parent', + 'source' => 'menu-id', 'hidden' => TRUE, - 'limit' => MENU_MAX_DEPTH - 1, + 'limit' => \Drupal::menuTree()->maxDepth() - 1, ), array( 'action' => 'order', diff --git a/core/modules/menu_ui/menu_ui.info.yml b/core/modules/menu_ui/menu_ui.info.yml index 26611f1..f3bf1be 100644 --- a/core/modules/menu_ui/menu_ui.info.yml +++ b/core/modules/menu_ui/menu_ui.info.yml @@ -6,4 +6,4 @@ version: VERSION core: 8.x configure: menu_ui.overview_page dependencies: - - menu_link + - menu_link_content diff --git a/core/modules/menu_ui/menu_ui.links.action.yml b/core/modules/menu_ui/menu_ui.links.action.yml index 0834821..af8716f 100644 --- a/core/modules/menu_ui/menu_ui.links.action.yml +++ b/core/modules/menu_ui/menu_ui.links.action.yml @@ -1,6 +1,7 @@ -menu_ui_link_add: - route_name: menu_ui.link_add +menu_ui.link_add: + route_name: menu_link_content.link_add title: 'Add link' + class: \Drupal\menu_ui\Plugin\Menu\LocalAction\MenuLinkAdd appears_on: - menu_ui.menu_edit diff --git a/core/modules/menu_ui/menu_ui.module b/core/modules/menu_ui/menu_ui.module index 70b5b59..79cd051 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -10,13 +10,13 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\block\BlockPluginInterface; +use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\node\NodeTypeInterface; use Drupal\system\Entity\Menu; use Symfony\Component\HttpFoundation\JsonResponse; -use Drupal\menu_link\Entity\MenuLink; -use Drupal\menu_link\MenuLinkStorage; use Drupal\node\NodeInterface; /** @@ -72,31 +72,11 @@ function menu_ui_entity_type_build(array &$entity_types) { ->setFormClass('edit', 'Drupal\menu_ui\MenuForm') ->setFormClass('delete', 'Drupal\menu_ui\Form\MenuDeleteForm') ->setListBuilderClass('Drupal\menu_ui\MenuListBuilder') - ->setLinkTemplate('add-form', 'menu_ui.link_add') + ->setLinkTemplate('add-form', 'menu_ui.menu_add') ->setLinkTemplate('delete-form', 'menu_ui.delete_menu') ->setLinkTemplate('edit-form', 'menu_ui.menu_edit'); - - $entity_types['menu_link'] - ->setFormClass('delete', 'Drupal\menu_ui\Form\MenuLinkDeleteForm') - ->setFormClass('reset', 'Drupal\menu_ui\Form\MenuLinkResetForm') - ->setLinkTemplate('delete-form', 'menu_ui.link_delete'); } -/** - * Implements hook_entity_bundle_info(). - */ -function menu_ui_entity_bundle_info() { - $bundles = array(); - $config_names = \Drupal::configFactory()->listAll('system.menu.'); - foreach ($config_names as $config_name) { - $config = \Drupal::config($config_name); - $bundles['menu_link'][$config->get('id')] = array( - 'label' => $config->get('label'), - ); - } - - return $bundles; -} /** * Implements hook_theme(). @@ -119,23 +99,6 @@ function menu_ui_menu_insert(Menu $menu) { if (\Drupal::moduleHandler()->moduleExists('block')) { \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } - - if ($menu->isSyncing()) { - return; - } - - // Make sure the menu is present in the active menus variable so that its - // items may appear in the menu active trail. - // See menu_set_active_menu_names(). - $config = \Drupal::config('system.menu'); - - $active_menus = $config->get('active_menus_default') ?: array_keys(menu_ui_get_menus()); - if (!in_array($menu->id(), $active_menus)) { - $active_menus[] = $menu->id(); - $config - ->set('active_menus_default', $active_menus) - ->save(); - } } /** @@ -154,20 +117,9 @@ function menu_ui_menu_update(Menu $menu) { */ function menu_ui_menu_predelete(Menu $menu) { // Delete all links from the menu. - menu_delete_links($menu->id()); - - // Remove menu from active menus variable. - $config = \Drupal::config('system.menu'); - $active_menus = $config->get('active_menus_default') ?: array_keys(menu_ui_get_menus()); - if (in_array($menu->id(), $active_menus)) { - $active_menus = array_diff($active_menus, array($menu->id())); - // Prevent the gap left by the removed menu from causing array indices to - // be saved. - $active_menus = array_values($active_menus); - $config - ->set('active_menus_default', $active_menus) - ->save(); - } + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_link_manager->deleteLinksInMenu($menu->id()); } /** @@ -183,110 +135,13 @@ function menu_ui_menu_delete(Menu $menu) { } /** - * Returns a list of menu links that are valid possible parents for the given - * menu link. - * - * @param array $menus - * An array of menu names and titles, such as from menu_ui_get_menus(). - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * The menu link for which to generate a list of parents. - * If $menu_link->id() == 0 then the complete tree is returned. - * @param string $type - * The node type for which to generate a list of parents. - * If $item itself is a node type then $type is ignored. - * - * @return array - * An array of menu link titles keyed by a string containing the menu name and - * mlid. The list excludes the given item and its children. - * - * @todo This has to be turned into a #process form element callback. The - * 'override_parent_selector' variable is entirely superfluous. - */ -function menu_ui_parent_options(array $menus, MenuLink $menu_link = NULL, $type = NULL) { - // The menu_links table can be practically any size and we need a way to - // allow contrib modules to provide more scalable pattern choosers. - // hook_form_alter is too late in itself because all the possible parents are - // retrieved here, unless override_parent_selector is set to TRUE. - if (\Drupal::config('menu_ui.settings')->get('override_parent_selector')) { - return array(); - } - - if (!$menu_link) { - $menu_link = entity_create('menu_link', array('mlid' => 0)); - } - - $available_menus = array(); - if (!$type) { - // If no node type is set, use all menus given to this function. - $available_menus = $menus; - } - else { - // If a node type is set, use all available menus for this type. - $type_menus = \Drupal::config("menu.entity.node.$type")->get('available_menus'); - foreach ($type_menus as $menu) { - $available_menus[$menu] = $menu; - } - } - - return _menu_ui_get_options($menus, $available_menus, $menu_link); -} - -/** - * Helper function to get the items of the given menu. - */ -function _menu_ui_get_options($menus, $available_menus, $item) { - // If the item has children, there is an added limit to the depth of valid parents. - if (isset($item['parent_depth_limit'])) { - $limit = $item['parent_depth_limit']; - } - else { - $limit = _menu_ui_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->buildAllData($menu_name, NULL); - $options[$menu_name . ':0'] = '<' . $title . '>'; - _menu_ui_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit); - } - } - return $options; -} - -/** - * Recursive helper function for menu_ui_parent_options(). - */ -function _menu_ui_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, $depth_limit) { - foreach ($tree as $data) { - if ($data['link']['depth'] > $depth_limit) { - // Don't iterate through any links on this level. - break; - } - if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) { - $title = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, FALSE); - if ($data['link']['hidden']) { - $title .= ' (' . t('disabled') . ')'; - } - $options[$menu_name . ':' . $data['link']['mlid']] = $title; - if ($data['below']) { - _menu_ui_parents_recurse($data['below'], $menu_name, $indent . '--', $options, $exclude, $depth_limit); - } - } - } -} - -/** * Implements hook_block_view_BASE_BLOCK_ID_alter() for 'system_menu_block'. */ function menu_ui_block_view_system_menu_block_alter(array &$build, BlockPluginInterface $block) { // Add contextual links for system menu blocks. $menus = menu_list_system_menus(); $menu_name = $block->getDerivativeId(); - if (isset($menus[$menu_name])) { + if (isset($menus[$menu_name]) && isset($build['content'])) { $build['#contextual_links']['menu'] = array( 'route_parameters' => array('menu' => $menu_name), ); @@ -316,7 +171,7 @@ function menu_ui_node_type_insert(NodeTypeInterface $type) { } \Drupal::config('menu.entity.node.' . $type->id()) ->set('available_menus', array('main')) - ->set('parent', 'main:0') + ->set('parent', 'main:') ->save(); } @@ -334,25 +189,35 @@ function menu_ui_node_type_delete(NodeTypeInterface $type) { * Helper for hook_ENTITY_TYPE_insert() and hook_ENTITY_TYPE_update() for nodes. */ function menu_ui_node_save(EntityInterface $node) { - if (isset($node->menu)) { - $link = &$node->menu; - if (empty($link['enabled'])) { - if (!$link->isNew()) { - menu_link_delete($link['mlid']); - } - } - elseif (trim($link['link_title'])) { - $link['link_title'] = trim($link['link_title']); - $link['link_path'] = 'node/' . $node->id(); - if (trim($link['description'])) { - $link['options']['attributes']['title'] = trim($link['description']); + if (!empty($node->menu)) { + /** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $entity */ + $definition = $node->menu; + if (trim($definition['title'])) { + if (!empty($definition['entity_id'])) { + $entity = entity_load('menu_link_content', $definition['entity_id']); + $entity->hidden->value = 0; + $entity->title->value = trim($definition['title']); + $entity->description->value = trim($definition['description']); + $entity->menu_name->value = $definition['menu_name']; + $entity->parent->value = $definition['parent']; + $entity->weight->value = isset($definition['weight']) ? $definition['weight'] : 0; } else { - // If the description field was left empty, remove the title attribute - // from the menu link. - unset($link['options']['attributes']['title']); + // Create a new menu_link_content entity. + $entity = entity_create('menu_link_content', array( + 'title' => trim($definition['title']), + 'description' => trim($definition['description']), + 'route_name' => 'node.view', + 'route_parameters' => array('node' => $node->id()), + 'menu_name' => $definition['menu_name'], + 'parent' => $definition['parent'], + 'weight' => isset($definition['weight']) ? $definition['weight'] : 0, + 'hidden' => 0, + 'bundle' => 'menu_link_content', + 'langcode' => $node->getUntranslated()->language()->id, + )); } - if (!menu_link_save($link)) { + if (!$entity->save()) { drupal_set_message(t('There was an error saving the menu link.'), 'error'); } } @@ -363,14 +228,17 @@ function menu_ui_node_save(EntityInterface $node) { * Implements hook_ENTITY_TYPE_predelete() for node entities. */ function menu_ui_node_predelete(EntityInterface $node) { - // Delete all Menu UI module links that point to this node. - $query = \Drupal::entityQuery('menu_link') - ->condition('link_path', 'node/' . $node->id()) - ->condition('module', 'menu'); - $result = $query->execute(); + // Delete all MenuLinkContent links that point to this node. + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $result = $menu_link_manager->loadLinksByRoute('node.view', array('node' => $node->id())); if (!empty($result)) { - menu_link_delete_multiple($result); + foreach ($result as $id => $instance) { + if ($instance->isDeletable() && strpos($id, 'menu_link_content:') === 0) { + $instance->deleteLink(); + } + } } } @@ -378,67 +246,69 @@ function menu_ui_node_predelete(EntityInterface $node) { * Implements hook_node_prepare_form(). */ function menu_ui_node_prepare_form(NodeInterface $node, $operation, array &$form_state) { - if (empty($node->menu)) { + if (empty($form_state['menu_link_definition'])) { // Prepare the node for the edit form so that $node->menu always exists. $node_type_config = \Drupal::config('menu.entity.node.' . $node->getType()); $menu_name = strtok($node_type_config->get('parent'), ':'); - $menu_link = FALSE; + $definition = FALSE; if ($node->id()) { - $mlid = FALSE; + $id = FALSE; // Give priority to the default menu $type_menus = $node_type_config->get('available_menus'); if (in_array($menu_name, $type_menus)) { - $query = \Drupal::entityQuery('menu_link') - ->condition('link_path', 'node/' . $node->id()) + $query = \Drupal::entityQuery('menu_link_content') + ->condition('route_name', 'node.view') + ->condition('route_parameters', serialize(array('node' => $node->id()))) ->condition('menu_name', $menu_name) - ->condition('module', 'menu_ui') - ->sort('mlid', 'ASC') + ->sort('id', 'ASC') ->range(0, 1); $result = $query->execute(); - $mlid = (!empty($result)) ? reset($result) : FALSE; + $id = (!empty($result)) ? reset($result) : FALSE; } // Check all allowed menus if a link does not exist in the default menu. - if (!$mlid && !empty($type_menus)) { - $query = \Drupal::entityQuery('menu_link') - ->condition('link_path', 'node/' . $node->id()) + if (!$id && !empty($type_menus)) { + $query = \Drupal::entityQuery('menu_link_content') + ->condition('route_name', 'node.view') + ->condition('route_parameters', serialize(array('node' => $node->id()))) ->condition('menu_name', array_values($type_menus), 'IN') - ->condition('module', 'menu_ui') - ->sort('mlid', 'ASC') + ->sort('id', 'ASC') ->range(0, 1); $result = $query->execute(); - $mlid = (!empty($result)) ? reset($result) : FALSE; + $id = (!empty($result)) ? reset($result) : FALSE; } - if ($mlid) { - $menu_link = menu_link_load($mlid); + if ($id) { + $menu_link = MenuLinkContent::load($id); + $definition = array( + 'entity_id' => $menu_link->id(), + 'id' => $menu_link->getPluginId(), + 'title' => $menu_link->getTitle(), + 'description' => $menu_link->getDescription(), + 'menu_name' => $menu_link->getMenuName(), + 'parent' => $menu_link->getParentId(), + 'weight' => $menu_link->getWeight(), + ); } } - if (!$menu_link) { - $menu_link = entity_create('menu_link', array( - 'mlid' => 0, - 'plid' => 0, + if (!$definition) { + $definition = array( + 'entity_id' => 0, + 'id' => '', + 'title' => '', + 'description' => '', 'menu_name' => $menu_name, - )); + 'parent' => '', + 'weight' => 0, + ); } // Set default values. - $node->menu = $menu_link; - } - // Find the depth limit for the parent select. - if (!isset($node->menu['parent_depth_limit'])) { - $node->menu['parent_depth_limit'] = _menu_ui_parent_depth_limit($node->menu); + $form_state['menu_link_definition'] = $definition; } } /** - * Find the depth limit for items in the parent select. - */ -function _menu_ui_parent_depth_limit($item) { - return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($item) : 0); -} - -/** * Implements hook_form_BASE_FORM_ID_alter() for node_form. * * Adds menu item fields to the node form. @@ -449,11 +319,25 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { // Generate a list of possible parents (not including this link or descendants). // @todo This must be handled in a #process handler. $node = $form_state['controller']->getEntity(); - $link = $node->menu; + $definition = $form_state['menu_link_definition']; $type = $node->getType(); - $options = menu_ui_parent_options(menu_ui_get_menus(), $link, $type); + /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */ + $menu_parent_selector = \Drupal::service('menu.parent_form_selector'); + $menu_names = menu_ui_get_menus(); + $type_menus = \Drupal::config("menu.entity.node.$type")->get('available_menus'); + $available_menus = array(); + foreach ($type_menus as $menu) { + $available_menus[$menu] = $menu_names[$menu]; + } + if ($definition['id']) { + $default = $definition['menu_name'] . ':' . $definition['parent']; + } + else { + $default = \Drupal::config('menu.entity.node.'.$type)->get('parent'); + } + $parent_element = $menu_parent_selector->parentSelectElement($default, $definition['id'], $available_menus); // If no possible parent menu items were found, there is nothing to display. - if (empty($options)) { + if (empty($parent_element)) { return; } @@ -461,7 +345,7 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { '#type' => 'details', '#title' => t('Menu settings'), '#access' => \Drupal::currentUser()->hasPermission('administer menu'), - '#open' => !empty($link['link_title']), + '#open' => (bool) $definition['id'], '#group' => 'advanced', '#attached' => array( 'library' => array('menu/drupal.menu'), @@ -473,7 +357,7 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { $form['menu']['enabled'] = array( '#type' => 'checkbox', '#title' => t('Provide a menu link'), - '#default_value' => (int) (bool) $link['mlid'], + '#default_value' => (int) (bool) $definition['id'], ); $form['menu']['link'] = array( '#type' => 'container', @@ -486,57 +370,32 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { ); // Populate the element with the link data. - foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden', 'parent_depth_limit') as $key) { - $form['menu']['link'][$key] = array('#type' => 'value', '#value' => $link[$key]); + foreach (array('id', 'entity_id') as $key) { + $form['menu']['link'][$key] = array('#type' => 'value', '#value' => $definition[$key]); } - $form['menu']['link']['link_title'] = array( + $form['menu']['link']['title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), - '#default_value' => $link['link_title'], + '#default_value' => $definition['title'], ); $form['menu']['link']['description'] = array( '#type' => 'textarea', '#title' => t('Description'), - '#default_value' => isset($link['options']['attributes']['title']) ? $link['options']['attributes']['title'] : '', + '#default_value' => $definition['description'], '#rows' => 1, '#description' => t('Shown when hovering over the menu link.'), ); - if ($link['mlid']) { - $default = $link['menu_name'] . ':' . $link['plid']; - } - else { - $default = \Drupal::config('menu.entity.node.'.$type)->get('parent'); - } - // If the current parent menu item is not present in options, use the first - // available option as default value. - // @todo User should not be allowed to access menu link settings in such a - // case. - if (!isset($options[$default])) { - $array = array_keys($options); - $default = reset($array); - } - $form['menu']['link']['parent'] = array( - '#type' => 'select', - '#title' => t('Parent item'), - '#default_value' => $default, - '#options' => $options, - '#attributes' => array('class' => array('menu-parent-select')), - ); + $form['menu']['link']['menu_parent'] = $parent_element; + $form['menu']['link']['menu_parent']['#title'] = t('Parent item'); + $form['menu']['link']['menu_parent']['#attributes']['class'][] = 'menu-parent-select'; - // Get number of items in menu so the weight selector is sized appropriately. - $delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name); - if ($delta < 50) { - // Old hardcoded value - $delta = 50; - } $form['menu']['link']['weight'] = array( - '#type' => 'weight', + '#type' => 'number', '#title' => t('Weight'), - '#delta' => $delta, - '#default_value' => $link['weight'], + '#default_value' => $definition['weight'], '#description' => t('Menu links with lower weights are displayed before links with higher weights.'), ); } @@ -548,18 +407,26 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { */ function menu_ui_node_submit(EntityInterface $node, $form, $form_state) { if (!empty($form_state['values']['menu'])) { - $original_menu_id = !empty($node->menu) ? $node->menu->id() : NULL; - $node->menu = entity_create('menu_link', $form_state['values']['menu']); - // @todo Do not create a new entity in order to update it, see - // https://drupal.org/node/2241865 - // If this menu had a previous menu link associated, mark it as not new. - if ($original_menu_id) { - $node->menu->setOriginalId($original_menu_id); + $definition = $form_state['values']['menu']; + if (empty($definition['enabled'])) { + if ($definition['entity_id']) { + $entity = entity_load('menu_link_content', $definition['entity_id']); + $entity->delete(); + } } - // Decompose the selected menu parent option into 'menu_name' and 'plid', if - // the form used the default parent selection widget. - if (!empty($form_state['values']['menu']['parent'])) { - list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + elseif (trim($definition['title'])) { + // Decompose the selected menu parent option into 'menu_name' and 'parent', + // if the form used the default parent selection widget. + if (!empty($definition['menu_parent'])) { + list($menu_name, $parent) = explode(':', $definition['menu_parent'], 2); + $definition['menu_name'] = $menu_name; + $definition['parent'] = $parent; + } + // @todo Figure out how to save this data without adding non-Field API + // properties to the node entity. https://www.drupal.org/node/2310173 + // We have to tack this onto the node so we can save it later when we + // have a node ID for any new node. + $node->menu = $definition; } } } @@ -573,6 +440,8 @@ function menu_ui_node_submit(EntityInterface $node, $form, $form_state) { * @see menu_ui_form_node_type_form_submit(). */ function menu_ui_form_node_type_form_alter(&$form, $form_state) { + /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */ + $menu_parent_selector = \Drupal::service('menu.parent_form_selector'); $menu_options = menu_ui_get_menus(); $type = $form_state['controller']->getEntity(); if ($type->id()) { @@ -581,7 +450,7 @@ function menu_ui_form_node_type_form_alter(&$form, $form_state) { else { $config_values = array( 'available_menus' => array('main'), - 'parent' => 'main:0', + 'parent' => 'main:', ); } $form['menu'] = array( @@ -599,12 +468,12 @@ function menu_ui_form_node_type_form_alter(&$form, $form_state) { '#options' => $menu_options, '#description' => t('The menus available to place links in for this content type.'), ); - // To avoid an 'illegal option' error after saving the form we have to load - // all available menu items. - // Otherwise it is not possible to dynamically add options to the list. - // @todo Convert menu_ui_parent_options() into a #process callback. - $menu_link = entity_create('menu_link', array('mlid' => 0)); - $options = menu_ui_parent_options(menu_ui_get_menus(), $menu_link); + // @todo See if we can avoid pre-loading all options by changing the form or + // using a #process callback. https://www.drupal.org/node/2310319 + // To avoid an 'illegal option' error after saving the form we have to load + // all available menu parents. Otherwise, it is not possible to dynamically + // add options to the list using ajax. + $options = $menu_parent_selector->getParentSelectOptions(''); $form['menu']['menu_parent'] = array( '#type' => 'select', '#title' => t('Default parent item'), @@ -636,7 +505,8 @@ function menu_ui_form_node_type_form_submit(&$form, $form_state) { * @param $all * If FALSE return only user-added menus, or if TRUE also include * the menus defined by the system. - * @return + * + * @return array * An array with the machine-readable names as the keys, and human-readable * titles as the values. */ @@ -661,3 +531,19 @@ function menu_ui_preprocess_block(&$variables) { $variables['attributes']['role'] = 'navigation'; } } + + +/** + * Implements hook_system_breadcrumb_alter(). + */ +function menu_ui_system_breadcrumb_alter(array &$breadcrumb, RouteMatchInterface $route_match, array $context) { + // Custom breadcrumb behavior for editing menu links, we append a link to + // the menu in which the link is found. + if (($route_match->getRouteName() == 'menu_ui.link_edit') && $menu_link = $route_match->getParameter('menu_link_plugin')) { + if (($menu_link instanceof MenuLinkInterface)) { + // Add a link to the menu admin screen. + $menu = entity_load('menu', $menu_link->getMenuName()); + $breadcrumb[] = \Drupal::l($menu->label(), 'menu_ui.menu_edit', array('menu' => $menu->id())); + } + } +} diff --git a/core/modules/menu_ui/menu_ui.routing.yml b/core/modules/menu_ui/menu_ui.routing.yml index 4be3bbf..b095c34 100644 --- a/core/modules/menu_ui/menu_ui.routing.yml +++ b/core/modules/menu_ui/menu_ui.routing.yml @@ -21,37 +21,30 @@ menu_ui.parent_options_js: requirements: _permission: 'administer menu' -menu_ui.link_add: - path: '/admin/structure/menu/manage/{menu}/add' - defaults: - _content: '\Drupal\menu_ui\Controller\MenuController::addLink' - _title: 'Add menu link' - requirements: - _entity_create_access: 'menu_link' - menu_ui.link_edit: - path: '/admin/structure/menu/item/{menu_link}/edit' + path: '/admin/structure/menu/link/{menu_link_plugin}/edit' defaults: - _entity_form: 'menu_link' + _form: 'Drupal\menu_ui\Form\MenuLinkEditForm' _title: 'Edit menu link' + options: + parameters: + menu_link_plugin: + type: menu_link_plugin requirements: - _entity_access: 'menu_link.update' + _permission: 'administer menu' menu_ui.link_reset: - path: '/admin/structure/menu/item/{menu_link}/reset' + path: '/admin/structure/menu/link/{menu_link_plugin}/reset' defaults: - _entity_form: 'menu_link.reset' + _form: 'Drupal\menu_ui\Form\MenuLinkResetForm' _title: 'Reset menu link' + options: + parameters: + menu_link_plugin: + type: menu_link_plugin requirements: - _entity_access: 'menu_link.reset' - -menu_ui.link_delete: - path: '/admin/structure/menu/item/{menu_link}/delete' - defaults: - _entity_form: 'menu_link.delete' - _title: 'Delete menu link' - requirements: - _entity_access: 'menu_link.delete' + _permission: 'administer menu' + _custom_access: '\Drupal\menu_ui\Form\MenuLinkResetForm::linkIsResettable' menu_ui.menu_add: path: '/admin/structure/menu/add' diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php index fad5de5..95788bd 100644 --- a/core/modules/menu_ui/src/Controller/MenuController.php +++ b/core/modules/menu_ui/src/Controller/MenuController.php @@ -9,7 +9,9 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Menu\MenuParentFormSelectorInterface; use Drupal\system\MenuInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -19,6 +21,30 @@ class MenuController extends ControllerBase { /** + * The menu parent form service. + * + * @var \Drupal\Core\Menu\MenuParentFormSelectorInterface + */ + protected $menuParentSelector; + + /** + * Creates a new MenuController object. + * + * @param \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_form + * The menu parent form service. + */ + public function __construct(MenuParentFormSelectorInterface $menu_parent_form) { + $this->menuParentSelector = $menu_parent_form; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('menu.parent_form_selector')); + } + + /** * Gets all the available menus and menu items as a JavaScript array. * * @param \Symfony\Component\HttpFoundation\Request $request @@ -34,30 +60,12 @@ public function getParentOptions(Request $request) { $available_menus[$menu] = $menu; } } - $options = _menu_ui_get_options(menu_ui_get_menus(), $available_menus, array('mlid' => 0)); + $options = $this->menuParentSelector->getParentSelectOptions('', $available_menus); return new JsonResponse($options); } /** - * Provides the menu link submission form. - * - * @param \Drupal\system\MenuInterface $menu - * An entity representing a custom menu. - * - * @return array - * Returns the menu link submission form. - */ - public function addLink(MenuInterface $menu) { - $menu_link = $this->entityManager()->getStorage('menu_link')->create(array( - 'mlid' => 0, - 'plid' => 0, - 'menu_name' => $menu->id(), - )); - return $this->entityFormBuilder()->getForm($menu_link); - } - - /** * Route title callback. * * @param \Drupal\system\MenuInterface $menu diff --git a/core/modules/menu_ui/src/Form/MenuDeleteForm.php b/core/modules/menu_ui/src/Form/MenuDeleteForm.php index 13cbf94..5c3cd96 100644 --- a/core/modules/menu_ui/src/Form/MenuDeleteForm.php +++ b/core/modules/menu_ui/src/Form/MenuDeleteForm.php @@ -9,7 +9,7 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityConfirmFormBase; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -19,11 +19,11 @@ class MenuDeleteForm extends EntityConfirmFormBase { /** - * The menu link storage. + * The menu link manager. * - * @var \Drupal\Core\Entity\EntityStorageInterface + * @var \Drupal\Core\Menu\MenuLinkManagerInterface */ - protected $storage; + protected $menuLinkManager; /** * The database connection. @@ -35,13 +35,13 @@ class MenuDeleteForm extends EntityConfirmFormBase { /** * Constructs a new MenuDeleteForm. * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The menu link storage. + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager + * The menu link manager. * @param \Drupal\Core\Database\Connection $connection * The database connection. */ - public function __construct(EntityStorageInterface $storage, Connection $connection) { - $this->storage = $storage; + public function __construct(MenuLinkManagerInterface $menu_link_manager, Connection $connection) { + $this->menuLinkManager = $menu_link_manager; $this->connection = $connection; } @@ -50,7 +50,7 @@ public function __construct(EntityStorageInterface $storage, Connection $connect */ public static function create(ContainerInterface $container) { return new static( - $container->get('entity.manager')->getStorage('menu_link'), + $container->get('plugin.manager.menu.link'), $container->get('database') ); } @@ -74,7 +74,7 @@ public function getCancelUrl() { */ public function getDescription() { $caption = ''; - $num_links = $this->storage->countMenuLinks($this->entity->id()); + $num_links = $this->menuLinkManager->countMenuLinks($this->entity->id()); if ($num_links) { $caption .= '

    ' . format_plural($num_links, 'Warning: There is currently 1 menu link in %title. It will be deleted (system-defined items will be reset).', 'Warning: There are currently @count menu links in %title. They will be deleted (system-defined links will be reset).', array('%title' => $this->entity->label())) . '

    '; } @@ -100,21 +100,16 @@ public function submit(array $form, array &$form_state) { return; } - // Reset all the menu links defined by the menu_link.static service. - $result = \Drupal::entityQuery('menu_link') - ->condition('menu_name', $this->entity->id()) - ->condition('module', '', '>') - ->condition('machine_name', '', '>') - ->sort('depth', 'ASC') - ->execute(); - $menu_links = $this->storage->loadMultiple($result); - foreach ($menu_links as $link) { - $link->reset(); - } - // Delete all links to the overview page for this menu. - $menu_links = $this->storage->loadByProperties(array('link_path' => 'admin/structure/menu/manage/' . $this->entity->id())); - menu_link_delete_multiple(array_keys($menu_links)); + // @todo Add a more generic helper function to the menu link plugin + // manager to remove links to a entity or other ID used as a route + // parameter that is being removed. Also, consider moving this to + // menu_ui.module as part of a generic response to entity deletion. + // https://www.drupal.org/node/2310329 + $menu_links = $this->menuLinkManager->loadLinksByRoute('menu_ui.menu_edit', array('menu' => $this->entity->id()), TRUE); + foreach ($menu_links as $id => $link) { + $this->menuLinkManager->removeDefinition($id); + } // Delete the custom menu and all its menu links. $this->entity->delete(); diff --git a/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php b/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php deleted file mode 100644 index 0a971d9..0000000 --- a/core/modules/menu_ui/src/Form/MenuLinkDeleteForm.php +++ /dev/null @@ -1,45 +0,0 @@ - $this->entity->link_title)); - } - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - return new Url('menu_ui.menu_edit', array( - 'menu' => $this->entity->menu_name, - )); - } - - /** - * {@inheritdoc} - */ - public function submit(array $form, array &$form_state) { - menu_link_delete($this->entity->id()); - $t_args = array('%title' => $this->entity->link_title); - drupal_set_message(t('The menu link %title has been deleted.', $t_args)); - watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE); - $form_state['redirect_route'] = $this->getCancelUrl(); - } - -} diff --git a/core/modules/menu_ui/src/Form/MenuLinkEditForm.php b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php index 4e87ffb..cbe096c 100644 --- a/core/modules/menu_ui/src/Form/MenuLinkEditForm.php +++ b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php @@ -22,7 +22,7 @@ class MenuLinkEditForm extends FormBase { /** - * The class resolver + * The class resolver. * * @var \Drupal\Core\DependencyInjection\ClassResolverInterface */ diff --git a/core/modules/menu_ui/src/Form/MenuLinkResetForm.php b/core/modules/menu_ui/src/Form/MenuLinkResetForm.php index b5663aa..6bb22e1 100644 --- a/core/modules/menu_ui/src/Form/MenuLinkResetForm.php +++ b/core/modules/menu_ui/src/Form/MenuLinkResetForm.php @@ -7,19 +7,63 @@ namespace Drupal\menu_ui\Form; -use Drupal\Core\Entity\EntityConfirmFormBase; use Drupal\Core\Url; +use Drupal\Core\Form\ConfirmFormBase; +use Drupal\Core\Menu\MenuLinkManagerInterface; +use Drupal\Core\Menu\MenuLinkInterface; +use Drupal\Core\Routing\Access\AccessInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a confirmation form for resetting a single modified menu link. */ -class MenuLinkResetForm extends EntityConfirmFormBase { +class MenuLinkResetForm extends ConfirmFormBase { + + /** + * The menu link manager. + * + * @var \Drupal\Core\Menu\MenuLinkManagerInterface + */ + protected $menuLinkManager; + + /** + * The menu link. + * + * @var \Drupal\Core\Menu\MenuLinkInterface + */ + protected $link; + + /** + * Constructs a MenuLinkResetForm object. + * + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager + * The menu link manager. + */ + public function __construct(MenuLinkManagerInterface $menu_link_manager) { + $this->menuLinkManager = $menu_link_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.menu.link') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'menu_link_reset_confirm'; + } /** * {@inheritdoc} */ public function getQuestion() { - return t('Are you sure you want to reset the link %item to its default values?', array('%item' => $this->entity->link_title)); + return $this->t('Are you sure you want to reset the link %item to its default values?', array('%item' => $this->link->getTitle())); } /** @@ -27,7 +71,7 @@ public function getQuestion() { */ public function getCancelUrl() { return new Url('menu_ui.menu_edit', array( - 'menu' => $this->entity->menu_name, + 'menu' => $this->link->getMenuName(), )); } @@ -35,23 +79,47 @@ public function getCancelUrl() { * {@inheritdoc} */ public function getDescription() { - return t('Any customizations will be lost. This action cannot be undone.'); + return $this->t('Any customizations will be lost. This action cannot be undone.'); } /** * {@inheritdoc} */ public function getConfirmText() { - return t('Reset'); + return $this->t('Reset'); } /** * {@inheritdoc} */ - public function submit(array $form, array &$form_state) { - $this->entity = $this->entity->reset(); - drupal_set_message(t('The menu link was reset to its default settings.')); + public function buildForm(array $form, array &$form_state, MenuLinkInterface $menu_link_plugin = NULL) { + $this->link = $menu_link_plugin; + + $form = parent::buildForm($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, array &$form_state) { + $this->link = $this->menuLinkManager->resetLink($this->link->getPluginId()); + drupal_set_message($this->t('The menu link was reset to its default settings.')); $form_state['redirect_route'] = $this->getCancelUrl(); } + /** + * Checks access based on whether the link can be reset. + * + * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin + * The menu link plugin being checked. + * + * @return string + * AccessInterface::ALLOW when access was granted, otherwise + * AccessInterface::DENY. + */ + public function linkIsResettable(MenuLinkInterface $menu_link_plugin) { + return $menu_link_plugin->isResettable() ? AccessInterface::ALLOW : AccessInterface::DENY; + } + } diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index d854f3c..5e32eaf 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -11,9 +11,13 @@ use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; +use Drupal\Core\Menu\MenuLinkTreeElement; +use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Render\Element; -use Drupal\menu_link\MenuLinkStorageInterface; -use Drupal\menu_link\MenuTreeInterface; +use Drupal\Core\Routing\UrlGeneratorTrait; +use Drupal\Core\Utility\LinkGeneratorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -29,20 +33,27 @@ class MenuForm extends EntityForm { protected $entityQueryFactory; /** - * The menu link storage. + * The menu link manager. * - * @var \Drupal\menu_link\MenuLinkStorageInterface + * @var \Drupal\Core\Menu\MenuLinkManagerInterface */ - protected $menuLinkStorage; + protected $menuLinkManager; /** * The menu tree service. * - * @var \Drupal\menu_link\MenuTreeInterface + * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ protected $menuTree; /** + * The link generator. + * + * @var \Drupal\Core\Utility\LinkGeneratorInterface + */ + protected $linkGenerator; + + /** * The overview tree form. * * @var array @@ -54,15 +65,18 @@ class MenuForm extends EntityForm { * * @param \Drupal\Core\Entity\Query\QueryFactory $entity_query_factory * The factory for entity queries. - * @param \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage - * The menu link storage. - * @param \Drupal\menu_link\MenuTreeInterface $menu_tree + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager + * The menu link manager. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree * The menu tree service. + * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator + * The link generator. */ - public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageInterface $menu_link_storage, MenuTreeInterface $menu_tree) { + public function __construct(QueryFactory $entity_query_factory, MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator) { $this->entityQueryFactory = $entity_query_factory; - $this->menuLinkStorage = $menu_link_storage; + $this->menuLinkManager = $menu_link_manager; $this->menuTree = $menu_tree; + $this->linkGenerator = $link_generator; } /** @@ -71,8 +85,9 @@ public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageI public static function create(ContainerInterface $container) { return new static( $container->get('entity.query'), - $container->get('entity.manager')->getStorage('menu_link'), - $container->get('menu_link.tree') + $container->get('plugin.manager.menu.link'), + $container->get('menu.link_tree'), + $container->get('link_generator') ); } @@ -88,16 +103,16 @@ public function form(array $form, array &$form_state) { $form['label'] = array( '#type' => 'textfield', - '#title' => t('Title'), + '#title' => $this->t('Title'), '#default_value' => $menu->label(), '#required' => TRUE, ); $form['id'] = array( '#type' => 'machine_name', - '#title' => t('Menu name'), + '#title' => $this->t('Menu name'), '#default_value' => $menu->id(), '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI, - '#description' => t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), + '#description' => $this->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), '#machine_name' => array( 'exists' => array($this, 'menuNameExists'), 'source' => array('label'), @@ -120,28 +135,11 @@ public function form(array $form, array &$form_state) { '#languages' => LanguageInterface::STATE_ALL, '#default_value' => $menu->language()->getId(), ); - // Unlike the menu langcode, the default language configuration for menu - // links only works with language module installed. - if ($this->moduleHandler->moduleExists('language')) { - $form['default_menu_links_language'] = array( - '#type' => 'details', - '#title' => t('Menu links language'), - '#open' => TRUE, - ); - $form['default_menu_links_language']['default_language'] = array( - '#type' => 'language_configuration', - '#entity_information' => array( - 'entity_type' => 'menu_link', - 'bundle' => $menu->id(), - ), - '#default_value' => language_get_default_configuration('menu_link', $menu->id()), - ); - } // Add menu links administration form for existing menus. if (!$menu->isNew() || $menu->isLocked()) { // Form API supports constructing and validating self-contained sections - // within forms, but does not allow to handle the form section's submission + // within forms, but does not allow handling the form section's submission // equally separated yet. Therefore, we use a $form_state key to point to // the parents of the form section. // @see self::submitOverviewForm() @@ -169,41 +167,7 @@ public function menuNameExists($value) { } // Check for a link assigned to this menu. - return $this->entityQueryFactory->get('menu_link')->condition('menu_name', $value)->range(0, 1)->count()->execute(); - } - - /** - * {@inheritdoc} - */ - protected function actions(array $form, array &$form_state) { - $actions = parent::actions($form, $form_state); - - // Add the language configuration submit handler. This is needed because the - // submit button has custom submit handlers. - if ($this->moduleHandler->moduleExists('language')) { - array_unshift($actions['submit']['#submit'],'language_configuration_element_submit'); - array_unshift($actions['submit']['#submit'], array($this, 'languageConfigurationSubmit')); - } - // We cannot leverage the regular submit handler definition because we have - // button-specific ones here. Hence we need to explicitly set it for the - // submit action, otherwise it would be ignored. - if ($this->moduleHandler->moduleExists('content_translation')) { - array_unshift($actions['submit']['#submit'], 'content_translation_language_configuration_element_submit'); - } - return $actions; - } - - /** - * Submit handler to update the bundle for the default language configuration. - */ - public function languageConfigurationSubmit(array &$form, array &$form_state) { - // Since the machine name is not known yet, and it can be changed anytime, - // we have to also update the bundle property for the default language - // configuration in order to have the correct bundle value. - $form_state['language']['default_language']['bundle'] = $form_state['values']['id']; - // Clear cache so new menus (bundles) show on the language settings admin - // page. - \Drupal::entityManager()->clearCachedBundles(); + return $this->menuLinkManager->menuNameInUse($value); } /** @@ -217,13 +181,13 @@ public function save(array $form, array &$form_state) { $status = $menu->save(); - $edit_link = \Drupal::linkGenerator()->generateFromUrl($this->t('Edit'), $this->entity->urlInfo()); + $edit_link = $this->linkGenerator->generateFromUrl($this->t('Edit'), $this->entity->urlInfo()); if ($status == SAVED_UPDATED) { - drupal_set_message(t('Menu %label has been updated.', array('%label' => $menu->label()))); + drupal_set_message($this->t('Menu %label has been updated.', array('%label' => $menu->label()))); watchdog('menu', 'Menu %label has been updated.', array('%label' => $menu->label()), WATCHDOG_NOTICE, $edit_link); } else { - drupal_set_message(t('Menu %label has been added.', array('%label' => $menu->label()))); + drupal_set_message($this->t('Menu %label has been added.', array('%label' => $menu->label()))); watchdog('menu', 'Menu %label has been added.', array('%label' => $menu->label()), WATCHDOG_NOTICE, $edit_link); } @@ -253,26 +217,30 @@ protected function buildOverviewForm(array &$form, array &$form_state) { $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/css/menu.admin.css'); - $links = array(); - $query = $this->entityQueryFactory->get('menu_link') - ->condition('menu_name', $this->entity->id()); - for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { - $query->sort('p' . $i, 'ASC'); - } - $result = $query->execute(); + $tree = $this->menuTree->load($this->entity->id(), new MenuTreeParameters()); - if (!empty($result)) { - $links = $this->menuLinkStorage->loadMultiple($result); - } - - $delta = max(count($links), 50); // We indicate that a menu administrator is running the menu access check. $this->getRequest()->attributes->set('_menu_admin', TRUE); - $tree = $this->menuTree->buildTreeData($links); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + ); + $tree = $this->menuTree->transform($tree, $manipulators); $this->getRequest()->attributes->set('_menu_admin', FALSE); + // Determine the delta; the number of weights to be made available. + $count = function(array $tree) { + $sum = function ($carry, MenuLinkTreeElement $item) { + return $carry + $item->count(); + }; + return array_reduce($tree, $sum); + }; + $delta = max($count($tree), 50); + $form = array_merge($form, $this->buildOverviewTreeForm($tree, $delta)); - $form['#empty_text'] = t('There are no menu links yet. Add link.', array('@link' => url('admin/structure/menu/manage/' . $this->entity->id() .'/add'))); + $destination = $this->getUrlGenerator()->getPathFromRoute('menu_ui.menu_edit', array('menu' => $this->entity->id())); + $url = $destination = $this->url('menu_link_content.link_add', array('menu' => $this->entity->id()), array('query' => array('destination' => $destination))); + $form['#empty_text'] = $this->t('There are no menu links yet. Add link.', array('@url' => $url)); return $form; } @@ -281,7 +249,7 @@ protected function buildOverviewForm(array &$form, array &$form_state) { * Recursive helper function for buildOverviewForm(). * * @param $tree - * The menu_tree retrieved by menu_tree_data. + * The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load(). * @param $delta * The default number of menu items used in the menu weight selector is 50. * @@ -290,70 +258,87 @@ protected function buildOverviewForm(array &$form, array &$form_state) { */ protected function buildOverviewTreeForm($tree, $delta) { $form = &$this->overviewTreeForm; - foreach ($tree as $data) { - $item = $data['link']; - // Don't show callbacks; these have $item['hidden'] < 0. - if ($item && $item['hidden'] >= 0) { - $mlid = 'mlid:' . $item['mlid']; - $form[$mlid]['#item'] = $item; - $form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); - $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']); - if ($item['hidden']) { - $form[$mlid]['title']['#markup'] .= ' (' . t('disabled') . ')'; + foreach ($tree as $element) { + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $element->link; + if ($link) { + $id = 'menu_plugin_id:' . $link->getPluginId(); + $form[$id]['#item'] = $element; + $form[$id]['#attributes'] = $link->isHidden() ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); + $form[$id]['title']['#markup'] = $this->linkGenerator->generateFromUrl($link->getTitle(), $link->getUrlObject(), $link->getOptions()); + if ($link->isHidden()) { + $form[$id]['title']['#markup'] .= ' (' . $this->t('disabled') . ')'; } - elseif ($item['link_path'] == 'user' && $item['module'] == 'user') { - $form[$mlid]['title']['#markup'] .= ' (' . t('logged in users only') . ')'; + elseif (($url = $link->getUrlObject()) && !$url->isExternal() && $url->getRouteName() == 'user.page') { + $form[$id]['title']['#markup'] .= ' (' . $this->t('logged in users only') . ')'; } - $form[$mlid]['hidden'] = array( + $form[$id]['enabled'] = array( '#type' => 'checkbox', - '#title' => t('Enable @title menu link', array('@title' => $item['title'])), + '#title' => $this->t('Enable @title menu link', array('@title' => $link->getTitle())), '#title_display' => 'invisible', - '#default_value' => !$item['hidden'], + '#default_value' => !$link->isHidden(), ); - $form[$mlid]['weight'] = array( + $form[$id]['weight'] = array( '#type' => 'weight', '#delta' => $delta, - '#default_value' => $item['weight'], - '#title' => t('Weight for @title', array('@title' => $item['title'])), + '#default_value' => $link->getWeight(), + '#title' => $this->t('Weight for @title', array('@title' => $link->getTitle())), '#title_display' => 'invisible', ); - $form[$mlid]['mlid'] = array( + $form[$id]['id'] = array( '#type' => 'hidden', - '#value' => $item['mlid'], + '#value' => $link->getPluginId(), ); - $form[$mlid]['plid'] = array( + $form[$id]['parent'] = array( '#type' => 'hidden', - '#default_value' => $item['plid'], + '#default_value' => $link->getParent(), ); // Build a list of operations. $operations = array(); $operations['edit'] = array( - 'title' => t('Edit'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit', + 'title' => $this->t('Edit'), ); - // Only items created by the Menu UI module can be deleted. - if ($item->access('delete')) { - $operations['delete'] = array( - 'title' => t('Delete'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete', + // Allow for a custom edit link per plugin. + $edit_route = $link->getEditRoute(); + if ($edit_route) { + $operations['edit'] += $edit_route; + // Bring the user back to the menu overview. + $operations['edit']['query']['destination'] = $this->entity->url(); + } + else { + // Fall back to the standard edit link. + $operations['edit'] += array( + 'route_name' => 'menu_ui.link_edit', + 'route_parameters' => array('menu_link_plugin' => $link->getPluginId()), ); } - // Set the reset column. - elseif ($item->access('reset')) { + // Links can either be reset or deleted, not both. + if ($link->isResettable()) { $operations['reset'] = array( - 'title' => t('Reset'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset', + 'title' => $this->t('Reset'), + 'route_name' => 'menu_ui.link_reset', + 'route_parameters' => array('menu_link_plugin' => $link->getPluginId()), ); } - $form[$mlid]['operations'] = array( + elseif ($delete_link = $link->getDeleteRoute()) { + $operations['delete'] = $delete_link; + $operations['delete']['query']['destination'] = $this->entity->url(); + $operations['delete']['title'] = $this->t('Delete'); + } + if ($link->isTranslatable()) { + $operations['translate'] = array( + 'title' => $this->t('Translate'), + ) + (array) $link->getTranslateRoute(); + } + $form[$id]['operations'] = array( '#type' => 'operations', '#links' => $operations, ); } - if ($data['below']) { - $this->buildOverviewTreeForm($data['below'], $delta); + if ($element->subtree) { + $this->buildOverviewTreeForm($element->subtree, $delta); } } return $form; @@ -363,7 +348,7 @@ protected function buildOverviewTreeForm($tree, $delta) { * Submit handler for the menu overview form. * * This function takes great care in saving parent items first, then items - * underneath them. Saving items in the incorrect order can break the menu tree. + * underneath them. Saving items in the incorrect order can break the tree. */ protected function submitOverviewForm(array $complete_form, array &$form_state) { // Form API supports constructing and validating self-contained sections @@ -384,32 +369,30 @@ protected function submitOverviewForm(array $complete_form, array &$form_state) // Update our original form with the new order. $form = array_intersect_key(array_merge($order, $form), $form); - $updated_items = array(); - $fields = array('weight', 'plid'); - foreach (Element::children($form) as $mlid) { - if (isset($form[$mlid]['#item'])) { - $element = $form[$mlid]; + $fields = array('weight', 'parent', 'enabled'); + foreach (Element::children($form) as $id) { + if (isset($form[$id]['#item'])) { + $element = $form[$id]; + $updated_values = array(); // Update any fields that have changed in this menu item. foreach ($fields as $field) { if ($element[$field]['#value'] != $element[$field]['#default_value']) { - $element['#item'][$field] = $element[$field]['#value']; - $updated_items[$mlid] = $element['#item']; + // Hidden is a special case, the form value needs to be reversed. + if ($field == 'enabled') { + $updated_values['hidden'] = $element['enabled']['#value'] ? 0 : 1; + } + else { + $updated_values[$field] = $element[$field]['#value']; + } } } - // Hidden is a special case, the value needs to be reversed. - if ($element['hidden']['#value'] != $element['hidden']['#default_value']) { - // Convert to integer rather than boolean due to PDO cast to string. - $element['#item']['hidden'] = $element['hidden']['#value'] ? 0 : 1; - $updated_items[$mlid] = $element['#item']; + if ($updated_values) { + // Use the ID from the actual plugin instance since the hidden value + // in the form could be tampered with. + $this->menuLinkManager->updateDefinition($element['#item']->link->getPLuginId(), $updated_values); } } } - - // Save all our changed items to the database. - foreach ($updated_items as $item) { - $item['customized'] = 1; - $item->save(); - } } } diff --git a/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php b/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php new file mode 100644 index 0000000..9fee161 --- /dev/null +++ b/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php @@ -0,0 +1,38 @@ +attributes->has(RouteObjectInterface::ROUTE_NAME)) { + $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); + $raw_variables = array(); + if ($request->attributes->has('_raw_variables')) { + $raw_variables = $request->attributes->get('_raw_variables')->all(); + } + // @todo Use RouteMatch instead of Request. + // https://www.drupal.org/node/2294157 + $options['query']['destination'] = \Drupal::urlGenerator()->generateFromRoute($route_name, $raw_variables); + } + return $options; + } + +} diff --git a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php index c863b8d..4a19182 100644 --- a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php +++ b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\menu\Tests\MenuCacheTagsTest. + * Contains \Drupal\menu_ui\Tests\MenuCacheTagsTest. */ namespace Drupal\menu_ui\Tests; @@ -37,12 +37,10 @@ public function testMenuBlock() { 'description' => 'Description text', )); $menu->save(); - $menu_link = entity_create('menu_link', array( - 'link_path' => '', - 'link_title' => 'Vicuña', - 'menu_name' => 'llama', - )); - $menu_link->save(); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + // Move a link into the new menu. + $menu_link = $menu_link_manager->updateDefinition('test_page_test.test_page', array('menu_name' => 'llama', 'parent' => '')); $block = $this->drupalPlaceBlock('system_menu_block:llama', array('label' => 'Llama', 'provider' => 'system', 'region' => 'footer')); // Prime the page cache. @@ -60,7 +58,6 @@ public function testMenuBlock() { ); $this->verifyPageCache($path, 'HIT', $expected_tags); - // Verify that after modifying the menu, there is a cache miss. $this->pass('Test modification of menu.', 'Debug'); $menu->label = 'Awesome llama'; @@ -70,23 +67,23 @@ public function testMenuBlock() { // Verify a cache hit. $this->verifyPageCache($path, 'HIT'); - - // Verify that after modifying the menu link, there is a cache miss. + // Verify that after modifying the menu link weight, there is a cache miss. + $menu_link_manager->updateDefinition('test_page_test.test_page', array('weight' => -10)); $this->pass('Test modification of menu link.', 'Debug'); - $menu_link->link_title = 'Guanaco'; - $menu_link->save(); $this->verifyPageCache($path, 'MISS'); // Verify a cache hit. $this->verifyPageCache($path, 'HIT'); - // Verify that after adding a menu link, there is a cache miss. $this->pass('Test addition of menu link.', 'Debug'); - $menu_link_2 = entity_create('menu_link', array( - 'link_path' => '', - 'link_title' => 'Alpaca', + $menu_link_2 = entity_create('menu_link_content', array( + 'id' => '', + 'parent' => '', + 'title' => 'Alpaca', 'menu_name' => 'llama', + 'route_name' => '', + 'bundle' => 'menu_name', )); $menu_link_2->save(); $this->verifyPageCache($path, 'MISS'); @@ -94,16 +91,15 @@ public function testMenuBlock() { // Verify a cache hit. $this->verifyPageCache($path, 'HIT'); - - // Verify that after deleting the first menu link, there is a cache miss. - $this->pass('Test deletion of menu link.', 'Debug'); - $menu_link->delete(); + // Verify that after resetting the first menu link, there is a cache miss. + $this->pass('Test reset of menu link.', 'Debug'); + $this->assertTrue($menu_link->isResettable(), 'First link can be reset'); + $menu_link = $menu_link_manager->resetLink($menu_link->getPluginId()); $this->verifyPageCache($path, 'MISS'); // Verify a cache hit. $this->verifyPageCache($path, 'HIT', $expected_tags); - // Verify that after deleting the menu, there is a cache miss. $this->pass('Test deletion of menu.', 'Debug'); $menu->delete(); diff --git a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php index 9267c28..88d9150 100644 --- a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php +++ b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php @@ -59,21 +59,12 @@ function testMenuLanguage() { 'description' => '', 'label' => $label, 'langcode' => 'aa', - 'default_language[langcode]' => 'bb', - 'default_language[language_show]' => TRUE, ); $this->drupalPostForm('admin/structure/menu/add', $edit, t('Save')); + language_save_default_configuration('menu_link_content', 'menu_link_content', array('langcode' => 'bb', 'language_show' => TRUE)); - // Check that the language settings were saved. - $this->assertEqual(entity_load('menu', $menu_name)->language()->getId(), $edit['langcode']); - $language_settings = language_get_default_configuration('menu_link', $menu_name); - $this->assertEqual($language_settings['langcode'], 'bb'); - $this->assertEqual($language_settings['language_show'], TRUE); - - // Check menu language and item language configuration. + // Check menu language. $this->assertOptionSelected('edit-langcode', $edit['langcode'], 'The menu language was correctly selected.'); - $this->assertOptionSelected('edit-default-language-langcode', $edit['default_language[langcode]'], 'The menu link default language was correctly selected.'); - $this->assertFieldChecked('edit-default-language-language-show'); // Test menu link language. $link_path = ''; @@ -81,41 +72,35 @@ function testMenuLanguage() { // Add a menu link. $link_title = $this->randomString(); $edit = array( - 'link_title' => $link_title, - 'link_path' => $link_path, + 'title[0][value]' => $link_title, + 'url' => $link_path, ); $this->drupalPostForm("admin/structure/menu/manage/$menu_name/add", $edit, t('Save')); // Check the link was added with the correct menu link default language. - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $link_title)); + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $link_title)); $menu_link = reset($menu_links); - $this->assertMenuLink($menu_link->id(), array( + $this->assertMenuLink($menu_link->getPluginId(), array( 'menu_name' => $menu_name, - 'link_path' => $link_path, + 'route_name' => '', 'langcode' => 'bb', )); // Edit menu link default, changing it to cc. - $edit = array( - 'default_language[langcode]' => 'cc', - ); - $this->drupalPostForm("admin/structure/menu/manage/$menu_name", $edit, t('Save')); - - // Check cc is the menu link default. - $this->assertOptionSelected('edit-default-language-langcode', $edit['default_language[langcode]'], 'The menu link default language was correctly selected.'); + language_save_default_configuration('menu_link_content', 'menu_link_content', array('langcode' => 'cc', 'language_show' => TRUE)); // Add a menu link. $link_title = $this->randomString(); $edit = array( - 'link_title' => $link_title, - 'link_path' => $link_path, + 'title[0][value]' => $link_title, + 'url' => $link_path, ); $this->drupalPostForm("admin/structure/menu/manage/$menu_name/add", $edit, t('Save')); // Check the link was added with the correct new menu link default language. - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $link_title)); + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $link_title)); $menu_link = reset($menu_links); - $this->assertMenuLink($menu_link->id(), array( + $this->assertMenuLink($menu_link->getPluginId(), array( 'menu_name' => $menu_name, - 'link_path' => $link_path, + 'route_name' => '', 'langcode' => 'cc', )); @@ -124,9 +109,9 @@ function testMenuLanguage() { 'langcode' => 'bb', ); $this->drupalPostForm('admin/structure/menu/item/' . $menu_link->id() . '/edit', $edit, t('Save')); - $this->assertMenuLink($menu_link->id(), array( + $this->assertMenuLink($menu_link->getPluginId(), array( 'menu_name' => $menu_name, - 'link_path' => $link_path, + 'route_name' => '', 'langcode' => 'bb', )); @@ -138,16 +123,7 @@ function testMenuLanguage() { $this->assertOptionSelected('edit-langcode', 'bb', 'The menu link language was correctly selected.'); // Edit menu to hide the language select on menu link item add. - $edit = array( - 'default_language[language_show]' => FALSE, - ); - $this->drupalPostForm("admin/structure/menu/manage/$menu_name", $edit, t('Save')); - $this->assertNoFieldChecked('edit-default-language-language-show'); - - // Check that the language settings were saved. - $language_settings = language_get_default_configuration('menu_link', $menu_name); - $this->assertEqual($language_settings['langcode'], 'cc'); - $this->assertEqual($language_settings['language_show'], FALSE); + language_save_default_configuration('menu_link_content', 'menu_link_content', array('langcode' => 'cc', 'language_show' => FALSE)); // Check that the language selector is not available on menu link add page. $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); diff --git a/core/modules/menu_ui/src/Tests/MenuNodeTest.php b/core/modules/menu_ui/src/Tests/MenuNodeTest.php index 819bd56..7251397 100644 --- a/core/modules/menu_ui/src/Tests/MenuNodeTest.php +++ b/core/modules/menu_ui/src/Tests/MenuNodeTest.php @@ -2,7 +2,7 @@ /** * @file - * Definition of Drupal\menu_ui\Tests\MenuNodeTest. + * Contains \Drupal\menu_ui\Tests\MenuNodeTest. */ namespace Drupal\menu_ui\Tests; @@ -65,7 +65,7 @@ function testMenuNodeFormWidget() { $edit = array( 'menu_options[main]' => 1, 'menu_options[tools]' => 1, - 'menu_parent' => 'main:0', + 'menu_parent' => 'main:', ); $this->drupalPostForm('admin/structure/types/manage/page', $edit, t('Save content type')); @@ -93,7 +93,7 @@ function testMenuNodeFormWidget() { // Edit the node and create a menu link. $edit = array( 'menu[enabled]' => 1, - 'menu[link_title]' => $node_title, + 'menu[title]' => $node_title, 'menu[weight]' => 17, ); $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); @@ -102,7 +102,7 @@ function testMenuNodeFormWidget() { $this->assertLink($node_title); $this->drupalGet('node/' . $node->id() . '/edit'); - $this->assertOptionSelected('edit-menu-weight', 17, 'Menu weight correct in edit form'); + $this->assertFieldById('edit-menu-weight', 17, 'Menu weight correct in edit form'); // Edit the node and remove the menu link. $edit = array( @@ -114,10 +114,12 @@ function testMenuNodeFormWidget() { $this->assertNoLink($node_title); // Add a menu link to the Administration menu. - $item = entity_create('menu_link', array( - 'link_path' => 'node/' . $node->id(), - 'link_title' => $this->randomName(16), + $item = entity_create('menu_link_content', array( + 'route_name' => 'node.view', + 'route_parameters' => array('node' => $node->id()), + 'title' => $this->randomName(16), 'menu_name' => 'admin', + 'bundle' => 'menu_link_content', )); $item->save(); @@ -127,27 +129,30 @@ function testMenuNodeFormWidget() { $this->assertText('Provide a menu link', 'Link in not allowed menu not shown in node edit form'); // Assert that the link is still in the Administration menu after save. $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); - $link = menu_link_load($item['mlid']); + $link = entity_load('menu_link_content', $item->id()); $this->assertTrue($link, 'Link in not allowed menu still exists after saving node'); // Move the menu link back to the Tools menu. - $item['menu_name'] = 'tools'; - menu_link_save($item); + $item->menu_name->value = 'tools'; + $item->save(); // Create a second node. $child_node = $this->drupalCreateNode(array('type' => 'article')); // Assign a menu link to the second node, being a child of the first one. - $child_item = entity_create('menu_link', array( - 'link_path' => 'node/'. $child_node->id(), - 'link_title' => $this->randomName(16), - 'plid' => $item['mlid'], + $child_item = entity_create('menu_link_content', array( + 'route_name' => 'node.view', + 'route_parameters' => array('node' => $child_node->id()), + 'title' => $this->randomName(16), + 'parent' => $item->getPluginId(), + 'menu_name' => $item->getMenuName(), + 'bundle' => 'menu_link_content', )); $child_item->save(); // Edit the first node. $this->drupalGet('node/'. $node->id() .'/edit'); // Assert that it is not possible to set the parent of the first node to itself or the second node. - $this->assertNoOption('edit-menu-parent', 'tools:'. $item['mlid']); - $this->assertNoOption('edit-menu-parent', 'tools:'. $child_item['mlid']); + $this->assertNoOption('edit-menu-menu-parent', 'tools:'. $item->getPluginId()); + $this->assertNoOption('edit-menu-menu-parent', 'tools:'. $child_item->getPluginId()); // Assert that unallowed Administration menu is not available in options. - $this->assertNoOption('edit-menu-parent', 'admin:0'); + $this->assertNoOption('edit-menu-menu-parent', 'admin:'); } } diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php index f707eb9..8fe9e2d 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -2,12 +2,14 @@ /** * @file - * Definition of Drupal\menu_ui\Tests\MenuTest. + * Contains \Drupal\menu_ui\Tests\MenuTest. */ namespace Drupal\menu_ui\Tests; use Drupal\Component\Serialization\Json; +use Drupal\Core\Menu\MenuLinkInterface; +use Drupal\menu_link_content\Entity\MenuLinkContent; use Drupal\system\Entity\Menu; /** @@ -49,7 +51,7 @@ class MenuTest extends MenuWebTestBase { /** * An array of test menu links. * - * @var array + * @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface[] */ protected $items; @@ -78,17 +80,20 @@ function testMenu() { // Verify that the menu links rebuild is idempotent and leaves the same // number of links in the table. - $before_count = db_query('SELECT COUNT(*) FROM {menu_links}')->fetchField(); - menu_link_rebuild_defaults(); - $after_count = db_query('SELECT COUNT(*) FROM {menu_links}')->fetchField(); - $this->assertIdentical($before_count, $after_count, 'menu_link_rebuild_defaults() does not add more links'); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $before_count = $menu_link_manager->countMenuLinks(NULL); + $menu_link_manager->rebuild(); + $after_count = $menu_link_manager->countMenuLinks(NULL); + $this->assertIdentical($before_count, $after_count, 'MenuLinkManager::rebuild() does not add more links'); // Do standard user tests. // Login the user. $this->drupalLogin($this->authenticated_user); $this->verifyAccess(403); + foreach ($this->items as $item) { // Paths were set as 'node/$nid'. - $node = node_load(substr($item['link_path'], 5)); + $node = node_load($item->getRouteParameters()['node']); $this->verifyMenuLink($item, $node); } @@ -104,20 +109,20 @@ function testMenu() { $this->deleteCustomMenu(); // Modify and reset a standard menu link. - $item = $this->getStandardMenuLink(); - $old_title = $item['link_title']; - $this->modifyMenuLink($item); - $item = entity_load('menu_link', $item['mlid']); - // Verify that a change to the description is saved. - $description = $this->randomName(16); - $item['options']['attributes']['title'] = $description; - $return_value = menu_link_save($item); - // Save the menu link again to test the return value of the procedural save - // helper. - $this->assertIdentical($return_value, $item->save(), 'Return value of menu_link_save() is identical to the return value of $menu_link->save().'); - $saved_item = entity_load('menu_link', $item['mlid']); - $this->assertEqual($description, $saved_item['options']['attributes']['title'], 'Saving an existing link updates the description (title attribute)'); - $this->resetMenuLink($item, $old_title); + $instance = $this->getStandardMenuLink(); + $old_weight = $instance->getWeight(); + // Edit the static menu link. + $edit = array(); + $edit['weight'] = 10; + $id = $instance->getPluginId(); + $this->drupalPostForm("admin/structure/menu/link/$id/edit", $edit, t('Save')); + $this->assertResponse(200); + $this->assertText('The menu link has been saved.'); + $menu_link_manager->resetDefinitions(); + + $instance = $menu_link_manager->createInstance($instance->getPluginId()); + $this->assertEqual($edit['weight'], $instance->getWeight(), 'Saving an existing link updates the weight.'); + $this->resetMenuLink($instance, $old_weight); } /** @@ -214,7 +219,7 @@ function deleteCustomMenu() { $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $label)), 'Custom menu was deleted'); $this->assertNull(Menu::load($menu_name), 'Custom menu was deleted'); // Test if all menu links associated to the menu were removed from database. - $result = entity_load_multiple_by_properties('menu_link', array('menu_name' => $menu_name)); + $result = entity_load_multiple_by_properties('menu_link_content', array('menu_name' => $menu_name)); $this->assertFalse($result, 'All menu links associated to the custom menu were deleted.'); // Make sure there's no delete button on system menus. @@ -245,33 +250,32 @@ function doMenuTests() { )); // Add menu links. - $item1 = $this->addMenuLink(0, 'node/' . $node1->id(), $menu_name); - $item2 = $this->addMenuLink($item1['mlid'], 'node/' . $node2->id(), $menu_name, FALSE); - $item3 = $this->addMenuLink($item2['mlid'], 'node/' . $node3->id(), $menu_name); - $this->assertMenuLink($item1['mlid'], array( - 'depth' => 1, - 'has_children' => 1, - 'p1' => $item1['mlid'], - 'p2' => 0, + $item1 = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE); + $item2 = $this->addMenuLink($item1->getPluginId(), 'node/' . $node2->id(), $menu_name, FALSE); + $item3 = $this->addMenuLink($item2->getPluginId(), 'node/' . $node3->id(), $menu_name); + + // Hierarchy + // <$menu_name> + // - item1 + // -- item2 + // --- item3 + + $this->assertMenuLink($item1->getPluginId(), array( + 'children' => array($item2->getPluginId(), $item3->getPluginId()), + 'parents' => array($item1->getPluginId()), // We assert the language code here to make sure that the language // selection element degrades gracefully without the Language module. 'langcode' => 'en', )); - $this->assertMenuLink($item2['mlid'], array( - 'depth' => 2, 'has_children' => 1, - 'p1' => $item1['mlid'], - 'p2' => $item2['mlid'], - 'p3' => 0, + $this->assertMenuLink($item2->getPluginId(), array( + 'children' => array($item3->getPluginId()), + 'parents' => array($item2->getPluginId(), $item1->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item3['mlid'], array( - 'depth' => 3, - 'has_children' => 0, - 'p1' => $item1['mlid'], - 'p2' => $item2['mlid'], - 'p3' => $item3['mlid'], - 'p4' => 0, + $this->assertMenuLink($item3->getPluginId(), array( + 'children' => array(), + 'parents' => array($item3->getPluginId(), $item2->getPluginId(), $item1->getPluginId()), // See above. 'langcode' => 'en', )); @@ -282,34 +286,37 @@ function doMenuTests() { $this->verifyMenuLink($item3, $node3, $item2, $node2); // Add more menu links. - $item4 = $this->addMenuLink(0, 'node/' . $node4->id(), $menu_name); - $item5 = $this->addMenuLink($item4['mlid'], 'node/' . $node5->id(), $menu_name); + $item4 = $this->addMenuLink('', 'node/' . $node4->id(), $menu_name); + $item5 = $this->addMenuLink($item4->getPluginId(), 'node/' . $node5->id(), $menu_name); // Create a menu link pointing to an alias. - $item6 = $this->addMenuLink($item4['mlid'], 'node5', $menu_name, TRUE, '0', 'node/' . $node5->id()); - $this->assertMenuLink($item4['mlid'], array( - 'depth' => 1, - 'has_children' => 1, - 'p1' => $item4['mlid'], - 'p2' => 0, + $item6 = $this->addMenuLink($item4->getPluginId(), 'node5', $menu_name, TRUE, '0'); + + // Hierarchy + // <$menu_name> + // - item1 + // -- item2 + // --- item3 + // - item4 + // -- item5 + // -- item6 + + $this->assertMenuLink($item4->getPluginId(), array( + 'children' => array($item5->getPluginId(), $item6->getPluginId()), + 'parents' => array($item4->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item5['mlid'], array( - 'depth' => 2, - 'has_children' => 0, - 'p1' => $item4['mlid'], - 'p2' => $item5['mlid'], - 'p3' => 0, - // See above. + $this->assertMenuLink($item5->getPluginId(), array( + 'children' => array(), + 'parents' => array($item5->getPluginId(), $item4->getPluginId()), 'langcode' => 'en', )); - $this->assertMenuLink($item6['mlid'], array( - 'depth' => 2, - 'has_children' => 0, - 'p1' => $item4['mlid'], - 'p2' => $item6['mlid'], - 'p3' => 0, - 'link_path' => 'node/' . $node5->id(), + $this->assertMenuLink($item6->getPluginId(), array( + 'children' => array(), + 'parents' => array($item6->getPluginId(), $item4->getPluginId()), + 'route_name' => 'node.view', + 'route_parameters' => array('node' => $node5->id()), + 'url' => '', // See above. 'langcode' => 'en', )); @@ -323,50 +330,44 @@ function doMenuTests() { $this->toggleMenuLink($item2); // Move link and verify that descendants are updated. - $this->moveMenuLink($item2, $item5['mlid'], $menu_name); - $this->assertMenuLink($item1['mlid'], array( - 'depth' => 1, - 'has_children' => 0, - 'p1' => $item1['mlid'], - 'p2' => 0, + $this->moveMenuLink($item2, $item5->getPluginId(), $menu_name); + // Hierarchy + // <$menu_name> + // - item1 (disabled) + // - item4 + // -- item5 + // --- item2 (disabled) + // ---- item3 + // -- item6 + + $this->assertMenuLink($item1->getPluginId(), array( + 'children' => array(), + 'parents' => array($item1->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item4['mlid'], array( - 'depth' => 1, - 'has_children' => 1, - 'p1' => $item4['mlid'], - 'p2' => 0, + $this->assertMenuLink($item4->getPluginId(), array( + 'children' => array($item5->getPluginId(), $item6->getPluginId(), $item2->getPluginId(), $item3->getPluginId()), + 'parents' => array($item4->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item5['mlid'], array( - 'depth' => 2, - 'has_children' => 1, - 'p1' => $item4['mlid'], - 'p2' => $item5['mlid'], - 'p3' => 0, + + $this->assertMenuLink($item5->getPluginId(), array( + 'children' => array($item2->getPluginId(), $item3->getPluginId()), + 'parents' => array($item5->getPluginId(), $item4->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item2['mlid'], array( - 'depth' => 3, - 'has_children' => 1, - 'p1' => $item4['mlid'], - 'p2' => $item5['mlid'], - 'p3' => $item2['mlid'], - 'p4' => 0, + $this->assertMenuLink($item2->getPluginId(), array( + 'children' => array($item3->getPluginId()), + 'parents' => array($item2->getPluginId(), $item5->getPluginId(), $item4->getPluginId()), // See above. 'langcode' => 'en', )); - $this->assertMenuLink($item3['mlid'], array( - 'depth' => 4, - 'has_children' => 0, - 'p1' => $item4['mlid'], - 'p2' => $item5['mlid'], - 'p3' => $item2['mlid'], - 'p4' => $item3['mlid'], - 'p5' => 0, + $this->assertMenuLink($item3->getPluginId(), array( + 'children' => array(), + 'parents' => array($item3->getPluginId(), $item2->getPluginId(), $item5->getPluginId(), $item4->getPluginId()), // See above. 'langcode' => 'en', )); @@ -375,33 +376,31 @@ function doMenuTests() { // item's weight doesn't get changed because of the old hardcoded delta=50. $items = array(); for ($i = -50; $i <= 51; $i++) { - $items[$i] = $this->addMenuLink(0, 'node/' . $node1->id(), $menu_name, TRUE, strval($i)); + $items[$i] = $this->addMenuLink('', 'node/' . $node1->id(), $menu_name, TRUE, strval($i)); } - $this->assertMenuLink($items[51]['mlid'], array('weight' => '51')); + $this->assertMenuLink($items[51]->getPluginId(), array('weight' => '51')); // Enable a link via the overview form. $this->disableMenuLink($item1); $edit = array(); - // Note in the UI the 'links[mlid:x][hidden]' form element maps to enabled, - // or NOT hidden. - $edit['links[mlid:' . $item1['mlid'] . '][hidden]'] = TRUE; - $this->drupalPostForm('admin/structure/menu/manage/' . $item1['menu_name'], $edit, t('Save')); + $edit['links[menu_plugin_id:' . $item1->getPluginId() . '][enabled]'] = TRUE; + $this->drupalPostForm('admin/structure/menu/manage/' . $item1->getMenuName(), $edit, t('Save')); // Verify in the database. - $this->assertMenuLink($item1['mlid'], array('hidden' => 0)); + $this->assertMenuLink($item1->getPluginId(), array('hidden' => 0)); // Add an external link. - $item7 = $this->addMenuLink(0, 'http://drupal.org', $menu_name); - $this->assertMenuLink($item7['mlid'], array('link_path' => 'http://drupal.org', 'external' => 1)); + $item7 = $this->addMenuLink('', 'http://drupal.org', $menu_name); + $this->assertMenuLink($item7->getPluginId(), array('url' => 'http://drupal.org')); // Add menu item. - $item8 = $this->addMenuLink(0, '', $menu_name); - $this->assertMenuLink($item8['mlid'], array('link_path' => '', 'external' => 1)); + $item8 = $this->addMenuLink('', '', $menu_name); + $this->assertMenuLink($item8->getPluginId(), array('route_name' => '')); $this->drupalGet(''); $this->assertResponse(200); // Make sure we get routed correctly. - $this->clickLink($item8['link_title']); + $this->clickLink($item8->getTitle()); $this->assertResponse(200); // Save menu links for later tests. @@ -417,16 +416,16 @@ function testMenuQueryAndFragment() { // Make a path with query and fragment on. $path = 'test-page?arg1=value1&arg2=value2'; - $item = $this->addMenuLink(0, $path); + $item = $this->addMenuLink('', $path); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); - $this->assertFieldByName('link_path', $path, 'Path is found with both query and fragment.'); + $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); + $this->assertFieldByName('url', $path, 'Path is found with both query and fragment.'); // Now change the path to something without query and fragment. $path = 'test-page'; - $this->drupalPostForm('admin/structure/menu/item/' . $item['mlid'] . '/edit', array('link_path' => $path), t('Save')); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); - $this->assertFieldByName('link_path', $path, 'Path no longer has query or fragment.'); + $this->drupalPostForm('admin/structure/menu/item/' . $item->id() . '/edit', array('url' => $path), t('Save')); + $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); + $this->assertFieldByName('url', $path, 'Path no longer has query or fragment.'); } /** @@ -456,15 +455,15 @@ function testUnpublishedNodeMenuItem() { 'status' => NODE_NOT_PUBLISHED, )); - $item = $this->addMenuLink(0, 'node/' . $node->id()); + $item = $this->addMenuLink('', 'node/' . $node->id()); $this->modifyMenuLink($item); // Test that a user with 'administer menu' but without 'bypass node access' // cannot see the menu item. $this->drupalLogout(); $this->drupalLogin($this->admin_user); - $this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']); - $this->assertNoText($item['link_title'], "Menu link pointing to unpublished node is only visible to users with 'bypass node access' permission"); + $this->drupalGet('admin/structure/menu/manage/' . $item->getMenuName()); + $this->assertNoText($item->getTitle(), "Menu link pointing to unpublished node is only visible to users with 'bypass node access' permission"); } /** @@ -490,42 +489,12 @@ public function testBlockContextualLinks() { } /** - * Tests menu link bundles. - */ - public function testMenuBundles() { - $this->drupalLogin($this->admin_user); - $menu = $this->addCustomMenu(); - // Clear the entity cache to ensure the static caches are rebuilt. - \Drupal::entityManager()->clearCachedBundles(); - $bundles = entity_get_bundles('menu_link'); - $this->assertTrue(isset($bundles[$menu->id()])); - $menus = menu_list_system_menus(); - $menus[$menu->id()] = $menu->label(); - ksort($menus); - $this->assertIdentical(array_keys($bundles), array_keys($menus)); - - // Test if moving a menu link between menus changes the bundle. - $node = $this->drupalCreateNode(array('type' => 'article')); - $item = $this->addMenuLink(0, 'node/' . $node->id(), 'tools'); - $this->moveMenuLink($item, 0, $menu->id()); - $this->assertEqual($item->bundle(), 'tools', 'Menu link bundle matches the menu'); - - $moved_item = entity_load('menu_link', $item->id(), TRUE); - $this->assertNotEqual($moved_item->bundle(), $item->bundle(), 'Menu link bundle was changed'); - $this->assertEqual($moved_item->bundle(), $menu->id(), 'Menu link bundle matches the menu'); - - $unsaved_item = entity_create('menu_link', array('menu_name' => $menu->id(), 'link_title' => $this->randomName(16), 'link_path' => '')); - $this->assertEqual($unsaved_item->bundle(), $menu->id(), 'Unsaved menu link bundle matches the menu'); - $this->assertEqual($unsaved_item->menu_name, $menu->id(), 'Unsaved menu link menu name matches the menu'); - } - - /** * Adds a menu link using the UI. * - * @param integer $plid + * @param string $parent * Optional parent menu link id. - * @param string $link - * Link path. Defaults to the front page. + * @param string $path + * The path to enter on the form. Defaults to the front page. * @param string $menu_name * Menu name. Defaults to 'tools'. * @param bool $expanded @@ -534,40 +503,36 @@ public function testMenuBundles() { * to FALSE. * @param string $weight * Menu weight. Defaults to 0. - * @param string $actual_link - * Actual link path in case $link is an alias. * - * @return \Drupal\menu_link\Entity\MenuLink + * @return \Drupal\menu_link_content\Entity\MenuLinkContent * A menu link entity. */ - function addMenuLink($plid = 0, $link = '', $menu_name = 'tools', $expanded = TRUE, $weight = '0', $actual_link = FALSE) { + function addMenuLink($parent = '', $path = '', $menu_name = 'tools', $expanded = FALSE, $weight = '0') { // View add menu link page. $this->drupalGet("admin/structure/menu/manage/$menu_name/add"); $this->assertResponse(200); $title = '!link_' . $this->randomName(16); $edit = array( - 'link_path' => $link, - 'link_title' => $title, - 'description' => '', - 'enabled' => TRUE, - 'expanded' => $expanded, - 'parent' => $menu_name . ':' . $plid, - 'weight' => $weight, + 'url' => $path, + 'title[0][value]' => $title, + 'description[0][value]' => '', + 'enabled' => 1, + 'expanded[value]' => $expanded, + 'menu_parent' => $menu_name . ':' . $parent, + 'weight[0][value]' => $weight, ); - if (!$actual_link) { - $actual_link = $link; - } // Add menu link. $this->drupalPostForm(NULL, $edit, t('Save')); $this->assertResponse(200); $this->assertText('The menu link has been saved.'); - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $title)); + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $title)); + $menu_link = reset($menu_links); $this->assertTrue($menu_link, 'Menu link was found in database.'); - $this->assertMenuLink($menu_link->id(), array('menu_name' => $menu_name, 'link_path' => $actual_link, 'has_children' => 0, 'plid' => $plid)); + $this->assertMenuLink($menu_link->getPluginId(), array('menu_name' => $menu_name, 'children' => array(), 'parent' => $parent)); return $menu_link; } @@ -578,27 +543,27 @@ function addMenuLink($plid = 0, $link = '', $menu_name = 'tools', $expand function addInvalidMenuLink() { foreach (array('-&-', 'admin/people/permissions', '#') as $link_path) { $edit = array( - 'link_path' => $link_path, - 'link_title' => 'title', + 'url' => $link_path, + 'title[0][value]' => 'title', ); $this->drupalPostForm("admin/structure/menu/manage/{$this->menu->id()}/add", $edit, t('Save')); - $this->assertRaw(t("The path '@path' is either invalid or you do not have access to it.", array('@path' => $link_path)), 'Menu link was not created'); + $this->assertRaw(t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $link_path)), 'Menu link was not created'); } } /** * Verifies a menu link using the UI. * - * @param array $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item * Menu link. * @param object $item_node * Menu link content node. - * @param array $parent + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $parent * Parent menu link. * @param object $parent_node * Parent menu link content node. */ - function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) { + function verifyMenuLink(MenuLinkContent $item, $item_node, MenuLinkContent $parent = NULL, $parent_node = NULL) { // View home page. $this->drupalGet(''); $this->assertResponse(200); @@ -606,7 +571,7 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) // Verify parent menu link. if (isset($parent)) { // Verify menu link. - $title = $parent['link_title']; + $title = $parent->getTitle(); $this->assertLink($title, 0, 'Parent menu link was displayed'); // Verify menu link link. @@ -616,7 +581,7 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) } // Verify menu link. - $title = $item['link_title']; + $title = $item->getTitle(); $this->assertLink($title, 0, 'Menu link was displayed'); // Verify menu link link. @@ -628,18 +593,18 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) /** * Changes the parent of a menu link using the UI. * - * @param array $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContentInterface $item * The menu link item to move. - * @param int $plid + * @param int $parent * The id of the new parent. * @param string $menu_name * The menu the menu link will be moved to. */ - function moveMenuLink($item, $plid, $menu_name) { - $mlid = $item['mlid']; + function moveMenuLink(MenuLinkContent $item, $parent, $menu_name) { + $mlid = $item->id(); $edit = array( - 'parent' => $menu_name . ':' . $plid, + 'menu_parent' => $menu_name . ':' . $parent, ); $this->drupalPostForm("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); $this->assertResponse(200); @@ -648,58 +613,54 @@ function moveMenuLink($item, $plid, $menu_name) { /** * Modifies a menu link using the UI. * - * @param array $item - * Menu link passed by reference. + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item + * Menu link entity. */ - function modifyMenuLink(&$item) { - $item['link_title'] = $this->randomName(16); + function modifyMenuLink(MenuLinkContent $item) { + $item->title->value = $this->randomName(16); - $mlid = $item['mlid']; - $title = $item['link_title']; + $mlid = $item->id(); + $title = $item->getTitle(); // Edit menu link. $edit = array(); - $edit['link_title'] = $title; + $edit['title[0][value]'] = $title; $this->drupalPostForm("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); $this->assertResponse(200); $this->assertText('The menu link has been saved.'); // Verify menu link. - $this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']); + $this->drupalGet('admin/structure/menu/manage/' . $item->getMenuName()); $this->assertText($title, 'Menu link was edited'); } /** * Resets a standard menu link using the UI. * - * @param array $item - * Menu link. - * @param string $old_title + * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link + * The Menu link. + * @param int $old_weight * Original title for menu link. */ - function resetMenuLink($item, $old_title) { - $mlid = $item['mlid']; - $title = $item['link_title']; - + function resetMenuLink(MenuLinkInterface $menu_link, $old_weight) { // Reset menu link. - $this->drupalPostForm("admin/structure/menu/item/$mlid/reset", array(), t('Reset')); + $this->drupalPostForm("admin/structure/menu/link/{$menu_link->getPluginId()}/reset", array(), t('Reset')); $this->assertResponse(200); $this->assertRaw(t('The menu link was reset to its default settings.'), 'Menu link was reset'); // Verify menu link. - $this->drupalGet(''); - $this->assertNoText($title, 'Menu link was reset'); - $this->assertText($old_title, 'Menu link was reset'); + $instance = \Drupal::service('plugin.manager.menu.link')->createInstance($menu_link->getPluginId()); + $this->assertEqual($old_weight, $instance->getWeight(), 'Resets to the old weight.'); } /** * Deletes a menu link using the UI. * - * @param array $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item * Menu link. */ - function deleteMenuLink($item) { - $mlid = $item['mlid']; - $title = $item['link_title']; + function deleteMenuLink(MenuLinkContent $item) { + $mlid = $item->id(); + $title = $item->getTitle(); // Delete menu link. $this->drupalPostForm("admin/structure/menu/item/$mlid/delete", array(), t('Confirm')); @@ -714,51 +675,51 @@ function deleteMenuLink($item) { /** * Alternately disables and enables a menu link. * - * @param $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item * Menu link. */ - function toggleMenuLink($item) { + function toggleMenuLink(MenuLinkContent $item) { $this->disableMenuLink($item); // Verify menu link is absent. $this->drupalGet(''); - $this->assertNoText($item['link_title'], 'Menu link was not displayed'); + $this->assertNoText($item->getTitle(), 'Menu link was not displayed'); $this->enableMenuLink($item); // Verify menu link is displayed. $this->drupalGet(''); - $this->assertText($item['link_title'], 'Menu link was displayed'); + $this->assertText($item->getTitle(), 'Menu link was displayed'); } /** * Disables a menu link. * - * @param $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item * Menu link. */ - function disableMenuLink($item) { - $mlid = $item['mlid']; + function disableMenuLink(MenuLinkContent $item) { + $mlid = $item->id(); $edit['enabled'] = FALSE; $this->drupalPostForm("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); // Unlike most other modules, there is no confirmation message displayed. // Verify in the database. - $this->assertMenuLink($mlid, array('hidden' => 1)); + $this->assertMenuLink($item->getPluginId(), array('hidden' => 1)); } /** * Enables a menu link. * - * @param $item + * @param \Drupal\menu_link_content\Entity\MenuLinkContent $item * Menu link. */ - function enableMenuLink($item) { - $mlid = $item['mlid']; + function enableMenuLink(MenuLinkContent $item) { + $mlid = $item->id(); $edit['enabled'] = TRUE; $this->drupalPostForm("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); // Verify in the database. - $this->assertMenuLink($mlid, array('hidden' => 0)); + $this->assertMenuLink($item->getPluginId(), array('hidden' => 0)); } /** @@ -782,27 +743,19 @@ public function testMenuParentsJsAccess() { /** * Returns standard menu link. * - * @return \Drupal\menu_link\Entity\MenuLink - * A menu link entity. + * @return \Drupal\Core\Menu\MenuLinkInterface + * A menu link plugin. */ private function getStandardMenuLink() { - $mlid = 0; // Retrieve menu link id of the Log out menu link, which will always be on // the front page. - $query = \Drupal::entityQuery('menu_link') - ->condition('module', 'user') - ->condition('machine_name', 'user.logout'); - $result = $query->execute(); - if (!empty($result)) { - $mlid = reset($result); - } + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $result = $menu_link_manager->loadLinksByRoute('user.logout'); + $instance = reset($result); - $this->assertTrue($mlid > 0, 'Standard menu link id was found'); - // Load menu link. - // Use api function so that link is translated for rendering. - $item = entity_load('menu_link', $mlid); - $this->assertTrue((bool) $item, 'Standard menu link was loaded'); - return $item; + $this->assertTrue((bool) $instance, 'Standard menu link was loaded'); + return $instance; } /** @@ -833,9 +786,9 @@ private function verifyAccess($response = 200) { $this->assertText(t('Tools'), 'Tools menu page was displayed'); } - // View menu edit page. + // View menu edit page for a static link. $item = $this->getStandardMenuLink(); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); + $this->drupalGet('admin/structure/menu/link/' . $item->getPluginId() . '/edit'); $this->assertResponse($response); if ($response == 200) { $this->assertText(t('Edit menu item'), 'Menu edit page was displayed'); diff --git a/core/modules/menu_ui/src/Tests/MenuWebTestBase.php b/core/modules/menu_ui/src/Tests/MenuWebTestBase.php index 5f16536..6069197 100644 --- a/core/modules/menu_ui/src/Tests/MenuWebTestBase.php +++ b/core/modules/menu_ui/src/Tests/MenuWebTestBase.php @@ -19,28 +19,58 @@ * * @var array */ - public static $modules = array('menu_ui'); + public static $modules = array('menu_ui', 'menu_link_content'); /** * Fetchs the menu item from the database and compares it to expected item. * - * @param int $mlid + * @param int $menu_plugin_id * Menu item id. - * @param array $item + * @param array $expected_item * Array containing properties to verify. */ - function assertMenuLink($mlid, array $expected_item) { + function assertMenuLink($menu_plugin_id, array $expected_item) { // Retrieve menu link. - $item = entity_load('menu_link', $mlid); - $options = $item->options; - if (!empty($options['query'])) { - $item['link_path'] .= '?' . \Drupal::urlGenerator()->httpBuildQuery($options['query']); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_link_manager->resetDefinitions(); + // Reset the static load cache. + \Drupal::entityManager()->getStorage('menu_link_content')->resetCache(); + $definition = $menu_link_manager->getDefinition($menu_plugin_id); + + $entity = NULL; + + // Pull the path from the menu link content. + if (strpos($menu_plugin_id, 'menu_link_content') === 0) { + list(, $uuid) = explode(':', $menu_plugin_id, 2); + /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $entity */ + $entity = \Drupal::entityManager()->loadEntityByUuid('menu_link_content', $uuid); + } + + if (isset($expected_item['children'])) { + $child_ids = array_values($menu_link_manager->getChildIds($menu_plugin_id)); + sort($expected_item['children']); + if ($child_ids) { + sort($child_ids); + } + $this->assertEqual($expected_item['children'], $child_ids); + unset($expected_item['children']); } - if (!empty($options['fragment'])) { - $item['link_path'] .= '#' . $options['fragment']; + + if (isset($expected_item['parents'])) { + $parent_ids = array_values($menu_link_manager->getParentIds($menu_plugin_id)); + $this->assertEqual($expected_item['parents'], $parent_ids); + unset($expected_item['parents']); + } + + if (isset($expected_item['langcode']) && $entity) { + $this->assertEqual($entity->langcode->value, $expected_item['langcode']); + unset($expected_item['langcode']); } + foreach ($expected_item as $key => $value) { - $this->assertEqual($item[$key], $value); + $this->assertTrue(isset($definition[$key])); + $this->assertEqual($definition[$key], $value); } } diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index dd47b47..56154d1 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -452,10 +452,10 @@ function hook_node_validate(\Drupal\node\NodeInterface $node, $form, &$form_stat * @ingroup entity_crud */ function hook_node_submit(\Drupal\node\NodeInterface $node, $form, &$form_state) { - // Decompose the selected menu parent option into 'menu_name' and 'plid', if + // Decompose the selected menu parent option into 'menu_name' and 'parent', if // the form used the default parent selection widget. if (!empty($form_state['values']['menu']['parent'])) { - list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + list($node->menu['menu_name'], $node->menu['parent']) = explode(':', $form_state['values']['menu']['parent']); } } diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 69e4235..288cf4d82 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -214,7 +214,7 @@ function testInstallConfig() { */ function testEnableModulesFixedList() { // Install system module. - $this->container->get('module_handler')->install(array('system', 'menu_link')); + $this->container->get('module_handler')->install(array('system', 'menu_link_content')); $entity_manager = \Drupal::entityManager(); // entity_test is loaded via $modules; its entity type should exist. diff --git a/core/modules/system/config/install/system.menu.yml b/core/modules/system/config/install/system.menu.yml deleted file mode 100644 index e73636c..0000000 --- a/core/modules/system/config/install/system.menu.yml +++ /dev/null @@ -1 +0,0 @@ -active_menus_default: [] diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 5375524..2e9219d 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -168,17 +168,6 @@ system.logging: type: string label: 'Error messages to display' -system.menu: - type: mapping - label: 'Menu settings' - mapping: - active_menus_default: - type: sequence - label: 'Active menus' - sequence: - - type: string - label: 'Menu' - system.performance: type: mapping label: 'Performance settings' diff --git a/core/modules/system/src/Controller/AdminController.php b/core/modules/system/src/Controller/AdminController.php index 7445d52..d9800f0 100644 --- a/core/modules/system/src/Controller/AdminController.php +++ b/core/modules/system/src/Controller/AdminController.php @@ -36,7 +36,7 @@ public function index() { // Sort links by title. uasort($admin_tasks, array('\Drupal\Component\Utility\SortArray', 'sortByTitleElement')); // Move 'Configure permissions' links to the bottom of each section. - $permission_key = "user.admin.people.permissions.$module"; + $permission_key = "user.admin_permissions.$module"; if (isset($admin_tasks[$permission_key])) { $permission_task = $admin_tasks[$permission_key]; unset($admin_tasks[$permission_key]); diff --git a/core/modules/system/src/Controller/SystemController.php b/core/modules/system/src/Controller/SystemController.php index b960968..c811e85 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -12,6 +12,8 @@ use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Theme\ThemeAccessCheck; use Drupal\system\SystemManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -57,6 +59,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 @@ -69,13 +78,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; } /** @@ -87,66 +99,49 @@ 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') ); } /** * Provide the administration overview page. * - * @param string $path - * The administrative path for which to display child links. + * @param string $link_id + * The ID of the administrative path link for which to display child links. * * @return array * A renderable array of the administration overview page. */ - public function overview($path) { + public function overview($link_id) { // Check for status report errors. 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'); } + // Load all menu links below it. + $parameters = new MenuTreeParameters(); + $parameters->setRoot($link_id)->excludeRoot()->setTopLevelOnly()->excludeHiddenLinks(); + $tree = $this->menuLinkTree->load(NULL, $parameters); + $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 links on $path and menu links below it. - $query = $this->queryFactory->get('menu_link') - ->condition('link_path', $path) - ->condition('module', 'system'); - $result = $query->execute(); - $menu_link_storage = $this->entityManager()->getStorage('menu_link'); - if ($system_link = $menu_link_storage->loadMultiple($result)) { - $system_link = reset($system_link); - $query = $this->queryFactory->get('menu_link') - ->condition('link_path', 'admin/help', '<>') - ->condition('menu_name', $system_link->menu_name) - ->condition('plid', $system_link->id()) - ->condition('hidden', 0); - $result = $query->execute(); - if (!empty($result)) { - $menu_links = $menu_link_storage->loadMultiple($result); - foreach ($menu_links as $item) { - _menu_link_translate($item); - if (!$item['access']) { - continue; - } - // The link description, either derived from 'description' in hook_menu() - // or customized via Menu UI module is used as title attribute. - if (!empty($item['localized_options']['attributes']['title'])) { - $item['description'] = $item['localized_options']['attributes']['title']; - unset($item['localized_options']['attributes']['title']); - } - $block = $item; - $block['content'] = array( - '#theme' => 'admin_block_content', - '#content' => $this->systemManager->getAdminBlock($item), - ); + foreach ($tree as $key => $element) { + $link = $element->link; + $block['title'] = $link->getTitle(); + $block['description'] = $link->getDescription(); + $block['content'] = array( + '#theme' => 'admin_block_content', + '#content' => $this->systemManager->getAdminBlock($link), + ); - if (!empty($block['content']['#content'])) { - // Prepare for sorting as in function _menu_tree_check_access(). - // The weight is offset so it is always positive, with a uniform 5-digits. - $blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block; - } - } + if (!empty($block['content']['#content'])) { + $blocks[$key] = $block; } } + if ($blocks) { ksort($blocks); return array( diff --git a/core/modules/system/src/Form/ModulesListForm.php b/core/modules/system/src/Form/ModulesListForm.php index da1ecd0..53dcd57 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -9,18 +9,20 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; +use Drupal\Core\Controller\TitleResolverInterface; use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\Query\QueryFactory; -use Drupal\Core\Entity\Query\QueryFactoryInterface; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Routing\RouteProviderInterface; use Drupal\Core\Session\AccountInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Access\AccessManager; +use Symfony\Component\HttpFoundation\Request; /** * Provides module installation interface. @@ -61,11 +63,18 @@ class ModulesListForm extends FormBase { protected $entityManager; /** - * The query factory. + * The title resolver. * - * @var \Drupal\Core\Entity\Query\QueryFactory + * @var \Drupal\Core\Controller\TitleResolverInterface */ - protected $queryFactory; + protected $titleResolver; + + /** + * The route provider. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; /** * The current route match. @@ -75,6 +84,13 @@ class ModulesListForm extends FormBase { protected $routeMatch; /** + * The menu link manager. + * + * @var \Drupal\Core\Menu\MenuLinkManagerInterface + */ + protected $menuLinkManager; + + /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { @@ -83,9 +99,11 @@ public static function create(ContainerInterface $container) { $container->get('keyvalue.expirable')->get('module_list'), $container->get('access_manager'), $container->get('entity.manager'), - $container->get('entity.query'), $container->get('current_user'), - $container->get('current_route_match') + $container->get('current_route_match'), + $container->get('title_resolver'), + $container->get('router.route_provider'), + $container->get('plugin.manager.menu.link') ); } @@ -100,21 +118,27 @@ public static function create(ContainerInterface $container) { * Access manager. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. - * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory - * The entity query factory. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match * The current route match. + * @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver + * The title resolver. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. + * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager + * The menu link manager. */ - public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable, AccessManager $access_manager, EntityManagerInterface $entity_manager, QueryFactory $query_factory, AccountInterface $current_user, RouteMatchInterface $route_match) { + public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable, AccessManager $access_manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, RouteMatchInterface $route_match, TitleResolverInterface $title_resolver, RouteProviderInterface $route_provider, MenuLinkManagerInterface $menu_link_manager) { $this->moduleHandler = $module_handler; $this->keyValueExpirable = $key_value_expirable; $this->accessManager = $access_manager; $this->entityManager = $entity_manager; - $this->queryFactory = $query_factory; $this->currentUser = $current_user; $this->routeMatch = $route_match; + $this->titleResolver = $title_resolver; + $this->routeProvider = $route_provider; + $this->menuLinkManager = $menu_link_manager; } /** @@ -249,11 +273,23 @@ protected function buildRow(array $modules, Extension $module, $distribution) { if ($module->status && isset($module->info['configure'])) { $route_parameters = isset($module->info['configure_parameters']) ? $module->info['configure_parameters'] : array(); if ($this->accessManager->checkNamedRoute($module->info['configure'], $route_parameters, $this->currentUser)) { - $result = $this->queryFactory->get('menu_link') - ->condition('route_name', $module->info['configure']) - ->execute(); - $menu_items = $this->entityManager->getStorage('menu_link')->loadMultiple($result); - $item = reset($menu_items); + + $links = $this->menuLinkManager->loadLinksByRoute($module->info['configure']); + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = reset($links); + // Most configure links have a corresponding menu link, though some just + // have a route. + if ($link) { + $description = $link->getDescription(); + } + else { + $request = new Request(); + $request->attributes->set('_route_name', $module->info['configure']); + $route_object = $this->routeProvider->getRouteByName($module->info['configure']); + $request->attributes->set('_route', $route_object); + $description = $this->titleResolver->getTitle($request, $route_object); + } + $row['links']['configure'] = array( '#type' => 'link', '#title' => $this->t('Configure'), @@ -262,7 +298,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) { '#options' => array( 'attributes' => array( 'class' => array('module-link', 'module-link-configure'), - 'title' => $item['description'], + 'title' => $description, ), ), ); diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index 5381471..258ffbb 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -10,7 +10,8 @@ use Drupal\Component\Utility\NestedArray; use Drupal\block\BlockBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\menu_link\MenuTreeInterface; +use Drupal\Core\Menu\MenuActiveTrailInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -27,13 +28,20 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { /** - * The menu tree. + * The menu link tree service. * - * @var \Drupal\menu_link\MenuTreeInterface + * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ protected $menuTree; /** + * The active menu trail service. + * + * @var \Drupal\Core\Menu\MenuActiveTrailInterface + */ + protected $menuActiveTrail; + + /** * Constructs a new SystemMenuBlock. * * @param array $configuration @@ -42,12 +50,15 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterfa * 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. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree + * The menu tree service. + * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail + * The active menu trail service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuTreeInterface $menu_tree) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->menuTree = $menu_tree; + $this->menuActiveTrail = $menu_active_trail; } /** @@ -58,7 +69,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('menu_link.tree') + $container->get('menu.link_tree'), + $container->get('menu.active_trail') ); } @@ -66,8 +78,15 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function build() { - $menu = $this->getDerivativeId(); - return $this->menuTree->renderMenu($menu); + $menu_name = $this->getDerivativeId(); + $parameters = $this->menuTree->getCurrentRouteMenuTreeParameters($menu_name); + $tree = $this->menuTree->load($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->build($tree); } /** @@ -91,9 +110,7 @@ public function defaultConfiguration() { public function getCacheKeys() { // Add a key for the active menu trail. $menu = $this->getDerivativeId(); - $active_trail = $this->menuTree->getActiveTrailIds($menu); - $active_trail_key = 'trail.' . implode('|', $active_trail); - return array_merge(parent::getCacheKeys(), array($active_trail_key)); + return array_merge(parent::getCacheKeys(), array($this->menuActiveTrail->getActiveTrailCacheKey($menu))); } /** @@ -114,7 +131,7 @@ public function getCacheTags() { protected function getRequiredCacheContexts() { // Menu blocks must be cached per role: different roles may have access to // different menu links. - return array('cache_context.user.roles'); + return array('cache_context.user.roles', 'cache_context.language'); } } diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 696efc1..00f97aa 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -6,8 +6,11 @@ namespace Drupal\system; -use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Menu\MenuActiveTrailInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuLinkInterface; +use Drupal\Core\Menu\MenuTreeParameters; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -33,18 +36,25 @@ class SystemManager { protected $database; /** - * The menu link storage. + * The request stack. * - * @var \Drupal\menu_link\MenuLinkStorageInterface + * @var \Symfony\Component\HttpFoundation\RequestStack */ - protected $menuLinkStorage; + protected $requestStack; /** - * The request stack. + * The menu link tree manager. * - * @var \Symfony\Component\HttpFoundation\RequestStack + * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ - protected $requestStack; + protected $menuTree; + + /** + * The active menu trail service. + * + * @var \Drupal\Core\Menu\MenuActiveTrailInterface + */ + protected $menuActiveTrail; /** * A static cache of menu items. @@ -79,12 +89,17 @@ class SystemManager { * The entity manager. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The request stack. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree + * The menu tree manager. + * @param \Drupal\Core\Menu\MenuActiveTrailInterface $menu_active_trail + * The active menu trail service. */ - public function __construct(ModuleHandlerInterface $module_handler, Connection $database, EntityManagerInterface $entity_manager, RequestStack $request_stack) { + public function __construct(ModuleHandlerInterface $module_handler, Connection $database, EntityManagerInterface $entity_manager, RequestStack $request_stack, MenuLinkTreeInterface $menu_tree, MenuActiveTrailInterface $menu_active_trail) { $this->moduleHandler = $module_handler; $this->database = $database; - $this->menuLinkStorage = $entity_manager->getStorage('menu_link'); $this->requestStack = $request_stack; + $this->menuTree = $menu_tree; + $this->menuActiveTrail = $menu_active_trail; } /** @@ -171,11 +186,10 @@ public function getMaxSeverity(&$requirements) { * A render array suitable for drupal_render. */ public function getBlockContents() { - $request = $this->requestStack->getCurrentRequest(); - $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); - $items = $this->menuLinkStorage->loadByProperties(array('route_name' => $route_name)); - $item = reset($items); - if ($content = $this->getAdminBlock($item)) { + // We hard-code the menu name here since otherwise a link in the tools menu + // or elsewhere could give us a blank block. + $link = $this->menuActiveTrail->getActiveLink('admin'); + if ($link && $content = $this->getAdminBlock($link)) { $output = array( '#theme' => 'admin_block_content', '#content' => $content, @@ -192,48 +206,33 @@ public function getBlockContents() { /** * Provide a single block on the administration overview page. * - * @param \Drupal\menu_link\MenuLinkInterface|array $item + * @param \Drupal\Core\Menu\MenuLinkInterface $instance * The menu item to be displayed. * * @return array * An array of menu items, as expected by theme_admin_block_content(). */ - public function getAdminBlock($item) { - if (!isset($item['mlid'])) { - $menu_links = $this->menuLinkStorage->loadByProperties(array('link_path' => $item['path'], 'module' => 'system')); - if ($menu_links) { - $menu_link = reset($menu_links); - $item['mlid'] = $menu_link->id(); - $item['menu_name'] = $menu_link->menu_name; - } - else { - return array(); - } - } - - if (isset($this->menuItems[$item['mlid']])) { - return $this->menuItems[$item['mlid']]; - } - + public function getAdminBlock(MenuLinkInterface $instance) { $content = array(); - $menu_links = $this->menuLinkStorage->loadByProperties(array('plid' => $item['mlid'], 'menu_name' => $item['menu_name'], 'hidden' => 0)); - foreach ($menu_links as $link) { - _menu_link_translate($link); - if ($link['access']) { - // The link description, either derived from 'description' in - // hook_menu() or customized via Menu UI module is used as title attribute. - if (!empty($link['localized_options']['attributes']['title'])) { - $link['description'] = $link['localized_options']['attributes']['title']; - unset($link['localized_options']['attributes']['title']); - } - // Prepare for sorting as in function _menu_tree_check_access(). - // The weight is offset so it is always positive, with a uniform 5-digits. - $key = (50000 + $link['weight']) . ' ' . Unicode::strtolower($link['title']) . ' ' . $link['mlid']; - $content[$key] = $link; - } + // Only find the children of this link. + $link_id = $instance->getPluginId(); + $parameters = new MenuTreeParameters(); + $parameters->setRoot($link_id)->excludeRoot()->setTopLevelOnly()->excludeHiddenLinks(); + $tree = $this->menuTree->load(NULL, $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 => $element) { + /** @var $link \Drupal\Core\Menu\MenuLinkInterface */ + $link = $element->link; + $content[$key]['title'] = $link->getTitle(); + $content[$key]['options'] = $link->getOptions(); + $content[$key]['description'] = $link->getDescription(); + $content[$key]['url'] = $link->getUrlObject(); } ksort($content); - $this->menuItems[$item['mlid']] = $content; return $content; } diff --git a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php index f1bf640..ec6201a 100644 --- a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php @@ -83,15 +83,10 @@ function testBreadCrumbs() { ); $this->assertBreadcrumb('admin/structure/menu/manage/tools', $trail); - $mlid_node_add = \Drupal::entityQuery('menu_link') - ->condition('machine_name', 'node.add_page') - ->condition('module', 'node') - ->execute(); - $mlid_node_add = reset($mlid_node_add); $trail += array( 'admin/structure/menu/manage/tools' => t('Tools'), ); - $this->assertBreadcrumb("admin/structure/menu/item/$mlid_node_add/edit", $trail); + $this->assertBreadcrumb("admin/structure/menu/link/node.add_page/edit", $trail); $this->assertBreadcrumb('admin/structure/menu/manage/tools/add', $trail); // Verify Node administration breadcrumbs. @@ -165,7 +160,7 @@ function testBreadCrumbs() { // Alter node type menu settings. \Drupal::config("menu.entity.node.$type") ->set('available_menus', $menus) - ->set('parent', 'tools:0') + ->set('parent', 'tools:') ->save(); foreach ($menus as $menu) { @@ -174,13 +169,13 @@ function testBreadCrumbs() { $node2 = $this->drupalCreateNode(array( 'type' => $type, 'title' => $title, - 'menu' => entity_create('menu_link', array( - 'enabled' => 1, - 'link_title' => 'Parent ' . $title, + 'menu' => array( + 'hidden' => 0, + 'title' => 'Parent ' . $title, 'description' => '', 'menu_name' => $menu, - 'plid' => 0, - )), + 'parent' => '', + ), )); if ($menu == 'tools') { @@ -192,26 +187,26 @@ function testBreadCrumbs() { // link below it, and verify a full breadcrumb for the last child node. $menu = 'tools'; $edit = array( - 'link_title' => 'Root', - 'link_path' => 'node', + 'title[0][value]' => 'Root', + 'url' => 'node', ); $this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Root')); + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => 'Root')); $link = reset($menu_links); $edit = array( - 'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'], + 'menu[menu_parent]' => $link->getMenuName() . ':' . $link->getPluginId(), ); $this->drupalPostForm('node/' . $parent->id() . '/edit', $edit, t('Save and keep published')); $expected = array( - "node" => $link['link_title'], + "node" => $link->getTitle(), ); $trail = $home + $expected; $tree = $expected + array( - 'node/' . $parent->id() => $parent->menu['link_title'], + 'node/' . $parent->id() => $parent->menu['title'], ); $trail += array( - 'node/' . $parent->id() => $parent->menu['link_title'], + 'node/' . $parent->id() => $parent->menu['title'], ); // Add a taxonomy term/tag to last node, and add a link for that term to the @@ -241,32 +236,36 @@ function testBreadCrumbs() { } $parent_tid = $term->id(); } - $parent_mlid = 0; + $parent_mlid = ''; foreach ($tags as $name => $data) { $term = $data['term']; $edit = array( - 'link_title' => "$name link", - 'link_path' => "taxonomy/term/{$term->id()}", - 'parent' => "$menu:{$parent_mlid}", + 'title[0][value]' => "$name link", + 'url' => "taxonomy/term/{$term->id()}", + 'menu_parent' => "$menu:{$parent_mlid}", ); $this->drupalPostForm("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $menu_links = entity_load_multiple_by_properties('menu_link_content', array('title' => $edit['title[0][value]'], 'route_name' => 'taxonomy.term_page', 'route_parameters' => serialize(array('taxonomy_term' => $term->id())))); $tags[$name]['link'] = reset($menu_links); - $tags[$name]['link']['link_path'] = $edit['link_path']; - $parent_mlid = $tags[$name]['link']['mlid']; + $parent_mlid = $tags[$name]['link']->getPluginId(); } // Verify expected breadcrumbs for menu links. $trail = $home; $tree = array(); + // Logout the user because we want to check the active class as well, which + // is just rendered as anonymous user. + $this->drupalLogout(); foreach ($tags as $name => $data) { $term = $data['term']; + /** @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface $link */ $link = $data['link']; + $link_path = $link->getUrlObject()->getInternalPath(); $tree += array( - $link['link_path'] => $link['link_title'], + $link_path => $link->getTitle(), ); - $this->assertBreadcrumb($link['link_path'], $trail, $term->getName(), $tree); + $this->assertBreadcrumb($link_path, $trail, $term->getName(), $tree); $this->assertRaw(String::checkPlain($parent->getTitle()), 'Tagged node found.'); // Additionally make sure that this link appears only once; i.e., the @@ -275,14 +274,14 @@ function testBreadCrumbs() { // other than the breadcrumb trail. $elements = $this->xpath('//div[@id=:menu]/descendant::a[@href=:href]', array( ':menu' => 'block-bartik-tools', - ':href' => url($link['link_path']), + ':href' => url($link_path), )); - $this->assertTrue(count($elements) == 1, "Link to {$link['link_path']} appears only once."); + $this->assertTrue(count($elements) == 1, "Link to {$link_path} appears only once."); // Next iteration should expect this tag as parent link. // Note: Term name, not link name, due to taxonomy_term_page(). $trail += array( - $link['link_path'] => $term->getName(), + $link_path => $term->getName(), ); } @@ -292,7 +291,6 @@ function testBreadCrumbs() { user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( 'access user profiles', )); - $this->drupalLogout(); // Verify breadcrumb on front page. $this->assertBreadcrumb('', array()); @@ -359,4 +357,5 @@ function testBreadCrumbs() { $this->assertBreadcrumb('admin/reports/dblog', $trail, t('Recent log messages')); $this->assertNoResponse(403); } + } diff --git a/core/modules/system/src/Tests/Menu/LinksTest.php b/core/modules/system/src/Tests/Menu/LinksTest.php index eecfb8c..552b1ae 100644 --- a/core/modules/system/src/Tests/Menu/LinksTest.php +++ b/core/modules/system/src/Tests/Menu/LinksTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Menu; +use Drupal\Component\Utility\String; use Drupal\locale\TranslationString; use Drupal\simpletest\WebTestBase; @@ -14,6 +15,9 @@ * Tests handling of menu links hierarchies. * * @group Menu + * + * @todo Move this under menu_link_content module. + * https://www.drupal.org/node/2310353 */ class LinksTest extends WebTestBase { @@ -22,7 +26,14 @@ class LinksTest extends WebTestBase { * * @var array */ - public static $modules = array('router_test'); + public static $modules = array('router_test', 'menu_link_content'); + + /** + * The menu link plugin manager. + * + * @var \Drupal\Core\Menu\MenuLinkManagerInterface $menuLinkManager + */ + protected $menuLinkManager; /** * {@inheritdoc} @@ -30,6 +41,8 @@ class LinksTest extends WebTestBase { public function setUp() { parent::setUp(); + $this->menuLinkManager = \Drupal::service('plugin.manager.menu.link'); + entity_create('menu', array( 'id' => 'menu_test', 'label' => 'Test menu', @@ -41,55 +54,60 @@ public function setUp() { * Create a simple hierarchy of links. */ function createLinkHierarchy($module = 'menu_test') { - // First remove all the menu links. - $menu_links = menu_link_load_multiple(); - menu_link_delete_multiple(array_keys($menu_links), TRUE, TRUE); + // First remove all the menu links in the menu. + $this->menuLinkManager->deleteLinksInMenu('menu_test'); // Then create a simple link hierarchy: - // - $parent - // - $child-1 - // - $child-1-1 - // - $child-1-2 - // - $child-2 + // - parent + // - child-1 + // - child-1-1 + // - child-1-2 + // - child-2 $base_options = array( - 'link_title' => 'Menu link test', - 'module' => $module, + 'title' => 'Menu link test', + 'provider' => $module, 'menu_name' => 'menu_test', + 'bundle' => 'menu_link_content' ); - $links['parent'] = $base_options + array( - 'link_path' => 'menu-test/parent', + $parent = $base_options + array( + 'route_name' => 'menu_test.hierarchy_parent', ); - $links['parent'] = entity_create('menu_link', $links['parent']); - $links['parent']->save(); + $link = entity_create('menu_link_content', $parent); + $link->save(); + $links['parent'] = $link->getPluginId(); - $links['child-1'] = $base_options + array( - 'link_path' => 'menu-test/parent/child-1', - 'plid' => $links['parent']['mlid'], + $child_1 = $base_options + array( + 'route_name' => 'menu_test.hierarchy_parent_child', + 'parent' => $links['parent'], ); - $links['child-1'] = entity_create('menu_link', $links['child-1']); - $links['child-1']->save(); + $link = entity_create('menu_link_content', $child_1); + $link->save(); + $links['child-1'] = $link->getPluginId(); - $links['child-1-1'] = $base_options + array( - 'link_path' => 'menu-test/parent/child-1/child-1-1', - 'plid' => $links['child-1']['mlid'], + $child_1_1 = $base_options + array( + 'route_name' => 'menu_test.hierarchy_parent_child2', + 'parent' => $links['child-1'], ); - $links['child-1-1'] = entity_create('menu_link', $links['child-1-1']); - $links['child-1-1']->save(); + $link = entity_create('menu_link_content', $child_1_1); + $link->save(); + $links['child-1-1'] = $link->getPluginId(); - $links['child-1-2'] = $base_options + array( - 'link_path' => 'menu-test/parent/child-1/child-1-2', - 'plid' => $links['child-1']['mlid'], + $child_1_2 = $base_options + array( + 'route_name' => 'menu_test.hierarchy_parent_child2', + 'parent' => $links['child-1'], ); - $links['child-1-2'] = entity_create('menu_link', $links['child-1-2']); - $links['child-1-2']->save(); + $link = entity_create('menu_link_content', $child_1_2); + $link->save(); + $links['child-1-2'] = $link->getPluginId(); - $links['child-2'] = $base_options + array( - 'link_path' => 'menu-test/parent/child-2', - 'plid' => $links['parent']['mlid'], + $child_2 = $base_options + array( + 'route_name' => 'menu_test.hierarchy_parent_child', + 'parent' => $links['parent'], ); - $links['child-2'] = entity_create('menu_link', $links['child-2']); - $links['child-2']->save(); + $link = entity_create('menu_link_content', $child_2); + $link->save(); + $links['child-2'] = $link->getPluginId(); return $links; } @@ -98,13 +116,12 @@ function createLinkHierarchy($module = 'menu_test') { * Assert that at set of links is properly parented. */ function assertMenuLinkParents($links, $expected_hierarchy) { - foreach ($expected_hierarchy as $child => $parent) { - $mlid = $links[$child]['mlid']; - $plid = $parent ? $links[$parent]['mlid'] : 0; + foreach ($expected_hierarchy as $id => $parent) { + /* @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */ + $menu_link_plugin = $this->menuLinkManager->createInstance($links[$id]); + $expected_parent = isset($links[$parent]) ? $links[$parent] : ''; - $menu_link = menu_link_load($mlid); - menu_link_save($menu_link); - $this->assertEqual($menu_link['plid'], $plid, format_string('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link['plid'], '%expected_plid' => $plid))); + $this->assertEqual($menu_link_plugin->getParent(), $expected_parent, String::format('Menu link %id has parent of %parent, expected %expected_parent.', array('%id' => $id, '%parent' => $menu_link_plugin->getParent(), '%expected_parent' => $expected_parent))); } } @@ -116,7 +133,7 @@ function testMenuLinkReparenting($module = 'menu_test') { $links = $this->createLinkHierarchy($module); $expected_hierarchy = array( - 'parent' => FALSE, + 'parent' => '', 'child-1' => 'parent', 'child-1-1' => 'child-1', 'child-1-2' => 'child-1', @@ -127,11 +144,15 @@ function testMenuLinkReparenting($module = 'menu_test') { // Start over, and move child-1 under child-2, and check that all the // childs of child-1 have been moved too. $links = $this->createLinkHierarchy($module); - $links['child-1']['plid'] = $links['child-2']['mlid']; - menu_link_save($links['child-1']); + /* @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */ + $this->menuLinkManager->updateDefinition($links['child-1'], array('parent' => $links['child-2'])); + // Verify that the entity was updated too. + $menu_link_plugin = $this->menuLinkManager->createInstance($links['child-1']); + $entity = \Drupal::entityManager()->loadEntityByUuid('menu_link_content', $menu_link_plugin->getDerivativeId()); + $this->assertEqual($entity->getParentId(), $links['child-2']); $expected_hierarchy = array( - 'parent' => FALSE, + 'parent' => '', 'child-1' => 'child-2', 'child-1-1' => 'child-1', 'child-1-2' => 'child-1', @@ -140,29 +161,9 @@ function testMenuLinkReparenting($module = 'menu_test') { $this->assertMenuLinkParents($links, $expected_hierarchy); // Start over, and delete child-1, and check that the children of child-1 - // have been reassigned to the parent. menu_link_delete() will cowardly - // refuse to delete a menu link defined by the system module, so skip the - // test in that case. - if ($module != 'system') { - $links = $this->createLinkHierarchy($module); - menu_link_delete($links['child-1']['mlid']); - - $expected_hierarchy = array( - 'parent' => FALSE, - 'child-1-1' => 'parent', - 'child-1-2' => 'parent', - 'child-2' => 'parent', - ); - $this->assertMenuLinkParents($links, $expected_hierarchy); - } - - // Start over, forcefully delete child-1 from the database, simulating a - // database crash. Check that the children of child-1 have been reassigned - // to the parent, going up on the old path hierarchy stored in each of the - // links. + // have been reassigned to the parent. $links = $this->createLinkHierarchy($module); - // Don't do that at home. - entity_delete_multiple('menu_link', array($links['child-1']['mlid'])); + $this->menuLinkManager->removeDefinition($links['child-1']); $expected_hierarchy = array( 'parent' => FALSE, @@ -172,119 +173,26 @@ function testMenuLinkReparenting($module = 'menu_test') { ); $this->assertMenuLinkParents($links, $expected_hierarchy); - // Start over, forcefully delete the parent from the database, simulating a - // database crash. Check that the children of parent are now top-level. - $links = $this->createLinkHierarchy($module); - // Don't do that at home. - db_delete('menu_links') - ->condition('mlid', $links['parent']['mlid']) - ->execute(); - - $expected_hierarchy = array( - 'child-1-1' => 'child-1', - 'child-1-2' => 'child-1', - 'child-2' => FALSE, - ); - $this->assertMenuLinkParents($links, $expected_hierarchy); - } - - /** - * Tests automatic reparenting. - * - * Runs tests on menu links defined by the menu_link.static service. - */ - function testMenuLinkRouterReparenting() { - // Run all the standard parenting tests on menu links derived from - // menu routers. - $this->testMenuLinkReparenting('system'); - - // Additionnaly, test reparenting based on path. - $links = $this->createLinkHierarchy('system'); - - // Move child-1-2 has a child of child-2, making the link hierarchy - // inconsistent with the path hierarchy. - $links['child-1-2']['plid'] = $links['child-2']['mlid']; - menu_link_save($links['child-1-2']); - - // Check the new hierarchy. - $expected_hierarchy = array( - 'parent' => FALSE, - 'child-1' => 'parent', - 'child-1-1' => 'child-1', - 'child-2' => 'parent', - 'child-1-2' => 'child-2', - ); - $this->assertMenuLinkParents($links, $expected_hierarchy); - - // Now delete 'parent' directly from the database, simulating a database - // crash. 'child-1' and 'child-2' should get moved to the - // top-level. - // Don't do that at home. - db_delete('menu_links') - ->condition('mlid', $links['parent']['mlid']) - ->execute(); - $expected_hierarchy = array( - 'child-1' => FALSE, - 'child-1-1' => 'child-1', - 'child-2' => FALSE, - 'child-1-2' => 'child-2', - ); - $this->assertMenuLinkParents($links, $expected_hierarchy); - - // Now delete 'child-2' directly from the database, simulating a database - // crash. 'child-1-2' will get reparented to the top. - // Don't do that at home. - db_delete('menu_links') - ->condition('mlid', $links['child-2']['mlid']) - ->execute(); - $expected_hierarchy = array( - 'child-1' => FALSE, - 'child-1-1' => 'child-1', - 'child-1-2' => FALSE, - ); - $this->assertMenuLinkParents($links, $expected_hierarchy); - } - - /** - * Tests the router system integration (route_name and route_parameters). - */ - public function testRouterIntegration() { - $menu_link = entity_create('menu_link', array( - 'link_path' => 'router_test/test1', - )); - $menu_link->save(); - $this->assertEqual($menu_link->route_name, 'router_test.1'); - $this->assertEqual($menu_link->route_parameters, array()); - - $menu_link = entity_create('menu_link', array( - 'link_path' => 'router_test/test3/test', - )); - $menu_link->save(); - $this->assertEqual($menu_link->route_name, 'router_test.3'); - $this->assertEqual($menu_link->route_parameters, array('value' => 'test')); - - $menu_link = entity_load('menu_link', $menu_link->id()); - $this->assertEqual($menu_link->route_name, 'router_test.3'); - $this->assertEqual($menu_link->route_parameters, array('value' => 'test')); + // @todo Figure out what makes sense to test in terms of automatic + // re-parenting. https://www.drupal.org/node/2309531 } /** - * Tests uninstall a module providing default links. + * Tests uninstalling a module providing default links. */ public function testModuleUninstalledMenuLinks() { \Drupal::moduleHandler()->install(array('menu_test')); \Drupal::service('router.builder')->rebuild(); - menu_link_rebuild_defaults(); - $result = $menu_link = \Drupal::entityQuery('menu_link')->condition('machine_name', 'menu_test')->execute(); - $menu_links = \Drupal::entityManager()->getStorage('menu_link')->loadMultiple($result); + \Drupal::service('plugin.manager.menu.link')->rebuild(); + $menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test'); $this->assertEqual(count($menu_links), 1); $menu_link = reset($menu_links); - $this->assertEqual($menu_link->machine_name, 'menu_test'); + $this->assertEqual($menu_link->getPluginId(), 'menu_test'); // Uninstall the module and ensure the menu link got removed. \Drupal::moduleHandler()->uninstall(array('menu_test')); - $result = $menu_link = \Drupal::entityQuery('menu_link')->condition('machine_name', 'menu_test')->execute(); - $menu_links = \Drupal::entityManager()->getStorage('menu_link')->loadMultiple($result); + \Drupal::service('plugin.manager.menu.link')->rebuild(); + $menu_links = $this->menuLinkManager->loadLinksByRoute('menu_test.menu_test'); $this->assertEqual(count($menu_links), 0); } diff --git a/core/modules/system/src/Tests/Menu/MenuLinkDefaultIntegrationTest.php b/core/modules/system/src/Tests/Menu/MenuLinkDefaultIntegrationTest.php new file mode 100644 index 0000000..6078875 --- /dev/null +++ b/core/modules/system/src/Tests/Menu/MenuLinkDefaultIntegrationTest.php @@ -0,0 +1,70 @@ +installSchema('system', array('router')); + } + + /** + * Tests moving a static menu link without a specified menu to the root. + */ + public function testMoveToRoot() { + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_link_manager->rebuild(); + + $menu_link = $menu_link_manager->getDefinition('menu_test.child'); + $this->assertEqual($menu_link['parent'], 'menu_test.parent'); + $this->assertEqual($menu_link['menu_name'], 'test'); + + $tree = \Drupal::menuTree()->load('test', new MenuTreeParameters()); + $this->assertEqual(count($tree), 1); + $this->assertEqual($tree['menu_test.parent']->link->getPluginId(), 'menu_test.parent'); + $this->assertEqual($tree['menu_test.parent']->subtree['menu_test.child']->link->getPluginId(), 'menu_test.child'); + + // Ensure that the menu name is not forgotten. + $menu_link_manager->updateDefinition('menu_test.child', array('parent' => '')); + $menu_link = $menu_link_manager->getDefinition('menu_test.child'); + + $this->assertEqual($menu_link['parent'], ''); + $this->assertEqual($menu_link['menu_name'], 'test'); + + $tree = \Drupal::menuTree()->load('test', new MenuTreeParameters()); + $this->assertEqual(count($tree), 2); + $this->assertEqual($tree['menu_test.parent']->link->getPluginId(), 'menu_test.parent'); + $this->assertEqual($tree['menu_test.child']->link->getPluginId(), 'menu_test.child'); + + $this->assertTrue(TRUE); + } + +} diff --git a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php index 1b97c98..98b9685 100644 --- a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php +++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php @@ -43,7 +43,6 @@ class MenuLinkTreeTest extends KernelTestBase { public static $modules = array( 'system', 'menu_test', - 'menu_link', 'menu_link_content', 'field', ); diff --git a/core/modules/system/src/Tests/Menu/MenuRouterRebuildTest.php b/core/modules/system/src/Tests/Menu/MenuRouterRebuildTest.php deleted file mode 100644 index e56796f..0000000 --- a/core/modules/system/src/Tests/Menu/MenuRouterRebuildTest.php +++ /dev/null @@ -1,51 +0,0 @@ - 'nl')); - language_save($language); - } - - /** - * Tests configuration context when rebuilding the menu router table. - */ - public function testMenuRouterRebuildContext() { - // Enter a language context before rebuilding the menu router tables. - \Drupal::languageManager()->setConfigOverrideLanguage(language_load('nl')); - \Drupal::service('router.builder')->rebuild(); - - // Check that the language context was not used for building the menu item. - $menu_items = \Drupal::entityManager()->getStorage('menu_link')->loadByProperties(array('route_name' => 'menu_test.context')); - $menu_item = reset($menu_items); - $this->assertTrue($menu_item['link_title'] == 'English', 'Config context overrides are ignored when rebuilding menu router items.'); - } - -} diff --git a/core/modules/system/src/Tests/Menu/MenuRouterTest.php b/core/modules/system/src/Tests/Menu/MenuRouterTest.php index 26a0beb..270badc 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -52,11 +52,7 @@ public function testMenuIntegration() { $this->doTestMenuOptionalPlaceholders(); $this->doTestMenuOnRoute(); $this->doTestMenuName(); - $this->doTestMenuLinkDefaultsAlter(); - $this->doTestMenuItemTitlesCases(); - $this->doTestMenuLinkMaintain(); - $this->doTestMenuLinkOptions(); - $this->doTestMenuItemHooks(); + $this->doTestMenuLinksDiscoveredAlter(); $this->doTestHookMenuIntegration(); $this->doTestExoticPath(); } @@ -109,182 +105,60 @@ protected function doTestDescriptionMenuItems() { } /** - * Tests for menu_link_maintain(). - */ - protected function doTestMenuLinkMaintain() { - $admin_user = $this->drupalCreateUser(array('administer site configuration')); - $this->drupalLogin($admin_user); - - // Create three menu items. - menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/1', 'Menu link #1'); - menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/1', 'Menu link #1-main'); - menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/2', 'Menu link #2'); - - // Move second link to the main-menu, to test caching later on. - $menu_links_to_update = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Menu link #1-main', 'customized' => 0, 'module' => 'menu_test')); - foreach ($menu_links_to_update as $menu_link) { - $menu_link->menu_name = 'main'; - $menu_link->save(); - } - - // Load front page. - $this->drupalGet(''); - $this->assertLink('Menu link #1'); - $this->assertLink('Menu link #1-main'); - $this->assertLink('Menu link #2'); - - // Rename all links for the given path. - menu_link_maintain('menu_test', 'update', 'menu_test_maintain/1', 'Menu link updated'); - // Load a different page to be sure that we have up to date information. - $this->drupalGet('menu_test_maintain/1'); - $this->assertLink('Menu link updated'); - $this->assertNoLink('Menu link #1'); - $this->assertNoLink('Menu link #1-main'); - $this->assertLink('Menu link #2'); - - // Delete all links for the given path. - menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/1', ''); - // Load a different page to be sure that we have up to date information. - $this->drupalGet('menu_test_maintain/2'); - $this->assertNoLink('Menu link updated'); - $this->assertNoLink('Menu link #1'); - $this->assertNoLink('Menu link #1-main'); - $this->assertLink('Menu link #2'); - } - - /** * Tests for menu_name parameter for default menu links. */ protected function doTestMenuName() { $admin_user = $this->drupalCreateUser(array('administer site configuration')); $this->drupalLogin($admin_user); - - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu_name_test')); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.menu_name_test'); $menu_link = reset($menu_links); - $this->assertEqual($menu_link->menu_name, 'original', 'Menu name is "original".'); + $this->assertEqual($menu_link->getMenuName(), 'original', 'Menu name is "original".'); // Change the menu_name parameter in menu_test.module, then force a menu // rebuild. menu_test_menu_name('changed'); - \Drupal::service('router.builder')->rebuild(); + $menu_link_manager->rebuild(); - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu_name_test')); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.menu_name_test'); $menu_link = reset($menu_links); - $this->assertEqual($menu_link->menu_name, 'changed', 'Menu name was successfully changed after rebuild.'); + $this->assertEqual($menu_link->getMenuName(), 'changed', 'Menu name was successfully changed after rebuild.'); } /** - * Tests menu links added in hook_menu_link_defaults_alter(). + * Tests menu links added in hook_menu_links_discovered_alter(). */ - protected function doTestMenuLinkDefaultsAlter() { + protected function doTestMenuLinksDiscoveredAlter() { // Check that machine name does not need to be defined since it is already // set as the key of each menu link. - $menu_links = entity_load_multiple_by_properties('menu_link', array('route_name' => 'menu_test.custom')); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.custom'); $menu_link = reset($menu_links); - $this->assertEqual($menu_link->machine_name, 'menu_test.custom', 'Menu links added at hook_menu_link_defaults_alter() obtain the machine name from the $links key.'); + $this->assertEqual($menu_link->getPluginId(), 'menu_test.custom', 'Menu links added at hook_menu_links_discovered_alter() obtain the machine name from the $links key.'); // Make sure that rebuilding the menu tree does not produce duplicates of - // links added by hook_menu_link_defaults_alter(). + // links added by hook_menu_links_discovered_alter(). \Drupal::service('router.builder')->rebuild(); $this->drupalGet('menu-test'); - $this->assertUniqueText('Custom link', 'Menu links added by hook_menu_link_defaults_alter() do not duplicate after a menu rebuild.'); + $this->assertUniqueText('Custom link', 'Menu links added by hook_menu_links_discovered_alter() do not duplicate after a menu rebuild.'); } /** * Tests for menu hierarchy. */ protected function doTestMenuHierarchy() { - $parent_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent')); - $parent_link = reset($parent_links); - $child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child')); - $child_link = reset($child_links); - $unattached_child_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child2/child')); - $unattached_child_link = reset($unattached_child_links); - - $this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.'); - $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.'); - } - - /** - * Test menu maintenance hooks. - */ - protected function doTestMenuItemHooks() { - // Create an item. - menu_link_maintain('menu_test', 'insert', 'menu_test_maintain/4', 'Menu link #4'); - $this->assertEqual(menu_test_static_variable(), 'insert', 'hook_menu_link_insert() fired correctly'); - // Update the item. - menu_link_maintain('menu_test', 'update', 'menu_test_maintain/4', 'Menu link updated'); - $this->assertEqual(menu_test_static_variable(), 'update', 'hook_menu_link_update() fired correctly'); - // Delete the item. - menu_link_maintain('menu_test', 'delete', 'menu_test_maintain/4', ''); - $this->assertEqual(menu_test_static_variable(), 'delete', 'hook_menu_link_delete() fired correctly'); - } - - /** - * Test menu link 'options' storage and rendering. - */ - protected function doTestMenuLinkOptions() { - // Create a menu link with options. - $menu_link = entity_create('menu_link', array( - 'link_title' => 'Menu link options test', - 'link_path' => 'test-page', - 'module' => 'menu_test', - 'options' => array( - 'attributes' => array( - 'title' => 'Test title attribute', - ), - 'query' => array( - 'testparam' => 'testvalue', - ), - ), - )); - menu_link_save($menu_link); - - // Load front page. - $this->drupalGet('test-page'); - $this->assertRaw('title="Test title attribute"', 'Title attribute of a menu link renders.'); - $this->assertRaw('testparam=testvalue', 'Query parameter added to menu link.'); - } - - /** - * Tests the possible ways to set the title for menu items. - * Also tests that menu item titles work with string overrides. - */ - protected function doTestMenuItemTitlesCases() { - - // Build array with string overrides. - $test_data = array( - 1 => array('Example title - Case 1' => 'Alternative example title - Case 1'), - 2 => array('Example title' => 'Alternative example title'), - 3 => array('Example title' => 'Alternative example title'), - ); - - foreach ($test_data as $case_no => $override) { - $this->menuItemTitlesCasesHelper($case_no); - $this->addCustomTranslations('en', array('' => $override)); - $this->writeCustomTranslations(); - - $this->menuItemTitlesCasesHelper($case_no, TRUE); - $this->addCustomTranslations('en', array()); - $this->writeCustomTranslations(); - } - } - - /** - * Get a URL and assert the title given a case number. If override is true, - * the title is asserted to begin with "Alternative". - */ - protected function menuItemTitlesCasesHelper($case_no, $override = FALSE) { - $this->drupalGet('menu-title-test/case' . $case_no); - $this->assertResponse(200); - $asserted_title = $override ? 'Alternative example title - Case ' . $case_no : 'Example title - Case ' . $case_no; - $this->assertTitle($asserted_title . ' | Drupal', format_string('Menu title is: %title.', array('%title' => $asserted_title)), 'Menu'); - } - - /** - * Load the router for a given path. - */ - protected function menuLoadRouter($router_path) { - return db_query('SELECT * FROM {menu_router} WHERE path = :path', array(':path' => $router_path))->fetchAssoc(); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent'); + $parent_link = reset($menu_links); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent.child'); + $child_link = reset($menu_links); + $menu_links = $menu_link_manager->loadLinksByRoute('menu_test.hierarchy_parent.child2.child'); + $unattached_child_link = reset($menu_links); + + $this->assertEqual($child_link->getParent(), $parent_link->getPluginId(), 'The parent of a directly attached child is correct.'); + $this->assertEqual($unattached_child_link->getParent(), $parent_link->getPluginId(), 'The parent of a non-directly attached child is correct.'); } /** diff --git a/core/modules/system/src/Tests/System/AdminTest.php b/core/modules/system/src/Tests/System/AdminTest.php index 950194f..f5609c7 100644 --- a/core/modules/system/src/Tests/System/AdminTest.php +++ b/core/modules/system/src/Tests/System/AdminTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\System; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\simpletest\WebTestBase; /** @@ -62,9 +63,10 @@ function testAdminPages() { // Verify that all visible, top-level administration links are listed on // the main administration page. foreach ($this->getTopLevelMenuLinks() as $item) { - $this->assertLink($item['title']); - $this->assertLinkByHref($item['link_path']); - $this->assertText($item['localized_options']['attributes']['title']); + $this->assertLink($item->getTitle()); + $this->assertLinkByHref($item->getUrlObject()->toString()); + // The description should appear below the link. + $this->assertText($item->getDescription()); } // For each administrative listing page on which the Locale module appears, @@ -119,26 +121,28 @@ function testAdminPages() { /** * Returns all top level menu links. * - * @return \Drupal\menu_link\MenuLinkInterface[] + * @return \Drupal\Core\Menu\MenuLinkInterface[] */ protected function getTopLevelMenuLinks() { - $route_provider = \Drupal::service('router.route_provider'); - $routes = array(); - foreach ($route_provider->getAllRoutes() as $key => $value) { - $path = $value->getPath(); - if (strpos($path, '/admin/') === 0 && count(explode('/', $path)) == 3) { - $routes[$key] = $key; - } - } - $menu_link_ids = \Drupal::entityQuery('menu_link') - ->condition('route_name', $routes) - ->execute(); + $menu_tree = \Drupal::menuTree(); + + // The system.admin link is normally the parent of all top-level admin links. + $parameters = new MenuTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->setTopLevelOnly()->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:flatten'), + ); + $tree = $menu_tree->transform($tree, $manipulators); - $menu_items = \Drupal::entityManager()->getStorage('menu_link')->loadMultiple($menu_link_ids); - foreach ($menu_items as &$menu_item) { - _menu_link_translate($menu_item); + // Transform the tree to a list of menu links. + $menu_links = array(); + foreach ($tree as $element) { + $menu_links[] = $element->link; } - return $menu_items; + + return $menu_links; } /** diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index 71b0cec..1fa53e8 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -59,7 +59,7 @@ function template_preprocess_admin_block_content(&$variables) { $variables['attributes']['class'][] = 'compact'; } foreach ($variables['content'] as $key => $item) { - $variables['content'][$key]['link'] = l($item['title'], $item['link_path'], $item['localized_options']); + $variables['content'][$key]['link'] = \Drupal::linkGenerator()->generateFromUrl($item['title'], $item['url']); if (!$compact && isset($item['description'])) { $variables['content'][$key]['description'] = Xss::filterAdmin($item['description']); } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 111ddfe..1e6e364 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -397,13 +397,13 @@ function hook_page_build(&$page) { } /** - * Alter links for menus. + * Alters all the menu links discovered by the menu link plugin manager. * * @param array $links * The link definitions to be altered. * * @return array - * An array of default menu links. Each link has a key that is the machine + * An array of discovered menu links. Each link has a key that is the machine * name, which must be unique. By default, use the route name as the * machine name. In cases where multiple links use the same route name, such * as two links to the same page in different menus, or two links using the @@ -440,7 +440,7 @@ function hook_page_build(&$page) { * * @ingroup menu */ -function hook_menu_link_defaults_alter(&$links) { +function hook_menu_links_discovered_alter(&$links) { // Change the weight and title of the user.logout link. $links['user.logout']['weight'] = -10; $links['user.logout']['title'] = 'Logout'; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 117dc49..2da9ad1 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -11,6 +11,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\block\BlockPluginInterface; use Drupal\user\UserInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -1455,76 +1456,48 @@ function system_admin_compact_mode() { * An array of task links. */ function system_get_module_admin_tasks($module, array $info) { - $links = &drupal_static(__FUNCTION__); - - if (!isset($links)) { - $links = array(); - $menu_links = entity_get_controller('menu_link')->loadModuleAdminTasks(); - foreach ($menu_links as $link) { - _menu_link_translate($link); - if ($link['access']) { - $links[$link['machine_name']] = $link; - } - } + $tree = &drupal_static(__FUNCTION__); + + $menu_tree = \Drupal::menuTree(); + + if (!isset($tree)) { + $parameters = new MenuTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->excludeHiddenLinks(); + $tree = $menu_tree->load('system.admin', $parameters); + $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 $item) { - if ($item['module'] != $module) { + foreach ($tree as $element) { + $link = $element->link; + if ($link->getProvider() != $module) { continue; } - $machine_name = $item['machine_name']; - if (isset($links[$machine_name])) { - $task = $links[$machine_name]; - // The link description, either derived from 'description' in the default - // menu link or customized via Menu UI module is used as title attribute. - if (!empty($task['localized_options']['attributes']['title'])) { - $task['description'] = $task['localized_options']['attributes']['title']; - unset($task['localized_options']['attributes']['title']); - } - - // Check the admin tasks for duplicate names. If one is found, - // append the parent menu item's title to differentiate. - $duplicate_path = array_search($task['title'], $titles); - if ($duplicate_path !== FALSE) { - if ($parent = menu_link_load($task['plid'])) { - // Append the parent item's title to this task's title. - $task['title'] = t('@original_title (@parent_title)', array('@original_title' => $task['title'], '@parent_title' => $parent['title'])); - } - if ($parent = menu_link_load($admin_tasks[$duplicate_path]['plid'])) { - // Append the parent item's title to the duplicated task's title. - // We use $links[$duplicate_path] in case there are triplicates. - $admin_tasks[$duplicate_path]['title'] = t('@original_title (@parent_title)', array('@original_title' => $links[$duplicate_path]['title'], '@parent_title' => $parent['title'])); - } - } - else { - $titles[$machine_name] = $task['title']; - } - - $admin_tasks[$machine_name] = $task; - } + $admin_tasks[] = array( + 'title' => $link->getTitle(), + 'description' => $link->getDescription(), + 'url' => $link->getUrlObject(), + ); } // Append link for permissions. if (\Drupal::moduleHandler()->implementsHook($module, 'permission')) { /** @var \Drupal\Core\Access\AccessManager $access_manager */ $access_manager = \Drupal::service('access_manager'); - /** @var \Drupal\menu_link\MenuLinkStorageInterface $menu_link_storage */ - $menu_link_storage = \Drupal::entityManager() - ->getStorage('menu_link'); if ($access_manager->checkNamedRoute('user.admin_permissions', array(), \Drupal::currentUser())) { - $path = \Drupal::urlGenerator() - ->getPathFromRoute('user.admin_permissions'); - $options = array(); - $options['fragment'] = 'module-' . $module; - $menu_link = $menu_link_storage->create(array( - 'route_name' => 'user.admin_permissions', - 'link_path' => $path, + /** @var \Drupal\Core\Url $url */ + $url = new \Drupal\Core\Url('user.admin_permissions'); + $url->setOption('fragment', 'module-' . $module); + $admin_tasks["user.admin_permissions.$module"] = array( 'title' => t('Configure @module permissions', array('@module' => $info['name'])), - 'localized_options' => $options - )); - $admin_tasks["user.admin.people.permissions.$module"] = $menu_link; + 'description' => '', + 'url' => $url, + ); } } diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 35e17f7..c22d93b 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -382,7 +382,7 @@ system.admin_config: path: '/admin/config' defaults: _content: '\Drupal\system\Controller\SystemController::overview' - path: 'admin/config' + link_id: 'system.admin_config' _title: 'Configuration' requirements: _permission: 'access administration pages' @@ -414,7 +414,7 @@ system.admin_content: path: '/admin/content' defaults: _content: '\Drupal\system\Controller\SystemController::overview' - path: 'admin/content' + link_id: 'system.admin_content' _title: 'Content' requirements: _permission: 'access administration pages' diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 2b7046c..9e09d19 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -5,7 +5,7 @@ services: - { name: access_check, applies_to: _access_system_cron } system.manager: class: Drupal\system\SystemManager - arguments: ['@module_handler', '@database', '@entity.manager', '@request_stack'] + arguments: ['@module_handler', '@database', '@entity.manager', '@request_stack', '@menu.link_tree', '@menu.active_trail'] system.breadcrumb.default: class: Drupal\system\PathBasedBreadcrumbBuilder arguments: ['@router.request_context', '@access_manager', '@router', '@path_processor_manager', '@config.factory', '@title_resolver', '@current_user'] diff --git a/core/modules/system/templates/menu-tree.html.twig b/core/modules/system/templates/menu-tree.html.twig new file mode 100644 index 0000000..03acca6 --- /dev/null +++ b/core/modules/system/templates/menu-tree.html.twig @@ -0,0 +1,40 @@ +{# +/** + * @file + * Default theme implementation for a menu tree. + * + * Available variables: + * - attributes: Attributes for the UL containing the tree of links. + * - tree: Menu tree to be output. + * - heading: (optional) A heading to precede the links. + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - attributes: (optional) A keyed list of attributes for the heading. + * If the heading is a string, it will be used as the text of the heading and + * the level will default to 'h2'. + * + * Headings should be used on navigation menus and any list of links that + * consistently appears on multiple pages. To make the heading invisible use + * the 'visually-hidden' CSS class. Do not use 'display:none', which + * removes it from screen-readers and assistive technology. Headings allow + * screen-reader and keyboard only users to navigate to or skip the links. + * See http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + * + * @see template_preprocess_menu_tree() + * + * @ingroup themeable + */ +#} +{% if tree -%} + {%- if heading -%} + {%- if heading.level -%} + <{{ heading.level }}{{ heading.attributes }}>{{ heading.text }} + {%- else -%} + {{ heading.text }} + {%- endif -%} + {%- endif -%} + + {{ tree }} + +{%- endif %} diff --git a/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml b/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml index d9e7a39..914fbd5 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml +++ b/core/modules/system/tests/modules/menu_test/menu_test.links.menu.yml @@ -69,3 +69,13 @@ menu_test.menu-title-test.case3: menu_test.context: title: '' route_name: menu_test.context + +menu_test.parent: + title: 'Test menu_name parent' + route_name: menu_test.menu_name_test + menu_name: test + +menu_test.child: + title: 'Test menu_name child' + route_name: menu_test.menu_name_test + parent: menu_test.parent 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 c222ce8..b4f76a2 100644 --- a/core/modules/system/tests/modules/menu_test/menu_test.module +++ b/core/modules/system/tests/modules/menu_test/menu_test.module @@ -5,12 +5,10 @@ * Module that implements various hooks for menu tests. */ -use Drupal\menu_link\Entity\MenuLink; - /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_discovered_alter(). */ -function menu_test_menu_link_defaults_alter(&$links) { +function menu_test_menu_links_discovered_alter(&$links) { // Many of the machine names here are slightly different from the route name. // Since the machine name is arbitrary, this helps ensure that core does not // add mistaken assumptions about the correlation. @@ -97,29 +95,6 @@ function menu_test_callback() { } /** - * Page callback: Tests menu_test_menu_tree_set_path(). - * - * Retrieves the current menu path and if the menu path is not empty updates - * the menu path that is used to determine the active menu trail. - * - * @return string - * A string that can be used for comparison. - * - * @see menu_test_menu(). - * - * @deprecated Use \Drupal\menu_test\Controller\MenuTestController::menuTrail() - */ -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->setPath($menu_path['menu_name'], $menu_path['path']); - } - return 'This is menu_test_menu_trail_callback().'; -} - -/** * Page callback: Tests the theme negotiation functionality. * * @param bool $inherited @@ -167,44 +142,6 @@ function menu_test_menu_name($new_name = '') { } /** - * Implements hook_ENTITY_TYPE_insert() for menu_link entities. - */ -function menu_test_menu_link_insert(MenuLink $item) { - menu_test_static_variable('insert'); -} - -/** - * Implements hook_ENTITY_TYPE_update() for menu_link entities. - */ -function menu_test_menu_link_update(MenuLink $item) { - menu_test_static_variable('update'); -} - -/** - * Implements hook_ENTITY_TYPE_delete() for menu_link entities. - */ -function menu_test_menu_link_delete(MenuLink $item) { - menu_test_static_variable('delete'); -} - -/** - * Sets a static variable for testing hook results. - * - * @param null|string $value - * (optional) The value to set or NULL to return the current value. - * - * @return null|string - * A text string for comparison to test assertions. - */ -function menu_test_static_variable($value = NULL) { - static $variable; - if (!empty($value)) { - $variable = $value; - } - return $variable; -} - -/** * Title callback: Concatenates the title and case number. * * @param string $title diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.links.menu.yml b/core/modules/system/tests/modules/test_page_test/test_page_test.links.menu.yml new file mode 100644 index 0000000..291fd70 --- /dev/null +++ b/core/modules/system/tests/modules/test_page_test/test_page_test.links.menu.yml @@ -0,0 +1,4 @@ +test_page_test.test_page: + route_name: test_page_test.test_page + title: 'Test front page link' + weight: 0 diff --git a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php index a227a7f..c01ac71 100644 --- a/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php +++ b/core/modules/toolbar/src/Tests/ToolbarAdminMenuTest.php @@ -125,30 +125,16 @@ function testModuleStatusChangeSubtreesHashCacheClear() { } /** - * Tests toolbar_menu_link_update() hook implementation. + * Tests toolbar cache tags implementation. */ function testMenuLinkUpdateSubtreesHashCacheClear() { - // Get subtree items for the admin menu. - $query = \Drupal::entityQuery('menu_link'); - for ($i = 1; $i <= 3; $i++) { - $query->sort('p' . $i, 'ASC'); - } - $query->condition('menu_name', 'admin'); - $query->condition('depth', '2', '>='); - - // Build an ordered array of links using the query result object. - $links = array(); - if ($result = $query->execute()) { - $links = menu_link_load_multiple($result); - } - // Get the first link in the set. - $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/item/" . $link['mlid'] . "/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.info.yml b/core/modules/toolbar/toolbar.info.yml index 269f7ec..6967bf6 100644 --- a/core/modules/toolbar/toolbar.info.yml +++ b/core/modules/toolbar/toolbar.info.yml @@ -6,4 +6,3 @@ package: Core version: VERSION dependencies: - breakpoint - - menu_link diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index d8e5363..9963b37 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -6,13 +6,13 @@ */ use Drupal\Core\Cache\Cache; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Template\Attribute; use Drupal\Component\Datetime\DateTimePlus; use Drupal\Component\Utility\Crypt; use Drupal\Component\Utility\String; -use Drupal\menu_link\MenuLinkInterface; use Drupal\user\RoleInterface; use Drupal\user\UserInterface; @@ -326,8 +326,6 @@ function toolbar_pre_render_item($element) { * Implements hook_toolbar(). */ function toolbar_toolbar() { - $items = array(); - // The 'Home' tab is a simple link, with no corresponding tray. $items['home'] = array( '#type' => 'toolbar_item', @@ -352,33 +350,13 @@ function toolbar_toolbar() { '#weight' => -20, ); - // Retrieve the administration menu from the database. - $tree = toolbar_get_menu_tree(); - - // 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( - '#type' => 'container', - '#attributes' => array( - 'class' => array('toolbar-menu-administration'), - ), - 'administration_menu' => $menu_tree->renderTree($tree), - ), - ); - // To conserve bandwidth, we only include the top-level links in the HTML. // The subtrees are fetched through a JSONP script that is generated at the // toolbar_subtrees route. We provide the JavaScript requesting that JSONP // script here with the hash parameter that is needed for that route. // @see toolbar_subtrees_jsonp() $langcode = \Drupal::languageManager()->getCurrentLanguage()->id; - $menu['toolbar_administration']['#attached']['js'][] = array( + $subtrees_attached['js'][] = array( 'type' => 'setting', 'data' => array('toolbar' => array( 'subtreesHash' => _toolbar_get_subtrees_hash($langcode), @@ -405,7 +383,19 @@ function toolbar_toolbar() { 'data-drupal-subtrees' => '', ), ), - 'tray' => $menu, + 'tray' => array( + '#heading' => t('Administration menu'), + '#attached' => $subtrees_attached, + 'toolbar_administration' => array( + '#pre_render' => array( + 'toolbar_prerender_toolbar_administration_tray', + ), + '#type' => 'container', + '#attributes' => array( + 'class' => array('toolbar-menu-administration'), + ), + ), + ), '#weight' => -15, ); @@ -413,90 +403,102 @@ function toolbar_toolbar() { } /** - * Gets only the top level items below the 'admin' path. + * Renders the toolbar's administration tray. * - * @return - * An array containing a menu tree of top level items below the 'admin' path. + * @param array $element + * A renderable array. + * + * @return array + * The updated renderable array. + * + * @see drupal_render() */ -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') - ->condition('link_path', 'admin'); - $result = $query->execute(); - if (!empty($result)) { - $admin_link = menu_link_load(reset($result)); - $tree = $menu_tree->buildTree('admin', array( - 'expanded' => array($admin_link['mlid']), - 'min_depth' => $admin_link['depth'] + 1, - 'max_depth' => $admin_link['depth'] + 1, - )); - } - - return $tree; +function toolbar_prerender_toolbar_administration_tray(array $element) { + $menu_tree = \Drupal::menuTree(); + // Render the top-level administration menu links. + $parameters = new MenuTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->setTopLevelOnly()->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $parameters); + $manipulators = array( + array('callable' => 'menu.default_tree_manipulators:checkAccess'), + array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'), + array('callable' => 'toolbar_menu_navigation_links'), + ); + $tree = $menu_tree->transform($tree, $manipulators); + $element['administration_menu'] = $menu_tree->build($tree); + return $element; } /** - * Generates an array of links from a menu tree array. + * Adds toolbar-specific attributes to the menu link tree. * - * Based on menu_navigation_links(). Adds path based IDs and icon placeholders - * to the links. + * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree + * The menu link tree to manipulate. * - * @return - * An array of links as defined above. + * @return \Drupal\Core\Menu\MenuLinkTreeElement[] + * The manipulated menu link tree. */ -function toolbar_menu_navigation_links(&$tree) { - foreach ($tree as $key => $item) { - // Configure sub-items. - if (!empty($item['below'])) { - toolbar_menu_navigation_links($tree[$key]['below']); +function toolbar_menu_navigation_links(array $tree) { + foreach ($tree as $element) { + if ($element->subtree) { + toolbar_menu_navigation_links($element->subtree); } + // Make sure we have a path specific ID in place, so we can attach icons - // and behaviors to the items. - $tree[$key]['link']['localized_options']['attributes'] = array( - 'id' => 'toolbar-link-' . str_replace(array('/', '<', '>'), array('-', '', ''), $item['link']['link_path']), - 'class' => array( - 'toolbar-icon', - 'toolbar-icon-' . strtolower(str_replace(' ', '-', $item['link']['link_title'])), - ), - 'title' => String::checkPlain($item['link']['description']), - ); + // and behaviors to the menu links. + $link = $element->link; + $url = $link->getUrlObject(); + if ($url->isExternal()) { + // 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()); + } + + // Get the non-localized title to make the icon class. + $definition = $link->getPluginDefinition(); + + $element->options['attributes']['id'] = 'toolbar-link-' . $id; + $element->options['attributes']['class'][] = 'toolbar-icon'; + // @todo Change to use the plugin ID as class since titles might change. + // https://www.drupal.org/node/2310365 + $element->options['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(' ', '-', $definition['title'])); + $element->options['attributes']['title'] = String::checkPlain($link->getDescription()); } + return $tree; } /** * Returns the rendered subtree of each top-level toolbar link. */ function toolbar_get_rendered_subtrees() { + $menu_tree = \Drupal::menuTree(); + $parameters = new MenuTreeParameters(); + $parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(3)->excludeHiddenLinks(); + $tree = $menu_tree->load(NULL, $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); $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']; - if (!$item['hidden'] && $item['access']) { - if ($item['has_children']) { - $query = \Drupal::entityQuery('menu_link') - ->condition('has_children', 1); - for ($i=1; $i <= $item['depth']; $i++) { - $query->condition('p' . $i, $item['p' . $i]); - } - $parents = $query->execute(); - $subtree = $menu_tree->buildTree($item['menu_name'], array('expanded' => $parents, 'min_depth' => $item['depth']+1)); - toolbar_menu_navigation_links($subtree); - $subtree = $menu_tree->renderTree($subtree); - $subtree = drupal_render($subtree); - } - else { - $subtree = ''; - } - - $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['link_path']); - $subtrees[$id] = $subtree; + foreach ($tree as $element) { + /** @var \Drupal\Core\Menu\MenuLinkInterface $item */ + $link = $element->link; + if ($element->subtree) { + $subtree = $menu_tree->build($element->subtree); + $output = drupal_render($subtree); } + else { + $output = ''; + } + // Many routes have dots as route name, while some special ones like + // have <> characters in them. + $id = str_replace(array('.', '<', '>'), array('-', '', '' ), $link->getUrlObject()->getRouteName()); + + $subtrees[$id] = $output; } return $subtrees; } @@ -523,7 +525,7 @@ function _toolbar_get_subtrees_hash($langcode) { // caches later, based on the user's ID regardless of language. // Clear the cache when the 'locale' tag is deleted. This ensures a fresh // subtrees rendering when string translations are made. - \Drupal::cache('toolbar')->set($cid, $hash, Cache::PERMANENT, array('user' => array($uid), 'locale' => TRUE,)); + \Drupal::cache('toolbar')->set($cid, $hash, Cache::PERMANENT, array('user' => array($uid), 'locale' => TRUE, 'menu' => 'admin')); } return $hash; } @@ -543,15 +545,6 @@ function toolbar_modules_uninstalled($modules) { } /** - * Implements hook_ENTITY_TYPE_update() for menu_link entities. - */ -function toolbar_menu_link_update(MenuLinkInterface $menu_link) { - if ($menu_link->menu_name === 'admin') { - _toolbar_clear_user_cache(); - } -} - -/** * Implements hook_ENTITY_TYPE_update() for user entities. */ function toolbar_user_update(UserInterface $user) { diff --git a/core/modules/user/src/Plugin/Menu/MyAccountMenuLink.php b/core/modules/user/src/Plugin/Menu/MyAccountMenuLink.php new file mode 100644 index 0000000..91768cd --- /dev/null +++ b/core/modules/user/src/Plugin/Menu/MyAccountMenuLink.php @@ -0,0 +1,37 @@ +pluginDefinition['hidden'] || \Drupal::currentUser()->isAnonymous(); + } + + /** + * {@inheritdoc} + */ + public function isCacheable() { + return FALSE; + } + +} diff --git a/core/modules/user/src/Tests/UserAccountLinksTests.php b/core/modules/user/src/Tests/UserAccountLinksTests.php index d57e39b..e602120 100644 --- a/core/modules/user/src/Tests/UserAccountLinksTests.php +++ b/core/modules/user/src/Tests/UserAccountLinksTests.php @@ -7,6 +7,7 @@ namespace Drupal\user\Tests; +use Drupal\Core\Menu\MenuTreeParameters; use Drupal\simpletest\WebTestBase; /** @@ -43,14 +44,14 @@ function testSecondaryMenu() { // For a logged-in user, expect the secondary menu to have links for "My // account" and "Log out". $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user', ':text' => 'My account', )); $this->assertEqual(count($link), 1, 'My account link is in secondary menu.'); $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user/logout', ':text' => 'Log out', )); @@ -61,13 +62,15 @@ function testSecondaryMenu() { $this->drupalGet(''); // For a logged-out user, expect no secondary links. - /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ - $menu_tree = \Drupal::service('menu_link.tree'); - $tree = $menu_tree->buildTree('account'); + $menu_tree = \Drupal::menuTree(); + $tree = $menu_tree->load('account', new MenuTreeParameters()); + $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']; - $this->assertTrue((bool) $link->hidden, 'The menu link is hidden.'); + $element = reset($tree); + $this->assertTrue($element->link->isHidden(), 'The menu link is hidden.'); } /** @@ -80,7 +83,7 @@ function testDisabledAccountLink() { // Verify that the 'My account' link exists before we check for its // disappearance. $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user', ':text' => 'My account', )); @@ -94,10 +97,7 @@ function testDisabledAccountLink() { $this->assertFieldChecked((string) $label[0], "The 'My account' link is enabled by default."); // Disable the 'My account' link. - $input = $this->xpath('//input[@id=:field_id]/@name', array(':field_id' => (string)$label[0])); - $edit = array( - (string) $input[0] => FALSE, - ); + $edit['links[menu_plugin_id:user.page][enabled]'] = FALSE; $this->drupalPostForm('admin/structure/menu/manage/account', $edit, t('Save')); // Get the homepage. @@ -105,7 +105,7 @@ function testDisabledAccountLink() { // Verify that the 'My account' link does not appear when disabled. $link = $this->xpath('//ul[@class=:menu_class]/li/a[contains(@href, :href) and text()=:text]', array( - ':menu_class' => 'links', + ':menu_class' => 'menu', ':href' => 'user', ':text' => 'My account', )); diff --git a/core/modules/user/user.links.menu.yml b/core/modules/user/user.links.menu.yml index bc421f0..3490a8d 100644 --- a/core/modules/user/user.links.menu.yml +++ b/core/modules/user/user.links.menu.yml @@ -3,6 +3,7 @@ user.page: weight: -10 route_name: user.page menu_name: account + class: Drupal\user\Plugin\Menu\MyAccountMenuLink user.logout: title: 'Log out' route_name: user.logout diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 2253d1c..0b3d453 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -634,19 +634,6 @@ function template_preprocess_username(&$variables) { } /** - * Implements hook_menu_link_presave(). - */ -function user_menu_link_presave(MenuLink $menu_link) { - // The path 'user' must be accessible for anonymous users, but only visible - // for authenticated users. Authenticated users should see "My account", but - // anonymous users should not see it at all. Therefore, invoke - // user_menu_link_load() to conditionally hide the link. - if ($menu_link->machine_name == 'user.page') { - $menu_link->options['alter'] = TRUE; - } -} - -/** * Implements hook_menu_breadcrumb_alter(). */ function user_menu_breadcrumb_alter(&$active_trail, $item) { @@ -658,16 +645,6 @@ function user_menu_breadcrumb_alter(&$active_trail, $item) { } /** - * Implements hook_translated_menu_link_alter(). - */ -function user_translated_menu_link_alter(MenuLink &$menu_link) { - // Hide the "User account" link for anonymous users. - if ($menu_link->machine_name == 'user.page' && \Drupal::currentUser()->isAnonymous()) { - $menu_link->hidden = 1; - } -} - -/** * Finalizes the login process and logs in a user. * * The function logs in the user, records a watchdog message about the new diff --git a/core/modules/views/src/Plugin/Derivative/ViewsMenuLink.php b/core/modules/views/src/Plugin/Derivative/ViewsMenuLink.php new file mode 100644 index 0000000..e83af2b --- /dev/null +++ b/core/modules/views/src/Plugin/Derivative/ViewsMenuLink.php @@ -0,0 +1,53 @@ +derivatives)) { + $this->getDerivativeDefinitions($base_plugin_definition); + } + if (isset($this->derivatives[$derivative_id])) { + return $this->derivatives[$derivative_id]; + } + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $links = array(); + // @todo Replace uses_hook_menu with an annotation. + // https://www.drupal.org/node/2310371 + $views = Views::getApplicableViews('uses_hook_menu'); + foreach ($views as $data) { + /** @var \Drupal\views\ViewExecutable $view */ + list($view, $display_id) = $data; + if ($result = $view->getMenuLinks($display_id)) { + foreach ($result as $link_id => $link) { + $links[$link_id] = $link + $base_plugin_definition; + } + } + } + + return $links; + } + +} diff --git a/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php new file mode 100644 index 0000000..d97b204 --- /dev/null +++ b/core/modules/views/src/Plugin/Menu/Form/ViewsMenuLinkForm.php @@ -0,0 +1,83 @@ + 'textfield', + '#title' => $this->t('Title'), + // @todo Ensure that the view is not loaded with a localized title. + // https://www.drupal.org/node/2309507 + '#default_value' => $this->menuLink->getTitle(), + '#weight' => -10, + ); + + $form['description'] = array( + '#type' => 'textfield', + '#title' => $this->t('Description'), + '#description' => $this->t('Shown when hovering over the menu link.'), + // @todo Ensure that the view is not loaded with a localized description. + // https://www.drupal.org/node/2309507 + '#default_value' => $this->menuLink->getDescription(), + '#weight' => -5, + ); + + $form += parent::buildConfigurationForm($form, $form_state); + + $form['info']['#weight'] = -8; + $form['path']['#weight'] = -7; + + $view = $this->menuLink->loadView(); + $id = $view->storage->id(); + $label = $view->storage->label(); + if ($this->moduleHandler->moduleExists('views_ui')) { + $message = $this->t('This link is provided by the Views module. The path can be changed by editing the view @label', array('@url' => \Drupal::url('views_ui.edit', array('view' => $id)), '@label' => $label)); + } + else { + $message = $this->t('This link is provided by the Views module from view %label.', array('%label' => $label)); + } + $form['info']['#title'] = $message; + return $form; + } + + /** + * {@inheritdoc} + */ + public function extractFormValues(array &$form, array &$form_state) { + $definition = parent::extractFormValues($form, $form_state); + $definition['title'] = $form_state['values']['title']; + $definition['description'] = $form_state['values']['description']; + + return $definition; + } + +} diff --git a/core/modules/views/src/Plugin/Menu/ViewsMenuLink.php b/core/modules/views/src/Plugin/Menu/ViewsMenuLink.php new file mode 100644 index 0000000..4e164d7 --- /dev/null +++ b/core/modules/views/src/Plugin/Menu/ViewsMenuLink.php @@ -0,0 +1,157 @@ + 1, + 'parent' => 1, + 'weight' => 1, + 'expanded' => 1, + 'hidden' => 1, + 'title' => 1, + 'description' => 1, + 'metadata' => 1, + ); + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The view executable factory. + * + * @var \Drupal\views\ViewExecutableFactory + */ + protected $viewExecutableFactory; + + /** + * The view executable of the menu link. + * + * @var \Drupal\views\ViewExecutable + */ + protected $view; + + /** + * Constructs a new ViewsMenuLink. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager + * @param \Drupal\views\ViewExecutableFactory $view_executable_factory + * The view executable factory + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, ViewExecutableFactory $view_executable_factory) { + $this->configuration = $configuration; + $this->pluginId = $plugin_id; + $this->pluginDefinition = $plugin_definition; + + $this->entityManager = $entity_manager; + $this->viewExecutableFactory = $view_executable_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('views.executable') + ); + } + + /** + * Initializes the proper view. + * + * @return \Drupal\views\ViewExecutable + * The view executable. + */ + public function loadView() { + if (empty($this->view)) { + $metadata = $this->getMetaData(); + $view_id = $metadata['view_id']; + $display_id = $metadata['display_id']; + $view_entity = $this->entityManager->getStorage('view')->load($view_id); + $view = $this->viewExecutableFactory->get($view_entity); + $view->setDisplay($display_id); + $view->initDisplay(); + $this->view = $view; + } + return $this->view; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + // @todo Get the translated value from the config without instantiating the + // view. https://www.drupal.org/node/2310379 + return $this->loadView()->display_handler->getOption('menu')['title']; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->loadView()->display_handler->getOption('menu')['description']; + } + + /** + * {@inheritdoc} + */ + public function updateLink(array $new_definition_values, $persist) { + $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed); + if ($persist) { + $view = $this->loadView(); + $display = &$view->storage->getDisplay($view->current_display); + // Just save the title to the original view. + $changed = FALSE; + foreach (array('title' => 'title', 'weight' => 'weight', 'menu' => 'name', 'description' => 'description') as $definition_key => $views_key) { + if ($display['display_options']['menu'][$views_key] != $new_definition_values[$definition_key]) { + $display['display_options']['menu'][$views_key] = $new_definition_values[$definition_key]; + $changed = TRUE; + } + } + if ($changed) { + // @todo Improve this to not trigger a full rebuild of everything, if we + // just changed some properties. https://www.drupal.org/node/2310389 + $view->storage->save(); + } + } + // Update the definition. + $this->pluginDefinition = $overrides + $this->pluginDefinition; + return $this->pluginDefinition; + } + +} diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 3d3d6a5..6ce6c07 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -2119,17 +2119,14 @@ public function renderMoreLink() { } /** - * Creates menu links, if this display provides some. - * - * @param array $existing_links - * An array of already existing menu items provided by drupal. + * Gets menu links, if this display provides some. * * @return array * The menu links registers for this display. * - * @see hook_menu_link_defaults() + * @see \Drupal\views\Plugin\Derivative\ViewsMenuLink */ - public function executeHookMenuLinkDefaults(array &$existing_links) { + public function getMenuLinks() { return array(); } diff --git a/core/modules/views/src/Plugin/views/display/PathPluginBase.php b/core/modules/views/src/Plugin/views/display/PathPluginBase.php index 44127ec..a6e1759 100644 --- a/core/modules/views/src/Plugin/views/display/PathPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/PathPluginBase.php @@ -279,7 +279,7 @@ public function alterRoutes(RouteCollection $collection) { /** * {@inheritdoc} */ - public function executeHookMenuLinkDefaults(array &$existing_links) { + public function getMenuLinks() { $links = array(); // Replace % with the link to our standard views argument loader @@ -299,7 +299,10 @@ public function executeHookMenuLinkDefaults(array &$existing_links) { $view_route_names = $this->state->get('views.view_route_names') ?: array(); $path = implode('/', $bits); - $menu_link_id = 'views.' . str_replace('/', '.', $path); + $view_id = $this->view->storage->id(); + $display_id = $this->display['id']; + $view_id_display = "{$view_id}.{$display_id}"; + $menu_link_id = 'views.' . str_replace('/', '.', $view_id_display); if ($path) { $menu = $this->getOption('menu'); @@ -307,12 +310,11 @@ public function executeHookMenuLinkDefaults(array &$existing_links) { $links[$menu_link_id] = array(); // Some views might override existing paths, so we have to set the route // name based upon the altering. - $view_id_display = "{$this->view->storage->id()}.{$this->display['id']}"; $links[$menu_link_id] = array( 'route_name' => isset($view_route_names[$view_id_display]) ? $view_route_names[$view_id_display] : "view.$view_id_display", // Identify URL embedded arguments and correlate them to a handler. 'load arguments' => array($this->view->storage->id(), $this->display['id'], '%index'), - 'machine_name' => $menu_link_id, + 'id' => $menu_link_id, ); $links[$menu_link_id]['title'] = $menu['title']; $links[$menu_link_id]['description'] = $menu['description']; @@ -323,6 +325,11 @@ public function executeHookMenuLinkDefaults(array &$existing_links) { // Insert item into the proper menu. $links[$menu_link_id]['menu_name'] = $menu['name']; + // Keep track of where we came from. + $links[$menu_link_id]['metadata'] = array( + 'view_id' => $view_id, + 'display_id' => $display_id, + ); } } diff --git a/core/modules/views/src/Tests/Plugin/DisplayPageTest.php b/core/modules/views/src/Tests/Plugin/DisplayPageTest.php index 0067ce3..97e5cb9 100644 --- a/core/modules/views/src/Tests/Plugin/DisplayPageTest.php +++ b/core/modules/views/src/Tests/Plugin/DisplayPageTest.php @@ -7,6 +7,7 @@ namespace Drupal\views\Tests\Plugin; +use Drupal\Core\Session\AnonymousUserSession; use Drupal\views\Views; use Drupal\views\Tests\ViewUnitTestBase; use Symfony\Component\HttpFoundation\Request; @@ -32,7 +33,7 @@ class DisplayPageTest extends ViewUnitTestBase { * * @var array */ - public static $modules = array('system', 'user', 'menu_link', 'field', 'entity'); + public static $modules = array('system', 'user', 'field', 'entity'); /** * The router dumper to get all routes. @@ -49,13 +50,13 @@ protected function setUp() { // Setup the needed tables in order to make the drupal router working. $this->installSchema('system', array('url_alias')); - $this->installSchema('menu_link', 'menu_links'); } /** * Checks the behavior of the page for access denied/not found behaviors. */ public function testPageResponses() { + \Drupal::currentUser()->setAccount(new AnonymousUserSession()); $subrequest = Request::create('/test_page_display_403', 'GET'); $response = $this->container->get('http_kernel')->handle($subrequest, HttpKernelInterface::SUB_REQUEST); $this->assertEqual($response->getStatusCode(), 403); diff --git a/core/modules/views/src/Tests/Wizard/MenuTest.php b/core/modules/views/src/Tests/Wizard/MenuTest.php index 3e9b8be..a245f57 100644 --- a/core/modules/views/src/Tests/Wizard/MenuTest.php +++ b/core/modules/views/src/Tests/Wizard/MenuTest.php @@ -7,6 +7,8 @@ namespace Drupal\views\Tests\Wizard; +use Drupal\Component\Utility\String; + /** * Tests the ability of the views wizard to put views in a menu. * @@ -39,15 +41,14 @@ function testMenus() { $this->assertLinkByHref(url($view['page[path]'])); // Make sure the link is associated with the main menu. - $links = menu_load_links('main'); - $found = FALSE; - foreach ($links as $link) { - if ($link['link_path'] == $view['page[path]']) { - $found = TRUE; - break; - } - } - $this->assertTrue($found, t('Found a link to %path in the main menu', array('%path' => $view['page[path]']))); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $menu_link_manager->createInstance('views_view:views.' . $view['id'] . '.page_1'); + $url = $link->getUrlObject(); + $this->assertEqual($url->getRouteName(), 'view.' . $view['id'] . '.page_1', String::format('Found a link to %path in the main menu', array('%path' => $view['page[path]']))); + $metadata = $link->getMetaData(); + $this->assertEqual(array('view_id' => $view['id'], 'display_id' => 'page_1'), $metadata); } } diff --git a/core/modules/views/src/ViewExecutable.php b/core/modules/views/src/ViewExecutable.php index ad8e801..c7e810b 100644 --- a/core/modules/views/src/ViewExecutable.php +++ b/core/modules/views/src/ViewExecutable.php @@ -1509,17 +1509,16 @@ public function attachDisplays() { } /** - * Returns default menu links from the view and the named display handler. + * Returns menu links from the view and the named display handler. * * @param string $display_id * A display ID. - * @param array $links - * An array of default menu link items passed from - * views_menu_link_defaults_alter(). * * @return array|bool + * The generated menu links for this view and display, FALSE if the call + * to ::setDisplay failed. */ - public function executeHookMenuLinkDefaults($display_id = NULL, &$links = array()) { + public function getMenuLinks($display_id = NULL) { // Prepare the view with the information we have. This was probably already // called, but it's good to be safe. if (!$this->setDisplay($display_id)) { @@ -1528,7 +1527,7 @@ public function executeHookMenuLinkDefaults($display_id = NULL, &$links = array( // Execute the hook. if (isset($this->display_handler)) { - return $this->display_handler->executeHookMenuLinkDefaults($links); + return $this->display_handler->getMenuLinks(); } } diff --git a/core/modules/views/views.links.menu.yml b/core/modules/views/views.links.menu.yml new file mode 100644 index 0000000..6188677 --- /dev/null +++ b/core/modules/views/views.links.menu.yml @@ -0,0 +1,4 @@ +views_view: + class: Drupal\views\Plugin\Menu\ViewsMenuLink + form_class: Drupal\views\Plugin\Menu\Form\ViewsMenuLinkForm + deriver: \Drupal\views\Plugin\Derivative\ViewsMenuLink diff --git a/core/modules/views/views.module b/core/modules/views/views.module index d9d9805..7ad98f5 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -305,23 +305,6 @@ function views_permission() { } /** - * Implements hook_menu_link_defaults_alter(). - */ -function views_menu_link_defaults_alter(array &$links) { - // @todo Decide what to do with all the crazy logic in views_menu_alter() in - // https://drupal.org/node/2107533. - $views = Views::getApplicableViews('uses_hook_menu'); - foreach ($views as $data) { - /** @var \Drupal\views\ViewExecutable $view */ - list($view, $display_id) = $data; - $result = $view->executeHookMenuLinkDefaults($display_id, $links); - foreach ($result as $link_id => $link) { - $links[$link_id] = $link; - } - } -} - -/** * Implements hook_page_alter(). */ function views_page_alter(&$page) { diff --git a/core/profiles/standard/standard.info.yml b/core/profiles/standard/standard.info.yml index 7029739..a1c07b2 100644 --- a/core/profiles/standard/standard.info.yml +++ b/core/profiles/standard/standard.info.yml @@ -14,6 +14,7 @@ dependencies: - comment - contextual - contact + - menu_link_content - datetime - block_content - quickedit diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index 30656be..8c129fe 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -43,16 +43,11 @@ function standard_install() { ->fields(array('uid' => 1, 'rid' => 'administrator')) ->execute(); - // Create a Home link in the main menu. - $menu_link = entity_create('menu_link', array( - 'link_title' => t('Home'), - 'link_path' => '', - 'menu_name' => 'main', - )); - $menu_link->save(); - // Enable the Contact link in the footer menu. - menu_link_maintain('contact', 'enable', 'contact'); + /** @var \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager */ + $menu_link_manager = \Drupal::service('plugin.manager.menu.link'); + $menu_link_manager->updateDefinition('contact.site_page', array('hidden' => 0)); + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access site-wide contact form')); user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access site-wide contact form')); diff --git a/core/profiles/standard/standard.links.menu.yml b/core/profiles/standard/standard.links.menu.yml new file mode 100644 index 0000000..2278b21 --- /dev/null +++ b/core/profiles/standard/standard.links.menu.yml @@ -0,0 +1,4 @@ +standard.front_page: + title: 'Home' + route_name: '' + menu_name: main diff --git a/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php b/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php index 7086aa6..9a9e98e 100644 --- a/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php +++ b/core/tests/Drupal/Tests/Core/Menu/MenuLinkMock.php @@ -28,7 +28,6 @@ class MenuLinkMock extends MenuLinkBase { 'options' => array(), 'expanded' => '0', 'hidden' => '0', - 'discovered' => '1', 'provider' => 'simpletest', 'metadata' => array(), 'class' => 'Drupal\\Tests\\Core\Menu\\MenuLinkMock', diff --git a/core/themes/bartik/bartik.theme b/core/themes/bartik/bartik.theme index 836a8f7..27f8973 100644 --- a/core/themes/bartik/bartik.theme +++ b/core/themes/bartik/bartik.theme @@ -55,18 +55,15 @@ function bartik_preprocess_page(&$variables) { // Store back the classes to the htmlpage object. $attributes['class'] = $classes; - // Pass the main menu and secondary menu to the template as render arrays. + // Set additional attributes on the primary and secondary navigation menus. if (!empty($variables['main_menu'])) { $variables['main_menu']['#attributes']['id'] = 'main-menu-links'; - $variables['main_menu']['#attributes']['class'] = array('links', 'clearfix'); + $variables['main_menu']['#attributes']['class'][] = 'links'; } if (!empty($variables['secondary_menu'])) { $variables['secondary_menu']['#attributes']['id'] = 'secondary-menu-links'; - $variables['secondary_menu']['#attributes']['class'] = array( - 'links', - 'inline', - 'clearfix', - ); + $variables['secondary_menu']['#attributes']['class'][] = 'links'; + $variables['secondary_menu']['#attributes']['class'][] = 'inline'; } // Set the options that apply to both page and maintenance page. @@ -138,10 +135,12 @@ function bartik_preprocess_block(&$variables) { } /** - * Implements THEME_menu_tree(). + * Implements hook_preprocess_HOOK() for menu-tree.html.twig. + * + * @see template_preprocess_menu_tree() */ -function bartik_menu_tree($variables) { - return ''; +function bartik_preprocess_menu_tree(&$variables) { + $variables['attributes']['class'][] = 'clearfix'; } /** diff --git a/core/themes/seven/seven.theme b/core/themes/seven/seven.theme index abf6c4f..e656626 100644 --- a/core/themes/seven/seven.theme +++ b/core/themes/seven/seven.theme @@ -148,7 +148,7 @@ function seven_preprocess_block_content_add_list(&$variables) { function seven_preprocess_admin_block_content(&$variables) { if (!empty($variables['content'])) { foreach ($variables['content'] as $key => $item) { - $variables['content'][$key]['url'] = url($item['link_path']); + $variables['content'][$key]['url'] = $item['url']->toString(); } } }