diff --git a/core/core.services.yml b/core/core.services.yml index be51f03..75ffe15 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -54,6 +54,13 @@ services: factory_method: get factory_service: cache_factory arguments: [entity] + cache.menu: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory_method: get + factory_service: cache_factory + arguments: [menu] cache.render: class: Drupal\Core\Cache\CacheBackendInterface tags: @@ -246,6 +253,12 @@ services: plugin.manager.action: class: Drupal\Core\Action\ActionManager arguments: ['@container.namespaces', '@cache.discovery', '@language_manager', '@module_handler'] + # @todo We seriously need to find a better name. + plugin.manager.menu.link_tree: + class: Drupal\Core\Menu\MenuLinkTree + arguments: ['@menu.tree_storage', '@menu_link.static.overrides', '@request_stack', '@router.route_provider', '@module_handler', '@cache.menu', '@language_manager', '@access_manager', '@current_user', '@entity.manager', '@config.factory'] + menu.link_tree: + alias: plugin.manager.menu.link_tree plugin.manager.menu.local_action: class: Drupal\Core\Menu\LocalActionManager arguments: ['@controller_resolver', '@request_stack', '@router.route_provider', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user'] @@ -257,6 +270,18 @@ services: arguments: ['@controller_resolver', '@module_handler', '@cache.discovery', '@language_manager', '@access_manager', '@current_user', '@request_stack'] plugin.cache_clearer: class: Drupal\Core\Plugin\CachedDiscoveryClearer + paramconverter.menu_link: + class: Drupal\Core\ParamConverter\MenuLinkPluginConverter + tags: + - { name: paramconverter } + arguments: ['@plugin.manager.menu.link_tree'] + menu.tree_storage: + class: Drupal\Core\Menu\MenuTreeStorage + arguments: ['@database', '@url_generator', 'menu_tree'] + public: false # This service is private to plugin.manager.menu.link_tree + menu_link.static.overrides: + class: Drupal\Core\Menu\StaticMenuLinkOverrides + arguments: ['@config.factory'] request: class: Symfony\Component\HttpFoundation\Request synthetic: true diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 46de223..3f91a85 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -103,123 +103,6 @@ */ /** - * @defgroup menu_tree_parameters Menu tree parameters - * @{ - * Parameters for a menu tree. - */ - -/** - * The maximum depth of a menu links tree - matches the number of p columns. - * - * @todo Move this constant to MenuLinkStorage along with all the tree - * functionality. - */ -const MENU_MAX_DEPTH = 9; - - -/** - * @} End of "defgroup menu_tree_parameters". - */ - -/** - * Reserved key to identify the most specific menu link for a given path. - * - * 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 - * - * @see menu_link_get_preferred() - */ -const MENU_PREFERRED_LINK = '1cf698d64d1aa4b83907cf6ed55db3a7f8e92c91'; - -/** - * Localizes a menu link title using t() if possible. - * - * 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. - * - * @param $item - * A menu link entity. - */ -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. - */ -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()); - } - // For performance, don't localize a link the user can't access. - if ($item['access']) { - _menu_item_localize($item); - } - } - - // 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); - } -} /** * Implements template_preprocess_HOOK() for theme_menu_tree(). @@ -258,8 +141,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"; } @@ -416,15 +301,15 @@ function menu_navigation_links($menu_name, $level = 0) { } // Get the menu hierarchy for the current page. - /** @var \Drupal\menu_link\MenuTreeInterface $menu_tree */ - $menu_tree = \Drupal::service('menu_link.tree'); + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $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 ($item['in_active_trail']) { // If the item is in the active trail, we continue in the subtree. $tree = empty($item['below']) ? array() : $item['below']; break; @@ -435,18 +320,30 @@ function menu_navigation_links($menu_name, $level = 0) { // 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; + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $item['link']; + + if ($link->isHidden()) { + continue; } + + $class = ''; + $url = $link->getUrlObject(); + $l = $url->getOptions(); + $l['title'] = $link->getTitle(); + if ($url->isExternal()) { + $l['href'] = $url->getPath(); + } + else { + $l['route_name'] = $url->getRouteName(); + $l['route_parameters'] = $url->getRouteParameters(); + } + if ($item['in_active_trail']) { + $class = ' active-trail'; + $l['attributes']['class'][] = 'active-trail'; + } + // Keyed with the unique ID to generate classes in links.html.twig. + $links['menu-' . $link->getPluginId() . $class] = $l; } return $links; } @@ -592,119 +489,6 @@ 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 @@ -712,197 +496,14 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { */ 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. + * Builds menu links for the items discovered as plugins. */ 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'])) { - watchdog('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); + $menu_tree = \Drupal::menuTree(); + $menu_tree->rebuild(); } /** @@ -916,35 +517,14 @@ function _menu_clear_page_cache() { if ($cache_cleared == 0) { Cache::invalidateTags(array('content' => TRUE)); // Keep track of which menus have expanded items. - _menu_set_expanded_menus(); $cache_cleared = 1; } elseif ($cache_cleared == 1) { drupal_register_shutdown_function('Drupal\Core\Cache\Cache::invalidateTags', array('content' => TRUE)); - // Keep track of which menus have expanded items. - drupal_register_shutdown_function('_menu_set_expanded_menus'); $cache_cleared = 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); -} - -/** * @} End of "defgroup menu". */ diff --git a/core/includes/schema.inc b/core/includes/schema.inc index a386e62..a5aafe9 100644 --- a/core/includes/schema.inc +++ b/core/includes/schema.inc @@ -413,12 +413,7 @@ function drupal_write_record($table, &$record, $primary_keys = array()) { } // Build array of fields to update or insert. - if (empty($info['serialize'])) { - $fields[$field] = $object->$field; - } - else { - $fields[$field] = serialize($object->$field); - } + $fields[$field] = $object->$field; // Type cast to proper datatype, except when the value is NULL and the // column allows this. @@ -525,6 +520,9 @@ function drupal_schema_get_field_value(array $info, $value) { elseif (!is_array($value)) { $value = (string) $value; } + elseif (!empty($info['serialize'])) { + $value = serialize($value); + } } return $value; } diff --git a/core/lib/Drupal.php b/core/lib/Drupal.php index 83fe15f..b208d8b 100644 --- a/core/lib/Drupal.php +++ b/core/lib/Drupal.php @@ -636,4 +636,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 6926603..4e4c0d9 100644 --- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php +++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php @@ -263,10 +263,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/Menu/Form/MenuLinkDefaultForm.php b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php new file mode 100644 index 0000000..16f0b43 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/Form/MenuLinkDefaultForm.php @@ -0,0 +1,162 @@ +menuTree = $menu_tree; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('menu.link_tree'), + $container->get('string_translation') + ); + } + + /** + * Injects the menu link plugin. + * + * @param MenuLinkInterface $menu_link + * A menu link plugin instance. + */ + public function setMenuLinkInstance(MenuLinkInterface $menu_link) { + $this->menuLink = $menu_link; + } + + /** + * {@inheritdoc} + */ + public function buildEditForm(array &$form, array &$form_state) { + $form['#title'] = $this->t('Edit menu link %title', array('%title' => $this->menuLink->getTitle())); + + $form['info'] = array( + '#type' => 'item', + '#title' => $this->t('This is a module-provided link. The label and path cannot be changed.'), + ); + $form['path'] = array( + 'link' => $this->menuLink->build(), + '#type' => 'item', + '#title' => $this->t('Link'), + ); + + $form['enabled'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Enable'), + '#description' => $this->t('Menu links that are not enabled will not be listed in any menu.'), + '#default_value' => !$this->menuLink->isHidden(), + ); + $form['expanded'] = array( + '#type' => 'checkbox', + '#title' => t('Show as expanded'), + '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'), + '#default_value' => $this->menuLink->isExpanded(), + ); + $delta = max(abs($this->menuLink->getWeight()), 50); + $form['weight'] = array( + '#type' => 'weight', + '#delta' => $delta, + '#default_value' => $this->menuLink->getWeight(), + '#title' => $this->t('Weight'), + '#description' => $this->t('Link weight among links in the same menu at the same depth.'), + ); + + $options = $this->menuTree->getParentSelectOptions($this->menuLink->getPluginId()); + $menu_parent = $this->menuLink->getMenuName() . ':' . $this->menuLink->getParent(); + + if (!isset($options[$menu_parent])) { + // Put it at the top level in the current menu. + $menu_parent = $this->menuLink->getMenuName() . ':'; + } + $form['menu_parent'] = array( + '#type' => 'select', + '#title' => $this->t('Parent link'), + '#options' => $options, + '#default_value' => $menu_parent, + '#description' => $this->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' => $this->menuTree->maxDepth())), + '#attributes' => array('class' => array('menu-title-select')), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function extractFormValues(array &$form, array &$form_state) { + $new_definition = array(); + $new_definition['hidden'] = $form_state['values']['enabled'] ? 0 : 1; + $new_definition['weight'] = (int) $form_state['values']['weight']; + $new_definition['expanded'] = $form_state['values']['expanded'] ? 1 : 0; + list($menu_name, $parent) = explode(':', $form_state['values']['menu_parent'], 2); + if (!empty($menu_name)) { + $new_definition['menu_name'] = $menu_name; + } + if (isset($parent)) { + $new_definition['parent'] = $parent; + } + return $new_definition; + } + + /** + * {@inheritdoc} + */ + public function validateEditForm(array &$form, array &$form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitEditForm(array &$form, array &$form_state) { + $new_definition = $this->extractFormValues($form, $form_state); + + return $this->menuTree->updateLink($this->menuLink->getPluginId(), $new_definition); + } + +} diff --git a/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php b/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php new file mode 100644 index 0000000..27d3cb0 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/Form/MenuLinkFormInterface.php @@ -0,0 +1,77 @@ +getOptions(); + $description = $this->getDescription(); + if ($title_attribute && $description) { + $options['attributes']['title'] = $description; + } + $build = array( + '#type' => 'link', + '#route_name' => $this->pluginDefinition['route_name'], + '#route_parameters' => $this->pluginDefinition['route_parameters'], + '#title' => $this->getTitle(), + '#options' => $options, + ); + return $build; + } + + /** + * Returns the weight of the menu link. + * + * @return int + * The weight of the menu link, 0 by default. + */ + public function getWeight() { + // By default the weight is 0. + if (!isset($this->pluginDefinition['weight'])) { + $this->pluginDefinition['weight'] = 0; + } + return $this->pluginDefinition['weight']; + } + + /** + * {@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']; + } + + /** + * {@inheritdoc} + */ + public function getProvider() { + return $this->pluginDefinition['provider']; + } + + /** + * {@inheritdoc} + */ + public function getParent() { + return $this->pluginDefinition['parent']; + } + + /** + * {@inheritdoc} + */ + public function isHidden() { + return (bool) $this->pluginDefinition['hidden']; + } + + /** + * {@inheritdoc} + */ + public function isExpanded() { + return (bool) $this->pluginDefinition['expanded']; + } + + /** + * {@inheritdoc} + */ + public function isDiscovered() { + return (bool) $this->pluginDefinition['discovered']; + } + + /** + * {@inheritdoc} + */ + public function isResetable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function isTranslatable() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function isDeletable() { + return (bool) $this->getDeleteRoute(); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + if ($this->pluginDefinition['description']) { + return $this->t($this->pluginDefinition['description']); + } + return ''; + } + + /** + * {@inheritdoc} + */ + public function getOptions() { + return $this->pluginDefinition['options'] ?: array(); + } + + /** + * {@inheritdoc} + */ + public function getMetaData() { + return $this->pluginDefinition['metadata'] ?: array(); + } + + /** + * {@inheritdoc} + */ + public function getUrlObject($title_attribute = TRUE) { + $options = $this->getOptions(); + $description = $this->getDescription(); + if ($title_attribute && $description) { + $options['attributes']['title'] = $description; + } + if (empty($this->pluginDefinition['url'])) { + return new Url($this->pluginDefinition['route_name'], $this->pluginDefinition['route_parameters'], $options); + } + else { + $url = Url::createFromPath($this->pluginDefinition['url']); + $url->setOptions($options); + return $url; + } + } + + /** + * {@inheritdoc} + */ + public function getFormClass() { + return $this->pluginDefinition['form_class']; + } + + /** + * {@inheritdoc} + */ + public function getDeleteRoute() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getEditRoute() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getTranslateRoute() { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function deleteLink() { + throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $this->getPluginId())); + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuLinkDefault.php b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php new file mode 100644 index 0000000..170ea19 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkDefault.php @@ -0,0 +1,96 @@ + 1, + 'parent' => 1, + 'weight' => 1, + 'expanded' => 1, + 'hidden' => 1, + ); + + /** + * The static menu link service used to store updates to weight/parent etc. + * + * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface + */ + protected $staticOverride; + + /** + * Constructs a new MenuLinkDefault. + * + * @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\Menu\StaticMenuLinkOverridesInterface $static_override + * The static override storage. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, StaticMenuLinkOverridesInterface $static_override) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->staticOverride = $static_override; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('menu_link.static.overrides') + ); + } + + /** + * {@inheritdoc} + */ + public function isResetable() { + // The link can be reset if it was discovered and has an override. + return $this->pluginDefinition['discovered'] && $this->staticOverride->loadOverride($this->getPluginId()); + } + + /** + * {@inheritdoc} + */ + public function updateLink(array $new_definition_values, $persist) { + $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed); + if ($persist) { + $this->staticOverride->saveOverride($this->getPluginId(), $overrides); + } + // Update the definition. + $this->pluginDefinition = $overrides + $this->getPluginDefinition(); + return $this->pluginDefinition; + } + + /** + * {@inheritdoc} + */ + public function persistLinkDeletion() { + // @todo - what should this do by default? + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuLinkInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php new file mode 100644 index 0000000..2e42d52 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkInterface.php @@ -0,0 +1,205 @@ + 'tools', + // (required) The name of the route this links to, unless it's external. + 'route_name' => '', + // Parameters for route variables when generating a link. + 'route_parameters' => array(), + // The external URL if this link has one (required if route_name is empty). + 'url' => '', + // The static title for the menu link. + 'title' => '', + 'title_arguments' => array(), + 'title_context' => '', + // The description. + 'description' => '', + // The plugin ID of the parent link (or NULL for a top-level link). + 'parent' => '', + // The weight of the link. + 'weight' => 0, + // The default link options. + 'options' => array(), + 'expanded' => 0, + 'hidden' => 0, + // Flag for whether this plugin was discovered. Should be set to 0 or NULL + // for definitions that are added via a direct save. + 'discovered' => 0, + 'provider' => '', + 'metadata' => array(), + // Default class for local task implementations. + 'class' => 'Drupal\Core\Menu\MenuLinkDefault', + 'form_class' => 'Drupal\Core\Menu\Form\MenuLinkDefaultForm', + // The plugin id. Set by the plugin system based on the top-level YAML key. + 'id' => '', + ); + + /** + * The object that discovers plugins managed by this manager. + * + * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface + */ + protected $discovery; + + /** + * The object that instantiates plugins managed by this manager. + * + * @var \Drupal\Component\Plugin\Factory\FactoryInterface + */ + protected $factory; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Cache backend instance for the extracted tree data. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $treeCacheBackend; + + /** + * The menu link tree storage. + * + * @var \Drupal\Core\Menu\MenuTreeStorageInterface + */ + protected $treeStorage; + + /** + * Service providing overrides for static links + * + * @var \Drupal\Core\Menu\StaticMenuLinkOverridesInterface + */ + protected $overrides; + + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ + protected $requestStack; + + /** + * The plugin instances. + * + * @var array + */ + protected $instances = array(); + + /** + * The statically cached definitions. + * + * @var array + */ + protected $definitions = array(); + + /** + * The route provider to load routes by name. + * + * @var \Drupal\Core\Routing\RouteProviderInterface + */ + protected $routeProvider; + + /** + * The access manager. + * + * @var \Drupal\Core\Access\AccessManager + */ + protected $accessManager; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $account; + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The configuration factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Stores the menu tree used by the doBuildTree method, keyed by a cache ID. + * + * This cache ID is built using the $menu_name, the current language and + * some parameters passed into an entity query. + */ + protected $menuTree; + + /** + * Stores the menu tree data on the current page keyed by a cache ID. + * + * This contains less information than a tree built with buildAllData. + * + * @var array + */ + protected $menuPageTrees; + + /** + * Stores the preferred menu link keyed by route_name + parameters. + * + * @var array + */ + protected $preferredLinks = array(); + + /** + * Stores the active menu names. + * + * @var array + */ + protected $activeMenus = array(); + + /** + * Stores the parameters for buildAllData keyed by cached ID. + * + * @var array + */ + protected $buildAllDataParameters = array(); + + /** + * Constructs a \Drupal\Core\Menu\MenuLinkTree object. + * + * @param \Drupal\Core\Menu\MenuTreeStorageInterface $tree_storage + * The menu link tree storage. + * @param \Drupal\Core\Menu\StaticMenuLinkOverridesInterface $overrides + * Service providing overrides for static links + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * A request object for the controller resolver and finding the preferred + * menu and link for the current page. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider to load routes by name. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Cache\CacheBackendInterface $tree_cache_backend + * Cache backend instance for the extracted tree data. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Access\AccessManager $access_manager + * The access manager. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * Configuration factory. + */ + public function __construct(MenuTreeStorageInterface $tree_storage, StaticMenuLinkOverridesInterface $overrides, RequestStack $request_stack, RouteProviderInterface $route_provider, ModuleHandlerInterface $module_handler, CacheBackendInterface $tree_cache_backend, LanguageManagerInterface $language_manager, AccessManager $access_manager, AccountInterface $account, EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) { + $this->treeStorage = $tree_storage; + $this->overrides = $overrides; + $this->factory = new ContainerFactory($this); + $this->requestStack = $request_stack; + $this->routeProvider = $route_provider; + $this->accessManager = $access_manager; + $this->account = $account; + $this->moduleHandler = $module_handler; + $this->treeCacheBackend = $tree_cache_backend; + $this->languageManager = $language_manager; + $this->entityManager = $entity_manager; + $this->configFactory = $config_factory; + } + + /** + * Performs extra processing on plugin definitions. + * + * By default we add defaults for the type to the definition. If a type has + * additional processing logic they can do that by replacing or extending the + * method. + */ + protected function processDefinition(&$definition, $plugin_id) { + $definition = NestedArray::mergeDeep($this->defaults, $definition); + $definition['parent'] = (string) $definition['parent']; + $definition['id'] = $plugin_id; + } + + /** + * Instanciates the discovery. + */ + protected function getDiscovery() { + if (empty($this->discovery)) { + $yaml = new YamlDiscovery('menu_links', $this->moduleHandler->getModuleDirectories()); + $this->discovery = new ContainerDerivativeDiscoveryDecorator($yaml); + } + return $this->discovery; + } + + /** + * {@inheritdoc} + */ + public function getDefinitions() { + // Since this function is called rarely, instantiate the discovery here. + $definitions = $this->getDiscovery()->getDefinitions(); + + $this->moduleHandler->alter('menu_links', $definitions); + + foreach ($definitions as $plugin_id => &$definition) { + $definition['id'] = $plugin_id; + $this->processDefinition($definition, $plugin_id); + } + + // If this plugin was provided by a module that does not exist, remove the + // plugin definition. + foreach ($definitions as $plugin_id => $plugin_definition) { + if (!empty($plugin_definition['provider']) && !$this->moduleHandler->moduleExists($plugin_definition['provider'])) { + unset($definitions[$plugin_id]); + } + else { + // Any link found here is flagged as discovered, so it can be purged + // if it does exit in the future. + $definitions[$plugin_id]['discovered'] = 1; + } + } + return $definitions; + } + + /** + * {@inheritdoc} + */ + public function rebuild() { + // Fetch the list of existing menus, in case some are not longer populated + // after the rebuild. + $before_menus = $this->treeStorage->getMenuNames(); + $definitions = $this->getDefinitions(); + // Apply overrides from config. + $overrides = $this->overrides->loadMultipleOverrides(array_keys($definitions)); + foreach ($overrides as $id => $changes) { + if (!empty($definitions[$id])) { + $definitions[$id] = $changes + $definitions[$id]; + } + } + $this->treeStorage->rebuild($definitions); + $this->treeCacheBackend->deleteAll(); + $affected_menus = $this->treeStorage->getMenuNames() + $before_menus; + Cache::invalidateTags(array('menu' => $affected_menus)); + } + + /** + * {@inheritdoc} + */ + public function getDefinition($plugin_id, $exception_on_invalid = TRUE) { + // When building tress, we will usually have the definitions already loaded. + // This makes the call to $this->factory->createInstance() faster. + // @todo Normal plugin managers throw an exception in case it doesn't exist. + if (!isset($this->definitions[$plugin_id])) { + $this->definitions[$plugin_id] = $this->treeStorage->load($plugin_id); + } + if (empty($this->definitions[$plugin_id]) && $exception_on_invalid) { + throw new PluginNotFoundException("$plugin_id could not be found."); + } + return $this->definitions[$plugin_id]; + } + + /** + * {@inheritdoc} + */ + public function hasDefinition($plugin_id) { + return (bool) $this->getDefinition($plugin_id, FALSE); + } + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Menu\MenuLinkInterface + * A menu link instance. + */ + public function createInstance($plugin_id, array $configuration = array()) { + return $this->factory->createInstance($plugin_id, $configuration); + } + + /** + * {@inheritdoc} + */ + public function getInstance(array $options) { + if (isset($options['id'])) { + return $this->createInstance($options['id']); + } + } + + /** + * Returns an array containing all links for a menu. + * + * @param string $menu_name + * The name of the menu whose links should be returned. + * + * @return \Drupal\Core\Menu\MenuLinkInterface[] + * An array of menu link plugin instances keyed by ID. + */ + public function loadLinks($menu_name) { + $instances = array(); + $loaded = $this->treeStorage->loadByProperties(array('menu_name' => $menu_name)); + foreach ($loaded as $plugin_id => $definition) { + // Setting the definition here means it will be used by getDefinition() + // which is called by createInstance() from the factory. + $this->definitions[$plugin_id] = $definition; + $instances[$plugin_id] = $this->createInstance($plugin_id); + } + return $instances; + } + + /** + * Deletes all links for a menu. + * + * @todo - this should really only be called as part of the flow of + * deleting a menu entity, so maybe we should load it and make sure it's + * not locked? + * + * @param string $menu_name + * The name of the menu whose links will be deleted. + */ + public function deleteLinksInMenu($menu_name) { + $affected_menus = array($menu_name => $menu_name); + foreach ($this->treeStorage->loadByProperties(array('menu_name' => $menu_name)) as $plugin_id => $definition) { + // Setting the definition here means it will be used by getDefinition() + // which is called by createInstance() from the factory. + $this->definitions[$plugin_id] = $definition; + $instance = $this->createInstance($plugin_id); + if ($instance->isResetable()) { + $new_instance = $this->resetInstance($instance); + $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName(); + } + elseif ($instance->isDeletable()) { + $this->deleteInstance($instance, TRUE); + } + } + Cache::invalidateTags(array('menu' => $affected_menus)); + } + + /** + * Helper function to delete a specific instance. + */ + protected function deleteInstance(MenuLinkInterface $instance, $persist) { + $id = $instance->getPluginId(); + if ($instance->isDeletable()) { + if ($persist) { + $instance->deleteLink(); + } + } + else { + throw new PluginException(sprintf("Menu link plugin with ID %s does not support deletion", $id)); + } + $this->treeStorage->delete($id); + } + + /** + * {@inheritdoc} + */ + public function deleteLink($id, $persist = TRUE) { + $definition = $this->treeStorage->load($id); + // It's possible the definition has already been deleted, or doesn't exist. + if ($definition) { + // Setting the definition here means it will be used by getDefinition() + // which is called by createInstance() from the factory. + $this->definitions[$id] = $definition; + $instance = $this->createInstance($id); + $this->deleteInstance($instance, $persist); + // Many children may have moved. + $this->resetDefinitions(); + Cache::invalidateTags(array('menu' => array($definition['menu_name']))); + } + $this->resetDefinition($id); + } + + /** + * {@inheritdoc} + */ + public function countMenuLinks($menu_name = NULL) { + return $this->treeStorage->countMenuLinks($menu_name); + } + + /** + * {@inheritdoc} + */ + public function loadLinksByRoute($route_name, array $route_parameters = array(), $include_hidden = FALSE) { + $instances = array(); + $loaded = $this->treeStorage->loadByRoute($route_name, $route_parameters, $include_hidden); + foreach ($loaded as $plugin_id => $definition) { + // Setting the definition here means it will be used by getDefinition() + // which is called by createInstance() from the factory. + $this->definitions[$plugin_id] = $definition; + $instances[$plugin_id] = $this->createInstance($plugin_id); + } + return $instances; + } + + /** + * {@inheritdoc} + */ + public function maxDepth() { + return $this->treeStorage->maxDepth(); + } + + /** + * {@inheritdoc} + */ + public function buildRenderTree($tree) { + $build = array(); + + foreach ($tree as $data) { + $class = array(); + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $data['link']; + // Generally we only deal with visible links, but just in case. + if ($link->isHidden()) { + continue; + } + // Set a class for the
  • -tag. Only set 'expanded' class if the link + // also has visible children within the current tree. + if ($data['has_children'] && $data['below']) { + $class[] = 'expanded'; + } + elseif ($data['has_children']) { + $class[] = 'collapsed'; + } + else { + $class[] = 'leaf'; + } + // Set a class if the link is in the active trail. + if ($data['in_active_trail']) { + $class[] = 'active-trail'; + } + + // Allow menu-specific theme overrides. + $element['#theme'] = 'menu_link__' . strtr($link->getMenuName(), '-', '_'); + $element['#attributes']['class'] = $class; + $element['#title'] = $link->getTitle(); + $element['#url'] = $link->getUrlObject(); + $element['#below'] = $data['below'] ? $this->buildRenderTree($data['below']) : array(); + $element['#original_link'] = $link; + // Index using the link's unique ID. + $build[$link->getPluginId()] = $element; + } + if ($build) { + // Make sure drupal_render() does not re-order the links. + $build['#sorted'] = TRUE; + // Get the menu name from the last link. + $menu_name = $link->getMenuName(); + // Add the theme wrapper for outer markup. + // Allow menu-specific theme overrides. + $build['#theme_wrappers'][] = 'menu_tree__' . strtr($menu_name, '-', '_'); + // Set cache tag. + $build['#cache']['tags']['menu'][$menu_name] = $menu_name; + } + + return $build; + } + + /** + * {@inheritdoc} + */ + public function getActiveTrailIds($menu_name) { + // Parent ids; used both as key and value to ensure uniqueness. + // We always want all the top-level links with parent == ''. + $active_trail = array('' => ''); + + $request = $this->requestStack->getCurrentRequest(); + + if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { + $route_parameters = $request->attributes->get('_raw_variables')->all(); + $page_is_403 = $request->attributes->get('_exception_statuscode') == 403; + // Find a menu link corresponding to the current path. If + // $active_path is NULL, let $this->menuLinkGetPreferred() determine the + // path. + if (!$page_is_403) { + $active_link = $this->menuLinkGetPreferred($route_name, $route_parameters, $menu_name); + if ($active_link && $active_link->getMenuName() == $menu_name) { + $active_trail += $this->treeStorage->getRootPathIds($active_link->getPluginId()); + } + } + } + return $active_trail; + } + + /** + * {@inheritdoc} + */ + public function menuLinkGetPreferred($route_name = NULL, array $route_parameters = array(), $selected_menu = NULL) { + if (!isset($route_name)) { + $request = $this->requestStack->getCurrentRequest(); + + $route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME); + $route_parameters = $request->attributes->get('_raw_variables')->all(); + } + + $access = $this->accessManager->checkNamedRoute($route_name, $route_parameters, $this->account); + if (!$access) { + return NULL; + } + asort($route_parameters); + $route_key = $route_name . serialize($route_parameters); + + if (empty($selected_menu)) { + // Use an illegal menu name as the key for the preferred menu link. + $selected_menu = '%'; + } + + if (!isset($this->preferredLinks[$route_key])) { + // Retrieve a list of menu names, ordered by preference. + $menu_names = $this->getActiveMenuNames(); + // Put the selected menu at the front of the list. + array_unshift($menu_names, $selected_menu); + // If this menu name is not fond later, we want to just get NULL. + $this->preferredLinks[$route_key][$selected_menu] = NULL; + + // Only load non-hidden links. + $definitions = $this->treeStorage->loadByRoute($route_name, $route_parameters); + // Sort candidates by menu name. + $candidates = array(); + foreach ($definitions as $candidate) { + $candidates[$candidate['menu_name']] = $candidate; + $menu_names[] = $candidate['menu_name']; + } + foreach ($menu_names as $menu_name) { + if (isset($candidates[$menu_name]) && !isset($this->preferredLinks[$route_key][$menu_name])) { + $candidate = $candidates[$menu_name]; + $this->definitions[$candidate['id']] = $candidate; + $instance = $this->createInstance($candidate['id']); + $this->preferredLinks[$route_key][$menu_name] = $instance; + if (!isset($this->preferredLinks[$route_key]['%'])) { + $this->preferredLinks[$route_key]['%'] = $instance; + } + } + } + + } + return isset($this->preferredLinks[$route_key][$selected_menu]) ? $this->preferredLinks[$route_key][$selected_menu] : NULL; + } + + /** + * {@inheritdoc} + */ + public function getActiveMenuNames() { + return $this->activeMenus; + } + + /** + * {@inheritdoc} + */ + public function setActiveMenuNames(array $menu_names) { + + if (isset($menu_names) && is_array($menu_names)) { + $this->activeMenus = $menu_names; + } + elseif (!isset($this->activeMenus)) { + $config = $this->configFactory->get('system.menu'); + $this->activeMenus = $config->get('active_menus_default') ?: array_keys($this->listSystemMenus()); + } + } + + /** + * Returns an array containing the names of system-defined (default) menus. + */ + protected function listSystemMenus() { + // For simplicity and performance, this is simply a hard-coded list copied + // from menu_list_system_menus() which is simply the list of all Menu config + // entities that are shipped with system module. + return array( + 'tools' => 'Tools', + 'admin' => 'Administration', + 'account' => 'User account menu', + 'main' => 'Main navigation', + 'footer' => 'Footer menu', + ); + } + + /** + * {@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(); + $page_is_403 = FALSE; + $system_path = NULL; + if ($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) { + $system_path = $request->attributes->get('_system_path'); + $page_is_403 = $request->attributes->get('_exception_statuscode') == 403; + } + + if (isset($max_depth)) { + $max_depth = min($max_depth, $this->treeStorage->maxDepth()); + } + // Generate a cache ID (cid) specific for this page. + $cid = 'links:' . $menu_name . ':page:' . $system_path . ':' . $language_interface->id . ':' . $page_is_403 . ':' . (int) $max_depth; + // If we are asked for the active trail only, and $menu_name has not been + // built and cached for this page yet, then this likely means that it + // won't be built anymore, as this function is invoked from + // template_preprocess_page(). So in order to not build a giant menu tree + // that needs to be checked for access on all levels, we simply check + // whether we have the menu already in cache, or otherwise, build a + // minimum tree containing the active trail only. + // @see menu_set_active_trail() + if (!isset($this->menuPageTrees[$cid]) && $only_active_trail) { + $cid .= ':trail'; + } + + // @todo Decide whether it makes sense to static cache page menu trees. + if (!isset($this->menuPageTrees[$cid])) { + // If the static variable doesn't have the data, check {cache_menu}. + $cache = $this->treeCacheBackend->get($cid); + if ($cache && isset($cache->data)) { + // If the cache entry exists, it contains the parameters for + // menu_build_tree(). + $tree_parameters = $cache->data; + } + else { + $tree_parameters = $this->doBuildPageDataTreeParameters($menu_name, $max_depth, $only_active_trail, $page_is_403); + + // Cache the tree building parameters using the page-specific cid. + $this->treeCacheBackend->set($cid, $tree_parameters, Cache::PERMANENT, array('menu' => $menu_name)); + } + + // Build the tree using the parameters; the resulting tree will be cached + // by $this->buildTree()). + $this->menuPageTrees[$cid] = $this->buildTree($menu_name, $tree_parameters); + } + return $this->menuPageTrees[$cid]; + } + + /** + * Determines the required tree parameters used for the page menu tree. + * + * This method takes into account the active trail of the current page. + * + * @param string $menu_name + * The menu name. + * @param int $max_depth + * The maximum allowed depth of menus. + * @param bool $only_active_trail + * If TRUE, just load level 0 plus the active trail, otherwise load the full + * menu tree. + * @param bool $page_is_403 + * Is the current request happening on a 403 subrequest. + * + * @return array + * An array of tree parameters. + */ + protected function doBuildPageDataTreeParameters($menu_name, $max_depth, $only_active_trail, $page_is_403) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + + // If this page is accessible to the current user, build the tree + // parameters accordingly. + if (!$page_is_403) { + $active_trail = $this->getActiveTrailIds($menu_name); + // 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; + + if (!$only_active_trail) { + // Collect all the links set to be expanded, and then add all of + // their children to the list as well. + $parents = $this->treeStorage->getExpanded($menu_name, $parents); + } + } + else { + // If access is denied, we only show top-level links in menus. + $active_trail = array('' => ''); + $parents = $active_trail; + } + $tree_parameters['expanded'] = $parents; + $tree_parameters['active_trail'] = $active_trail; + return $tree_parameters; + } + + /** + * {@inheritdoc} + * + * @todo should this accept a menu link instance or just the ID? + */ + public function buildAllData($menu_name, $id = NULL, $max_depth = NULL) { + $language_interface = $this->languageManager->getCurrentLanguage(); + + // Use ID as a flag for whether the data being loaded is for the whole + // tree. + $id = isset($id) ? $id : '%'; + // Generate a cache ID (cid) specific for this $menu_name, $link, $language, + // and depth. + $cid = 'links:' . $menu_name . ':all:' . $id . ':' . $language_interface->id . ':' . (int) $max_depth; + if (!isset($this->buildAllDataParameters[$cid])) { + $tree_parameters = array( + 'min_depth' => 1, + 'max_depth' => $max_depth, + ); + if ($id != '%') { + // The tree is for a single item, so we need to match the values in + // of all the IDs on the path to root. + $tree_parameters['active_trail'] = $this->treeStorage->getRootPathIds($id); + $tree_parameters['expanded'] = $tree_parameters['active_trail']; + // Include top-level links. + $tree_parameters['expanded'][''] = ''; + } + $this->buildAllDataParameters[$cid] = $tree_parameters; + } + // Build the tree using the parameters; the resulting tree will be cached + // by buildTree(). + return $this->buildTree($menu_name, $this->buildAllDataParameters[$cid]); + } + + /** + * {@inheritdoc} + */ + public function getChildLinks($id, $max_relative_depth = NULL) { + $links = array(); + $definitions = $this->treeStorage->loadAllChildLinks($id, $max_relative_depth); + foreach ($definitions as $id => $definition) { + $instance = $this->menuLinkCheckAccess($definition); + if ($instance) { + $links[$id] = $instance; + } + } + return $links; + } + + /** + * {@inheritdoc} + */ + public function getParentIds($id) { + if ($this->getDefinition($id, FALSE)) { + return $this->treeStorage->getRootPathIds($id); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getChildIds($id) { + if ($this->getDefinition($id, FALSE)) { + return $this->treeStorage->getAllChildIds($id); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function buildSubtree($id, $max_relative_depth = NULL) { + $subtree = $this->treeStorage->loadSubtree($id, $max_relative_depth); + if ($subtree) { + // Check access and instantiate. @todo rename these methods. + $instance = $this->menuLinkCheckAccess($subtree['definition']); + if ($instance) { + $subtree['link'] = $instance; + $route_names = $this->collectRoutes($subtree['below']); + // Pre-load all the route objects in the tree for access checks. + if ($route_names) { + $this->routeProvider->getRoutesByNames($route_names); + } + $this->treeCheckAccess($subtree['below']); + return $subtree; + } + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function buildTree($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. + asort($parameters); + 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->treeCacheBackend->get($tree_cid); + if ($cache && isset($cache->data)) { + $this->menuTree[$tree_cid] = $cache->data; + } + } + + if (!isset($this->menuTree[$tree_cid])) { + // Rebuild the links which are stored. + $data['tree'] = $this->treeStorage->loadTree($menu_name, $parameters); + $data['route_names'] = $this->collectRoutes($data['tree']); + // Cache the data, if it is not already in the cache. + $this->treeCacheBackend->set($tree_cid, $data, Cache::PERMANENT, array('menu' => $menu_name)); + $this->menuTree[$tree_cid] = $data; + } + else { + $data = $this->menuTree[$tree_cid]; + } + + // Pre-load all the route objects in the tree for access checks. + if ($data['route_names']) { + $this->routeProvider->getRoutesByNames($data['route_names']); + } + $tree = $data['tree']; + $this->treeCheckAccess($tree); + return $tree; + } + + /** + * Traverses the menu tree and collects all the route names. + * + * @param array $tree + * The menu tree you wish to operate on. + * + * @return array + * Array of route names, with all values being unique. + */ + protected function collectRoutes($tree) { + return array_values($this->doCollectRoutes($tree)); + } + + /** + * Recursive helper function to collect all the route names. + */ + protected function doCollectRoutes($tree) { + $route_names = array(); + foreach ($tree as $key => $v) { + $definition = $tree[$key]['definition']; + if (!empty($definition['route_name'])) { + $route_names[$definition['route_name']] = $definition['route_name']; + } + if ($tree[$key]['below']) { + $route_names += $this->doCollectRoutes($tree[$key]['below']); + } + } + return $route_names; + } + + /** + * Sorts the menu tree and recursively checks access for each item. + * + * @param array $tree + * The menu tree you wish to operate on. + */ + protected function treeCheckAccess(&$tree) { + $this->doTreeCheckAccess($tree); + $this->sortTree($tree); + } + + /** + * Helper function that recursively checks access for each item. + */ + protected function doTreeCheckAccess(&$tree) { + foreach ($tree as $key => $v) { + $definition = $tree[$key]['definition']; + // Setting the definition here means it will be used by getDefinition() + // which is called by createInstance() from the factory. + $this->definitions[$definition['id']] = $definition; + $instance = $this->menuLinkCheckAccess($definition); + if ($instance) { + $tree[$key]['link'] = $instance; + if ($tree[$key]['below']) { + $this->doTreeCheckAccess($tree[$key]['below']); + } + unset($tree[$key]['definition']); + } + else { + unset($tree[$key]); + } + } + } + + /** + * Sorts the menu tree and recursively using the weight and title. + * + * @param array $tree + * The menu tree you wish to operate on. + */ + protected function sortTree(&$tree) { + $new_tree = array(); + foreach ($tree as $key => $v) { + if ($tree[$key]['below']) { + $this->sortTree($tree[$key]['below']); + } + $instance = $tree[$key]['link']; + // The weights are made a uniform 5 digits by adding 50000 as an offset. + // After $this->menuLinkCheckAccess(), $instance->getTitle() has the + // localized or translated title. Adding the plugin id to the end of the + // index insures that it is unique. + $new_tree[(50000 + $instance->getWeight()) . ' ' . $instance->getTitle() . ' ' . $instance->getPluginId()] = $tree[$key]; + } + // Sort siblings in the tree based on the weights and localized titles. + ksort($new_tree); + $tree = $new_tree; + } + + /** + * Check access for the item and create an instance if it is accessible. + * + * @param array $definition + * The menu link definition. + * + * @return \Drupal\Core\Menu\MenuLinkInterface|NULL + * A plugin instance or NULL if the current user can not access its route. + */ + protected function menuLinkCheckAccess(array $definition) { + // 'url' should only be populated for external links. + if (!empty($definition['url']) && empty($definition['route_name'])) { + $access = TRUE; + } + else { + $access = $this->accessManager->checkNamedRoute($definition['route_name'], $definition['route_parameters'], $this->account); + } + // For performance, don't instantiate a link the user can't access. + if ($access) { + return $this->createInstance($definition['id']); + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function createLink($id, array $definition) { + // Add defaults and other stuff, so there is no requirement to specify + // everything. + $this->processDefinition($definition, $id); + + // Store the new link in the tree and invalidate some caches. + $affected_menus = $this->treeStorage->save($definition); + Cache::invalidateTags(array('menu' => $affected_menus)); + $this->resetDefinition($id); + return $this->createInstance($id); + } + + /** + * {@inheritdoc} + */ + public function updateLink($id, array $new_definition_values, $persist = TRUE) { + $instance = $this->createInstance($id); + if ($instance) { + $new_definition_values['id'] = $id; + $changed_definition = $instance->updateLink($new_definition_values, $persist); + $affected_menus = $this->treeStorage->save($changed_definition); + $this->moduleHandler->invokeAll('menu_link_update', array($changed_definition)); + $this->resetDefinitions(); + Cache::invalidateTags(array('menu' => $affected_menus)); + } + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getPluginForm(MenuLinkInterface $menu_link) { + $class_name = $menu_link->getFormClass(); + /** @var \Drupal\Core\Menu\Form\MenuLinkFormInterface $form */ + if (in_array('Drupal\Core\DependencyInjection\ContainerInjectionInterface', class_implements($class_name))) { + $form = $class_name::create(\Drupal::getContainer()); + } + else { + $form = new $class_name(); + } + $form->setMenuLinkInstance($menu_link); + return $form; + } + + /** + * {@inheritdoc} + */ + public function getParentSelectOptions($id = '', array $menus = array()) { + // @todo: Core allows you to replace the select element ... this is a sign + // that we might want to write a form element as well, which can be swapped. + if (empty($menus)) { + $menus = $this->getMenuOptions(); + } + + $options = array(); + $depth_limit = $this->getParentDepthLimit($id); + foreach ($menus as $menu_name => $menu_title) { + $options[$menu_name . ':'] = '<' . $menu_title . '>'; + + $tree = $this->buildAllData($menu_name, NULL, $depth_limit); + $this->parentSelectOptionsTreeWalk($tree, $menu_name, '--', $options, $id, $depth_limit); + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function getParentDepthLimit($id) { + if ($id) { + $limit = $this->treeStorage->maxDepth() - $this->treeStorage->getSubtreeHeight($id); + } + else { + $limit = $this->treeStorage->maxDepth() - 1; + } + return $limit; + } + + /** + * Iterates over all items in the tree to prepare the parents select options. + * + * @param array $tree + * The menu tree. + * @param string $menu_name + * The menu name. + * @param string $indent + * The indentation string used for the label. + * @param array $options + * The select options. + * @param string $exclude + * An excluded menu link. + * @param int $depth_limit + * The maximum depth of menu links considered for the select options. + */ + protected function parentSelectOptionsTreeWalk(array $tree, $menu_name, $indent, array &$options, $exclude, $depth_limit) { + foreach ($tree as $data) { + if ($data['depth'] > $depth_limit) { + // Don't iterate through any links on this level. + break; + } + /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ + $link = $data['link']; + if ($link->getPluginId() != $exclude) { + $title = $indent . ' ' . Unicode::truncate($link->getTitle(), 30, TRUE, FALSE); + if ($link->isHidden()) { + $title .= ' (' . t('disabled') . ')'; + } + $options[$menu_name . ':' . $link->getPluginId()] = $title; + if ($data['below']) { + $this->parentSelectOptionsTreeWalk($data['below'], $menu_name, $indent . '--', $options, $exclude, $depth_limit); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getMenuOptions(array $menu_names = NULL) { + $menus = $this->entityManager->getStorage('menu')->loadMultiple($menu_names); + $options = array(); + foreach ($menus as $menu) { + $options[$menu->id()] = $menu->label(); + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function menuNameInUse($menu_name) { + $this->treeStorage->menuNameInUse($menu_name); + } + + /** + * {@inheritdoc} + */ + public function resetLink($id) { + $instance = $this->createInstance($id); + $affected_menus[$instance->getMenuName()] = $instance->getMenuName(); + $new_instance = $this->resetInstance($instance); + $affected_menus[$new_instance->getMenuName()] = $new_instance->getMenuName(); + Cache::invalidateTags(array('menu' => $affected_menus)); + return $new_instance; + } + + /** + * Resets the menu link to its default settings. + * + * @param \Drupal\Core\Menu\MenuLinkInterface $instance + * The menu link which should be reset. + * + * @return \Drupal\Core\Menu\MenuLinkInterface + * The reset menu link. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * Thrown when the menu link is not resetable. + */ + protected function resetInstance(MenuLinkInterface $instance) { + $id = $instance->getPluginId(); + + if (!$instance->isResetable()) { + throw new PluginException(String::format('Menu link %id is not resetable', array('%id' => $id))); + } + // Get the original data from disk, reset the override and re-save the menu + // tree for this link. + $definition = $this->getDefinitions()[$id]; + $this->overrides->deleteOverride($id); + $this->resetDefinition($id, $definition); + $this->treeStorage->save($definition); + return $this->createInstance($id); + } + + /** + * {@inheritdoc} + */ + public function resetDefinitions() { + $this->definitions = array(); + $this->menuTree = array(); + $this->buildAllDataParameters = array(); + $this->menuPageTrees = array(); + } + + /** + * Resets the local definition cache for one plugin. + * + * @param string $id + * The menu link plugin ID. + * @param array $definition + * Optional new definition for the given plugin ID. + */ + protected function resetDefinition($id, $definition = NULL) { + $this->definitions[$id] = $definition; + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php new file mode 100644 index 0000000..a75e284 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuLinkTreeInterface.php @@ -0,0 +1,397 @@ +connection = $connection; + $this->urlGenerator = $url_generator; + $this->table = $table; + $this->options = $options; + } + + /** + * {@inheritdoc} + */ + public function maxDepth() { + return static::MAX_DEPTH; + } + + /** + * {@inheritdoc} + */ + public function rebuild(array $definitions) { + $links = array(); + $children = array(); + $top_links = array(); + if ($definitions) { + foreach ($definitions as $id => $link) { + if (!empty($link['parent'])) { + $children[$link['parent']][$id] = $id; + } + else { + // A top level link - we need them to root our tree. + $top_links[$id] = $id; + $link['parent'] = ''; + } + $links[$id] = $link; + } + } + foreach ($top_links as $id) { + $this->saveRecursive($id, $children, $links); + } + // Handle any children we didn't find starting from top-level links. + foreach ($children as $orphan_links) { + foreach ($orphan_links as $id) { + // Force it to the top level. + $links[$id]['parent'] = ''; + $this->saveRecursive($id, $children, $links); + } + } + // Find any previously discovered menu links that no longer exist. + if ($definitions) { + $query = $this->connection->select($this->table, NULL, $this->options); + $query->addField($this->table, 'id'); + $query->condition('discovered', 1); + $query->condition('id', array_keys($definitions), 'NOT IN'); + $query->orderBy('depth', 'DESC'); + $result = $query->execute()->fetchCol(); + } + 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) { + $this->purgeMultiple($result); + } + } + + /** + * Purges multiple menu links that no longer exist. + * + * @param array $ids + * An array of menu link IDs. + * @param bool $prevent_reparenting + * (optional) Disables the re-parenting logic from the deletion process. + * Defaults to FALSE. + */ + protected function purgeMultiple(array $ids, $prevent_reparenting = FALSE) { + if (!$prevent_reparenting) { + $loaded = $this->loadFullMultiple($ids); + foreach ($loaded as $id => $link) { + if ($link['has_children']) { + $children = $this->loadByProperties(array('parent' => $id)); + foreach ($children as $child) { + $child['parent'] = $link['parent']; + $this->save($child); + } + } + } + } + $query = $this->connection->delete($this->table, $this->options); + $query->condition('id', $ids, 'IN'); + $query->execute(); + } + + /** + * Executes a select query while making sure the database table exists. + * + * @param \Drupal\Core\Database\Query\SelectInterface $query + * The select object to be executed. + * + * @return \Drupal\Core\Database\StatementInterface|null + * A prepared statement, or NULL if the query is not valid. + * + * @throws \Exception + * If the table could not be created or the database connection failed. + */ + protected function safeExecuteSelect(SelectInterface $query) { + try { + return $query->execute(); + } + catch (\Exception $e) { + // If there was an exception, try to create the table. + if ($this->ensureTableExists()) { + return $query->execute(); + } + // Some other failure that we can not recover from. + throw $e; + } + } + + /** + * {@inheritdoc} + */ + public function save(array $link) { + $original = $this->loadFull($link['id']); + // @todo - should we just return here if the links values match the original + // values completely?. + + $affected_menus = array(); + + $transaction = $this->connection->startTransaction(); + try { + if ($original) { + $link['mlid'] = $original['mlid']; + $link['has_children'] = $original['has_children']; + $affected_menus[$original['menu_name']] = $original['menu_name']; + } + else { + // Generate a new mlid. + $options = array('return' => Database::RETURN_INSERT_ID) + $this->options; + $link['mlid'] = $this->connection->insert($this->table, $options) + ->fields(array('id' => $link['id'], 'menu_name' => $link['menu_name'])) + ->execute(); + } + $fields = $this->preSave($link, $original); + // We may be moving the link to a new menu. + $affected_menus[$fields['menu_name']] = $fields['menu_name']; + $query = $this->connection->update($this->table, $this->options); + $query->condition('mlid', $link['mlid']); + $query->fields($fields) + ->execute(); + if ($original) { + $this->updateParentalStatus($original); + } + $this->updateParentalStatus($link); + // Ignore slave server temporarily. + db_ignore_slave(); + } + catch (\Exception $e) { + $transaction->rollback(); + throw $e; + } + return $affected_menus; + } + + /** + * Using the link definition, but up all the fields needed for database save. + * + * @param array $link + * The link definition to be updated. + * @param array $original + * The link definition before the changes. May be empty if not found. + * + * @return array + * The values which will be stored. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * Thrown when the specific depth exceeds the maximum. + */ + protected function preSave(array &$link, array $original) { + static $schema_fields, $schema_defaults; + if (empty($schema_fields)) { + $schema = static::schemaDefinition(); + $schema_fields = $schema['fields']; + foreach ($schema_fields as $name => $spec) { + if (isset($spec['default'])) { + $schema_defaults[$name] = $spec['default']; + } + } + } + + // Try to find a parent link. If found, assign it and derive its menu. + $parent = $this->findParent($link, $original); + if ($parent) { + $link['parent'] = $parent['id']; + $link['menu_name'] = $parent['menu_name']; + } + else { + $link['parent'] = ''; + } + + // If no corresponding parent link was found, move the link to the + // top-level. + foreach ($schema_defaults as $name => $default) { + if (!isset($link[$name])) { + $link[$name] = $default; + } + } + $fields = array_intersect_key($link, $schema_fields); + asort($fields['route_parameters']); + // Since this will be urlencoded, it's safe to store and match against a + // text field. + $fields['route_param_key'] = $fields['route_parameters'] ? UrlHelper::buildQuery($fields['route_parameters']) : ''; + + foreach ($this->serializedFields() as $name) { + $fields[$name] = serialize($fields[$name]); + } + + // Directly fill parents for top-level links. + if (empty($link['parent'])) { + $fields['p1'] = $link['mlid']; + for ($i = 2; $i <= $this->maxDepth(); $i++) { + $fields["p$i"] = 0; + } + $fields['depth'] = 1; + } + // Otherwise, ensure that this link's depth is not beyond the maximum depth + // and fill parents based on the parent link. + else { + // @todo - we want to also check $original['has_children'] here, but that + // will be 0 even if there are children if those are hidden. + // has_children is really just the rendering hint. So, we either need + // to define another column (has_any_children), or do the extra query. + if ($original) { + $limit = $this->maxDepth() - $this->doFindChildrenRelativeDepth($original) - 1; + } + else { + $limit = $this->maxDepth() - 1; + } + if ($parent['depth'] > $limit) { + throw new PluginException(sprintf('The link with ID %s or its children exceeded the maximum depth of %d', $link['id'], $this->maxDepth())); + } + $this->setParents($fields, $parent); + } + + // Need to check both parent and menu_name, since parent can be empty in any + // menu. + if ($original && ($link['parent'] != $original['parent'] || $link['menu_name'] != $original['menu_name'])) { + $this->moveChildren($fields, $original); + } + // We needed the mlid above, but not in the update query. + unset($fields['mlid']); + + // Cast booleans to int, if needed. + $fields['hidden'] = (int) $fields['hidden']; + $fields['expanded'] = (int) $fields['expanded']; + return $fields; + } + + /** + * {@inheritdoc} + */ + public function delete($id) { + // Children get re-attached to the menu link's parent. + $item = $this->loadFull($id); + // It's possible the link is already deleted. + if ($item) { + $parent = $item['parent']; + $children = $this->loadByProperties(array('parent' => $id)); + foreach ($children as $child) { + $child['parent'] = $parent; + $this->save($child); + } + + $this->connection->delete($this->table, $this->options) + ->condition('id', $id) + ->execute(); + + $this->updateParentalStatus($item); + } + } + + /** + * {@inheritdoc} + */ + public function getSubtreeHeight($id) { + $original = $this->loadFull($id); + return $original ? $this->doFindChildrenRelativeDepth($original) + 1 : 0; + } + + /** + * Finds the relative depth of this link's deepest child. + * + * @param array $original + * The parent definition used to find the depth. + * + * @return int + * Returns the relative depth. + */ + protected function doFindChildrenRelativeDepth(array $original) { + $query = $this->connection->select($this->table, $this->options); + $query->addField($this->table, 'depth'); + $query->condition('menu_name', $original['menu_name']); + $query->orderBy('depth', 'DESC'); + $query->range(0, 1); + + for ($i = 1; $i <= static::MAX_DEPTH && $original["p$i"]; $i++) { + $query->condition("p$i", $original["p$i"]); + } + + $max_depth = $this->safeExecuteSelect($query)->fetchField(); + + return ($max_depth > $original['depth']) ? $max_depth - $original['depth'] : 0; + } + + /** + * Sets the materialized path field values based on the parent. + * + * @param array $fields + * The menu link. + * @param array $parent + * The parent menu link. + */ + protected function setParents(array &$fields, array $parent) { + $fields['depth'] = $parent['depth'] + 1; + $i = 1; + while ($i < $fields['depth']) { + $p = 'p' . $i++; + $fields[$p] = $parent[$p]; + } + $p = 'p' . $i++; + // The parent (p1 - p9) corresponding to the depth always equals the mlid. + $fields[$p] = $fields['mlid']; + while ($i <= static::MAX_DEPTH) { + $p = 'p' . $i++; + $fields[$p] = 0; + } + } + + /** + * Moves the link's children using the query fields value and original values. + * + * @param array $fields + * The changed menu link. + * @param array $original + * The original menu link. + */ + protected function moveChildren($fields, $original) { + $query = $this->connection->update($this->table, $this->options); + + $query->fields(array('menu_name' => $fields['menu_name'])); + + $expressions = array(); + for ($i = 1; $i <= $fields['depth']; $i++) { + $expressions[] = array("p$i", ":p_$i", array(":p_$i" => $fields["p$i"])); + } + $j = $original['depth'] + 1; + while ($i <= $this->maxDepth() && $j <= $this->maxDepth()) { + $expressions[] = array('p' . $i++, 'p' . $j++, array()); + } + while ($i <= $this->maxDepth()) { + $expressions[] = array('p' . $i++, 0, array()); + } + + $shift = $fields['depth'] - $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', $original['menu_name']); + + for ($i = 1; $i <= $this->maxDepth() && $original["p$i"]; $i++) { + $query->condition("p$i", $original["p$i"]); + } + + $query->execute(); + } + + /** + * Loads the parent definition if it exists. + * + * @param array $link + * The link definition to check. + * @param array|FALSE $original + * The original link, or FALSE. + * + * @return array|FALSE + * Returns a definition array, or FALSE if no parent was found. + */ + protected function findParent($link, $original) { + $parent = FALSE; + + // This item is explicitly top-level, skip the rest of the parenting. + if (isset($link['parent']) && empty($link['parent'])) { + return $parent; + } + + // If we have a parent link ID, try to use that. + $candidates = array(); + if (isset($link['parent'])) { + $candidates[] = $link['parent']; + } + elseif (!empty($original['parent']) && $link['menu_name'] == $original['menu_name']) { + $candidates[] = $original['parent']; + } + + // Else, if we have a link hierarchy try to find a valid parent in there. + // @todo - why does this make sense to do at all? + + foreach ($candidates as $id) { + $parent = $this->loadFull($id); + if ($parent) { + break; + } + } + return $parent; + } + + /** + * Set the has_children flag for the link's parent if it has visible children. + * + * @param array $link + * The link to get a parent ID from. + */ + protected function updateParentalStatus(array $link) { + // If parent is empty, there is nothing to update. + if (!empty($link['parent'])) { + // Check if at least one visible child exists in the table. + $query = $this->connection->select($this->table, $this->options); + $query->addExpression('1'); + $query->range(0, 1); + $query + ->condition('menu_name', $link['menu_name']) + ->condition('parent', $link['parent']) + ->condition('hidden', 0); + + $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0; + $this->connection->update($this->table, $this->options) + ->fields(array('has_children' => $parent_has_children)) + ->condition('id', $link['parent']) + ->execute(); + } + } + + /** + * {@inheritdoc} + */ + public function loadByProperties(array $properties) { + // @todo - only allow loading by plugin definition properties. + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, $this->definitionFields()); + foreach ($properties as $name => $value) { + $query->condition($name, $value); + } + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as &$link) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function loadByRoute($route_name, array $route_parameters = array(), $include_hidden = FALSE) { + asort($route_parameters); + // Since this will be urlencoded, it's safe to store and match against a + // text field. + // @todo - does this make more sense than using the system path? + $param_key = $route_parameters ? UrlHelper::buildQuery($route_parameters) : ''; + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, $this->definitionFields()); + $query->condition('route_name', $route_name); + $query->condition('route_param_key', $param_key); + if (!$include_hidden) { + $query->condition('hidden', 0); + } + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as &$link) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function loadMultiple(array $ids) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, $this->definitionFields()); + $query->condition('id', $ids, 'IN'); + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as &$link) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function load($id) { + $loaded = $this->loadMultiple(array($id)); + return isset($loaded[$id]) ? $loaded[$id] : FALSE; + } + + /** + * Loads all table fields, not just those that are in the plugin definition. + * + * @param string $id + * The menu link ID. + * + * @return array + * The loaded menu link definition or an empty array if not be found. + */ + protected function loadFull($id) { + $loaded = $this->loadFullMultiple(array($id)); + return isset($loaded[$id]) ? $loaded[$id] : array(); + } + + /** + * Loads multiple menu link definitions by ID. + * + * @param array $ids + * The IDs to load. + * + * @return array + * The loaded menu link definitions. + */ + protected function loadFullMultiple(array $ids) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table); + $query->condition('id', $ids, 'IN'); + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as &$link) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + } + return $loaded; + } + + /** + * {@inheritdoc} + */ + public function getRootPathIds($id) { + $subquery = $this->connection->select($this->table, $this->options); + // @todo - consider making this dynamic based on static::MAX_DEPTH + // or from the schema if that is generated using static::MAX_DEPTH. + $subquery->fields($this->table, array('p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9')); + $subquery->condition('id', $id); + $result = current($subquery->execute()->fetchAll(\PDO::FETCH_ASSOC)); + $ids = array_filter($result); + if ($ids) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, array('id')); + $query->orderBy('depth', 'DESC'); + $query->condition('mlid', $ids, 'IN'); + // @todo - cache this result in memory if we find it's being used more + // than once per page load. + return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); + } + return array(); + } + + /** + * {@inheritdoc} + */ + public function getExpanded($menu_name, array $parents) { + // @todo - go back to tracking in state or some other way + // which menus have expanded links? + do { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, array('id')); + $query->condition('menu_name', $menu_name); + $query->condition('expanded', 1); + $query->condition('has_children', 1); + $query->condition('hidden', 0); + $query->condition('parent', $parents, 'IN'); + $query->condition('id', $parents, 'NOT IN'); + $result = $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); + $parents += $result; + } while (!empty($result)); + return $parents; + } + + /** + * Saves menu links recursively. + */ + protected function saveRecursive($id, &$children, &$links) { + + if (!empty($links[$id]['parent']) && empty($links[$links[$id]['parent']])) { + // Invalid parent ID, so remove it. + $links[$id]['parent'] = ''; + } + $this->save($links[$id]); + + if (!empty($children[$id])) { + foreach ($children[$id] as $next_id) { + $this->saveRecursive($next_id, $children, $links); + } + } + // Remove processed link names so we can find stragglers. + unset($children[$id]); + } + + /** + * {@inheritdoc} + */ + public function loadTree($menu_name, array $parameters = array()) { + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table); + for ($i = 1; $i <= $this->maxDepth(); $i++) { + $query->orderBy('p' . $i, 'ASC'); + } + + $query->condition('menu_name', $menu_name); + + if (!empty($parameters['expanded'])) { + $query->condition('parent', $parameters['expanded'], 'IN'); + } + elseif (!empty($parameters['only_active_trail'])) { + $query->condition('id', $parameters['active_trail'], 'IN'); + } + $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : NULL); + if ($min_depth) { + $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 (!empty($parameters['conditions'])) { + // Only allow conditions that are testing definition fields. + $parameters['conditions'] = array_intersect_key($parameters['conditions'], array_flip($this->definitionFields())); + foreach ($parameters['conditions'] as $column => $value) { + $query->condition($column, $value); + } + } + $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); + $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC); + if (!isset($min_depth)) { + $first_link = reset($links); + if ($first_link) { + $min_depth = $first_link['depth']; + } + } + $tree = $this->doBuildTreeData($links, $active_trail, $min_depth); + return $tree; + } + + /** + * {@inheritdoc} + */ + public function loadSubtree($id, $max_relative_depth = NULL) { + $tree = array(); + $root = $this->loadFull($id); + if (!$root) { + return $tree; + } + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table); + for ($i = 1; $i <= $this->maxDepth(); $i++) { + $query->orderBy('p' . $i, 'ASC'); + } + $query->condition('hidden', 0); + $query->condition('menu_name', $root['menu_name']); + for ($i = 1; $i <= $root['depth']; $i++) { + $query->condition("p$i", $root["p$i"]); + } + if (!empty($max_relative_depth)) { + $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<='); + } + $links = $this->safeExecuteSelect($query)->fetchAll(\PDO::FETCH_ASSOC); + $tree = $this->doBuildTreeData($links, array(), $root['depth']); + $subtree = current($tree); + return $subtree; + } + + /** + * {@inheritdoc} + */ + public function menuNameInUse($menu_name) { + $query = $this->connection->select($this->table, $this->options); + $query->addField($this->table, 'mlid'); + $query->condition('menu_name', $menu_name); + $query->range(0, 1); + return (bool) $this->safeExecuteSelect($query); + } + + /** + * {@inheritdoc} + */ + public function getMenuNames() { + $query = $this->connection->select($this->table, $this->options); + $query->addField($this->table, 'menu_name'); + $query->distinct(); + return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); + } + + /** + * {@inheritdoc} + */ + public function countMenuLinks($menu_name = NULL) { + $query = $this->connection->select($this->table, $this->options); + if ($menu_name) { + $query->condition('menu_name', $menu_name); + } + return $this->safeExecuteSelect($query->countQuery())->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function getAllChildIds($id) { + $root = $this->loadFull($id); + if (!$root) { + return array(); + } + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, array('id')); + $query->condition('menu_name', $root['menu_name']); + for ($i = 1; $i <= $root['depth']; $i++) { + $query->condition("p$i", $root["p$i"]); + } + // The next p column should not be empty. This excludes the root link. + $query->condition("p$i", 0, '>'); + return $this->safeExecuteSelect($query)->fetchAllKeyed(0, 0); + } + + /** + * {@inheritdoc} + */ + public function loadAllChildLinks($id, $max_relative_depth = NULL) { + $tree = array(); + $root = $this->loadFull($id); + if (!$root || $root['depth'] == $this->maxDepth()) { + return $tree; + } + $query = $this->connection->select($this->table, $this->options); + $query->fields($this->table, $this->definitionFields()); + $query->condition('hidden', 0); + $query->condition('menu_name', $root['menu_name']); + for ($i = 1; $i <= $root['depth']; $i++) { + $query->condition("p$i", $root["p$i"]); + } + // The next p column should not be empty. This excludes the root link. + $query->condition("p$i", 0, '>'); + if (!empty($max_relative_depth)) { + $query->condition('depth', (int) $root['depth'] + $max_relative_depth, '<='); + } + $loaded = $this->safeExecuteSelect($query)->fetchAllAssoc('id', \PDO::FETCH_ASSOC); + foreach ($loaded as &$link) { + foreach ($this->serializedFields() as $name) { + $link[$name] = unserialize($link[$name]); + } + } + return $loaded; + } + + /** + * 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 + * The fully build tree. + */ + 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. + foreach ($this->serializedFields() as $name) { + $item[$name] = unserialize($item[$name]); + } + // Add the current link to the tree. + $tree[$item['id']] = array( + 'definition' => array_intersect_key($item, array_flip($this->definitionFields())), + 'has_children' => $item['has_children'], + 'in_active_trail' => in_array($item['id'], $parents), + 'below' => array(), + 'depth' => $item['depth'], + ); + for ($i = 1; $i <= $this->maxDepth(); $i++) { + $tree[$item['id']]['p' . $i] = $item['p' . $i]; + } + // 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['id']]['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; + } + + /** + * Checks if the tree table exists and create it if not. + * + * @return bool + * TRUE if the table was created, FALSE otherwise. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If a database error occurs. + */ + protected function ensureTableExists() { + try { + if (!$this->connection->schema()->tableExists($this->table)) { + $this->connection->schema()->createTable($this->table, static::schemaDefinition()); + return TRUE; + } + } + catch (SchemaObjectExistsException $e) { + // If another process has already created the config table, attempting to + // recreate it will throw an exception. In this case just catch the + // exception and do nothing. + return TRUE; + } + catch (\Exception $e) { + throw new PluginException($e->getMessage(), NULL, $e); + } + return FALSE; + } + + /** + * Helper function to determine serialized fields. + */ + protected function serializedFields() { + // For now, build the list from the schema since it's in active development. + if (empty($this->serializedFields)) { + $schema = static::schemaDefinition(); + foreach ($schema['fields'] as $name => $field) { + if (!empty($field['serialize'])) { + $this->serializedFields[] = $name; + } + } + } + return $this->serializedFields; + } + + /** + * Helper function to determine fields that are part of the plugin definition. + */ + protected function definitionFields() { + return $this->definitionFields; + } + + /** + * Defines the schema for the tree table. + */ + protected static function schemaDefinition() { + $schema = array( + 'description' => 'Contains the menu tree hierarchy.', + '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, + ), + 'id' => array( + 'description' => 'Unique machine name: the plugin ID.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'parent' => array( + 'description' => 'The plugin ID for the parent of this link.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'route_name' => array( + 'description' => 'The machine name of a defined Symfony Route this menu item represents.', + 'type' => 'varchar', + 'length' => 255, + ), + 'route_param_key' => array( + 'description' => 'An encoded string of route parameters for loading by route.', + '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, + ), + 'url' => array( + 'description' => 'The external path this link points to (when not using a route).', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The text displayed for the link.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title_arguments' => array( + 'description' => 'A serialized array of arguments to be passed to t() (if this plugin uses it).', + 'type' => 'blob', + 'size' => 'big', + 'not null' => FALSE, + 'serialize' => TRUE, + ), + 'title_context' => array( + 'description' => 'The translation context for the link title.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'description' => array( + 'description' => 'The description of this link - used for admin pages and title attribute.', + 'type' => 'text', + 'not null' => FALSE, + ), + 'class' => array( + 'description' => 'The class for this link plugin.', + 'type' => 'text', + 'not null' => FALSE, + ), + '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', + 'size' => 'big', + 'not null' => FALSE, + 'serialize' => TRUE, + ), + 'provider' => 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, 0 = a normal, visible link)', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + 'discovered' => array( + 'description' => 'A flag for whether the link was discovered, so can be purged on rebuild', + '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, + ), + 'metadata' => array( + 'description' => 'A serialized array of data that may be used by the plugin instance.', + 'type' => 'blob', + 'size' => 'big', + 'not null' => FALSE, + 'serialize' => TRUE, + ), + 'has_children' => array( + 'description' => 'Flag indicating whether any non-hidden links have this link as a parent (1 = children exist, 0 = no children).', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'small', + ), + 'depth' => array( + 'description' => 'The depth relative to the top level. A link with empty parent will have depth == 1.', + '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 parent link mlid. 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, + ), + 'form_class' => array( + 'description' => 'meh', + 'type' => 'varchar', + 'length' => 255, + ), + ), + 'indexes' => array( + 'menu_parents' => array('menu_name', 'p1', 'p2', 'p3', 'p4', 'p5', 'p6', 'p7', 'p8', 'p9'), + // @todo - test this index for effectiveness. + 'menu_parent_expand_child' => array('menu_name', 'expanded', 'has_children', array('parent', 16)), + 'route_values' => array(array('route_name', 32), array('route_param_key', 16)), + ), + 'primary key' => array('mlid'), + 'unique keys' => array( + 'id' => array('id'), + ), + ); + + return $schema; + } + +} diff --git a/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php new file mode 100644 index 0000000..d90a767 --- /dev/null +++ b/core/lib/Drupal/Core/Menu/MenuTreeStorageInterface.php @@ -0,0 +1,253 @@ +configFactory = $config_factory; + } + + /** + * Helper function to get the config object. + * + * Since this service is injected into all static menu link objects, but + * only used when updating one, avoid actually loading the config when it's + * not needed. + */ + protected function getConfig() { + if (empty($this->config)) { + $this->config = $this->configFactory->get($this->configName); + } + return $this->config; + } + + /** + * {@inheritdoc} + */ + public function reload() { + $this->config = NULL; + $this->configFactory->reset($this->configName); + } + + /** + * {@inheritdoc} + */ + public function loadOverride($id) { + $all_overrides = $this->getConfig()->get('definitions'); + $id = static::encodeId($id); + return $id && isset($all_overrides[$id]) ? $all_overrides[$id] : array(); + } + + /** + * {@inheritdoc} + */ + public function deleteMultipleOverrides(array $ids) { + $all_overrides = $this->getConfig()->get('definitions'); + $save = FALSE; + foreach ($ids as $id) { + $id = static::encodeId($id); + if (isset($all_overrides[$id])) { + unset($all_overrides[$id]); + $save = TRUE; + } + } + if ($save) { + $this->getConfig()->set('definitions', $all_overrides)->save(); + } + return $save; + } + + /** + * {@inheritdoc} + */ + public function deleteOverride($id) { + return $this->deleteMultipleOverrides(array($id)); + } + + /** + * {@inheritdoc} + */ + public function loadMultipleOverrides(array $ids) { + $result = array(); + if ($ids) { + $all_overrides = $this->getConfig()->get('definitions') ?: array(); + foreach ($ids as $id) { + $encoded_id = static::encodeId($id); + if (isset($all_overrides[$encoded_id])) { + $result[$id] = $all_overrides[$encoded_id]; + } + } + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function saveOverride($id, array $definition) { + // Remove unexpected keys. + $expected = array( + 'menu_name' => 1, + 'parent' => 1, + 'weight' => 1, + 'expanded' => 1, + 'hidden' => 1, + ); + $definition = array_intersect_key($definition, $expected); + if ($definition) { + $id = static::encodeId($id); + $all_overrides = $this->getConfig()->get('definitions'); + // Combine with any existing data. + $all_overrides[$id] = $definition + $this->loadOverride($id); + $this->getConfig()->set('definitions', $all_overrides)->save(); + } + return array_keys($definition); + } + + /** + * Encodes the ID by replacing dots with double underscores. + * + * This is done because config schema uses dots for its internal type + * hierarchy. + * + * @param string $id + * The menu plugin ID. + * + * @return string + * The menu plugin ID with double underscore instead of dots. + */ + protected static function encodeId($id) { + return str_replace('.', '__', $id); + } + +} diff --git a/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php new file mode 100644 index 0000000..5f0549b --- /dev/null +++ b/core/lib/Drupal/Core/Menu/StaticMenuLinkOverridesInterface.php @@ -0,0 +1,83 @@ +menuTree = $menu_tree; + } + + /** + * {@inheritdoc} + */ + public function convert($value, $definition, $name, array $defaults, Request $request) { + if ($value) { + try { + return $this->menuTree->createInstance($value); + } + catch (PluginException $e) { + // Suppress the error. + } + } + } + + /** + * {@inheritdoc} + */ + public function applies($definition, $name, Route $route) { + return (!empty($definition['type']) && $definition['type'] === 'menu_link_plugin'); + } + +} diff --git a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php index 5febcd3..9ab429f 100644 --- a/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php +++ b/core/lib/Drupal/Core/Plugin/CachedDiscoveryClearer.php @@ -21,7 +21,7 @@ class CachedDiscoveryClearer { * * @var \Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface[] */ - protected $cachedDiscoveries; + protected $cachedDiscoveries = array(); /** * Adds a plugin manager to the active list. diff --git a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php index 8bfde35..9efdcf2 100644 --- a/core/lib/Drupal/Core/Plugin/PluginManagerPass.php +++ b/core/lib/Drupal/Core/Plugin/PluginManagerPass.php @@ -23,7 +23,9 @@ public function process(ContainerBuilder $container) { $cache_clearer_definition = $container->getDefinition('plugin.cache_clearer'); foreach ($container->getDefinitions() as $service_id => $definition) { if (strpos($service_id, 'plugin.manager.') === 0 || $definition->hasTag('plugin_manager_cache_clear')) { - $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id))); + if (is_subclass_of($definition->getClass(), '\Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface')) { + $cache_clearer_definition->addMethodCall('addCachedDiscovery', array(new Reference($service_id))); + } } } } diff --git a/core/modules/book/book.admin.inc b/core/modules/book/book.admin.inc index 4379582..f13b403 100644 --- a/core/modules/book/book.admin.inc +++ b/core/modules/book/book.admin.inc @@ -5,6 +5,7 @@ * Administration page callbacks for the Book module. */ +use Drupal\book\BookManager; use Drupal\Core\Render\Element; /** @@ -86,7 +87,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 9cc4835..fefe20e 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/comment/comment.module b/core/modules/comment/comment.module index b09bfa8..edbf823 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -182,9 +182,9 @@ function comment_theme() { } /** - * Implements hook_menu_link_defaults_alter() + * Implements hook_menu_links_alter() */ -function comment_menu_link_defaults_alter(&$links) { +function comment_menu_links_alter(&$links) { if (isset($links['node.content_overview'])) { // Add comments to the description for admin/content if any. $links['node.content_overview']['description'] = 'Administer content and comments.'; diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module index 4e29f4c..a80d3ce 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -202,9 +202,9 @@ function content_translation_entity_operation_alter(array &$operations, \Drupal\ } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_alter(). */ -function content_translation_menu_link_defaults_alter(array &$links) { +function content_translation_menu_links_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 5220da7..679ed2d 100644 --- a/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php +++ b/core/modules/content_translation/src/Tests/ContentTranslationSettingsTest.php @@ -48,7 +48,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_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 6333187..4956534 100644 --- a/core/modules/dblog/dblog.module +++ b/core/modules/dblog/dblog.module @@ -37,9 +37,9 @@ function dblog_help($route_name, Request $request) { } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_alter(). */ -function dblog_menu_link_defaults_alter(&$links) { +function dblog_menu_links_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 9da1540..ad011f6 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -40,12 +40,12 @@ function editor_help($route_name, Request $request) { } /** - * Implements hook_menu_link_defaults_alter(). + * Implements hook_menu_links_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_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..9067f38 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' => Url::createFromPath('admin/structure/display-modes/view/add/' . $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' => Url::createFromPath('admin/structure/display-modes/form/add/' . $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 f9389bf..78b6863 100644 --- a/core/modules/help/src/Controller/HelpController.php +++ b/core/modules/help/src/Controller/HelpController.php @@ -106,8 +106,7 @@ public function helpPage($name, Request $request) { 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 57ed784..3973754 100644 --- a/core/modules/language/src/Tests/LanguageConfigSchemaTest.php +++ b/core/modules/language/src/Tests/LanguageConfigSchemaTest.php @@ -19,7 +19,7 @@ class LanguageConfigSchemaTest extends ConfigSchemaTestBase { * * @var array */ - public static $modules = array('language'); + public static $modules = array('language', 'menu_link_content'); /** * A user with administrative permissions. @@ -58,8 +58,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; @@ -70,7 +70,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/menu_link/menu_link.api.php b/core/modules/menu_link/menu_link.api.php deleted file mode 100644 index b4e8892..0000000 --- a/core/modules/menu_link/menu_link.api.php +++ /dev/null @@ -1,165 +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(); - } -} - -/** - * Alter menu links when loaded and before they are rendered. - * - * This hook is only invoked if $menu_link->options['alter'] has been set to a - * non-empty value (e.g., TRUE). This flag should be set using - * hook_menu_link_presave(). - * @ todo The paragraph above is lying! This hasn't been (re)implemented yet. - * - * Implementations of this hook are able to alter any property of the menu link. - * For example, this hook may be used to add a page-specific query string to all - * menu links, or hide a certain link by setting: - * @code - * 'hidden' => 1, - * @endcode - * - * @param array $menu_links - * An array of menu link entities. - * - * @see hook_menu_link_presave() - */ -function hook_menu_link_load($menu_links) { - foreach ($menu_links as $menu_link) { - if ($menu_link->href == 'devel/cache/clear') { - $menu_link->options['query'] = drupal_get_destination(); - } - } -} - - -/** - * Alter the data of a menu link entity before it is created or updated. - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - * - * @see hook_menu_link_load() - */ -function hook_menu_link_presave(\Drupal\menu_link\Entity\MenuLink $menu_link) { - // Make all new admin links hidden (a.k.a disabled). - if (strpos($menu_link->link_path, 'admin') === 0 && $menu_link->isNew()) { - $menu_link->hidden = 1; - } - // Flag a link to be altered by hook_menu_link_load(). - if ($menu_link->link_path == 'devel/cache/clear') { - $menu_link->options['alter'] = TRUE; - } - // Flag a menu link to be altered by hook_menu_link_load(), but only if it is - // derived from a menu router item; i.e., do not alter a custom menu link - // pointing to the same path that has been created by a user. - if ($menu_link->machine_name == 'user.page') { - $menu_link->options['alter'] = TRUE; - } -} - -/** - * Inform modules that a menu link has been created. - * - * This hook is used to notify modules that menu links have been - * created. Contributed modules may use the information to perform - * actions based on the information entered into the menu system. - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - * - * @see hook_menu_link_presave() - * @see hook_menu_link_update() - * @see hook_menu_link_delete() - */ -function hook_menu_link_insert(\Drupal\menu_link\Entity\MenuLink $menu_link) { - // In our sample case, we track menu items as editing sections - // of the site. These are stored in our table as 'disabled' items. - $record['mlid'] = $menu_link->id(); - $record['menu_name'] = $menu_link->menu_name; - $record['status'] = 0; - db_insert('menu_example')->fields($record)->execute(); -} - -/** - * Inform modules that a menu link has been updated. - * - * This hook is used to notify modules that menu items have been - * updated. Contributed modules may use the information to perform - * actions based on the information entered into the menu system. - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - * - * @see hook_menu_link_presave() - * @see hook_menu_link_insert() - * @see hook_menu_link_delete() - */ -function hook_menu_link_update(\Drupal\menu_link\Entity\MenuLink $menu_link) { - // If the parent menu has changed, update our record. - $menu_name = db_query("SELECT menu_name FROM {menu_example} WHERE mlid = :mlid", array(':mlid' => $menu_link->id()))->fetchField(); - if ($menu_name != $menu_link->menu_name) { - db_update('menu_example') - ->fields(array('menu_name' => $menu_link->menu_name)) - ->condition('mlid', $menu_link->id()) - ->execute(); - } -} - -/** - * Inform modules that a menu link has been deleted. - * - * This hook is used to notify modules that menu links have been - * deleted. Contributed modules may use the information to perform - * actions based on the information entered into the menu system. - * - * @param \Drupal\menu_link\Entity\MenuLink $menu_link - * A menu link entity. - * - * @see hook_menu_link_presave() - * @see hook_menu_link_insert() - * @see hook_menu_link_update() - */ -function hook_menu_link_delete(\Drupal\menu_link\Entity\MenuLink $menu_link) { - // Delete the record from our table. - db_delete('menu_example') - ->condition('mlid', $menu_link->id()) - ->execute(); -} - -/** - * @} 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 ac3c408..0000000 --- a/core/modules/menu_link/menu_link.module +++ /dev/null @@ -1,227 +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, array $attributes, array $context) { - // Custom breadcrumb behavior for editing menu links, we append a link to - // the menu in which the link is found. - if (!empty($attributes[RouteObjectInterface::ROUTE_NAME]) && $attributes[RouteObjectInterface::ROUTE_NAME] == 'menu_ui.link_edit' && !empty($attributes['menu_link'])) { - $menu_link = $attributes['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 d446643..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_clear_page_cache(); - } - - /** - * {@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(); - - // Now clear the cache. - _menu_clear_page_cache(); - } - - /** - * {@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 d428734..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)->langcode : $menu_link->langcode); - $language_show = FALSE; - } - - $form['langcode'] = array( - '#type' => 'language_select', - '#title' => t('Language'), - '#languages' => Language::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 slave server temporarily. - db_ignore_slave(); - 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('menu_links', $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 bd77a45..0000000 --- a/core/modules/menu_link/tests/src/MenuTreeTest.php +++ /dev/null @@ -1,554 +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} - */ - public static function getInfo() { - return array( - 'name' => 'Tests \Drupal\menu_link\MenuTree', - 'description' => '', - 'group' => 'Menu', - ); - } - - /** - * {@inheritdoc} - */ - protected function setUp() { - $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') - ->disableOriginalConstructor() - ->getMock(); - $this->cacheBackend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); - $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); - $this->requestStack = new RequestStack(); - $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); - $this->entityQueryFactory = $this->getMockBuilder('Drupal\Core\Entity\Query\QueryFactory') - ->disableOriginalConstructor() - ->getMock(); - $this->state = $this->getMock('Drupal\Core\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.info.yml b/core/modules/menu_link_content/menu_link_content.info.yml new file mode 100644 index 0000000..4c69873 --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.info.yml @@ -0,0 +1,6 @@ +name: 'Menu Links Content' +type: module +description: 'Allows administrators to create custom links' +package: Core +version: VERSION +core: 8.x 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..606ca6a --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.install @@ -0,0 +1,19 @@ +fetchCol(); + $menu_tree = \Drupal::menuTree(); + foreach ($uuids as $uuid) { + // Manually build the plugin ID, and remove it from the menu tree. + $menu_tree->deleteLink("menu_link_content:$uuid", FALSE); + } +} diff --git a/core/modules/menu_link_content/menu_link_content.local_tasks.yml b/core/modules/menu_link_content/menu_link_content.local_tasks.yml new file mode 100644 index 0000000..8195236 --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.local_tasks.yml @@ -0,0 +1,4 @@ +menu_link_content.link_edit: + route_name: menu_link_content.link_edit + base_route: menu_link_content.link_edit + title: Edit diff --git a/core/modules/menu_link_content/menu_link_content.module b/core/modules/menu_link_content/menu_link_content.module new file mode 100644 index 0000000..6648280 --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.module @@ -0,0 +1,17 @@ +getStorage('menu_link_content'); + $menu_links = $storage->loadByProperties(array('menu_name' => $menu->id())); + $storage->delete($menu_links); +} diff --git a/core/modules/menu_link_content/menu_link_content.routing.yml b/core/modules/menu_link_content/menu_link_content.routing.yml new file mode 100644 index 0000000..f96b22d --- /dev/null +++ b/core/modules/menu_link_content/menu_link_content.routing.yml @@ -0,0 +1,23 @@ +menu_link_content.link_add: + path: '/admin/structure/menu/manage/{menu}/add' + defaults: + _content: '\Drupal\menu_link_content\Controller\MenuController::addLink' + _title: 'Add menu link' + requirements: + _entity_create_access: 'menu_link_content' + +menu_link_content.link_edit: + path: '/admin/structure/menu/item/{menu_link_content}/edit' + defaults: + _entity_form: 'menu_link_content.default' + _title: 'Edit menu link' + requirements: + _entity_access: 'menu_link_content.update' + +menu_link_content.link_delete: + path: '/admin/structure/menu/item/{menu_link_content}/delete' + defaults: + _entity_form: 'menu_link_content.delete' + _title: 'Delete menu link' + requirements: + _entity_access: 'menu_link_content.delete' diff --git a/core/modules/menu_link_content/src/Controller/MenuController.php b/core/modules/menu_link_content/src/Controller/MenuController.php new file mode 100644 index 0000000..7ffa9f8 --- /dev/null +++ b/core/modules/menu_link_content/src/Controller/MenuController.php @@ -0,0 +1,34 @@ +entityManager()->getStorage('menu_link_content')->create(array( + 'id' => '', + 'parent' => '', + 'menu_name' => $menu->id(), + 'bundle' => 'menu_link_content', + )); + return $this->entityFormBuilder()->getForm($menu_link); + } + +} diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php new file mode 100644 index 0000000..c98ef42 --- /dev/null +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php @@ -0,0 +1,382 @@ +insidePlugin = TRUE; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + return $this->get('title')->value; + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return $this->get('route_name')->value; + } + + /** + * {@inheritdoc} + */ + public function getRouteParameters() { + return $this->get('route_parameters')->first()->getValue(); + } + + /** + * {@inheritdoc} + */ + public function setRouteParameters(array $route_parameters) { + $this->set('route_parameters', array($route_parameters)); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getUrl() { + return $this->get('url')->value ?: NULL; + } + + /** + * {@inheritdoc} + */ + public function getUrlObject() { + if ($route_name = $this->getRouteName()) { + $url = new Url($route_name, $this->getRouteParameters(), $this->getOptions()); + } + else { + $path = $this->getUrl(); + if (isset($path)) { + $url = Url::createFromPath($path); + } + else { + $url = new Url(''); + } + } + + return $url; + } + + /** + * {@inheritdoc} + */ + public function getMenuName() { + return $this->get('menu_name')->value; + } + + /** + * {@inheritdoc} + */ + public function getOptions() { + return $this->get('options')->first()->getValue(); + } + + /** + * {@inheritdoc} + */ + public function setOptions(array $options) { + $this->set('options', array($options)); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->get('description')->value; + } + + /** + * {@inheritdoc} + */ + public function getPluginId() { + return 'menu_link_content:' . $this->uuid(); + } + + /** + * {@inheritdoc} + */ + public function isHidden() { + return (bool) $this->get('hidden')->value; + } + + /** + * {@inheritdoc} + */ + public function isExpanded() { + return (bool) $this->get('expanded')->value; + } + + /** + * {@inheritdoc} + */ + public function getParentId() { + return $this->get('parent')->value; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return (int) $this->get('weight')->value; + } + + /** + * Builds up the menu link plugin definition for this entity. + * + * @return array + * The plugin definition corresponding to this entity. + * + * @see \Drupal\Core\Menu\MenuLinkTree::$defaults + */ + protected function getMenuDefinition() { + $definition = array(); + $definition['class'] = 'Drupal\menu_link_content\Plugin\Menu\MenuLinkContent'; + $definition['menu_name'] = $this->getMenuName(); + $definition['route_name'] = $this->getRouteName(); + $definition['route_parameters'] = $this->getRouteParameters(); + $definition['url'] = $this->getUrl(); + $definition['options'] = $this->getOptions(); + // Don't bother saving title and description strings, since they are never + // used. + $definition['title'] = $this->getTitle(); + $definition['description'] = $this->getDescription(); + $definition['weight'] = $this->getWeight(); + $definition['id'] = $this->getPluginId(); + $definition['metadata'] = array('entity_id' => $this->id()); + $definition['form_class'] = '\Drupal\menu_link_content\Form\MenuLinkContentForm'; + $definition['hidden'] = $this->isHidden() ? 1 : 0; + $definition['expanded'] = $this->isExpanded() ? 1 : 0; + $definition['provider'] = 'menu_link_content'; + $definition['discovered'] = 0; + $definition['parent'] = $this->getParentId(); + + return $definition; + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ + $menu_tree = \Drupal::menuTree(); + + // The menu link can just be updated if there is already an menu link entry + // on both entity and menu tree level. + if ($update && $menu_tree->getDefinition($this->getPluginId())) { + // When the entity is saved via a plugin instance, we should not call + // the menu tree manager to update the definition a second time. + if (!$this->insidePlugin) { + $menu_tree->updateLink($this->getPluginId(), $this->getMenuDefinition(), FALSE); + } + } + else { + $menu_tree->createLink($this->getPluginId(), $this->getMenuDefinition()); + } + } + + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + parent::preDelete($storage, $entities); + + foreach ($entities as $menu_link) { + /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $menu_link */ + \Drupal::menuTree()->deleteLink($menu_link->getPluginId(), FALSE); + } + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields['id'] = FieldDefinition::create('integer') + ->setLabel(t('Content menu link ID')) + ->setDescription(t('The menu link ID.')) + ->setReadOnly(TRUE) + ->setSetting('unsigned', TRUE); + + $fields['uuid'] = FieldDefinition::create('uuid') + ->setLabel(t('UUID')) + ->setDescription(t('The content menu link UUID.')) + ->setReadOnly(TRUE); + + $fields['bundle'] = FieldDefinition::create('string') + ->setLabel(t('Bundle')) + ->setDescription(t('The content menu link bundle.')) + ->setSetting('max_length', EntityTypeInterface::BUNDLE_MAX_LENGTH) + ->setReadOnly(TRUE); + + $fields['title'] = FieldDefinition::create('string') + ->setLabel(t('Menu link title')) + ->setDescription(t('The text to be used for this link in the menu.')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setSettings(array( + 'default_value' => '', + 'max_length' => 255, + )) + ->setDisplayOptions('view', array( + 'label' => 'hidden', + 'type' => 'string', + 'weight' => -5, + )) + ->setDisplayOptions('form', array( + 'type' => 'string', + 'weight' => -5, + )) + ->setDisplayConfigurable('form', TRUE); + + $fields['description'] = FieldDefinition::create('string') + ->setLabel(t('Description')) + ->setDescription(t('Shown when hovering over the menu link.')) + ->setTranslatable(TRUE) + ->setSettings(array( + 'default_value' => '', + 'max_length' => 255, + )) + ->setDisplayOptions('view', array( + 'label' => 'hidden', + 'type' => 'string', + 'weight' => 0, + )) + ->setDisplayOptions('form', array( + 'type' => 'string', + 'weight' => 0, + )); + + $fields['menu_name'] = FieldDefinition::create('string') + ->setLabel(t('Menu name')) + ->setDescription(t('The menu name. All links with the same menu name (such as "tools") are part of the same menu.')) + ->setSetting('default_value', 'tools'); + + // @todo use a link field in the end? see https://drupal.org/node/2235457 + $fields['route_name'] = FieldDefinition::create('string') + ->setLabel(t('Route name')) + ->setDescription(t('The machine name of a defined Symfony Route this menu item represents.')); + + $fields['route_parameters'] = FieldDefinition::create('map') + ->setLabel(t('Route parameters')) + ->setDescription(t('A serialized array of route parameters of this menu link.')); + + $fields['url'] = FieldDefinition::create('string') + ->setLabel(t('External link url')) + ->setDescription(t('The url of the link, in case you have an external link.')); + + $fields['options'] = FieldDefinition::create('map') + ->setLabel(t('Options')) + ->setDescription(t('A serialized array of options to be passed to the url() or l() function, such as a query string or HTML attributes.')) + ->setSetting('default_value', array()); + + $fields['external'] = FieldDefinition::create('boolean') + ->setLabel(t('External')) + ->setDescription(t('A flag to indicate if the link points to a full URL starting with a protocol, like http:// (1 = external, 0 = internal).')) + ->setSetting('default_value', 0); + + $fields['expanded'] = FieldDefinition::create('boolean') + ->setLabel(t('Expanded')) + ->setDescription(t('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).')) + ->setSetting('default_value', 0) + ->setDisplayOptions('view', array( + 'label' => 'hidden', + 'type' => 'boolean', + 'weight' => 0, + )) + ->setDisplayOptions('form', array( + 'type' => 'options_onoff', + 'weight' => 0, + )); + + $fields['hidden'] = FieldDefinition::create('boolean') + ->setLabel(t('Hidden')) + ->setDescription(t('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).')) + ->setSetting('default_value', 0); + + $fields['weight'] = FieldDefinition::create('integer') + ->setLabel(t('Weight')) + ->setDescription(t('Link weight among links in the same menu at the same depth.')) + ->setSetting('default_value', 0) + ->setDisplayOptions('view', array( + 'label' => 'hidden', + 'type' => 'integer', + 'weight' => 0, + )) + ->setDisplayOptions('form', array( + 'type' => 'integer', + 'weight' => 0, + )); + + $fields['langcode'] = FieldDefinition::create('language') + ->setLabel(t('Language code')) + ->setDescription(t('The node language code.')); + + $fields['parent'] = FieldDefinition::create('string') + ->setLabel(t('Parent menu link ID')) + ->setDescription(t('The parent menu link ID.')); + + return $fields; + } + +} diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php new file mode 100644 index 0000000..35a24a7 --- /dev/null +++ b/core/modules/menu_link_content/src/Entity/MenuLinkContentInterface.php @@ -0,0 +1,147 @@ +t('Are you sure you want to delete the custom menu link %item?', array('%item' => $this->entity->getTitle())); + } + + /** + * {@inheritdoc} + */ + public function getCancelRoute() { + return new Url('menu_ui.menu_edit', array('menu' => $this->entity->getMenuName())); + } + + /** + * {@inheritdoc} + */ + public function submit(array $form, array &$form_state) { + $storage = $this->entityManager->getStorage('menu_link_content'); + $storage->delete(array($this->entity)); + $t_args = array('%title' => $this->entity->getTitle()); + drupal_set_message($this->t('The menu link %title has been deleted.', $t_args)); + watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE); + $form_state['redirect_route'] = array( + 'route_name' => '', + ); + } + +} diff --git a/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php new file mode 100644 index 0000000..272ac64 --- /dev/null +++ b/core/modules/menu_link_content/src/Form/MenuLinkContentForm.php @@ -0,0 +1,400 @@ +menuTree = $menu_tree; + $this->pathAliasManager = $alias_manager; + $this->moduleHandler = $module_handler; + $this->requestContext = $request_context; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('menu.link_tree'), + $container->get('path.alias_manager'), + $container->get('module_handler'), + $container->get('router.request_context'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function setMenuLinkInstance(MenuLinkInterface $menu_link) { + // Load the entity for the entity form. + $metadata = $menu_link->getMetaData(); + if (!empty($metadata['entity_id'])) { + $this->entity = $this->entityManager->getStorage('menu_link_content')->load($metadata['entity_id']); + } + else { + // Fallback to the loading by the uuid. + $links = $this->entityManager->getStorage('menu_link_content')->loadByProperties(array('uuid' => $menu_link->getDerivativeId())); + $this->entity = reset($links); + } + } + + /** + * {@inheritdoc} + */ + public function buildEditForm(array &$form, array &$form_state) { + $this->setOperation('default'); + $this->init($form_state); + + return $this->form($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateEditForm(array &$form, array &$form_state) { + $this->doValidate($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 $this->menuTree->createInstance($this->entity->getPluginId()); + } + + /** + * Break up a user-entered URL or path into all the relevant parts. + * + * @param string $url + * The user-entered URL or path. + * + * @return array + * The extracted parts. + */ + protected function extractUrl($url) { + $extracted = UrlHelper::parse($url); + $external = UrlHelper::isExternal($url); + if ($external) { + $extracted['url'] = $extracted['path']; + $extracted['route_name'] = NULL; + $extracted['route_parameters'] = array(); + } + else { + $extracted['url'] = ''; + // If the path doesn't match a Drupal path, the route should end up empty. + $extracted['route_name'] = NULL; + $extracted['route_parameters'] = array(); + try { + // Find the route_name. + $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']); + $url_obj = Url::createFromPath($normal_path); + $extracted['route_name'] = $url_obj->getRouteName(); + $extracted['route_parameters'] = $url_obj->getRouteParameters(); + } + catch (MatchingRouteNotFoundException $e) { + // The path doesn't match a Drupal path. + } + catch (ParamNotConvertedException $e) { + // A path like node/99 matched a route, but the route parameter was + // invalid (e.g. node with ID 99 does not exist). + } + } + return $extracted; + } + + /** + * {@inheritdoc} + */ + public function extractFormValues(array &$form, array &$form_state) { + + $new_definition = array(); + $new_definition['expanded'] = !empty($form_state['values']['expanded']) ? 1 : 0; + $new_definition['hidden'] = empty($form_state['values']['enabled']) ? 1 : 0; + list($menu_name, $parent) = explode(':', $form_state['values']['menu_parent'], 2); + if (!empty($menu_name)) { + $new_definition['menu_name'] = $menu_name; + } + $new_definition['parent'] = isset($parent) ? $parent : ''; + + $extracted = $this->extractUrl($form_state['values']['url']); + $new_definition['url'] = $extracted['url']; + $new_definition['route_name'] = $extracted['route_name']; + $new_definition['route_parameters'] = $extracted['route_parameters']; + $new_definition['options'] = array(); + if ($extracted['query']) { + $new_definition['options']['query'] = $extracted['query']; + } + if ($extracted['fragment']) { + $new_definition['options']['fragment'] = $extracted['fragment']; + } + $new_definition['title'] = $form_state['values']['title'][0]['value']; + $new_definition['description'] = $form_state['values']['description'][0]['value']; + $new_definition['weight'] = (int) $form_state['values']['weight'][0]['value']; + + return $new_definition; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, array &$form_state) { + + $form = parent::form($form, $form_state); + + $language_configuration = $this->moduleHandler->invoke('language', 'get_default_configuration', array('menu_link_content', 'menu_link_content')); + if ($this->entity->isNew()) { + $default_language = isset($language_configuration['langcode']) ? $language_configuration['langcode'] : $this->languageManager->getDefaultLanguage()->id; + } + else { + $default_language = $this->entity->getUntranslated()->language()->id; + } + $form['langcode'] = array( + '#title' => t('Language'), + '#type' => 'language_select', + '#default_value' => $default_language, + '#languages' => Language::STATE_ALL, + '#access' => !empty($language_configuration['language_show']), + ); + + $form['enabled'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Enable menu link'), + '#description' => $this->t('Menu links that are not enabled will not be listed in any menu.'), + '#default_value' => !$this->entity->isHidden(), + ); + + // @TODO For whatever reason the expanded widget is not autogenerated. + $form['expanded'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Show as expanded'), + '#description' => $this->t('If selected and this menu link has children, the menu will always appear expanded.'), + '#default_value' => $this->entity->isExpanded(), + ); + + // We always show the internal path here. + $url = $this->getEntity()->getUrlObject(); + if ($url->isExternal()) { + $default_value = $url->toString(); + } + elseif ($url->getRouteName() == '') { + // The default route for new entities is , but we just want an + // empty form field. + $default_value = $this->getEntity()->isNew() ? '' : ''; + } + else { + // @TODO Maybe support options in + // \Drupal\Core\Routing\UrlGeneratorInterface::getInternalPath(). + // or a helper method to render just options? + $default_value = $url->getInternalPath(); + $options = $url->getOptions(); + if (isset($options['query'])) { + $default_value .= $options['query'] ? ('?' . UrlHelper::buildQuery($options['query'])) : ''; + } + if (isset($options['fragment']) && $options['fragment'] !== '') { + $default_value .= '#' . $options['fragment']; + } + } + $form['url'] = array( + '#title' => $this->t('Link path'), + '#type' => 'textfield', + '#description' => $this->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')), + '#default_value' => $default_value, + '#required' => TRUE, + ); + + $options = $this->menuTree->getParentSelectOptions($this->entity->getPluginId()); + $menu_parent = $this->entity->getMenuName() . ':' . $this->entity->getParentId(); + + if (!isset($options[$menu_parent])) { + // Put it at the top level in the current menu. + $menu_parent = $this->entity->getMenuName() . ':'; + } + $form['menu_parent'] = array( + '#type' => 'select', + '#title' => $this->t('Parent link'), + '#options' => $options, + '#default_value' => $menu_parent, + '#description' => $this->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' => $this->menuTree->maxDepth())), + '#attributes' => array('class' => array('menu-title-select')), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, array &$form_state) { + $element = parent::actions($form, $form_state); + $element['submit']['#button_type'] = 'primary'; + $element['delete']['#access'] = $this->entity->access('delete'); + + return $element; + } + + /** + * {@inheritdoc} + */ + public function validate(array $form, array &$form_state) { + $this->doValidate($form, $form_state); + + parent::validate($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function buildEntity(array $form, array &$form_state) { + /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $entity */ + $entity = parent::buildEntity($form, $form_state); + $new_definition = $this->extractFormValues($form, $form_state); + + $entity->parent->value = $new_definition['parent']; + $entity->menu_name->value = $new_definition['menu_name']; + $entity->hidden->value = (bool) $new_definition['hidden']; + $entity->expanded->value = $new_definition['expanded']; + + $entity->url->value = $new_definition['url']; + $entity->route_name->value = $new_definition['route_name']; + $entity->setRouteParameters($new_definition['route_parameters']); + $entity->setOptions($new_definition['options']); + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, array &$form_state) { + // The entity is rebuilt in parent::submit(). + $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_link_content.link_edit', + 'route_parameters' => array( + 'menu_link_content' => $menu_link->id(), + ), + ); + } + else { + drupal_set_message(t('There was an error saving the menu link.'), 'error'); + $form_state['rebuild'] = TRUE; + } + } + + /** + * Validates the form, both on the menu link edit and content menu link form. + */ + protected function doValidate(array $form, array &$form_state) { + $extracted = $this->extractUrl($form_state['values']['url']); + + // If both url and route_nae are empty, the entered value is not valid. + $valid = FALSE; + if ($extracted['url']) { + // This is an external link. + $valid = TRUE; + } + elseif ($extracted['route_name']) { + // Users are not allowed to add a link to a page they cannot access. + $valid = \Drupal::service('access_manager')->checkNamedRoute($extracted['route_name'], $extracted['route_parameters'], \Drupal::currentUser()); + } + if (!$valid) { + $this->setFormError('url', $form_state, $this->t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $form_state['values']['url']))); + } + elseif ($extracted['route_name']) { + // The user entered a Drupal path. + $normal_path = $this->pathAliasManager->getPathByAlias($extracted['path']); + if ($extracted['path'] != $normal_path) { + drupal_set_message($this->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' => $extracted['path'], + '%normal_path' => $normal_path, + ))); + } + } + } + +} diff --git a/core/modules/menu_link_content/src/MenuLinkContentAccessController.php b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php new file mode 100644 index 0000000..d895478 --- /dev/null +++ b/core/modules/menu_link_content/src/MenuLinkContentAccessController.php @@ -0,0 +1,55 @@ +hasPermission('administer menu') && ($entity->getUrl() || $this->accessManager()->checkNamedRoute($entity->getRouteName(), $entity->getRouteParameters(), $account)); + + case 'delete': + return !$entity->isNew() && $account->hasPermission('administer menu'); + } + } + + /** + * Returns the access manager. + * + * @return \Drupal\Core\Access\AccessManager + * The route provider. + */ + protected function accessManager() { + if (!$this->accessManager) { + $this->accessManager = \Drupal::service('access_manager'); + } + return $this->accessManager; + } +} diff --git a/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php new file mode 100644 index 0000000..15c5370 --- /dev/null +++ b/core/modules/menu_link_content/src/Plugin/Menu/MenuLinkContent.php @@ -0,0 +1,236 @@ + 1, + 'parent' => 1, + 'weight' => 1, + 'expanded' => 1, + 'hidden' => 1, + 'title' => 1, + 'description' => 1, + 'route_name' => 1, + 'route_parameters' => 1, + 'url' => 1, + 'options' => 1, + ); + + /** + * The menu link content entity connected to this plugin instance. + * + * @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface + */ + protected $entity; + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructs a new MenuLinkContent. + * + * @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\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + if (!empty($this->pluginDefinition['metadata']['entity_id'])) { + $entity_id = $this->pluginDefinition['metadata']['entity_id']; + static::$entityIdsToLoad[$entity_id] = $entity_id; + } + + $this->entityManager = $entity_manager; + $this->langaugeManager = $language_manager; + } + + /** + * {@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('language_manager') + ); + } + + /** + * Loads the entity associated with this menu link. + * + * @return \Drupal\menu_link_content\Entity\MenuLinkContentInterface + * The menu link content entity. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If the entity ID and uuid are both invalid or missing. + */ + protected function getEntity() { + if (empty($this->entity)) { + $entity = NULL; + $storage = $this->entityManager->getStorage('menu_link_content'); + if (!empty($this->pluginDefinition['metadata']['entity_id'])) { + $entity_id = $this->pluginDefinition['metadata']['entity_id']; + static::$entityIdsToLoad[$entity_id] = $entity_id; + $entities = $storage->loadMultiple(array_values(static::$entityIdsToLoad)); + $entity = isset($entities[$entity_id]) ? $entities[$entity_id] : NULL; + static::$entityIdsToLoad = array(); + } + else { + // Fallback to the loading by the uuid. + $uuid = $this->getDerivativeId(); + $links = $storage->loadByProperties(array('uuid' => $uuid)); + $entity = reset($links); + } + if (!$entity) { + throw new PluginException("Invalid entity ID or uuid"); + } + // Clone the entity object to avoid tampering with the static cache. + $this->entity = clone $entity; + $this->entity = $this->entityManager->getTranslationFromContext($this->entity); + $this->entity->setInsidePlugin(); + } + return $this->entity; + } + + /** + * {@inheritdoc} + */ + public function getTitle() { + // We only need to get the title from the actual entity if it may be + // a translation based on the current language context. This can only + // happen if the site configured to be multilingual. + if ($this->langaugeManager->isMultilingual()) { + return $this->getEntity()->getTitle(); + } + return $this->pluginDefinition['title']; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + if ($this->langaugeManager->isMultilingual()) { + return $this->getEntity()->getDescription(); + } + return $this->pluginDefinition['description']; + } + + /** + * {@inheritdoc} + */ + public function getDeleteRoute() { + return array( + 'route_name' => 'menu_link_content.link_delete', + 'route_parameters' => array('menu_link_content' => $this->getEntity()->id()), + ); + } + + /** + * {@inheritdoc} + */ + public function getEditRoute() { + return array( + 'route_name' => 'menu_link_content.link_edit', + 'route_parameters' => array('menu_link_content' => $this->getEntity()->id()), + ); + } + + /** + * {@inheritdoc} + */ + public function getTranslateRoute() { + $entity_type = $this->getEntity()->getEntityType()->id(); + return array( + 'route_name' => 'content_translation.translation_overview_' . $entity_type, + 'route_parameters' => array( + $entity_type => $this->getEntity()->id(), + ), + ); + } + + /** + * {@inheritdoc} + */ + public function updateLink(array $new_definition_values, $persist) { + $overrides = array_intersect_key($new_definition_values, $this->overrideAllowed); + // Update the definition. + $this->pluginDefinition = $overrides + $this->getPluginDefinition(); + if ($persist) { + $entity = $this->getEntity(); + foreach ($overrides as $key => $value) { + $entity->{$key}->value = $value; + } + $this->entityManager->getStorage('menu_link_content')->save($entity); + } + + return $this->pluginDefinition; + } + + /** + * {@inheritdoc} + */ + public function isDeletable() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function isTranslatable() { + return $this->getEntity()->isTranslatable(); + } + + /** + * {@inheritdoc} + */ + public function deleteLink() { + // @todo: Flag this call if possible so we don't call the menu tree manager. + $this->getEntity()->delete(); + } + +} diff --git a/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php b/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php new file mode 100644 index 0000000..7bbcf80 --- /dev/null +++ b/core/modules/menu_link_content/src/Tests/MenuLinkContentUITest.php @@ -0,0 +1,63 @@ + 'Menu link content translation UI', + 'description' => 'Tests the basic menu link content translation UI.', + 'group' => 'Menu', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->entityTypeId = 'menu_link_content'; + $this->bundle = 'menu_link_content'; + $this->fieldName = 'title'; + parent::setUp(); + } + + /** + * {@inheritdoc} + */ + protected function getTranslatorPermissions() { + return array_merge(parent::getTranslatorPermissions(), array('administer menu')); + } + + /** + * {@inheritdoc} + */ + protected function createEntity($values, $langcode, $bundle_name = NULL) { + $values['menu_name'] = 'tools'; + $values['route_name'] = 'menu_ui.overview_page'; + $values['title'] = 'Test title'; + + return parent::createEntity($values, $langcode, $bundle_name); + } + +} diff --git a/core/modules/menu_ui/menu_ui.admin.inc b/core/modules/menu_ui/menu_ui.admin.inc index 0d7a0fd..00d3e58 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\Core\Menu\MenuLinkTreeInterface; 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,17 +31,18 @@ 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', @@ -46,8 +51,8 @@ function theme_menu_overview_form($variables) { $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[] = array('data' => drupal_render($element['enabled']), 'class' => array('checkbox', 'menu-enabled')); + $row[] = 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::service('menu.link_tree')->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..9dd90da 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 \ No newline at end of file diff --git a/core/modules/menu_ui/menu_ui.local_actions.yml b/core/modules/menu_ui/menu_ui.local_actions.yml index 0834821..af8716f 100644 --- a/core/modules/menu_ui/menu_ui.local_actions.yml +++ b/core/modules/menu_ui/menu_ui.local_actions.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 3639d26..0ca4ac1 100644 --- a/core/modules/menu_ui/menu_ui.module +++ b/core/modules/menu_ui/menu_ui.module @@ -11,15 +11,14 @@ * URLs to be added to the main site navigation menu. */ -use Drupal\Core\Entity\EntityInterface; use Drupal\block\BlockPluginInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Render\Element; +use Drupal\node\NodeInterface; 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; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\HttpFoundation\Request; /** @@ -75,31 +74,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(). @@ -157,7 +136,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()); + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu.link_tree'); + $menu_tree->deleteLinksInMenu($menu->id()); // Remove menu from active menus variable. $config = \Drupal::config('system.menu'); @@ -186,113 +167,18 @@ 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])) { - $build['#contextual_links']['menu'] = array( - 'route_parameters' => array('menu' => $menu_name), - ); + if (isset($menus[$menu_name]) && isset($build['content'])) { + foreach (Element::children($build['content']) as $key) { + $build['#contextual_links']['menu'] = array( + 'route_parameters' => array('menu' => $menu_name), + ); + } } } @@ -310,7 +196,7 @@ function menu_ui_node_update(EntityInterface $node) { menu_ui_node_save($node); } -/** + /** * Implements hook_node_type_insert(). */ function menu_ui_node_type_insert(NodeTypeInterface $type) { @@ -319,7 +205,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(); } @@ -333,29 +219,40 @@ function menu_ui_node_type_delete(NodeTypeInterface $type) { \Drupal::config('menu.entity.node.' . $type->id())->delete(); } + /** * Helper for hook_node_insert() and hook_node_update(). */ 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 $link */ + $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'); } } @@ -366,14 +263,16 @@ function menu_ui_node_save(EntityInterface $node) { * Implements hook_node_predelete(). */ 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. + $menu_tree = \Drupal::menuTree(); + $result = $menu_tree->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(); + } + } } } @@ -381,64 +280,66 @@ 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'])) { // 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 = \Drupal::entityManager()->getStorage('menu_link_content')->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; + $form_state['menu_link'] = $definition; } - // 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); - } -} - -/** - * 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); } /** @@ -452,9 +353,17 @@ 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']; $type = $node->getType(); - $options = menu_ui_parent_options(menu_ui_get_menus(), $link, $type); + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $link_tree */ + $link_tree = \Drupal::menuTree(); + $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]; + } + $options = !$available_menus ? FALSE : $link_tree->getParentSelectOptions($definition['id'], $available_menus); // If no possible parent menu items were found, there is nothing to display. if (empty($options)) { return; @@ -464,7 +373,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'), @@ -476,7 +385,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', @@ -489,26 +398,26 @@ 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']; + if ($definition['id']) { + $default = $definition['menu_name'] . ':' . $definition['parent']; } else { $default = \Drupal::config('menu.entity.node.'.$type)->get('parent'); @@ -521,7 +430,7 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { $array = array_keys($options); $default = reset($array); } - $form['menu']['link']['parent'] = array( + $form['menu']['link']['menu_parent'] = array( '#type' => 'select', '#title' => t('Parent item'), '#default_value' => $default, @@ -530,7 +439,7 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { ); // Get number of items in menu so the weight selector is sized appropriately. - $delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name); + $delta = $link_tree->countMenuLinks($definition['menu_name']); if ($delta < 50) { // Old hardcoded value $delta = 50; @@ -539,7 +448,7 @@ function menu_ui_form_node_form_alter(&$form, $form_state) { '#type' => 'weight', '#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.'), ); } @@ -551,18 +460,24 @@ 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; + } + // Have to tack this onto the node so we can save it later when we have a + // a node ID for any new node. + $node->menu = $definition; } } } @@ -576,15 +491,18 @@ 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\MenuLinkTreeInterface $link_tree */ + $link_tree = \Drupal::menuTree(); $menu_options = menu_ui_get_menus(); $type = $form_state['controller']->getEntity(); if ($type->id()) { $config_values = \Drupal::config('menu.entity.node.' . $type->id())->get(); + //drupal_set_message(print_r($config_values,1)); } else { $config_values = array( 'available_menus' => array('main'), - 'parent' => 'main:0', + 'parent' => 'main:', ); } $form['menu'] = array( @@ -606,8 +524,7 @@ function menu_ui_form_node_type_form_alter(&$form, $form_state) { // 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); + $options = $link_tree->getParentSelectOptions(''); $form['menu']['menu_parent'] = array( '#type' => 'select', '#title' => t('Default parent item'), @@ -639,7 +556,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. */ @@ -664,3 +582,21 @@ function menu_ui_preprocess_block(&$variables) { $variables['attributes']['role'] = 'navigation'; } } + + +/** + * Implements hook_system_breadcrumb_alter(). + */ +function menu_ui_system_breadcrumb_alter(array &$breadcrumb, array $attributes, array $context) { + // Custom breadcrumb behavior for editing menu links, we append a link to + // the menu in which the link is found. + if (!empty($attributes[RouteObjectInterface::ROUTE_NAME]) && $attributes[RouteObjectInterface::ROUTE_NAME] == 'menu_ui.link_edit' && !empty($attributes['menu_link_plugin'])) { + $menu_link = $attributes['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..319d922 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::linkIsResetable' 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..02c59d0 100644 --- a/core/modules/menu_ui/src/Controller/MenuController.php +++ b/core/modules/menu_ui/src/Controller/MenuController.php @@ -34,30 +34,14 @@ public function getParentOptions(Request $request) { $available_menus[$menu] = $menu; } } - $options = _menu_ui_get_options(menu_ui_get_menus(), $available_menus, array('mlid' => 0)); + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('menu.link_tree'); + $options = $menu_tree->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 d6ceff4..e542542 100644 --- a/core/modules/menu_ui/src/Form/MenuDeleteForm.php +++ b/core/modules/menu_ui/src/Form/MenuDeleteForm.php @@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityConfirmFormBase; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Url; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,7 +24,7 @@ class MenuDeleteForm extends EntityConfirmFormBase { * * @var \Drupal\Core\Entity\EntityStorageInterface */ - protected $storage; + protected $menuTree; /** * The database connection. @@ -35,13 +36,13 @@ class MenuDeleteForm extends EntityConfirmFormBase { /** * Constructs a new MenuDeleteForm. * - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The menu link storage. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree + * 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(MenuLinkTreeInterface $menu_tree, Connection $connection) { + $this->menuTree = $menu_tree; $this->connection = $connection; } @@ -50,7 +51,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('menu.link_tree'), $container->get('database') ); } @@ -74,7 +75,7 @@ public function getCancelRoute() { */ public function getDescription() { $caption = ''; - $num_links = $this->storage->countMenuLinks($this->entity->id()); + $num_links = $this->menuTree->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 +101,12 @@ 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 - there ought to be a better way. + $menu_links = $this->menuTree->loadLinksByRoute('menu_ui.menu_edit', array('menu' => $this->entity->id()), TRUE); + foreach ($menu_links as $id => $link) { + $this->menuTree->deleteLink($id); + } // Delete the custom menu and all its menu links. $this->entity->delete(); diff --git a/core/modules/menu_ui/src/Form/MenuLinkEditForm.php b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php new file mode 100644 index 0000000..7ac65ac --- /dev/null +++ b/core/modules/menu_ui/src/Form/MenuLinkEditForm.php @@ -0,0 +1,93 @@ +menuTree = $menu_tree; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('menu.link_tree') + ); + } + + public function getFormId() { + return 'menu_link_edit'; + } + + /** + * {@inheritdoc} + * + * @param \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin + * The plugin instance to use for this form. + */ + public function buildForm(array $form, array &$form_state, MenuLinkInterface $menu_link_plugin = NULL) { + + $form['menu_link_id'] = array( + '#type' => 'value', + '#value' => $menu_link_plugin->getPluginId(), + ); + + $form['#plugin_form'] = $this->menuTree->getPluginForm($menu_link_plugin); + + $form += $form['#plugin_form']->buildEditForm($form, $form_state); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#button_type' => 'primary', + ); + return $form; + } + + public function validateForm(array &$form, array &$form_state) { + $form['#plugin_form']->validateEditForm($form, $form_state); + } + + public function submitForm(array &$form, array &$form_state) { + $link = $form['#plugin_form']->submitEditForm($form, $form_state); + + drupal_set_message($this->t('The menu link has been saved.')); + $form_state['redirect_route'] = array( + 'route_name' => 'menu_ui.menu_edit', + 'route_parameters' => array( + 'menu' => $link->getMenuName(), + ), + ); + } + +} + diff --git a/core/modules/menu_ui/src/Form/MenuLinkResetForm.php b/core/modules/menu_ui/src/Form/MenuLinkResetForm.php index d3d2a67..2fb02d7 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\MenuLinkTreeInterface; +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 tree service. + * + * @var \Drupal\Core\Menu\MenuLinkTreeInterface + */ + protected $menuTree; + + /** + * The menu link. + * + * @var \Drupal\Core\Menu\MenuLinkInterface + */ + protected $link; + + /** + * Constructs a MenuLinkEditForm object. + * + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree + * The menu tree service. + */ + public function __construct(MenuLinkTreeInterface $menu_tree) { + $this->menuTree = $menu_tree; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('menu.link_tree') + ); + } + + /** + * {@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 getCancelRoute() { return new Url('menu_ui.menu_edit', array( - 'menu' => $this->entity->menu_name, + 'menu' => $this->link->getMenuName(), )); } @@ -35,23 +79,47 @@ public function getCancelRoute() { * {@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->menuTree->resetLink($this->link->getPluginId()); + drupal_set_message($this->t('The menu link was reset to its default settings.')); $form_state['redirect_route'] = $this->getCancelRoute(); } + /** + * 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 + * Returns AccessInterface::ALLOW when access was granted, otherwise + * AccessInterface::DENY. + */ + public function linkIsResetable(MenuLinkInterface $menu_link_plugin) { + return $menu_link_plugin->isResetable() ? AccessInterface::ALLOW : AccessInterface::DENY; + } + } diff --git a/core/modules/menu_ui/src/MenuForm.php b/core/modules/menu_ui/src/MenuForm.php index a1c422d..c1eba69 100644 --- a/core/modules/menu_ui/src/MenuForm.php +++ b/core/modules/menu_ui/src/MenuForm.php @@ -11,9 +11,8 @@ use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Language\Language; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Drupal\Core\Render\Element; -use Drupal\menu_link\MenuLinkStorageInterface; -use Drupal\menu_link\MenuTreeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -29,16 +28,9 @@ class MenuForm extends EntityForm { protected $entityQueryFactory; /** - * The menu link storage. - * - * @var \Drupal\menu_link\MenuLinkStorageInterface - */ - protected $menuLinkStorage; - - /** * The menu tree service. * - * @var \Drupal\menu_link\MenuTreeInterface + * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ protected $menuTree; @@ -54,14 +46,11 @@ 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\MenuLinkTreeInterface $menu_tree * The menu tree service. */ - public function __construct(QueryFactory $entity_query_factory, MenuLinkStorageInterface $menu_link_storage, MenuTreeInterface $menu_tree) { + public function __construct(QueryFactory $entity_query_factory, MenuLinkTreeInterface $menu_tree) { $this->entityQueryFactory = $entity_query_factory; - $this->menuLinkStorage = $menu_link_storage; $this->menuTree = $menu_tree; } @@ -71,8 +60,7 @@ 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('menu.link_tree') ); } @@ -88,16 +76,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 +108,11 @@ public function form(array $form, array &$form_state) { '#languages' => Language::STATE_ALL, '#default_value' => $menu->langcode, ); - // 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 +140,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->menuTree->menuNameInUse($value); } /** @@ -219,11 +156,11 @@ public function save(array $form, array &$form_state) { $edit_link = \Drupal::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); } @@ -231,6 +168,19 @@ public function save(array $form, array &$form_state) { } /** + * Recursively count the number of menu links in a tree. + */ + protected function countElements($tree, $count = 0) { + foreach ($tree as $element) { + $count++; + if (!empty($element['below'])) { + $this->countElements($element['below'], $count); + } + } + return $count; + } + + /** * Form constructor to edit an entire menu tree at once. * * Shows for one menu the menu links accessible to the current user and @@ -253,26 +203,16 @@ 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->buildAllData($this->entity->id()); - if (!empty($result)) { - $links = $this->menuLinkStorage->loadMultiple($result); - } - - $delta = max(count($links), 50); + $count = $this->countElements($tree); + $delta = max($count, 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); $this->getRequest()->attributes->set('_menu_admin', FALSE); $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'))); + $form['#empty_text'] = $this->t('There are no menu links yet. Add link.', array('@link' => url('admin/structure/menu/manage/' . $this->entity->id() .'/add'))); return $form; } @@ -291,62 +231,79 @@ protected function buildOverviewForm(array &$form, array &$form_state) { protected function buildOverviewTreeForm($tree, $delta) { $form = &$this->overviewTreeForm; foreach ($tree as $data) { + /** @var \Drupal\Core\Menu\MenuLinkInterface $item */ $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') . ')'; + if ($item) { + $id = 'menu_plugin_id:' . $item->getPluginId(); + $form[$id]['#item'] = $data; + $form[$id]['#attributes'] = $item->isHidden() ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); + $form[$id]['title']['#markup'] = \Drupal::linkGenerator()->generateFromUrl($item->getTitle(), $item->getUrlObject(), $item->getOptions()); + if ($item->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 = $item->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' => $item->getTitle())), '#title_display' => 'invisible', - '#default_value' => !$item['hidden'], + '#default_value' => !$item->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' => $item->getWeight(), + '#title' => $this->t('Weight for @title', array('@title' => $item->getTitle())), '#title_display' => 'invisible', ); - $form[$mlid]['mlid'] = array( + $form[$id]['id'] = array( '#type' => 'hidden', - '#value' => $item['mlid'], + '#value' => $item->getPluginId(), ); - $form[$mlid]['plid'] = array( + $form[$id]['parent'] = array( '#type' => 'hidden', - '#default_value' => $item['plid'], + '#default_value' => $item->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 = $item->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' => $item->getPluginId()), ); } - // Set the reset column. - elseif ($item->access('reset')) { + // Links can either be reset or deleted, not both. + if ($item->isResetable()) { $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' => $item->getPluginId()), ); } - $form[$mlid]['operations'] = array( + elseif ($delete_link = $item->getDeleteRoute()) { + $operations['delete'] = $delete_link; + $operations['delete']['query']['destination'] = $this->entity->url(); + $operations['delete']['title'] = $this->t('Delete'); + } + if ($item->isTranslatable()) { + $operations['translate'] = array( + 'title' => $this->t('Translate'), + ) + (array) $item->getTranslateRoute(); + } + $form[$id]['operations'] = array( '#type' => 'operations', '#links' => $operations, ); @@ -363,7 +320,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 +341,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->menuTree->updateLink($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..88e1898 --- /dev/null +++ b/core/modules/menu_ui/src/Plugin/Menu/LocalAction/MenuLinkAdd.php @@ -0,0 +1,32 @@ +attributes->has('_system_path')) { + // @todo: is there a better value to get from the request? + $options['query']['destination'] = $request->attributes->get('_system_path'); + } + return $options; + } + +} diff --git a/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php b/core/modules/menu_ui/src/Tests/MenuCacheTagsTest.php index f2ef39d..138c132 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; @@ -46,12 +46,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\MenuLinkTreeInterface $menu_tree */ + $menu_tree = $this->container->get('menu.link_tree'); + // Move a link into the new menu. + $menu_link = $menu_tree->updateLink('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. @@ -69,7 +67,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'; @@ -79,23 +76,24 @@ 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_tree->updateLink('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'); @@ -103,16 +101,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->isResetable(), 'First link can be reset'); + $menu_link = $menu_tree->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 971e821..3826951 100644 --- a/core/modules/menu_ui/src/Tests/MenuLanguageTest.php +++ b/core/modules/menu_ui/src/Tests/MenuLanguageTest.php @@ -64,21 +64,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')); - - // Check that the language settings were saved. - $this->assertEqual(entity_load('menu', $menu_name)->langcode, $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); + language_save_default_configuration('menu_link_content', 'menu_link_content', array('langcode' => 'bb', 'language_show' => TRUE)); // Check menu language and item language configuration. $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 = ''; @@ -86,41 +77,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', )); @@ -129,9 +114,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', )); @@ -143,16 +128,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 9c1c67e..93da6ca 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; @@ -71,7 +71,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')); @@ -99,7 +99,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')); @@ -120,10 +120,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(); @@ -133,27 +135,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 e9ce5ef..a40e48c 100644 --- a/core/modules/menu_ui/src/Tests/MenuTest.php +++ b/core/modules/menu_ui/src/Tests/MenuTest.php @@ -2,13 +2,15 @@ /** * @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\system\Entity\Menu; +use Drupal\menu_link_content\Entity\MenuLinkContent; + /** * Defines a test class for testing menu and menu link functionality. @@ -46,7 +48,7 @@ class MenuTest extends MenuWebTestBase { /** * An array of test menu links. * - * @var array + * @var \Drupal\menu_link_content\Entity\MenuLinkContentInterface[] */ protected $items; @@ -83,18 +85,19 @@ 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(); + $before_count = \Drupal::menuTree()->countMenuLinks(NULL); menu_link_rebuild_defaults(); - $after_count = db_query('SELECT COUNT(*) FROM {menu_links}')->fetchField(); + $after_count = \Drupal::menuTree()->countMenuLinks(NULL); $this->assertIdentical($before_count, $after_count, 'menu_link_rebuild_defaults() 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)); - $this->verifyMenuLink($item, $node); + $node = node_load($item->getRouteParameters()['node']); + $this->verifyMenuLink($item, $node, NULL, NULL, "node/{$node->id()}"); } // Login the administrator. @@ -109,20 +112,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(); + /** @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ + $menu_tree = $this->container->get('menu.link_tree'); + // 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_tree->resetDefinitions(); + + $instance = $menu_tree->createInstance($instance->getPluginId()); + $this->assertEqual($edit['weight'], $instance->getWeight(), 'Saving an existing link updates the weight.'); } /** @@ -219,7 +222,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. @@ -250,33 +253,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', )); @@ -287,34 +289,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', )); @@ -328,50 +333,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', )); @@ -380,33 +379,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. @@ -422,16 +419,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.'); } /** @@ -461,15 +458,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"); } /** @@ -495,42 +492,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 @@ -539,40 +506,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, + 'url' => $path, + 'title[0][value]' => $title, + 'description[0][value]' => '', + 'enabled' => 1, 'expanded' => $expanded, - 'parent' => $menu_name . ':' . $plid, - 'weight' => $weight, + '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; } @@ -583,35 +546,35 @@ 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, $parent = NULL, $parent_node = NULL, $path = '') { // View home page. - $this->drupalGet(''); + $this->drupalGet($path); $this->assertResponse(200); // 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. @@ -621,7 +584,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. @@ -633,18 +596,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); @@ -653,58 +616,35 @@ 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 - * Original title for menu link. - */ - function resetMenuLink($item, $old_title) { - $mlid = $item['mlid']; - $title = $item['link_title']; - - // Reset menu link. - $this->drupalPostForm("admin/structure/menu/item/$mlid/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'); - } - - /** * 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')); @@ -719,51 +659,54 @@ 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')); + // Clear the internal cache of the menu tree in the test. + \Drupal::menuTree()->resetDefinitions(); + // 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)); } /** @@ -787,27 +730,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\MenuLinkTreeInterface $menu_tree */ + $menu_tree = $this->container->get('menu.link_tree'); + $result = $menu_tree->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; } /** @@ -838,9 +773,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..a2bffc1 100644 --- a/core/modules/menu_ui/src/Tests/MenuWebTestBase.php +++ b/core/modules/menu_ui/src/Tests/MenuWebTestBase.php @@ -19,28 +19,59 @@ * * @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\MenuLinkTreeInterface $menu_tree */ + $menu_tree = $this->container->get('menu.link_tree'); + $menu_tree->resetDefinitions(); + // Reset the static load cache. + \Drupal::entityManager()->getStorage('menu_link_content')->resetCache(); + $definition = $menu_tree->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); + $links = \Drupal::entityManager()->getStorage('menu_link_content')->loadByProperties(array('uuid' => $uuid)); + /** @var \Drupal\menu_link_content\Entity\MenuLinkContent $link */ + $entity = reset($links); + } + + if (isset($expected_item['children'])) { + $child_ids = array_values($menu_tree->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_tree->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 8293618..a4fb676 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -760,10 +760,10 @@ function hook_node_validate(\Drupal\node\NodeInterface $node, $form, &$form_stat * @ingroup node_api_hooks */ 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 1537393..97d168e 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -211,7 +211,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/menu_link.static.overrides.yml b/core/modules/system/config/install/menu_link.static.overrides.yml new file mode 100644 index 0000000..ca4ba7f --- /dev/null +++ b/core/modules/system/config/install/menu_link.static.overrides.yml @@ -0,0 +1 @@ +definitions: [] diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 6ce1be7..426050c 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -338,3 +338,30 @@ system.mail: system.theme.global: type: theme_settings label: 'Theme global settings' + +menu_link.static.overrides: + type: mapping + label: 'Menu link overrides' + mapping: + definitions: + type: sequence + label: Definitions + sequence: + - type: mapping + label: Definition + mapping: + menu_name: + type: string + label: 'Menu name' + parent: + type: string + label: 'Parent' + weight: + type: integer + label: 'Weight' + expanded: + type: boolean + label: 'Expanded' + hidden: + type: boolean + label: 'Hidden' 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 6227296..2953f21 100644 --- a/core/modules/system/src/Controller/SystemController.php +++ b/core/modules/system/src/Controller/SystemController.php @@ -7,6 +7,7 @@ namespace Drupal\system\Controller; +use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Component\Serialization\Json; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\Query\QueryFactory; @@ -102,48 +103,31 @@ public function overview() { if ($this->systemManager->checkRequirements() && $this->currentUser()->hasPermission('administer site configuration')) { drupal_set_message($this->t('One or more problems were detected with your Drupal installation. Check the status report for more information.', array('@status' => url('admin/reports/status'))), 'error'); } + /* @var \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree */ + $menu_tree = \Drupal::service('plugin.manager.menu.link_tree'); + $system_link = $menu_tree->createInstance('system.admin_config'); + // Only find the children of this link. + $parameters['expanded'][] = 'system.admin_config'; + $parameters['conditions']['hidden'] = 0; + $tree = $menu_tree->buildTree($system_link->getMenuName(), $parameters); $blocks = array(); // Load all links on admin/config and menu links below it. - $query = $this->queryFactory->get('menu_link') - ->condition('link_path', 'admin/config') - ->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), - ); - 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; - } - } + foreach ($tree as $key => $item) { + $block['title'] = $item['link']->getTitle(); + $block['description'] = $item['link']->getDescription(); + $block['content'] = array( + '#theme' => 'admin_block_content', + '#content' => $this->systemManager->getAdminBlock($item['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[$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 d061495..629d6d6 100644 --- a/core/modules/system/src/Form/ModulesListForm.php +++ b/core/modules/system/src/Form/ModulesListForm.php @@ -8,6 +8,7 @@ namespace Drupal\system\Form; 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; @@ -16,9 +17,11 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Render\Element; +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. @@ -59,11 +62,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; /** * {@inheritdoc} @@ -74,8 +84,9 @@ 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_user'), + $container->get('title_resolver'), + $container->get('router.route_provider') ); } @@ -90,18 +101,21 @@ 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\Controller\TitleResolverInterface $title_resolver + * The title resolver. + * @param \Drupal\Core\Routing\RouteProviderInterface $route_provider + * The route provider. */ - public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable, AccessManager $access_manager, EntityManagerInterface $entity_manager, QueryFactory $query_factory, AccountInterface $current_user) { + public function __construct(ModuleHandlerInterface $module_handler, KeyValueStoreExpirableInterface $key_value_expirable, AccessManager $access_manager, EntityManagerInterface $entity_manager, AccountInterface $current_user, TitleResolverInterface $title_resolver, RouteProviderInterface $route_provider) { $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->titleResolver = $title_resolver; + $this->routeProvider = $route_provider; } /** @@ -236,11 +250,13 @@ 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); + + $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); + $title = $this->titleResolver->getTitle($request, $route_object); + $row['links']['configure'] = array( '#type' => 'link', '#title' => $this->t('Configure'), @@ -249,7 +265,7 @@ protected function buildRow(array $modules, Extension $module, $distribution) { '#options' => array( 'attributes' => array( 'class' => array('module-link', 'module-link-configure'), - 'title' => $item['description'], + 'title' => $title, ), ), ); diff --git a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php index a698e1f..01ccca8 100644 --- a/core/modules/system/src/Plugin/Block/SystemMenuBlock.php +++ b/core/modules/system/src/Plugin/Block/SystemMenuBlock.php @@ -10,7 +10,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\block\BlockBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\menu_link\MenuTreeInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -27,9 +27,9 @@ class SystemMenuBlock extends BlockBase implements ContainerFactoryPluginInterface { /** - * The menu tree. + * The menu link tree manager. * - * @var \Drupal\menu_link\MenuTreeInterface + * @var \Drupal\Core\Menu\MenuLinkTreeInterface */ protected $menuTree; @@ -42,10 +42,10 @@ 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 + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree * The menu tree. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuTreeInterface $menu_tree) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, MenuLinkTreeInterface $menu_tree) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->menuTree = $menu_tree; } @@ -58,7 +58,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('menu_link.tree') + $container->get('menu.link_tree') ); } @@ -66,8 +66,9 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function build() { - $menu = $this->getDerivativeId(); - return $this->menuTree->renderMenu($menu); + $menu_name = $this->getDerivativeId(); + $tree = $this->menuTree->buildPageData($menu_name); + return $this->menuTree->buildRenderTree($tree); } /** @@ -114,7 +115,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.url', 'cache_context.language'); } } diff --git a/core/modules/system/src/SystemManager.php b/core/modules/system/src/SystemManager.php index 99b928b..93e361e 100644 --- a/core/modules/system/src/SystemManager.php +++ b/core/modules/system/src/SystemManager.php @@ -6,7 +6,8 @@ namespace Drupal\system; -use Drupal\Component\Utility\Unicode; +use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuLinkInterface; use Drupal\Core\Entity\EntityManagerInterface; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Drupal\Core\Database\Connection; @@ -33,18 +34,18 @@ 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; /** * A static cache of menu items. @@ -79,12 +80,14 @@ class SystemManager { * The entity manager. * @param \Symfony\Component\HttpFoundation\RequestStack * The request stack. + * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree + * The menu tree manager. */ - 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) { $this->moduleHandler = $module_handler; $this->database = $database; - $this->menuLinkStorage = $entity_manager->getStorage('menu_link'); $this->requestStack = $request_stack; + $this->menuTree = $menu_tree; } /** @@ -173,9 +176,9 @@ public function getMaxSeverity(&$requirements) { 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)) { + $route_parameters = $request->attributes->get('_raw_variables')->all(); + $instance = $this->menuTree->menuLinkGetPreferred($route_name, $route_parameters); + if ($instance && $content = $this->getAdminBlock($instance)) { $output = array( '#theme' => 'admin_block_content', '#content' => $content, @@ -192,48 +195,28 @@ 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. + $parameters['expanded'][] = $instance->getPluginId(); + $parameters['conditions']['hidden'] = 0; + $tree = $this->menuTree->buildTree($instance->getMenuName(), $parameters); + foreach ($tree as $key => $item) { + /** @var $link \Drupal\Core\Menu\MenuLinkInterface */ + $link = $item['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; + //$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 849aadc..448fbe1 100644 --- a/core/modules/system/src/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/src/Tests/Menu/BreadcrumbTest.php @@ -88,15 +88,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. @@ -170,7 +165,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) { @@ -179,13 +174,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') { @@ -197,26 +192,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 @@ -246,32 +241,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(check_plain($parent->getTitle()), 'Tagged node found.'); // Additionally make sure that this link appears only once; i.e., the @@ -280,14 +279,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(), ); } @@ -297,7 +296,6 @@ function testBreadCrumbs() { user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array( 'access user profiles', )); - $this->drupalLogout(); // Verify breadcrumb on front page. $this->assertBreadcrumb('', array()); @@ -364,4 +362,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 d0db0de..2f49ef7 100644 --- a/core/modules/system/src/Tests/Menu/LinksTest.php +++ b/core/modules/system/src/Tests/Menu/LinksTest.php @@ -12,6 +12,8 @@ /** * Tests for menu links. + * + * @todo: move this under menu_link_content module. */ class LinksTest extends WebTestBase { @@ -20,7 +22,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 maanger + * + * @var \Drupal\Core\Menu\MenuLinkTreeInterface $menuTree + */ + protected $menuTree; public static function getInfo() { return array( @@ -36,6 +45,8 @@ public static function getInfo() { public function setUp() { parent::setUp(); + $this->menuTree = $this->container->get('menu.link_tree'); + entity_create('menu', array( 'id' => 'menu_test', 'label' => 'Test menu', @@ -47,55 +58,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->menuTree->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; } @@ -104,13 +120,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->menuTree->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, format_string('Menu link %id has parent of %parent, expected %expected_parent.', array('%id' => $id, '%parent' => $menu_link_plugin->getParent(), '%expected_parent' => $expected_parent))); } } @@ -122,7 +137,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', @@ -133,11 +148,16 @@ 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->menuTree->updateLink($links['child-1'], array('parent' => $links['child-2'])); + // Verify that the entity was updated too. + /* @var \Drupal\Core\Menu\MenuLinkInterface $menu_link_plugin */ + $menu_link_plugin = $this->menuTree->createInstance($links['child-1']); + $entity = entity_load_by_uuid('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', @@ -146,22 +166,19 @@ 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); - } + // have been reassigned to the parent. + $links = $this->createLinkHierarchy($module); + $this->menuTree->deleteLink($links['child-1']); + + $expected_hierarchy = array( + 'parent' => FALSE, + 'child-1-1' => 'parent', + 'child-1-2' => 'parent', + 'child-2' => 'parent', + ); + $this->assertMenuLinkParents($links, $expected_hierarchy); + return; // 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 @@ -195,102 +212,21 @@ function testMenuLinkReparenting($module = 'menu_test') { } /** - * Tests automatic reparenting. - * - * Runs tests on menu links defined by the menu_link.static service. + * Tests uninstalling a module providing default links. */ - 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')); - } - - /** - * Tests uninstall a module providing default links. - */ - public function testModuleUninstalledMenuLinks() { + public function XtestModuleUninstalledMenuLinks() { \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); + $menu_links = $this->menuTree->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); + menu_link_rebuild_defaults(); + $menu_links = $this->menuTree->loadLinksByRoute('menu_test.menu_test'); $this->assertEqual(count($menu_links), 0); } diff --git a/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php new file mode 100644 index 0000000..4665d7a --- /dev/null +++ b/core/modules/system/src/Tests/Menu/MenuLinkTreeTest.php @@ -0,0 +1,109 @@ + 'Tests \Drupal\Core\Menu\MenuLinkTree', + 'description' => '', + 'group' => 'Menu' + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installSchema('system', array('router')); + $this->installEntitySchema('menu_link_content'); + + $this->linkTree = \Drupal::menuTree(); + } + + public function testDeleteLinksInMenu() { + \Drupal::service('router.builder')->rebuild(); + + \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu1'))->save(); + \Drupal::entityManager()->getStorage('menu')->create(array('id' => 'menu2'))->save(); + + \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save(); + \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content'))->save(); + \Drupal::entityManager()->getStorage('menu_link_content')->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu2', 'bundle' => 'menu_link_content'))->save(); + + $output = $this->linkTree->buildTree('menu1'); + $this->assertEqual(count($output), 2); + $output = $this->linkTree->buildTree('menu2'); + $this->assertEqual(count($output), 1); + + $this->linkTree->deleteLinksInMenu('menu1'); + $this->linkTree->resetDefinitions(); + + $output = $this->linkTree->buildTree('menu1'); + $this->assertEqual(count($output), 0); + + $output = $this->linkTree->buildTree('menu2'); + $this->assertEqual(count($output), 1); + } + + public function testGetParentDepthLimit() { + \Drupal::service('router.builder')->rebuild(); + + $storage = \Drupal::entityManager()->getStorage('menu_link_content'); + + // root + // - child1 + // -- child2 + // --- child3 + // ---- child4 + $root = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content')); + $root->save(); + $child1 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $root->getPluginId())); + $child1->save(); + $child2 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child1->getPluginId())); + $child2->save(); + $child3 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child2->getPluginId())); + $child3->save(); + $child4 = $storage->create(array('route_name' => 'menu_test.menu_name_test', 'menu_name' => 'menu1', 'bundle' => 'menu_link_content', 'parent' => $child3->getPluginId())); + $child4->save(); + + $this->assertEqual($this->linkTree->getParentDepthLimit($root->getPluginId()), 4); + $this->assertEqual($this->linkTree->getParentDepthLimit($child1->getPluginId()), 5); + $this->assertEqual($this->linkTree->getParentDepthLimit($child2->getPluginId()), 6); + $this->assertEqual($this->linkTree->getParentDepthLimit($child3->getPluginId()), 7); + $this->assertEqual($this->linkTree->getParentDepthLimit($child4->getPluginId()), 8); + } + +} + 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 59d0f50..0000000 --- a/core/modules/system/src/Tests/Menu/MenuRouterRebuildTest.php +++ /dev/null @@ -1,60 +0,0 @@ - 'Menu router rebuild', - 'description' => 'Tests menu_router_rebuild().', - 'group' => 'Menu', - ); - } - - /** - * {@inheritdoc} - */ - function setUp() { - parent::setUp(); - - $language = new Language(array('id' => '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 4383658..260300b 100644 --- a/core/modules/system/src/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/src/Tests/Menu/MenuRouterTest.php @@ -59,10 +59,6 @@ public function testMenuIntegration() { $this->doTestMenuOnRoute(); $this->doTestMenuName(); $this->doTestMenuLinkDefaultsAlter(); - $this->doTestMenuItemTitlesCases(); - $this->doTestMenuLinkMaintain(); - $this->doTestMenuLinkOptions(); - $this->doTestMenuItemHooks(); $this->doTestHookMenuIntegration(); $this->doTestExoticPath(); } @@ -115,68 +111,24 @@ 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')); + $menu_links = \Drupal::menuTree()->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(); + \Drupal::menuTree()->rebuild(); - $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu_name_test')); + $menu_links = \Drupal::menuTree()->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.'); } /** @@ -185,9 +137,9 @@ protected function doTestMenuName() { protected function doTestMenuLinkDefaultsAlter() { // 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')); + $menu_links = \Drupal::menuTree()->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_link_defaults_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(). \Drupal::service('router.builder')->rebuild(); @@ -199,98 +151,15 @@ protected function doTestMenuLinkDefaultsAlter() { * 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(); + $menu_links = \Drupal::menuTree()->loadLinksByRoute('menu_test.hierarchy_parent'); + $parent_link = reset($menu_links); + $menu_links = \Drupal::menuTree()->loadLinksByRoute('menu_test.hierarchy_parent.child'); + $child_link = reset($menu_links); + $menu_links = \Drupal::menuTree()->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/Menu/MenuTreeStorageTest.php b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php new file mode 100644 index 0000000..63b4075 --- /dev/null +++ b/core/modules/system/src/Tests/Menu/MenuTreeStorageTest.php @@ -0,0 +1,359 @@ + 'Menu tree storage tests', + 'description' => 'Tests menu tree storage tests', + 'group' => 'Menu' + ); + } + + protected function setUp() { + parent::setUp(); + + $this->treeStorage = new MenuTreeStorage($this->container->get('database'), $this->container->get('url_generator')); + $this->connection = $this->container->get('database'); + } + + /** + * Tests the tree storage when no tree was built yet. + */ + public function testBasicMethods() { + $this->doTestEmptyStorage(); + $this->doTestTable(); + } + + /** + * Ensures that there are no menu links by default. + */ + protected function doTestEmptyStorage() { + $this->assertEqual(0, $this->treeStorage->countMenuLinks()); + } + + /** + * Ensures that table gets created on the fly. + */ + protected function doTestTable() { + // Test that we can create a tree storage with an arbitrary table name and + // that selecting from the storage creates the table. + $tree_storage = new MenuTreeStorage($this->container->get('database'), $this->container->get('url_generator'), 'test_menu_tree'); + $this->assertFalse($this->connection->schema()->tableExists('test_menu_tree'), 'Test table is not yet created'); + $tree_storage->countMenuLinks(); + $this->assertTrue($this->connection->schema()->tableExists('test_menu_tree'), 'Test table was created'); + } + + /** + * Tests with a simple linear hierarchy. + */ + public function testSimpleHierarchy() { + // Add some links with parent on the previous one and test some values. + // + // - test1 + // -- test2 + // --- test3 + $this->addMenuLink('test1', ''); + $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1)); + + $this->addMenuLink('test2', 'test1'); + $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2')); + $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2), array('test1')); + + $this->addMenuLink('test3', 'test2'); + $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3')); + $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3')); + $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 3), array('test2', 'test1')); + } + + /** + * Tests the tree with moving links inside the hierarchy. + */ + public function testMenuLinkMoving() { + // Before the move. + // + // - test1 + // -- test2 + // --- test3 + // - test4 + // -- test5 + // --- test6 + + $this->addMenuLink('test1', ''); + $this->addMenuLink('test2', 'test1'); + $this->addMenuLink('test3', 'test2'); + $this->addMenuLink('test4', ''); + $this->addMenuLink('test5', 'test4'); + $this->addMenuLink('test6', 'test5'); + + $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test2', 'test3')); + $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 2), array('test1'), array('test3')); + $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test6')); + $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test6')); + $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4')); + + $this->moveMenuLink('test2', 'test5'); + // After the 1st move. + // + // - test1 + // - test4 + // -- test5 + // --- test2 + // ---- test3 + // --- test6 + + $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1)); + $this->assertMenuLink('test2', array('has_children' => 1, 'depth' => 3), array('test5', 'test4'), array('test3')); + $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 4), array('test2', 'test5', 'test4')); + $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6')); + $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test4'), array('test2', 'test3', 'test6')); + $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test4')); + + $this->moveMenuLink('test4', 'test1'); + $this->moveMenuLink('test3', 'test1'); + // After the next 2 moves. + // + // - test1 + // -- test3 + // -- test4 + // --- test5 + // ---- test2 + // ---- test6 + + $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test4', 'test5', 'test2', 'test3', 'test6')); + $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1')); + $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1')); + $this->assertMenuLink('test4', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test5', 'test6')); + $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 3), array('test4', 'test1'), array('test2', 'test6')); + $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 4), array('test5', 'test4', 'test1')); + + // Deleting a link in the middle should re-attach child links to the parent + $this->treeStorage->delete('test4'); + // After the delete. + // + // - test1 + // -- test3 + // -- test5 + // --- test2 + // --- test6 + $this->assertMenuLink('test1', array('has_children' => 1, 'depth' => 1), array(), array('test5', 'test2', 'test3', 'test6')); + $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 3), array('test5', 'test1')); + $this->assertMenuLink('test3', array('has_children' => 0, 'depth' => 2), array('test1')); + $this->assertFalse($this->treeStorage->load('test4')); + $this->assertMenuLink('test5', array('has_children' => 1, 'depth' => 2), array('test1'), array('test2', 'test6')); + $this->assertMenuLink('test6', array('has_children' => 0, 'depth' => 3), array('test5', 'test1')); + } + + /** + * Tests with hidden child links. + */ + public function testMenuHiddenChildLinks() { + // Add some links with parent on the previous one and test some values. + // + // - test1 + // -- test2 (hidden) + + $this->addMenuLink('test1', ''); + $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1)); + + $this->addMenuLink('test2', 'test1', '', array(), 'tools', array('hidden' => 1)); + // The 1st link does not have any visible children, so has_children is still 0. + $this->assertMenuLink('test1', array('has_children' => 0, 'depth' => 1)); + $this->assertMenuLink('test2', array('has_children' => 0, 'depth' => 2, 'hidden' => 1), array('test1')); + + // Add more links with parent on the previous one. + //