diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt index 2b6f2cc..bd6292d 100644 --- a/core/MAINTAINERS.txt +++ b/core/MAINTAINERS.txt @@ -239,6 +239,11 @@ Locale module Menu module - ? +Menu Link module +- Andrei Mateescu 'amateescu' http://drupal.org/user/729614 +- Károly Négyesi 'chx' http://drupal.org/user/9446 +- @todo Anyone else from the menu system? + Node module - Moshe Weitzman 'moshe weitzman' http://drupal.org/user/23 - David Strauss 'David Strauss' http://drupal.org/user/93254 diff --git a/core/includes/menu.inc b/core/includes/menu.inc index e6810b8..7ee1054 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -5,8 +5,11 @@ * API for the Drupal menu system. */ +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Template\Attribute; +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; +use Drupal\menu_link\MenuLinkStorageController; /** * @defgroup menu Menu system @@ -264,6 +267,9 @@ /** * The maximum depth of a menu links tree - matches the number of p columns. + * + * @todo Move this constant to MenuLinkStorageController along with all the tree + * functionality. */ const MENU_MAX_DEPTH = 9; @@ -1260,17 +1266,18 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = // Collect all the links set to be expanded, and then add all of // their children to the list as well. do { - $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC)) - ->fields('menu_links', array('mlid')) + $query = entity_query('menu_link') ->condition('menu_name', $menu_name) ->condition('expanded', 1) ->condition('has_children', 1) ->condition('plid', $parents, 'IN') - ->condition('mlid', $parents, 'NOT IN') - ->execute(); + ->condition('mlid', $parents, 'NOT IN'); + $result = $query->execute(); $num_rows = FALSE; - foreach ($result as $item) { - $parents[$item['mlid']] = $item['mlid']; + if (!empty($result)) { + foreach ($result as $mlid) { + $parents[$mlid] = $mlid; + } $num_rows = TRUE; } } while ($num_rows); @@ -1360,48 +1367,24 @@ function _menu_build_tree($menu_name, array $parameters = array()) { } if (!isset($trees[$tree_cid])) { - // Select the links from the table, and recursively build the tree. We - // LEFT JOIN since there is no match in {menu_router} for an external - // link. - $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); - $query->addTag('translatable'); - $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); - $query->fields('ml'); - $query->fields('m', array( - 'load_functions', - 'to_arg_functions', - 'access_callback', - 'access_arguments', - 'page_callback', - 'page_arguments', - 'tab_parent', - 'tab_root', - 'title', - 'title_callback', - 'title_arguments', - 'theme_callback', - 'theme_arguments', - 'type', - 'description', - 'description_callback', - 'description_arguments', - )); + $links = array(); + $query = entity_query('menu_link'); for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { - $query->orderBy('p' . $i, 'ASC'); + $query->sort('p' . $i, 'ASC'); } - $query->condition('ml.menu_name', $menu_name); + $query->condition('menu_name', $menu_name); if (!empty($parameters['expanded'])) { - $query->condition('ml.plid', $parameters['expanded'], 'IN'); + $query->condition('plid', $parameters['expanded'], 'IN'); } elseif (!empty($parameters['only_active_trail'])) { - $query->condition('ml.mlid', $parameters['active_trail'], 'IN'); + $query->condition('mlid', $parameters['active_trail'], 'IN'); } $min_depth = (isset($parameters['min_depth']) ? $parameters['min_depth'] : 1); if ($min_depth != 1) { - $query->condition('ml.depth', $min_depth, '>='); + $query->condition('depth', $min_depth, '>='); } if (isset($parameters['max_depth'])) { - $query->condition('ml.depth', $parameters['max_depth'], '<='); + $query->condition('depth', $parameters['max_depth'], '<='); } // Add custom query conditions, if any were passed. if (isset($parameters['conditions'])) { @@ -1410,10 +1393,8 @@ function _menu_build_tree($menu_name, array $parameters = array()) { } } - // Build an ordered array of links using the query result object. - $links = array(); - foreach ($query->execute() as $item) { - $links[] = $item; + if ($result = $query->execute()) { + $links = menu_link_load_multiple($result); } $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); @@ -2451,18 +2432,11 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { // Put the selected menu at the front of the list. array_unshift($menu_names, $selected_menu); - $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); - $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); - $query->fields('ml'); - // Weight must be taken from {menu_links}, not {menu_router}. - $query->addField('ml', 'weight', 'link_weight'); - $query->fields('m'); - $query->condition('ml.link_path', $path_candidates, 'IN'); + $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 ($query->execute() as $candidate) { - $candidate['weight'] = $candidate['link_weight']; + 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)) { @@ -2582,38 +2556,6 @@ function menu_get_active_title() { } /** - * Gets a translated, access-checked menu link that is ready for rendering. - * - * 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 $mlid - * The mlid of the menu item. - * - * @return - * A menu link, with $item['access'] filled and link translated for - * rendering. - */ -function menu_link_load($mlid) { - if (is_numeric($mlid)) { - $query = db_select('menu_links', 'ml'); - $query->leftJoin('menu_router', 'm', 'm.path = ml.router_path'); - $query->fields('ml'); - // Weight should be taken from {menu_links}, not {menu_router}. - $query->addField('ml', 'weight', 'link_weight'); - $query->fields('m'); - $query->condition('ml.mlid', $mlid); - if ($item = $query->execute()->fetchAssoc()) { - $item['weight'] = $item['link_weight']; - _menu_link_translate($item); - return $item; - } - } - return FALSE; -} - -/** * Clears the cached cached data for a single named menu. */ function menu_cache_clear($menu_name = 'tools') { @@ -2637,6 +2579,7 @@ function menu_cache_clear_all() { * Resets the menu system static cache. */ function menu_reset_static_cache() { + entity_get_controller('menu_link')->resetCache(); drupal_static_reset('_menu_build_tree'); drupal_static_reset('menu_tree'); drupal_static_reset('menu_tree_all_data'); @@ -2734,156 +2677,115 @@ function menu_get_router() { } /** - * Builds a link from a router item. + * Builds menu links for the items in the menu router. + * + * @todo This function should be removed/refactored. */ -function _menu_link_build($item) { - // Suggested items are disabled by default. - if ($item['type'] == MENU_SUGGESTED_ITEM) { - $item['hidden'] = 1; +function _menu_navigation_links_rebuild($menu) { + if (module_exists('menu_link')) { + $menu_link_controller = entity_get_controller('menu_link'); } - // Hide all items that are not visible in the tree. - elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) { - $item['hidden'] = -1; + else { + // The Menu link module is not available at install time, so we need to + // hardcode the default storage controller. + $menu_link_controller = new MenuLinkStorageController('menu_link'); } - // Note, we set this as 'system', so that we can be sure to distinguish all - // the menu links generated automatically from entries in {menu_router}. - $item['module'] = 'system'; - $item += array( - 'menu_name' => 'tools', - 'link_title' => $item['title'], - 'link_path' => $item['path'], - 'hidden' => 0, - 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), - ); - return $item; -} -/** - * Builds menu links for the items in the menu router. - */ -function _menu_navigation_links_rebuild($menu) { // Add normal and suggested items as links. - $menu_links = array(); - foreach ($menu as $path => $item) { - if ($item['_visible']) { - $menu_links[$path] = $item; - $sort[$path] = $item['_number_parts']; + $router_items = array(); + foreach ($menu as $path => $router_item) { + if ($router_item['_visible']) { + $router_items[$path] = $router_item; + $sort[$path] = $router_item['_number_parts']; } } - if ($menu_links) { - // Keep an array of processed menu links, to allow menu_link_save() to - // check this for parents instead of querying the database. + if ($router_items) { + // Keep an array of processed menu links, to allow + // Drupal\menu_link\MenuLinkStorageController::save() to check this for + // parents instead of querying the database. $parent_candidates = array(); // Make sure no child comes before its parent. - array_multisort($sort, SORT_NUMERIC, $menu_links); + array_multisort($sort, SORT_NUMERIC, $router_items); - foreach ($menu_links as $key => $item) { + foreach ($router_items as $key => $router_item) { + // For performance reasons, do a straight query now and convert to a menu + // link entity later. $existing_item = db_select('menu_links') - ->fields('menu_links') - ->condition('link_path', $item['path']) - ->condition('module', 'system') - ->execute()->fetchAssoc(); + ->fields('menu_links') + ->condition('link_path', $router_item['path']) + ->condition('module', 'system') + ->execute()->fetchAll(); if ($existing_item) { - $item['mlid'] = $existing_item['mlid']; + $existing_item = reset($existing_item); + $existing_item->options = unserialize($existing_item->options); + + $router_item['mlid'] = $existing_item->mlid; + $router_item['uuid'] = $existing_item->uuid; // A change in hook_menu may move the link to a different menu - if (empty($item['menu_name']) || ($item['menu_name'] == $existing_item['menu_name'])) { - $item['menu_name'] = $existing_item['menu_name']; - $item['plid'] = $existing_item['plid']; + if (empty($router_item['menu_name']) || ($router_item['menu_name'] == $existing_item->menu_name)) { + $router_item['menu_name'] = $existing_item->menu_name; + $router_item['plid'] = $existing_item->plid; } else { - // It moved to a new menu. Let menu_link_save() try to find a new - // parent based on the path. - unset($item['plid']); + // It moved to a new menu. + // Let Drupal\menu_link\MenuLinkStorageController::save() try to find + // a new parent based on the path. + unset($router_item['plid']); } - $item['has_children'] = $existing_item['has_children']; - $item['updated'] = $existing_item['updated']; + $router_item['has_children'] = $existing_item->has_children; + $router_item['updated'] = $existing_item->updated; + + // Convert the existing item to a typed object. + $existing_item = $menu_link_controller->create(get_object_vars($existing_item)); } - if ($existing_item && $existing_item['customized']) { - $parent_candidates[$existing_item['mlid']] = $existing_item; + else { + $existing_item = NULL; + } + + if ($existing_item && $existing_item->customized) { + $parent_candidates[$existing_item->mlid] = $existing_item; } else { - $item = _menu_link_build($item); - menu_link_save($item, $existing_item, $parent_candidates); - $parent_candidates[$item['mlid']] = $item; - unset($menu_links[$key]); + $menu_link = MenuLink::buildFromRouterItem($router_item); + $menu_link->original = $existing_item; + $menu_link->parentCandidates = $parent_candidates; + $menu_link_controller->save($menu_link); + $parent_candidates[$menu_link->id()] = $menu_link; + unset($router_items[$key]); } } } + $paths = array_keys($menu); // Updated and customized items whose router paths are gone need new ones. - $result = db_select('menu_links', NULL, array('fetch' => PDO::FETCH_ASSOC)) - ->fields('menu_links', array( - 'link_path', - 'mlid', - 'router_path', - 'updated', - )) - ->condition(db_or() - ->condition('updated', 1) - ->condition(db_and() - ->condition('router_path', $paths, 'NOT IN') - ->condition('external', 0) - ->condition('customized', 1) - ) - ) - ->execute(); - foreach ($result as $item) { - $router_path = _menu_find_router_path($item['link_path']); - if (!empty($router_path) && ($router_path != $item['router_path'] || $item['updated'])) { + $menu_links = $menu_link_controller->loadUpdatedCustomized($paths); + foreach ($menu_links as $menu_link) { + $router_path = _menu_find_router_path($menu_link->link_path); + if (!empty($router_path) && ($router_path != $menu_link->router_path || $menu_link->updated)) { // If the router path and the link path matches, it's surely a working // item, so we clear the updated flag. - $updated = $item['updated'] && $router_path != $item['link_path']; - db_update('menu_links') - ->fields(array( - 'router_path' => $router_path, - 'updated' => (int) $updated, - )) - ->condition('mlid', $item['mlid']) - ->execute(); + $updated = $menu_link->updated && $router_path != $menu_link->link_path; + + $menu_link->router_path = $router_path; + $menu_link->updated = (int) $updated; + $menu_link_controller->save($menu_link); } } + // Find any item whose router path does not exist any more. - $result = db_select('menu_links') - ->fields('menu_links') + $query = entity_query('menu_link') ->condition('router_path', $paths, 'NOT IN') ->condition('external', 0) ->condition('updated', 0) ->condition('customized', 0) - ->orderBy('depth', 'DESC') - ->execute(); - // Remove all such items. Starting from those with the greatest depth will - // minimize the amount of re-parenting done by menu_link_delete(). - foreach ($result as $item) { - _menu_delete_item($item, TRUE); - } -} + ->sort('depth', 'DESC'); + $result = $query->execute(); -/** - * Clones an array of menu links. - * - * @param $links - * An array of menu links to clone. - * @param $menu_name - * (optional) The name of a menu that the links will be cloned for. If not - * set, the cloned links will be in the same menu as the original set of - * links that were passed in. - * - * @return - * An array of menu links with the same properties as the passed-in array, - * but with the link identifiers removed so that a new link will be created - * when any of them is passed in to menu_link_save(). - * - * @see menu_link_save() - */ -function menu_links_clone($links, $menu_name = NULL) { - foreach ($links as &$link) { - unset($link['mlid']); - unset($link['plid']); - if (isset($menu_name)) { - $link['menu_name'] = $menu_name; - } + // 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 (!empty($result)) { + menu_link_delete_multiple($result, TRUE); } - return $links; } /** @@ -2896,18 +2798,19 @@ function menu_links_clone($links, $menu_name = NULL) { * An array of menu links. */ function menu_load_links($menu_name) { - $links = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)) - ->fields('ml') - ->condition('ml.menu_name', $menu_name) + $links = array(); + + $query = entity_query('menu_link') + ->condition('menu_name', $menu_name) // Order by weight so as to be helpful for menus that are only one level // deep. - ->orderBy('weight') - ->execute() - ->fetchAll(); + ->sort('weight'); + $result = $query->execute(); - foreach ($links as &$link) { - $link['options'] = unserialize($link['options']); + if (!empty($result)) { + $links = menu_link_load_multiple($result); } + return $links; } @@ -2919,333 +2822,7 @@ function menu_load_links($menu_name) { */ function menu_delete_links($menu_name) { $links = menu_load_links($menu_name); - foreach ($links as $link) { - // To speed up the deletion process, we reset some link properties that - // would trigger re-parenting logic in _menu_delete_item() and - // _menu_update_parental_status(). - $link['has_children'] = FALSE; - $link['plid'] = 0; - _menu_delete_item($link); - } -} - -/** - * Delete one or several menu links. - * - * @param $mlid - * A valid menu link mlid or NULL. If NULL, $path is used. - * @param $path - * The path to the menu items to be deleted. $mlid must be NULL. - */ -function menu_link_delete($mlid, $path = NULL) { - if (isset($mlid)) { - _menu_delete_item(db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc()); - } - else { - $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $path)); - foreach ($result as $link) { - _menu_delete_item($link); - } - } -} - -/** - * Deletes a single menu link. - * - * @param $item - * Item to be deleted. - * @param $force - * Forces deletion. Internal use only, setting to TRUE is discouraged. - * - * @see menu_link_delete() - */ -function _menu_delete_item($item, $force = FALSE) { - $item = is_object($item) ? get_object_vars($item) : $item; - if ($item && ($item['module'] != 'system' || $item['updated'] || $force)) { - // Children get re-attached to the item's parent. - if ($item['has_children']) { - $result = db_query("SELECT mlid FROM {menu_links} WHERE plid = :plid", array(':plid' => $item['mlid'])); - foreach ($result as $m) { - $child = menu_link_load($m->mlid); - $child['plid'] = $item['plid']; - menu_link_save($child); - } - } - - // Notify modules we are deleting the item. - module_invoke_all('menu_link_delete', $item); - - db_delete('menu_links')->condition('mlid', $item['mlid'])->execute(); - - // Update the has_children status of the parent. - _menu_update_parental_status($item); - menu_cache_clear($item['menu_name']); - _menu_clear_page_cache(); - } -} - -/** - * Saves a menu link. - * - * After calling this function, rebuild the menu cache using - * menu_cache_clear_all(). - * - * @param $item - * An associative array representing a menu link item, with elements: - * - link_path: (required) The path of the menu item, which should be - * normalized first by calling drupal_container()->get('path.alias_manager')->getSystemPath() on it. - * - link_title: (required) Title to appear in menu for the link. - * - menu_name: (optional) The machine name of the menu for the link. - * Defaults to 'tools'. - * - weight: (optional) Integer to determine position in menu. Default is 0. - * - expanded: (optional) Boolean that determines if the item is expanded. - * - options: (optional) An array of options, see l() for more. - * - mlid: (optional) Menu link identifier, the primary integer key for each - * menu link. Can be set to an existing value, or to 0 or NULL - * to insert a new link. - * - plid: (optional) The mlid of the parent. - * - router_path: (optional) The path of the relevant router item. - * @param $existing_item - * Optional, the current record from the {menu_links} table as an array. - * @param $parent_candidates - * Optional array of menu links keyed by mlid. Used by - * _menu_navigation_links_rebuild() only. - * - * @return - * The mlid of the saved menu link, or FALSE if the menu link could not be - * saved. - */ -function menu_link_save(&$item, $existing_item = array(), $parent_candidates = array()) { - drupal_alter('menu_link', $item); - - // This is the easiest way to handle the unique internal path '', - // since a path marked as external does not need to match a router path. - $item['external'] = (url_is_external($item['link_path']) || $item['link_path'] == '') ? 1 : 0; - // Load defaults. - $item += array( - 'menu_name' => 'tools', - 'weight' => 0, - 'link_title' => '', - 'hidden' => 0, - 'has_children' => 0, - 'expanded' => 0, - 'options' => array(), - 'module' => 'menu', - 'customized' => 0, - 'updated' => 0, - ); - if (isset($item['mlid'])) { - if (!$existing_item) { - $existing_item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array('mlid' => $item['mlid']))->fetchAssoc(); - } - if ($existing_item) { - $existing_item['options'] = unserialize($existing_item['options']); - } - } - else { - $existing_item = FALSE; - } - - // Try to find a parent link. If found, assign it and derive its menu. - $parent = _menu_link_find_parent($item, $parent_candidates); - if (!empty($parent['mlid'])) { - $item['plid'] = $parent['mlid']; - $item['menu_name'] = $parent['menu_name']; - } - // If no corresponding parent link was found, move the link to the top-level. - else { - $item['plid'] = 0; - } - $menu_name = $item['menu_name']; - - if (!$existing_item) { - $item['mlid'] = db_insert('menu_links') - ->fields(array( - 'menu_name' => $item['menu_name'], - 'plid' => $item['plid'], - 'link_path' => $item['link_path'], - 'hidden' => $item['hidden'], - 'external' => $item['external'], - 'has_children' => $item['has_children'], - 'expanded' => $item['expanded'], - 'weight' => $item['weight'], - 'module' => $item['module'], - 'link_title' => $item['link_title'], - 'options' => serialize($item['options']), - 'customized' => $item['customized'], - 'updated' => $item['updated'], - )) - ->execute(); - } - - // Directly fill parents for top-level links. - if ($item['plid'] == 0) { - $item['p1'] = $item['mlid']; - for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { - $item["p$i"] = 0; - } - $item['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 ($item['has_children'] && $existing_item) { - $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1; - } - else { - $limit = MENU_MAX_DEPTH - 1; - } - if ($parent['depth'] > $limit) { - return FALSE; - } - $item['depth'] = $parent['depth'] + 1; - _menu_link_parents_set($item, $parent); - } - // Need to check both plid and menu_name, since plid can be 0 in any menu. - if ($existing_item && ($item['plid'] != $existing_item['plid'] || $menu_name != $existing_item['menu_name'])) { - _menu_link_move_children($item, $existing_item); - } - // Find the router_path. - if (empty($item['router_path']) || !$existing_item || ($existing_item['link_path'] != $item['link_path'])) { - if ($item['external']) { - $item['router_path'] = ''; - } - else { - // Find the router path which will serve this path. - $item['parts'] = explode('/', $item['link_path'], MENU_MAX_PARTS); - $item['router_path'] = _menu_find_router_path($item['link_path']); - } - } - // If every value in $existing_item is the same in the $item, there is no - // reason to run the update queries or clear the caches. We use - // array_intersect_key() with the $item as the first parameter because - // $item may have additional keys left over from building a router entry. - // The intersect removes the extra keys, allowing a meaningful comparison. - if (!$existing_item || (array_intersect_key($item, $existing_item) != $existing_item)) { - db_update('menu_links') - ->fields(array( - 'menu_name' => $item['menu_name'], - 'plid' => $item['plid'], - 'link_path' => $item['link_path'], - 'router_path' => $item['router_path'], - 'hidden' => $item['hidden'], - 'external' => $item['external'], - 'has_children' => $item['has_children'], - 'expanded' => $item['expanded'], - 'weight' => $item['weight'], - 'depth' => $item['depth'], - 'p1' => $item['p1'], - 'p2' => $item['p2'], - 'p3' => $item['p3'], - 'p4' => $item['p4'], - 'p5' => $item['p5'], - 'p6' => $item['p6'], - 'p7' => $item['p7'], - 'p8' => $item['p8'], - 'p9' => $item['p9'], - 'module' => $item['module'], - 'link_title' => $item['link_title'], - 'options' => serialize($item['options']), - 'customized' => $item['customized'], - )) - ->condition('mlid', $item['mlid']) - ->execute(); - // Check the has_children status of the parent. - _menu_update_parental_status($item); - menu_cache_clear($menu_name); - if ($existing_item && $menu_name != $existing_item['menu_name']) { - menu_cache_clear($existing_item['menu_name']); - } - // Notify modules we have acted on a menu item. - $hook = 'menu_link_insert'; - if ($existing_item) { - $hook = 'menu_link_update'; - } - module_invoke_all($hook, $item); - // Now clear the cache. - _menu_clear_page_cache(); - } - return $item['mlid']; -} - -/** - * Finds a possible parent for a given menu link. - * - * Because the parent of a given link might not exist anymore in the database, - * we apply a set of heuristics to determine a proper parent: - * - * - use the passed parent link if specified and existing. - * - else, use the first existing link down the previous link hierarchy - * - else, for system menu links (derived from hook_menu()), reparent - * based on the path hierarchy. - * - * @param $menu_link - * A menu link. - * @param $parent_candidates - * An array of menu links keyed by mlid. - * - * @return - * A menu link structure of the possible parent or FALSE if no valid parent - * has been found. - */ -function _menu_link_find_parent($menu_link, $parent_candidates = array()) { - $parent = FALSE; - - // This item is explicitely top-level, skip the rest of the parenting. - if (isset($menu_link['plid']) && empty($menu_link['plid'])) { - return $parent; - } - - // If we have a parent link ID, try to use that. - $candidates = array(); - if (isset($menu_link['plid'])) { - $candidates[] = $menu_link['plid']; - } - - // Else, if we have a link hierarchy try to find a valid parent in there. - if (!empty($menu_link['depth']) && $menu_link['depth'] > 1) { - for ($depth = $menu_link['depth'] - 1; $depth >= 1; $depth--) { - $candidates[] = $menu_link['p' . $depth]; - } - } - - foreach ($candidates as $mlid) { - if (isset($parent_candidates[$mlid])) { - $parent = $parent_candidates[$mlid]; - } - else { - $parent = db_query("SELECT * FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchAssoc(); - } - if ($parent) { - return $parent; - } - } - - // If everything else failed, try to derive the parent from the path - // hierarchy. This only makes sense for links derived from menu router - // items (ie. from hook_menu()). - if ($menu_link['module'] == 'system') { - $query = db_select('menu_links'); - $query->condition('module', 'system'); - // We always respect the link's 'menu_name'; inheritance for router items is - // ensured in _menu_router_build(). - $query->condition('menu_name', $menu_link['menu_name']); - - // Find the parent - it must be unique. - $parent_path = $menu_link['link_path']; - do { - $parent = FALSE; - $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); - $new_query = clone $query; - $new_query->condition('link_path', $parent_path); - // Only valid if we get a unique result. - if ($new_query->countQuery()->execute()->fetchField() == 1) { - $parent = $new_query->fields('menu_links')->execute()->fetchAssoc(); - } - } while ($parent === FALSE && $parent_path); - } - - return $parent; + menu_link_delete_multiple(array_keys($links), FALSE, TRUE); } /** @@ -3321,188 +2898,6 @@ function _menu_find_router_path($link_path) { } /** - * 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. - * - * @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) { - switch ($op) { - case 'insert': - $menu_link = array( - 'link_title' => $link_title, - 'link_path' => $link_path, - 'module' => $module, - ); - return menu_link_save($menu_link); - - case 'update': - $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC); - foreach ($result as $link) { - $existing = $link; - if (isset($link_title)) { - $link['link_title'] = $link_title; - } - $link['options'] = unserialize($link['options']); - menu_link_save($link, $existing); - } - break; - - case 'enable': - case 'disable': - $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path AND module = :module AND customized = 0", array(':link_path' => $link_path, ':module' => $module))->fetchAll(PDO::FETCH_ASSOC); - foreach ($result as $link) { - $existing = $link; - $link['hidden'] = ($op == 'disable' ? 1 : 0); - $link['customized'] = 1; - if (isset($link_title)) { - $link['link_title'] = $link_title; - } - $link['options'] = unserialize($link['options']); - menu_link_save($link, $existing); - } - break; - - case 'delete': - menu_link_delete(NULL, $link_path); - break; - } -} - -/** - * Finds the depth of an item's children relative to its depth. - * - * For example, if the item has a depth of 2, and the maximum of any child in - * the menu link tree is 5, the relative depth is 3. - * - * @param $item - * An array representing a menu link item. - * - * @return - * The relative depth, or zero. - * - */ -function menu_link_children_relative_depth($item) { - $query = db_select('menu_links'); - $query->addField('menu_links', 'depth'); - $query->condition('menu_name', $item['menu_name']); - $query->orderBy('depth', 'DESC'); - $query->range(0, 1); - - $i = 1; - $p = 'p1'; - while ($i <= MENU_MAX_DEPTH && $item[$p]) { - $query->condition($p, $item[$p]); - $p = 'p' . ++$i; - } - - $max_depth = $query->execute()->fetchField(); - - return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0; -} - -/** - * Updates the children of a menu link that is being moved. - * - * The menu name, parents (p1 - p6), and depth are updated for all children of - * the link, and the has_children status of the previous parent is updated. - */ -function _menu_link_move_children($item, $existing_item) { - $query = db_update('menu_links'); - - $query->fields(array('menu_name' => $item['menu_name'])); - - $p = 'p1'; - $expressions = array(); - for ($i = 1; $i <= $item['depth']; $p = 'p' . ++$i) { - $expressions[] = array($p, ":p_$i", array(":p_$i" => $item[$p])); - } - $j = $existing_item['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 = $item['depth'] - $existing_item['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', $existing_item['menu_name']); - $p = 'p1'; - for ($i = 1; $i <= MENU_MAX_DEPTH && $existing_item[$p]; $p = 'p' . ++$i) { - $query->condition($p, $existing_item[$p]); - } - - $query->execute(); - - // Check the has_children status of the parent, while excluding this item. - _menu_update_parental_status($existing_item, TRUE); -} - -/** - * Checks and updates the 'has_children' status for the parent of a link. - */ -function _menu_update_parental_status($item, $exclude = FALSE) { - // If plid == 0, there is nothing to update. - if ($item['plid']) { - // Check if at least one visible child exists in the table. - $query = db_select('menu_links'); - $query->addField('menu_links', 'mlid'); - $query->condition('menu_name', $item['menu_name']); - $query->condition('hidden', 0); - $query->condition('plid', $item['plid']); - $query->range(0, 1); - if ($exclude) { - $query->condition('mlid', $item['mlid'], '<>'); - } - $parent_has_children = ((bool) $query->execute()->fetchField()) ? 1 : 0; - db_update('menu_links') - ->fields(array('has_children' => $parent_has_children)) - ->condition('mlid', $item['plid']) - ->execute(); - } -} - -/** - * Sets the p1 through p9 values for a menu link being saved. - */ -function _menu_link_parents_set(&$item, $parent) { - $i = 1; - while ($i < $item['depth']) { - $p = 'p' . $i++; - $item[$p] = $parent[$p]; - } - $p = 'p' . $i++; - // The parent (p1 - p9) corresponding to the depth always equals the mlid. - $item[$p] = $item['mlid']; - while ($i <= MENU_MAX_DEPTH) { - $p = 'p' . $i++; - $item[$p] = 0; - } -} - -/** * Builds the router table based on the data from hook_menu(). */ function _menu_router_build($callbacks) { diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module index 2fd5929..604c110 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -391,7 +391,7 @@ function aggregator_save_category($edit) { ->execute(); $op = 'insert'; } - if (isset($op)) { + if (isset($op) && module_exists('menu_link')) { menu_link_maintain('aggregator', $op, $link_path, $edit['title']); } } diff --git a/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php b/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php index a6a1dab..a09576f 100644 --- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php +++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php @@ -37,8 +37,8 @@ function testCategorizeFeedItem() { $this->assertTrue(!empty($category), 'The category found in database.'); $link_path = 'aggregator/categories/' . $category->cid; - $menu_link = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(':link_path' => $link_path))->fetch(); - $this->assertTrue(!empty($menu_link), 'The menu link associated with the category found in database.'); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path)); + $this->assertTrue(!empty($menu_links), 'The menu link associated with the category found in database.'); $feed = $this->createFeed(); db_insert('aggregator_category_feed') diff --git a/core/modules/book/book.info b/core/modules/book/book.info index b42a72b..d7fa3c9 100644 --- a/core/modules/book/book.info +++ b/core/modules/book/book.info @@ -3,5 +3,6 @@ description = Allows users to create and organize related content in an outline. package = Core version = VERSION core = 8.x +dependencies[] = menu_link dependencies[] = node configure = admin/content/book/settings diff --git a/core/modules/book/book.module b/core/modules/book/book.module index af436d4..e2cec99 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -5,9 +5,10 @@ * Allows users to create and organize related content in an outline. */ -use Drupal\node\Plugin\Core\Entity\Node; -use Drupal\entity\Plugin\Core\Entity\EntityDisplay; use Drupal\Core\Template\Attribute; +use Drupal\entity\Plugin\Core\Entity\EntityDisplay; +use Drupal\node\Plugin\Core\Entity\Node; +use Drupal\menu_link\MenuLinkStorageController; /** * Implements hook_help(). @@ -548,7 +549,9 @@ function _book_update_outline(Node $node) { } } - if (menu_link_save($node->book)) { + $node->book = entity_create('menu_link', $node->book); + try { + menu_link_save($node->book); if ($new) { // Insert new. db_insert('book') @@ -574,9 +577,10 @@ function _book_update_outline(Node $node) { return TRUE; } - - // Failed to save the menu link. - return FALSE; + catch (Exception $e) { + // Failed to save the menu link. + return FALSE; + } } /** @@ -916,7 +920,7 @@ function book_node_prepare(Node $node) { * The depth limit for items in the parent select. */ function _book_parent_depth_limit($book_link) { - return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? menu_link_children_relative_depth($book_link) : 0); + return MENU_MAX_DEPTH - 1 - (($book_link['mlid'] && $book_link['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($book_link) : 0); } /** diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php index cf01fbe..d731718 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php @@ -105,12 +105,12 @@ function testMenuNodeFormWidget() { $this->assertNoLink($node_title); // Add a menu link to the Administration menu. - $item = array( + $item = entity_create('menu_link', array( 'link_path' => 'node/' . $node->nid, 'link_title' => $this->randomName(16), 'menu_name' => 'admin', - ); - menu_link_save($item); + )); + $item->save(); // Assert that disabled Administration menu is not shown on the // node/$nid/edit page. @@ -127,12 +127,12 @@ function testMenuNodeFormWidget() { // 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 = array( + $child_item = entity_create('menu_link', array( 'link_path' => 'node/'. $child_node->nid, 'link_title' => $this->randomName(16), 'plid' => $item['mlid'], - ); - menu_link_save($child_item); + )); + $child_item->save(); // Edit the first node. $this->drupalGet('node/'. $node->nid .'/edit'); // Assert that it is not possible to set the parent of the first node to itself or the second node. diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index a0d8d28..76bad18 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -199,7 +199,7 @@ function deleteCustomMenu($menu) { $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $label)), 'Custom menu was deleted'); $this->assertFalse(menu_load($menu_name), 'Custom menu was deleted'); // Test if all menu links associated to the menu were removed from database. - $result = db_query("SELECT menu_name FROM {menu_links} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField(); + $result = entity_load_multiple_by_properties('menu_link', array('menu_name' => $menu_name)); $this->assertFalse($result, 'All menu links associated to the custom menu were deleted.'); } @@ -301,7 +301,9 @@ function testMenuQueryAndFragment() { * @param string $link Link path. * @param string $menu_name Menu name. * @param string $weight Menu weight - * @return array Menu link created. + * + * @return \Drupal\menu_link\Plugin\Core\Entity\MenuLink $menu_link + * A menu link entity. */ function addMenuLink($plid = 0, $link = '', $menu_name = 'tools', $expanded = TRUE, $weight = '0') { // View add menu link page. @@ -322,14 +324,14 @@ function addMenuLink($plid = 0, $link = '', $menu_name = 'tools', $expand // Add menu link. $this->drupalPost(NULL, $edit, t('Save')); $this->assertResponse(200); - // Unlike most other modules, there is no confirmation message displayed. - $this->assertText($title, 'Menu link was added'); + $this->assertText('The menu link has been saved.'); - $item = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => $title))->fetchAssoc(); - $this->assertTrue(t('Menu link was found in database.')); - $this->assertMenuLink($item['mlid'], array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid)); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $title)); + $menu_link = reset($menu_links); + $this->assertTrue('Menu link was found in database.'); + $this->assertMenuLink($menu_link->id(), array('menu_name' => $menu_name, 'link_path' => $link, 'has_children' => 0, 'plid' => $plid)); - return $item; + return $menu_link; } /** @@ -412,11 +414,7 @@ function modifyMenuLink(&$item) { $edit['link_title'] = $title; $this->drupalPost("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); $this->assertResponse(200); - // Unlike most other modules, there is no confirmation message displayed. - - // Verify menu link. - $this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']); - $this->assertText($title, 'Menu link was edited'); + $this->assertText('The menu link has been saved.'); } /** @@ -520,8 +518,8 @@ function enableMenuLink($item) { */ function assertMenuLink($mlid, array $expected_item) { // Retrieve menu link. - $item = db_query('SELECT * FROM {menu_links} WHERE mlid = :mlid', array(':mlid' => $mlid))->fetchAssoc(); - $options = unserialize($item['options']); + $item = menu_link_load($mlid); + $options = $item->options; if (!empty($options['query'])) { $item['link_path'] .= '?' . drupal_http_build_query($options['query']); } @@ -537,8 +535,16 @@ function assertMenuLink($mlid, array $expected_item) { * Get standard menu link. */ private function getStandardMenuLink() { + $mlid = 0; // Retrieve menu link id of the Log out menu link, which will always be on the front page. - $mlid = db_query("SELECT mlid FROM {menu_links} WHERE module = 'system' AND router_path = 'user/logout'")->fetchField(); + $query = entity_query('menu_link') + ->condition('module', 'system') + ->condition('router_path', 'user/logout'); + $result = $query->execute(); + if (!empty($result)) { + $mlid = 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. diff --git a/core/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc index de76a31..ad4036f 100644 --- a/core/modules/menu/menu.admin.inc +++ b/core/modules/menu/menu.admin.inc @@ -5,7 +5,7 @@ * Administrative page callbacks for menu module. */ -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; use Drupal\system\Plugin\Core\Entity\Menu; /** @@ -55,16 +55,18 @@ function menu_menu_edit(Menu $menu) { function menu_overview_form($form, &$form_state, $menu) { global $menu_admin; $form['#attached']['css'] = array(drupal_get_path('module', 'menu') . '/menu.admin.css'); - $sql = " - SELECT m.load_functions, m.to_arg_functions, m.access_callback, m.access_arguments, m.page_callback, m.page_arguments, m.title, m.title_callback, m.title_arguments, m.type, m.description, m.description_callback, m.description_arguments, ml.* - FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path - WHERE ml.menu_name = :menu - ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC, p6 ASC, p7 ASC, p8 ASC, p9 ASC"; - $result = db_query($sql, array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC)); $links = array(); - foreach ($result as $item) { - $links[] = $item; + $query = entity_query('menu_link') + ->condition('menu_name', $menu->id()); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $query->sort('p' . $i, 'ASC'); } + $result = $query->execute(); + + if (!empty($result)) { + $links = menu_link_load_multiple($result); + } + $delta = max(count($links), 50); $tree = menu_tree_data($links); $node_links = array(); @@ -278,193 +280,6 @@ function theme_menu_overview_form($variables) { } /** - * Menu callback; Build the menu link editing form. - */ -function menu_edit_item($form, &$form_state, $type, $item, $menu) { - if ($type == 'add' || empty($item)) { - // This is an add form, initialize the menu link. - $item = array('link_title' => '', 'mlid' => 0, 'plid' => 0, 'menu_name' => $menu->id(), 'weight' => 0, 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0); - } - else { - // Get the human-readable menu title from the given menu name. - $titles = menu_get_menus(); - $current_title = $titles[$item['menu_name']]; - - // Get the current breadcrumb and add a link to that menu's overview page. - $breadcrumb = menu_get_active_breadcrumb(); - $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $item['menu_name']); - drupal_set_breadcrumb($breadcrumb); - } - $form['actions'] = array('#type' => 'actions'); - $form['link_title'] = array( - '#type' => 'textfield', - '#title' => t('Menu link title'), - '#default_value' => $item['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' => $item[$key]); - } - // Any item created or edited via this interface is considered "customized". - $form['customized'] = array('#type' => 'value', '#value' => 1); - $form['original_item'] = array('#type' => 'value', '#value' => $item); - - $path = $item['link_path']; - if (isset($item['options']['query'])) { - $path .= '?' . drupal_http_build_query($item['options']['query']); - } - if (isset($item['options']['fragment'])) { - $path .= '#' . $item['options']['fragment']; - } - if ($item['module'] == 'menu') { - $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, - ); - $form['actions']['delete'] = array( - '#type' => 'submit', - '#value' => t('Delete'), - '#access' => $item['mlid'], - '#submit' => array('menu_item_delete_submit'), - '#weight' => 10, - ); - } - else { - $form['_path'] = array( - '#type' => 'item', - '#title' => t('Path'), - '#description' => l($item['link_title'], $item['href'], $item['options']), - ); - } - $form['description'] = array( - '#type' => 'textarea', - '#title' => t('Description'), - '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '', - '#rows' => 1, - '#description' => t('Shown when hovering over the menu link.'), - ); - $form['enabled'] = array( - '#type' => 'checkbox', - '#title' => t('Enabled'), - '#default_value' => !$item['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' => $item['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_parent_options(menu_get_menus(), $item); - $default = $item['menu_name'] . ':' . $item['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. - $sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu"; - $result = db_query($sql, array(':menu' => $item['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $row) { - foreach ($row as $menu_item_count) { - $delta = $menu_item_count; - } - } - if ($delta < 50) { - // Old hardcoded value. - $delta = 50; - } - $form['weight'] = array( - '#type' => 'weight', - '#title' => t('Weight'), - '#delta' => $delta, - '#default_value' => $item['weight'], - '#description' => t('Optional. In the menu, the heavier links will sink and the lighter links will be positioned nearer the top.'), - ); - $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'), '#button_type' => 'primary'); - - return $form; -} - -/** - * Validate form values for a menu link being added or edited. - */ -function menu_edit_item_validate($form, &$form_state) { - $item = &$form_state['values']; - $normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($item['link_path']); - if ($item['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' => $item['link_path'], '%normal_path' => $normal_path))); - $item['link_path'] = $normal_path; - } - if (!url_is_external($item['link_path'])) { - $parsed_link = parse_url($item['link_path']); - if (isset($parsed_link['query'])) { - $item['options']['query'] = drupal_get_query_array($parsed_link['query']); - } - else { - // Use unset() rather than setting to empty string - // to avoid redundant serialized data being stored. - unset($item['options']['query']); - } - if (isset($parsed_link['fragment'])) { - $item['options']['fragment'] = $parsed_link['fragment']; - } - else { - unset($item['options']['fragment']); - } - if (isset($parsed_link['path']) && $item['link_path'] != $parsed_link['path']) { - $item['link_path'] = $parsed_link['path']; - } - } - if (!trim($item['link_path']) || !drupal_valid_path($item['link_path'], TRUE)) { - form_set_error('link_path', t("The path '@link_path' is either invalid or you do not have access to it.", array('@link_path' => $item['link_path']))); - } -} - -/** - * Submit function for the delete button on the menu item editing form. - */ -function menu_item_delete_submit($form, &$form_state) { - $form_state['redirect'] = 'admin/structure/menu/item/' . $form_state['values']['mlid'] . '/delete'; -} - -/** - * Process menu and menu item add/edit form submissions. - */ -function menu_edit_item_submit($form, &$form_state) { - $item = &$form_state['values']; - - // The value of "hidden" is the opposite of the value - // supplied by the "enabled" checkbox. - $item['hidden'] = (int) !$item['enabled']; - unset($item['enabled']); - - $item['options']['attributes']['title'] = $item['description']; - list($item['menu_name'], $item['plid']) = explode(':', $item['parent']); - if (!menu_link_save($item)) { - drupal_set_message(t('There was an error saving the menu link.'), 'error'); - } - else { - drupal_set_message(t('Your configuration has been saved.')); - } - $form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name']; -} - -/** * Menu callback; check access and get a confirm form for deletion of a custom menu. */ function menu_delete_menu_page($menu) { @@ -482,7 +297,7 @@ function menu_delete_menu_page($menu) { function menu_delete_menu_confirm($form, &$form_state, Menu $menu) { $form['#menu'] = $menu; $caption = ''; - $num_links = db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu", array(':menu' => $menu->id()))->fetchField(); + $num_links = entity_get_controller('menu_link')->countMenuLinks($menu->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' => $menu->label())) . '

'; } @@ -504,16 +319,16 @@ function menu_delete_menu_confirm_submit($form, &$form_state) { } // Reset all the menu links defined by the system via hook_menu(). - $result = db_query("SELECT * FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $link) { - menu_reset_item($link); + // @todo Convert this to an EFQ once we figure out 'ORDER BY m.number_parts'. + $result = db_query("SELECT mlid FROM {menu_links} ml INNER JOIN {menu_router} m ON ml.router_path = m.path WHERE ml.menu_name = :menu AND ml.module = 'system' ORDER BY m.number_parts ASC", array(':menu' => $menu->id()), array('fetch' => PDO::FETCH_ASSOC))->fetchCol(); + $menu_links = menu_link_load_multiple($result); + foreach ($menu_links as $link) { + $link->reset(); } // Delete all links to the overview page for this menu. - $result = db_query("SELECT mlid FROM {menu_links} ml WHERE ml.link_path = :link", array(':link' => 'admin/structure/menu/manage/' . $menu->id()), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $link) { - menu_link_delete($link['mlid']); - } + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu/manage/' . $menu->id())); + menu_link_delete_multiple(array_keys($menu_links)); // Delete the custom menu and all its menu links. $menu->delete(); @@ -533,7 +348,7 @@ function menu_edit_menu_name_exists($value) { $custom_exists = entity_load('menu', $value); // 'menu-' is added to the menu name to avoid name-space conflicts. $value = 'menu-' . $value; - $link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $value))->fetchField(); + $link_exists = entity_query('menu_link')->condition('menu_name', $value)->range(0,1)->count()->execute(); return $custom_exists || $link_exists; } @@ -547,26 +362,23 @@ function menu_edit_menu_submit($form, &$form_state) { if ($form['#insert']) { // Add 'menu-' to the menu name to help avoid name-space conflicts. $menu['id'] = 'menu-' . $menu['id']; - $link['link_title'] = $menu['label']; - $link['link_path'] = $path . $menu['id']; - $link['router_path'] = $path . '%'; - $link['module'] = 'menu'; - $link['plid'] = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :link AND module = :module", array( - ':link' => 'admin/structure/menu', - ':module' => 'system' - )) - ->fetchField(); - - menu_link_save($link); + $system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system')); + $system_link = reset($system_link); + $menu_link = entity_create('menu_link', array( + 'link_title' => $menu['label'], + 'link_path' => $path . $menu['id'], + 'router_path' => $path . '%', + 'plid' => $system_link->id(), + )); + $menu_link->save(); menu_save($menu); } else { menu_save($menu); - $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path", array(':path' => $path . $menu['id']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $m) { - $link = menu_link_load($m['mlid']); - $link['link_title'] = $menu['label']; - menu_link_save($link); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path . $menu['id'])); + foreach ($menu_links as $menu_link) { + $menu_link->link_title = $menu['label']; + $menu_link->save(); } } drupal_set_message(t('Your configuration has been saved.')); @@ -574,53 +386,80 @@ function menu_edit_menu_submit($form, &$form_state) { } /** - * Menu callback; Check access and present a confirm form for deleting a menu link. + * Menu callback: Provides the menu link submission form. + * + * @param \Drupal\system\Plugin\Core\Entity\Menu $menu + * A menu object. + * + * @return + * Returns the menu link submission form. + */ +function menu_link_add(Menu $menu) { + $menu_link = entity_create('menu_link', array( + 'mlid' => 0, + 'plid' => 0, + 'menu_name' => $menu->id(), + )); + drupal_set_title(t('Add menu link')); + return entity_get_form($menu_link); +} + +/** + * Menu callback; Check access and present a confirm form for deleting a menu + * link. */ -function menu_item_delete_page($item) { +function menu_link_delete_page(MenuLink $menu_link) { // Links defined via hook_menu may not be deleted. Updated items are an // exception, as they can be broken. - if ($item['module'] == 'system' && !$item['updated']) { + if ($menu_link->module == 'system' && !$menu_link->updated) { throw new AccessDeniedHttpException(); } - return drupal_get_form('menu_item_delete_form', $item); + return drupal_get_form('menu_link_delete_form', $menu_link); } /** * Build a confirm form for deletion of a single menu link. */ -function menu_item_delete_form($form, &$form_state, $item) { - $form['#item'] = $item; - return confirm_form($form, t('Are you sure you want to delete the custom menu link %item?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name']); +function menu_link_delete_form($form, &$form_state, MenuLink $menu_link) { + $form['#menu_link'] = $menu_link; + return confirm_form($form, + t('Are you sure you want to delete the custom menu link %item?', array('%item' => $menu_link->link_title)), + 'admin/structure/menu/manage/' . $menu_link->menu_name + ); } /** - * Process menu delete form submissions. + * Processes menu link delete form submissions. */ -function menu_item_delete_form_submit($form, &$form_state) { - $item = $form['#item']; - menu_link_delete($item['mlid']); - $t_args = array('%title' => $item['link_title']); +function menu_link_delete_form_submit($form, &$form_state) { + $menu_link = $form['#menu_link']; + menu_link_delete($menu_link->id()); + $t_args = array('%title' => $menu_link->link_title); drupal_set_message(t('The menu link %title has been deleted.', $t_args)); watchdog('menu', 'Deleted menu link %title.', $t_args, WATCHDOG_NOTICE); - $form_state['redirect'] = 'admin/structure/menu/manage/' . $item['menu_name']; + $form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name; } /** - * Menu callback; reset a single modified menu link. + * Menu callback; Reset a single modified menu link. */ -function menu_reset_item_confirm($form, &$form_state, $item) { - $form['item'] = array('#type' => 'value', '#value' => $item); - return confirm_form($form, t('Are you sure you want to reset the link %item to its default values?', array('%item' => $item['link_title'])), 'admin/structure/menu/manage/' . $item['menu_name'], t('Any customizations will be lost. This action cannot be undone.'), t('Reset')); +function menu_link_reset_form($form, &$form_state, MenuLink $menu_link) { + $form['#menu_link'] = $menu_link; + return confirm_form($form, + t('Are you sure you want to reset the link %item to its default values?', array('%item' => $menu_link->link_title)), + 'admin/structure/menu/manage/' . $menu_link->menu_name, + t('Any customizations will be lost. This action cannot be undone.'), + t('Reset')); } /** - * Process menu reset item form submissions. + * Processes menu link reset form submissions. */ -function menu_reset_item_confirm_submit($form, &$form_state) { - $item = $form_state['values']['item']; - $new_item = menu_reset_item($item); +function menu_link_reset_form_submit($form, &$form_state) { + $menu_link = $form['#menu_link']; + $new_menu_link = $menu_link->reset(); drupal_set_message(t('The menu link was reset to its default settings.')); - $form_state['redirect'] = 'admin/structure/menu/manage/' . $new_item['menu_name']; + $form_state['redirect'] = 'admin/structure/menu/manage/' . $new_menu_link->menu_name; } /** diff --git a/core/modules/menu/menu.info b/core/modules/menu/menu.info index e5e2c8b..7aad518 100644 --- a/core/modules/menu/menu.info +++ b/core/modules/menu/menu.info @@ -3,4 +3,5 @@ description = Allows administrators to customize the site navigation menu. package = Core version = VERSION core = 8.x +dependencies[] = menu_link configure = admin/structure/menu diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index 6acb232..1ff17df 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -16,6 +16,8 @@ use Drupal\system\Plugin\Core\Entity\Menu; use Drupal\system\Plugin\block\block\SystemMenuBlock; use Symfony\Component\HttpFoundation\JsonResponse; +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; +use Drupal\menu_link\MenuLinkStorageController; /** * Maximum length of menu name as entered by the user. Database length is 32 @@ -114,9 +116,9 @@ function menu_menu() { 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, ); $items['admin/structure/menu/manage/%menu/add'] = array( - 'title' => 'Add link', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('menu_edit_item', 'add', NULL, 4), + 'title' => 'Add menu link', + 'page callback' => 'menu_link_add', + 'page arguments' => array(4), 'access arguments' => array('administer menu'), 'type' => MENU_LOCAL_ACTION, 'file' => 'menu.admin.inc', @@ -139,21 +141,20 @@ function menu_menu() { ); $items['admin/structure/menu/item/%menu_link/edit'] = array( 'title' => 'Edit menu link', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('menu_edit_item', 'edit', 4, NULL), + 'page callback' => 'entity_get_form', + 'page arguments' => array(4), 'access arguments' => array('administer menu'), - 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/item/%menu_link/reset'] = array( 'title' => 'Reset menu link', 'page callback' => 'drupal_get_form', - 'page arguments' => array('menu_reset_item_confirm', 4), + 'page arguments' => array('menu_link_reset_form', 4), 'access arguments' => array('administer menu'), 'file' => 'menu.admin.inc', ); $items['admin/structure/menu/item/%menu_link/delete'] = array( 'title' => 'Delete menu link', - 'page callback' => 'menu_item_delete_page', + 'page callback' => 'menu_link_delete_page', 'page arguments' => array(4), 'access arguments' => array('administer menu'), 'file' => 'menu.admin.inc', @@ -203,23 +204,29 @@ function menu_theme() { */ function menu_enable() { menu_router_rebuild(); - $base_link = db_query("SELECT mlid AS plid, menu_name FROM {menu_links} WHERE link_path = 'admin/structure/menu' AND module = 'system'")->fetchAssoc(); - $base_link['router_path'] = 'admin/structure/menu/manage/%'; - $base_link['module'] = 'menu'; + $system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/structure/menu', 'module' => 'system')); + $system_link = reset($system_link); + + $base_link = entity_create('menu_link', array( + 'menu_name' => $system_link->menu_name, + 'router_path' => 'admin/structure/menu/manage/%', + 'module' => 'menu', + )); + $menus = entity_load_multiple('menu'); foreach ($menus as $menu) { - // $link is passed by reference to menu_link_save(), so we make a copy of $base_link. - $link = $base_link; - $link['mlid'] = 0; - $link['link_title'] = $menu->label(); - $link['link_path'] = 'admin/structure/menu/manage/' . $menu->id(); - $menu_link = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND plid = :plid", array( - ':path' => $link['link_path'], - ':plid' => $link['plid'] - )) - ->fetchField(); - if (!$menu_link) { - menu_link_save($link); + $link = $base_link->createDuplicate(); + $link->plid = $system_link->id(); + $link->link_title = $menu->label(); + $link->link_path = 'admin/structure/menu/manage/' . $menu->id(); + + $query = entity_query('menu_link') + ->condition('link_path', $link->link_path) + ->condition('plid', $link->plid); + $result = $query->execute(); + + if (empty($result)) { + $link->save(); } } menu_cache_clear_all(); @@ -301,24 +308,26 @@ function menu_menu_delete(Menu $menu) { } /** - * Return a list of menu items that are valid possible parents for the given menu item. + * Returns a list of menu links that are valid possible parents for the given + * menu link. * - * @param $menus + * @param array $menus * An array of menu names and titles, such as from menu_get_menus(). - * @param $item - * The menu item or the node type for which to generate a list of parents. - * If $item['mlid'] == 0 then the complete tree is returned. - * @param $type + * @param \Drupal\menu_link\Plugin\Core\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 - * An array of menu link titles keyed on the a string containing the menu name - * and mlid. The list excludes the given item and its children. + * + * @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_parent_options($menus, $item, $type = '') { +function menu_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 @@ -327,14 +336,12 @@ function menu_parent_options($menus, $item, $type = '') { return array(); } - $available_menus = array(); - if (!is_array($item)) { - // If $item is not an array then it is a node type. - // Use it as $type and prepare a dummy menu item for _menu_get_options(). - $type = $item; - $item = array('mlid' => 0); + if (!$menu_link) { + $menu_link = entity_create('menu_link', array('mlid' => 0)); } - if (empty($type)) { + + $available_menus = array(); + if (!$type) { // If no node type is set, use all menus given to this function. $available_menus = $menus; } @@ -346,7 +353,7 @@ function menu_parent_options($menus, $item, $type = '') { } } - return _menu_get_options($menus, $available_menus, $item); + return _menu_get_options($menus, $available_menus, $menu_link); } /** @@ -411,26 +418,6 @@ function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, } /** - * Reset a system-defined menu link. - */ -function menu_reset_item($link) { - // To reset the link to its original values, we need to retrieve its - // definition from hook_menu(). 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 menu router does no harm. - $menu = menu_get_router(); - $router_item = $menu[$link['router_path']]; - $new_link = _menu_link_build($router_item); - // Merge existing menu link's ID and 'has_children' property. - foreach (array('mlid', 'has_children') as $key) { - $new_link[$key] = $link[$key]; - } - menu_link_save($new_link); - return $new_link; -} - -/** * Implements hook_block_view_alter(). */ function menu_block_view_alter(array &$build, Block $block) { @@ -463,7 +450,7 @@ function menu_node_save(Node $node) { if (isset($node->menu)) { $link = &$node->menu; if (empty($link['enabled'])) { - if (!empty($link['mlid'])) { + if (!$link->isNew()) { menu_link_delete($link['mlid']); } } @@ -490,9 +477,13 @@ function menu_node_save(Node $node) { */ function menu_node_predelete(Node $node) { // Delete all menu module links that point to this node. - $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu'", array(':path' => 'node/' . $node->nid), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $m) { - menu_link_delete($m['mlid']); + $query = entity_query('menu_link') + ->condition('link_path', 'node/' . $node->nid) + ->condition('module', 'menu'); + $result = $query->execute(); + + if (!empty($result)) { + menu_link_delete_multiple($result); } } @@ -503,42 +494,48 @@ function menu_node_prepare(Node $node) { if (empty($node->menu)) { // Prepare the node for the edit form so that $node->menu always exists. $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main:0'), ':'); - $item = array(); + $menu_link = FALSE; if (isset($node->nid)) { $mlid = FALSE; // Give priority to the default menu $type_menus = variable_get('menu_options_' . $node->type, array('main' => 'main')); if (in_array($menu_name, $type_menus)) { - $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND menu_name = :menu_name AND module = 'menu' ORDER BY mlid ASC", 0, 1, array( - ':path' => 'node/' . $node->nid, - ':menu_name' => $menu_name, - ))->fetchField(); + $query = entity_query('menu_link') + ->condition('link_path', 'node/' . $node->nid) + ->condition('menu_name', $menu_name) + ->condition('module', 'menu') + ->sort('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result)) ? reset($result) : FALSE; } // Check all allowed menus if a link does not exist in the default menu. if (!$mlid && !empty($type_menus)) { - $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = :path AND module = 'menu' AND menu_name IN (:type_menus) ORDER BY mlid ASC", 0, 1, array( - ':path' => 'node/' . $node->nid, - ':type_menus' => array_values($type_menus), - ))->fetchField(); + $query = entity_query('menu_link') + ->condition('link_path', 'node/' . $node->nid) + ->condition('menu_name', array_values($type_menus), 'IN') + ->condition('module', 'menu') + ->sort('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result)) ? reset($result) : FALSE; } if ($mlid) { - $item = menu_link_load($mlid); + $menu_link = menu_link_load($mlid); } } + + if (!$menu_link) { + $menu_link = entity_create('menu_link', array( + 'mlid' => 0, + 'plid' => 0, + 'menu_name' => $menu_name, + )); + } // Set default values. - $node->menu = $item + array( - 'link_title' => '', - 'mlid' => 0, - 'plid' => 0, - 'menu_name' => $menu_name, - 'weight' => 0, - 'options' => array(), - 'module' => 'menu', - 'expanded' => 0, - 'hidden' => 0, - 'has_children' => 0, - 'customized' => 0, - ); + $node->menu = $menu_link; } // Find the depth limit for the parent select. if (!isset($node->menu['parent_depth_limit'])) { @@ -550,7 +547,7 @@ function menu_node_prepare(Node $node) { * Find the depth limit for items in the parent select. */ function _menu_parent_depth_limit($item) { - return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? menu_link_children_relative_depth($item) : 0); + return MENU_MAX_DEPTH - 1 - (($item['mlid'] && $item['has_children']) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($item) : 0); } /** @@ -566,10 +563,7 @@ function menu_form_node_form_alter(&$form, $form_state) { $node = $form_state['controller']->getEntity($form_state); $link = $node->menu; $type = $node->type; - // menu_parent_options() is goofy and can actually handle either a menu link - // or a node type both as second argument. Pick based on whether there is - // a link already (menu_node_prepare() sets mlid default to 0). - $options = menu_parent_options(menu_get_menus(), $link['mlid'] ? $link : $type, $type); + $options = menu_parent_options(menu_get_menus(), $link, $type); // If no possible parent menu items were found, there is nothing to display. if (empty($options)) { return; @@ -641,13 +635,7 @@ function menu_form_node_form_alter(&$form, $form_state) { ); // Get number of items in menu so the weight selector is sized appropriately. - $sql = "SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu"; - $result = db_query($sql, array(':menu' => $link['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $row) { - foreach ($row as $menu_items) { - $delta = $menu_items; - } - } + $delta = entity_get_controller('menu_link')->countMenuLinks($link->menu_name); if ($delta < 50) { // Old hardcoded value $delta = 50; @@ -667,6 +655,7 @@ function menu_form_node_form_alter(&$form, $form_state) { * @see menu_form_node_form_alter() */ function menu_node_submit(Node $node, $form, $form_state) { + $node->menu = entity_create('menu_link', $form_state['values']['menu']); // 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'])) { @@ -703,7 +692,8 @@ function menu_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_parent_options() into a #process callback. - $options = menu_parent_options(menu_get_menus(), array('mlid' => 0)); + $menu_link = entity_create('menu_link', array('mlid' => 0)); + $options = menu_parent_options(menu_get_menus(), $menu_link); $form['menu']['menu_parent'] = array( '#type' => 'select', '#title' => t('Default parent item'), diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php new file mode 100644 index 0000000..fb8aa06 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php @@ -0,0 +1,228 @@ +isNew()) { + // Get the human-readable menu title from the given menu name. + $titles = menu_get_menus(); + $current_title = $titles[$menu_link->menu_name]; + + // Get the current breadcrumb and add a link to that menu's overview page. + $breadcrumb = menu_get_active_breadcrumb(); + $breadcrumb[] = l($current_title, 'admin/structure/menu/manage/' . $menu_link->menu_name); + drupal_set_breadcrumb($breadcrumb); + } + + $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 .= '?' . drupal_http_build_query($menu_link->options['query']); + } + if (isset($menu_link->options['fragment'])) { + $path .= '#' . $menu_link->options['fragment']; + } + if ($menu_link->module == 'menu') { + $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_parent_options(menu_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 = entity_get_controller('menu_link')->countMenuLinks($menu_link->menu_name); + if ($delta < 50) { + // Old hardcoded value. + $delta = 50; + } + $form['weight'] = array( + '#type' => 'weight', + '#title' => t('Weight'), + '#delta' => $delta, + '#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.'), + ); + + $form['langcode'] = array( + '#type' => 'language_select', + '#title' => t('Language'), + '#languages' => LANGUAGE_ALL, + '#default_value' => $menu_link->langcode, + ); + + return parent::form($form, $form_state, $menu_link); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::actions(). + */ + protected function actions(array $form, array &$form_state) { + $element = parent::actions($form, $form_state); + $element['submit']['#button_type'] = 'primary'; + $element['delete']['#access'] = $this->getEntity($form_state)->module == 'menu'; + + return $element; + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::validate(). + */ + public function validate(array $form, array &$form_state) { + $menu_link = $this->buildEntity($form, $form_state); + + $normal_path = drupal_container()->get('path.alias_manager')->getSystemPath($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; + } + if (!url_is_external($menu_link->link_path)) { + $parsed_link = parse_url($menu_link->link_path); + if (isset($parsed_link['query'])) { + $menu_link->options['query'] = drupal_get_query_array($parsed_link['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)) { + form_set_error('link_path', 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); + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::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 Drupal\Core\Entity\EntityFormController::save(). + */ + public function save(array $form, array &$form_state) { + $menu_link = $this->getEntity($form_state); + + $saved = $menu_link->save(); + + if ($saved) { + drupal_set_message(t('The menu link has been saved.')); + $form_state['redirect'] = 'admin/structure/menu/manage/' . $menu_link->menu_name; + } + else { + drupal_set_message(t('There was an error saving the menu link.'), 'error'); + $form_state['rebuild'] = TRUE; + } + } + + /** + * Overrides Drupal\Core\Entity\EntityFormController::delete(). + */ + public function delete(array $form, array &$form_state) { + $menu_link = $this->getEntity($form_state); + $form_state['redirect'] = 'admin/structure/menu/item/' . $menu_link->id() . '/delete'; + } +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php new file mode 100644 index 0000000..8ae2ecc --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -0,0 +1,562 @@ +leftJoin('menu_router', 'm', 'base.router_path = m.path'); + $query->fields('m', self::$routerItemFields); + return $query; + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::attachLoad(). + * + * @todo Don't call parent::attachLoad() at all because we want to be able to + * control the entity load hooks. + */ + protected function attachLoad(&$menu_links, $load_revision = FALSE) { + foreach ($menu_links as &$menu_link) { + $menu_link->options = unserialize($menu_link->options); + + // Use the weight property from the menu link. + $menu_link->router_item['weight'] = $menu_link->weight; + } + + parent::attachLoad($menu_links, $load_revision); + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::save(). + */ + public function save(EntityInterface $entity) { + // We return SAVED_UPDATED by default because the logic below might not + // update the entity if its values haven't changed, so returning FALSE + // would be confusing in that situation. + $return = SAVED_UPDATED; + + $transaction = db_transaction(); + try { + // Load the stored entity, if any. + if (!$entity->isNew() && !isset($entity->original)) { + $entity->original = entity_load_unchanged($this->entityType, $entity->id()); + } + + if ($entity->isNew()) { + $entity->mlid = db_insert($this->entityInfo['base_table'])->fields(array('menu_name' => 'tools'))->execute(); + $entity->enforceIsNew(); + } + + // Unlike the save() method from DatabaseStorageController, 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); + $this->preSave($entity); + + // 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->entityInfo['base_table'], $entity, $this->idKey); + + if ($return) { + if (!$entity->isNew()) { + $this->resetCache(array($entity->{$this->idKey})); + $this->postSave($entity, TRUE); + $this->invokeHook('update', $entity); + } + else { + $return = SAVED_NEW; + $this->resetCache(); + + $entity->enforceIsNew(FALSE); + $this->postSave($entity, 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->entityType, $e); + throw new EntityStorageException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::preSave(). + */ + protected function preSave(EntityInterface $entity) { + // This is the easiest way to handle the unique internal path '', + // since a path marked as external does not need to match a router path. + $entity->external = (url_is_external($entity->link_path) || $entity->link_path == '') ? 1 : 0; + + // Try to find a parent link. If found, assign it and derive its menu. + $parent_candidates = !empty($entity->parentCandidates) ? $entity->parentCandidates : array(); + $parent = $this->findParent($entity, $parent_candidates); + if ($parent) { + $entity->plid = $parent->id(); + $entity->menu_name = $parent->menu_name; + } + // If no corresponding parent link was found, move the link to the top-level. + else { + $entity->plid = 0; + } + + // Directly fill parents for top-level links. + if ($entity->plid == 0) { + $entity->p1 = $entity->id(); + for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) { + $parent_property = "p$i"; + $entity->$parent_property = 0; + } + $entity->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 ($entity->has_children && $entity->original) { + $limit = MENU_MAX_DEPTH - $this->findChildrenRelativeDepth($entity->original) - 1; + } + else { + $limit = MENU_MAX_DEPTH - 1; + } + if ($parent->depth > $limit) { + return FALSE; + } + $entity->depth = $parent->depth + 1; + $this->setParents($entity, $parent); + } + + // Need to check both plid and menu_name, since plid can be 0 in any menu. + if (isset($entity->original) && ($entity->plid != $entity->original->plid || $entity->menu_name != $entity->original->menu_name)) { + $this->moveChildren($entity, $entity->original); + } + // Find the router_path. + if (empty($entity->router_path) || empty($entity->original) || (isset($entity->original) && $entity->original->link_path != $entity->link_path)) { + if ($entity->external) { + $entity->router_path = ''; + } + else { + // Find the router path which will serve this path. + $entity->parts = explode('/', $entity->link_path, MENU_MAX_PARTS); + $entity->router_path = _menu_find_router_path($entity->link_path); + } + } + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::postSave(). + */ + function postSave(EntityInterface $entity, $update) { + // Check the has_children status of the parent. + $this->updateParentalStatus($entity); + + menu_cache_clear($entity->menu_name); + if (isset($entity->original) && $entity->menu_name != $entity->original->menu_name) { + menu_cache_clear($entity->original->menu_name); + } + + // Now clear the cache. + _menu_clear_page_cache(); + } + + /** + * Sets an internal flag that allows us to prevent the reparenting operations + * executed during deletion. + * + * @param bool $value + */ + public function preventReparenting($value = FALSE) { + $this->preventReparenting = $value; + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::preDelete(). + */ + protected function preDelete($entities) { + // Nothing to do if we don't want to reparent children. + if ($this->preventReparenting) { + return; + } + + foreach ($entities as $entity) { + // Children get re-attached to the item's parent. + if ($entity->has_children) { + $children = $this->loadByProperties(array('plid' => $entity->plid)); + foreach ($children as $child) { + $child->plid = $entity->plid; + $this->save($child); + } + } + } + } + + /** + * Overrides Drupal\entity\DatabaseStorageController::postDelete(). + */ + protected function postDelete($entities) { + $affected_menus = array(); + // Update the has_children status of the parent. + foreach ($entities as $entity) { + if (!$this->preventReparenting) { + $this->updateParentalStatus($entity); + } + + // Store all menu names for which we need to clear the cache. + if (!isset($affected_menus[$entity->menu_name])) { + $affected_menus[$entity->menu_name] = $entity->menu_name; + } + } + + foreach ($affected_menus as $menu_name) { + menu_cache_clear($menu_name); + } + _menu_clear_page_cache(); + } + + /** + * Loads updated and customized menu links for specific router paths. + * + * Note that this is a low-level method and it doesn't return fully populated + * menu link entities. (e.g. no fields are attached) + * + * @param array $router_paths + * An array of router paths. + * + * @return array + * An array of menu link objects indexed by their ids. + */ + 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(); + + if (!empty($this->entityInfo['class'])) { + // 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->entityInfo['class'], array(array(), $this->entityType)); + } + + return $query_result->fetchAllAssoc($this->idKey); + } + + /** + * Loads system menu link as needed by system_get_module_admin_tasks(). + * + * @return array + * An array of menu link entities indexed by their IDs. + */ + public function loadModuleAdminTasks() { + $query = $this->buildQuery(NULL); + $query + ->condition('base.link_path', 'admin/%', 'LIKE') + ->condition('base.hidden', 0, '>=') + ->condition('base.module', 'system') + ->condition('m.number_parts', 1, '>') + ->condition('m.page_callback', 'system_admin_menu_block_page', '<>'); + $ids = $query->execute()->fetchCol(1); + + return $this->load($ids); + } + + /** + * Checks and updates the 'has_children' property for the parent of a link. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A menu link entity. + */ + protected function updateParentalStatus(EntityInterface $entity, $exclude = FALSE) { + // If plid == 0, there is nothing to update. + if ($entity->plid && ($parent_entity = $this->load(array($entity->plid)))) { + // Check if at least one visible child exists in the table. + $query = entity_query($this->entityType); + $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; + $parent_entity = reset($parent_entity); + $parent_entity->has_children = $parent_has_children; + $parent_entity->save(); + } + } + + /** + * Finds a possible parent for a given menu link entity. + * + * Because the parent of a given link might not exist anymore in the database, + * we apply a set of heuristics to determine a proper parent: + * + * - use the passed parent link if specified and existing. + * - else, use the first existing link down the previous link hierarchy + * - else, for system menu links (derived from hook_menu()), reparent + * based on the path hierarchy. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A menu link entity. + * @param array $parent_candidates + * An array of menu link entities keyed by mlid. + * + * @return \Drupal\Core\Entity\EntityInterface|false + * A menu link entity structure of the possible parent or FALSE if no valid + * parent has been found. + */ + protected function findParent(EntityInterface $entity, array $parent_candidates = array()) { + $parent = FALSE; + + // This item is explicitely top-level, skip the rest of the parenting. + if (isset($entity->plid) && empty($entity->plid)) { + return $parent; + } + + // If we have a parent link ID, try to use that. + $candidates = array(); + if (isset($entity->plid)) { + $candidates[] = $entity->plid; + } + + // Else, if we have a link hierarchy try to find a valid parent in there. + if (!empty($entity->depth) && $entity->depth > 1) { + for ($depth = $entity->depth - 1; $depth >= 1; $depth--) { + $parent_property = "p$depth"; + $candidates[] = $entity->$parent_property; + } + } + + foreach ($candidates as $mlid) { + if (isset($parent_candidates[$mlid])) { + $parent = $parent_candidates[$mlid]; + } + else { + $parent = $this->load(array($mlid)); + $parent = reset($parent); + } + if ($parent) { + return $parent; + } + } + + // If everything else failed, try to derive the parent from the path + // hierarchy. This only makes sense for links derived from menu router + // items (ie. from hook_menu()). + if ($entity->module == 'system') { + // Find the parent - it must be unique. + $parent_path = $entity->link_path; + do { + $parent = FALSE; + $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); + + // @todo Return to the previous method of cloning the entity query when + // http://drupal.org/node/1829942 is fixed. + $query = entity_query($this->entityType); + $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); + + $count_query = clone $query; + // Only valid if we get a unique result. + if ($count_query->count()->execute() == 1) { + $result = $query->execute(); + $parent = $this->load($result); + $parent = reset($parent); + } + } while ($parent === FALSE && $parent_path); + } + + return $parent; + } + + /** + * Sets the p1 through p9 properties for a menu link entity being saved. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A menu link entity. + * @param \Drupal\Core\Entity\EntityInterface $parent + * A menu link entity. + */ + protected function setParents(EntityInterface $entity, EntityInterface $parent) { + $i = 1; + while ($i < $entity->depth) { + $p = 'p' . $i++; + $entity->{$p} = $parent->{$p}; + } + $p = 'p' . $i++; + // The parent (p1 - p9) corresponding to the depth always equals the mlid. + $entity->{$p} = $entity->id(); + while ($i <= MENU_MAX_DEPTH) { + $p = 'p' . $i++; + $entity->{$p} = 0; + } + } + + /** + * Finds the depth of an item's children relative to its depth. + * + * For example, if the item has a depth of 2 and the maximum of any child in + * the menu link tree is 5, the relative depth is 3. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A menu link entity. + * + * @return int + * The relative depth, or zero. + */ + 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 = db_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; + } + + /** + * Updates the children of a menu link that is being moved. + * + * The menu name, parents (p1 - p6), and depth are updated for all children of + * the link, and the has_children status of the previous parent is updated. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * A menu link entity. + */ + protected function moveChildren(EntityInterface $entity) { + $query = db_update($this->entityInfo['base_table']); + + $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); + } + + /** + * Returns the number of menu links from a menu. + * + * @param string $menu_name + * The unique name of a menu. + */ + public function countMenuLinks($menu_name) { + $query = entity_query($this->entityType); + $query + ->condition('menu_name', $menu_name) + ->count(); + return $query->execute(); + } +} diff --git a/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php new file mode 100644 index 0000000..cb5a5a9 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/Plugin/Core/Entity/MenuLink.php @@ -0,0 +1,286 @@ +mlid; + } + + /** + * Overrides Drupal\entity\Entity::createDuplicate(). + */ + public function createDuplicate() { + $duplicate = parent::createDuplicate(); + $duplicate->plid = NULL; + return $duplicate; + } + + /** + * Resets a system-defined menu link. + * + * @return \Drupal\Core\Entity\EntityInterface + * A menu link entity. + */ + public function reset() { + // To reset the link to its original values, we need to retrieve its + // definition from hook_menu(). 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 menu router does no harm. + $menu = menu_get_router(); + $router_item = $menu[$this->router_path]; + $new_link = self::buildFromRouterItem($router_item); + // 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; + } + + /** + * Builds a menu link entity from a router item. + * + * @param array $item + * A menu router item. + * + * @return MenuLink + * A menu link entity. + */ + public static function buildFromRouterItem(array $item) { + // Suggested items are disabled by default. + if ($item['type'] == MENU_SUGGESTED_ITEM) { + $item['hidden'] = 1; + } + // Hide all items that are not visible in the tree. + elseif (!($item['type'] & MENU_VISIBLE_IN_TREE)) { + $item['hidden'] = -1; + } + // Note, we set this as 'system', so that we can be sure to distinguish all + // the menu links generated automatically from entries in {menu_router}. + $item['module'] = 'system'; + $item += array( + 'link_title' => $item['title'], + 'link_path' => $item['path'], + 'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])), + ); + return entity_get_controller('menu_link')->create($item); + } + + /** + * 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}); + } +} diff --git a/core/modules/menu_link/menu_link.api.php b/core/modules/menu_link/menu_link.api.php new file mode 100644 index 0000000..0899cbf --- /dev/null +++ b/core/modules/menu_link/menu_link.api.php @@ -0,0 +1,138 @@ +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\Plugin\Core\Entity\MenuLink $menu_link + * A menu link entity. + * + * @see hook_menu_link_load() + */ +function hook_menu_link_presave($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->link_path == 'user' && $menu_link->module == 'system') { + $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\Plugin\Core\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($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; + drupal_write_record('menu_example', $record); +} + +/** + * 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\Plugin\Core\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($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\Plugin\Core\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($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 b/core/modules/menu_link/menu_link.info new file mode 100644 index 0000000..c5e92d7 --- /dev/null +++ b/core/modules/menu_link/menu_link.info @@ -0,0 +1,8 @@ +name = Menu Link +description = Provides menu links, trees and bunnies! +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 new file mode 100644 index 0000000..d45314a --- /dev/null +++ b/core/modules/menu_link/menu_link.install @@ -0,0 +1,213 @@ + '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, + ), + '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' => '', + ), + 'router_path' => array( + 'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.', + '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, which may be modified by a title callback stored in {menu_router}.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + 'translatable' => TRUE, + ), + '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, + 'translatable' => TRUE, + 'serialize' => TRUE, + ), + 'module' => array( + 'description' => 'The name of the module that generated this link.', + 'type' => 'varchar', + 'length' => 255, + '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', + ), + ), + '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'), + 'router_path' => array(array('router_path', 128)), + ), + 'primary key' => array('mlid'), + ); + + return $schema; +} diff --git a/core/modules/menu_link/menu_link.module b/core/modules/menu_link/menu_link.module new file mode 100644 index 0000000..9332219 --- /dev/null +++ b/core/modules/menu_link/menu_link.module @@ -0,0 +1,209 @@ + $menu_link->link_path, + ); +} + +/** + * 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\Plugin\Core\Entity\MenuLink|false + * A menu link entity. + */ +function menu_link_load($mlid = NULL, $reset = FALSE) { + return entity_load('menu_link', $mlid, $reset); +} + +/** + * 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\Plugin\Core\Entity\MenuLink> + * An array of menu link entities indexed by mlid. + * + * @see menu_link_load() + * @see entity_load_multiple() + */ +function menu_link_load_multiple(array $mlids = NULL, $reset = FALSE) { + return entity_load_multiple('menu_link', $mlids, $reset); +} + +/** + * 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 = entity_get_controller('menu_link'); + if (!$force) { + $entity_query = entity_query('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->load($result); + } + else { + $entities = $controller->load($mlids); + } + $controller->preventReparenting($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\Plugin\Core\Entity\MenuLink $menu_link + * The menu link entity to be saved. + */ +function menu_link_save(MenuLink $menu_link) { + $menu_link->save(); +} + +/** + * Clones an array of menu links. + * + * @param array $links + * An array of menu links to clone. + * @param string $menu_name + * (optional) The name of a menu that the links will be cloned for. If not + * set, the cloned links will be in the same menu as the original set of + * links that were passed in. + * + * @return array + * An array of menu links with the same properties as the passed-in array, + * but with the link identifiers removed so that a new link will be created + * when any of them is passed into + * Drupal\menu_link\MenuLinkStorageController::save(). + * + * @see Drupal\menu_link\MenuLinkStorageController::save() + */ +function menu_link_clone($links, $menu_name = NULL) { + foreach ($links as &$link) { + $link = $link->createDuplicate(); + if (isset($menu_name)) { + $link->menu_name = $menu_name; + } + } + return $links; +} + +/** + * 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 = entity_get_controller('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 = entity_query('menu_link')->condition('link_path', $link_path)->execute(); + if (!empty($result)) { + menu_link_delete_multiple($result); + } + break; + } +} diff --git a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php index 5a1146c..fe5c638 100644 --- a/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php +++ b/core/modules/shortcut/lib/Drupal/shortcut/ShortcutStorageController.php @@ -45,7 +45,8 @@ function postSave(EntityInterface $entity, $update) { // However, we do need to specify the correct menu name. $link['menu_name'] = 'shortcut-' . $entity->id(); $link['plid'] = 0; - menu_link_save($link); + $menu_link = drupal_container()->get('plugin.manager.entity')->getStorageController('menu_link')->create($link); + $menu_link->save(); } } diff --git a/core/modules/shortcut/shortcut.admin.inc b/core/modules/shortcut/shortcut.admin.inc index d0cf7c1..f3c03fa 100644 --- a/core/modules/shortcut/shortcut.admin.inc +++ b/core/modules/shortcut/shortcut.admin.inc @@ -5,6 +5,7 @@ * Administrative page callbacks for the shortcut module. */ +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** @@ -138,7 +139,7 @@ function shortcut_set_switch_submit($form, &$form_state) { $set = entity_create('shortcut', array( 'id' => $form_state['values']['id'], 'label' => $form_state['values']['label'], - 'links' => menu_links_clone($default_set->links), + 'links' => menu_link_clone($default_set->links), )); $set->save(); $replacements = array( @@ -361,10 +362,10 @@ function shortcut_link_edit($form, &$form_state, $shortcut_link) { */ function _shortcut_link_form_elements($shortcut_link = NULL) { if (!isset($shortcut_link)) { - $shortcut_link = array( + $shortcut_link = entity_create('menu_link', array( 'link_title' => '', 'link_path' => '' - ); + )); } else { $shortcut_link['link_path'] = ($shortcut_link['link_path'] == '') ? '' : drupal_container()->get('path.alias_manager')->getPathAlias($shortcut_link['link_path']); @@ -420,7 +421,10 @@ function shortcut_link_edit_submit($form, &$form_state) { } $form_state['values']['shortcut_link']['link_path'] = $shortcut_path; - $shortcut_link = array_merge($form_state['values']['original_shortcut_link'], $form_state['values']['shortcut_link']); + $shortcut_link = $form_state['values']['original_shortcut_link']; + foreach ($form_state['values']['shortcut_link'] as $key => $value) { + $shortcut_link[$key] = $value; + } menu_link_save($shortcut_link); $set_name = str_replace('shortcut-', '' , $shortcut_link['menu_name']); @@ -434,7 +438,7 @@ function shortcut_link_edit_submit($form, &$form_state) { function shortcut_link_add_submit($form, &$form_state) { // Add the shortcut link to the set. $shortcut_set = $form_state['values']['shortcut_set']; - $shortcut_link = $form_state['values']['shortcut_link']; + $shortcut_link = entity_create('menu_link', $form_state['values']['shortcut_link']); $shortcut_link['menu_name'] = $shortcut_set->id(); shortcut_admin_add_link($shortcut_link, $shortcut_set); $shortcut_set->save(); @@ -453,7 +457,7 @@ function shortcut_link_add_submit($form, &$form_state) { * at the end, and some existing links may be disabled (if the $limit * parameter is provided). */ -function shortcut_admin_add_link($shortcut_link, &$shortcut_set) { +function shortcut_admin_add_link(MenuLink $shortcut_link, &$shortcut_set) { // Normalize the path in case it is an alias. $shortcut_link['link_path'] = drupal_container()->get('path.alias_manager')->getSystemPath($shortcut_link['link_path']); if (empty($shortcut_link['link_path'])) { diff --git a/core/modules/shortcut/shortcut.info b/core/modules/shortcut/shortcut.info index 5ed5f2d..97f25c6 100644 --- a/core/modules/shortcut/shortcut.info +++ b/core/modules/shortcut/shortcut.info @@ -3,4 +3,5 @@ description = Allows users to manage customizable lists of shortcut links. package = Core version = VERSION core = 8.x +dependencies[] = menu_link configure = admin/config/user-interface/shortcut diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php index dcf53b0..fb104ac 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php @@ -209,13 +209,13 @@ function testBreadCrumbs() { $node2 = $this->drupalCreateNode(array( 'type' => $type, 'title' => $title, - 'menu' => array( + 'menu' => entity_create('menu_link', array( 'enabled' => 1, 'link_title' => 'Parent ' . $title, 'description' => '', 'menu_name' => $menu, 'plid' => 0, - ), + )), )); $nid2 = $node2->nid; @@ -237,13 +237,13 @@ function testBreadCrumbs() { $node3 = $this->drupalCreateNode(array( 'type' => $type, 'title' => $title, - 'menu' => array( + 'menu' => entity_create('menu_link', array( 'enabled' => 1, 'link_title' => 'Child ' . $title, 'description' => '', 'menu_name' => $menu, 'plid' => $node2->menu['mlid'], - ), + )), )); $nid3 = $node3->nid; @@ -277,7 +277,8 @@ function testBreadCrumbs() { 'link_path' => 'node', ); $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $link = db_query('SELECT * FROM {menu_links} WHERE link_title = :title', array(':title' => 'Root'))->fetchAssoc(); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Root')); + $link = reset($menu_links); $edit = array( 'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'], @@ -335,10 +336,8 @@ function testBreadCrumbs() { 'parent' => "$menu:{$parent_mlid}", ); $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $tags[$name]['link'] = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( - ':title' => $edit['link_title'], - ':href' => $edit['link_path'], - ))->fetchAssoc(); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $tags[$name]['link'] = reset($menu_links); $tags[$name]['link']['link_path'] = $edit['link_path']; $parent_mlid = $tags[$name]['link']['mlid']; } @@ -436,20 +435,16 @@ function testBreadCrumbs() { 'link_path' => 'user', ); $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $link_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( - ':title' => $edit['link_title'], - ':href' => $edit['link_path'], - ))->fetchAssoc(); + $menu_links_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $link_user = reset($menu_links_user); $edit = array( 'link_title' => $this->admin_user->name . ' link', 'link_path' => 'user/' . $this->admin_user->uid, ); $this->drupalPost("admin/structure/menu/manage/$menu/add", $edit, t('Save')); - $link_admin_user = db_query('SELECT * FROM {menu_links} WHERE link_title = :title AND link_path = :href', array( - ':title' => $edit['link_title'], - ':href' => $edit['link_path'], - ))->fetchAssoc(); + $menu_links_admin_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $link_admin_user = reset($menu_links_admin_user); // Verify expected breadcrumbs for the two separate links. $this->drupalLogout(); diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php index 7d22246..ddc834b 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/LinksTest.php @@ -26,7 +26,8 @@ public static function getInfo() { */ function createLinkHierarchy($module = 'menu_test') { // First remove all the menu links. - db_truncate('menu_links')->execute(); + $menu_links = menu_link_load_multiple(); + menu_link_delete_multiple(array_keys($menu_links), TRUE, TRUE); // Then create a simple link hierarchy: // - $parent @@ -43,31 +44,36 @@ function createLinkHierarchy($module = 'menu_test') { $links['parent'] = $base_options + array( 'link_path' => 'menu-test/parent', ); - menu_link_save($links['parent']); + $links['parent'] = entity_create('menu_link', $links['parent']); + $links['parent']->save(); $links['child-1'] = $base_options + array( 'link_path' => 'menu-test/parent/child-1', 'plid' => $links['parent']['mlid'], ); - menu_link_save($links['child-1']); + $links['child-1'] = entity_create('menu_link', $links['child-1']); + $links['child-1']->save(); $links['child-1-1'] = $base_options + array( 'link_path' => 'menu-test/parent/child-1/child-1-1', 'plid' => $links['child-1']['mlid'], ); - menu_link_save($links['child-1-1']); + $links['child-1-1'] = entity_create('menu_link', $links['child-1-1']); + $links['child-1-1']->save(); $links['child-1-2'] = $base_options + array( 'link_path' => 'menu-test/parent/child-1/child-1-2', 'plid' => $links['child-1']['mlid'], ); - menu_link_save($links['child-1-2']); + $links['child-1-2'] = entity_create('menu_link', $links['child-1-2']); + $links['child-1-2']->save(); $links['child-2'] = $base_options + array( 'link_path' => 'menu-test/parent/child-2', 'plid' => $links['parent']['mlid'], ); - menu_link_save($links['child-2']); + $links['child-2'] = entity_create('menu_link', $links['child-2']); + $links['child-2']->save(); return $links; } diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php index 35d23e8..6e64696 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php @@ -472,7 +472,7 @@ function testMenuItemHooks() { */ function testMenuLinkOptions() { // Create a menu link with options. - $menu_link = array( + $menu_link = entity_create('menu_link', array( 'link_title' => 'Menu link options test', 'link_path' => 'test-page', 'module' => 'menu_test', @@ -484,8 +484,8 @@ function testMenuLinkOptions() { 'testparam' => 'testvalue', ), ), - ); - menu_link_save($menu_link); + )); + $menu_link->save(); // Load front page. $this->drupalGet('test-page'); diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php index fac7841..1a7de01 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeDataUnitTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Menu; +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; use Drupal\simpletest\UnitTestBase; /** @@ -16,13 +17,7 @@ class TreeDataUnitTest extends UnitTestBase { /** * Dummy link structure acceptable for menu_tree_data(). */ - var $links = array( - 1 => array('mlid' => 1, 'depth' => 1), - 2 => array('mlid' => 2, 'depth' => 1), - 3 => array('mlid' => 3, 'depth' => 2), - 4 => array('mlid' => 4, 'depth' => 3), - 5 => array('mlid' => 5, 'depth' => 1), - ); + protected $links = array(); public static function getInfo() { return array( @@ -35,7 +30,15 @@ public static function getInfo() { /** * Validate the generation of a proper menu tree hierarchy. */ - function testMenuTreeData() { + public function testMenuTreeData() { + $this->links = array( + 1 => new MenuLink(array('mlid' => 1, 'depth' => 1), 'menu_link'), + 2 => new MenuLink(array('mlid' => 2, 'depth' => 1), 'menu_link'), + 3 => new MenuLink(array('mlid' => 3, 'depth' => 2), 'menu_link'), + 4 => new MenuLink(array('mlid' => 4, 'depth' => 3), 'menu_link'), + 5 => new MenuLink(array('mlid' => 5, 'depth' => 1), 'menu_link'), + ); + $tree = menu_tree_data($this->links); // Validate that parent items #1, #2, and #5 exist on the root level. diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php index e1b46ad..7e09d18 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Menu; +use Drupal\menu_link\Plugin\Core\Entity\MenuLink; use Drupal\simpletest\WebTestBase; /** @@ -16,24 +17,7 @@ class TreeOutputTest extends WebTestBase { /** * Dummy link structure acceptable for menu_tree_output(). */ - var $tree_data = array( - '1'=> array( - 'link' => array( 'menu_name' => 'main-menu', 'mlid' => 1, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>'')) ), - 'below' => array( - '2' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 2, 'hidden'=>0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access'=>1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>'')) ), - 'below' => array( - '3' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 3, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>'')) ), - 'below' => array() ), - '4' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 4, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access'=>1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>'')) ), - 'below' => array() ) - ) - ) - ) - ), - '5' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 5, 'hidden'=>1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access'=>1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), - '6' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 6, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access'=>0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ), - '7' => array('link' => array( 'menu_name' => 'main-menu', 'mlid' => 7, 'hidden'=>0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access'=>1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>'')) ), 'below' => array( ) ) - ); + protected $tree_data = array(); public static function getInfo() { return array( @@ -51,6 +35,26 @@ function setUp() { * Validate the generation of a proper menu tree output. */ function testMenuTreeData() { + // @todo Prettify this tree buildup code, it's very hard to read. + $this->tree_data = array( + '1'=> array( + 'link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 1, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 1', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), + 'below' => array( + '2' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 2, 'hidden' => 0, 'has_children' => 1, 'title' => 'Item 2', 'in_active_trail' => 1, 'access' => 1, 'href' => 'a/b', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), + 'below' => array( + '3' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 3, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 3', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/c', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), + 'below' => array() ), + '4' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 4, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 4', 'in_active_trail' => 0, 'access' => 1, 'href' => 'a/b/d', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), + 'below' => array() ) + ) + ) + ) + ), + '5' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 5, 'hidden' => 1, 'has_children' => 0, 'title' => 'Item 5', 'in_active_trail' => 0, 'access' => 1, 'href' => 'e', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array()), + '6' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 6, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 6', 'in_active_trail' => 0, 'access' => 0, 'href' => 'f', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array()), + '7' => array('link' => new MenuLink(array('menu_name' => 'main-menu', 'mlid' => 7, 'hidden' => 0, 'has_children' => 0, 'title' => 'Item 7', 'in_active_trail' => 0, 'access' => 1, 'href' => 'g', 'localized_options' => array('attributes' => array('title' =>''))), 'menu_link'), 'below' => array()) + ); + $output = menu_tree_output($this->tree_data); // Validate that the - in main-menu is changed into an underscore diff --git a/core/modules/system/system.admin.inc b/core/modules/system/system.admin.inc index c3c2ce4..df2c806 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -19,13 +19,19 @@ function system_admin_config_page() { drupal_set_message(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'); } $blocks = array(); - if ($admin = db_query("SELECT menu_name, mlid FROM {menu_links} WHERE link_path = 'admin/config' AND module = 'system'")->fetchAssoc()) { - $result = db_query(" - SELECT m.*, ml.* - FROM {menu_links} ml - INNER JOIN {menu_router} m ON ml.router_path = m.path - WHERE ml.link_path <> 'admin/help' AND menu_name = :menu_name AND ml.plid = :mlid AND hidden = 0", $admin, array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $item) { + if ($system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/config', 'module' => 'system'))) { + $system_link = reset($system_link); + $query = entity_query('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_load_multiple($result); + } + + foreach ($menu_links as $item) { _menu_link_translate($item); if (!$item['access']) { continue; diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 7e95a03..accf46b 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -917,127 +917,6 @@ function hook_menu_alter(&$items) { } /** - * Alter the data being saved to the {menu_links} table by menu_link_save(). - * - * @param $item - * Associative array defining a menu link as passed into menu_link_save(). - * - * @see hook_translated_menu_link_alter() - */ -function hook_menu_link_alter(&$item) { - // Make all new admin links hidden (a.k.a disabled). - if (strpos($item['link_path'], 'admin') === 0 && empty($item['mlid'])) { - $item['hidden'] = 1; - } - // Flag a link to be altered by hook_translated_menu_link_alter(). - if ($item['link_path'] == 'devel/cache/clear') { - $item['options']['alter'] = TRUE; - } - // Flag a link to be altered by hook_translated_menu_link_alter(), 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 ($item['link_path'] == 'user' && $item['module'] == 'system') { - $item['options']['alter'] = TRUE; - } -} - -/** - * Alter a menu link after it has been translated and before it is rendered. - * - * This hook is invoked from _menu_link_translate() after a menu link has been - * translated; i.e., after dynamic path argument placeholders (%) have been - * replaced with actual values, the user access to the link's target page has - * been checked, and the link has been localized. It is only invoked if - * $item['options']['alter'] has been set to a non-empty value (e.g., TRUE). - * This flag should be set using hook_menu_link_alter(). - * - * 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 $item - * Associative array defining a menu link after _menu_link_translate() - * @param $map - * Associative array containing the menu $map (path parts and/or objects). - * - * @see hook_menu_link_alter() - */ -function hook_translated_menu_link_alter(&$item, $map) { - if ($item['href'] == 'devel/cache/clear') { - $item['localized_options']['query'] = drupal_get_destination(); - } -} - -/** - * Inform modules that a menu link has been created. - * - * This hook is used to notify modules that menu items have been - * created. Contributed modules may use the information to perform - * actions based on the information entered into the menu system. - * - * @param $link - * Associative array defining a menu link as passed into menu_link_save(). - * - * @see hook_menu_link_update() - * @see hook_menu_link_delete() - */ -function hook_menu_link_insert($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'] = $link['mlid']; - $record['menu_name'] = $link['menu_name']; - $record['status'] = 0; - drupal_write_record('menu_example', $record); -} - -/** - * 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 $link - * Associative array defining a menu link as passed into menu_link_save(). - * - * @see hook_menu_link_insert() - * @see hook_menu_link_delete() - */ -function hook_menu_link_update($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' => $link['mlid']))->fetchField(); - if ($menu_name != $link['menu_name']) { - db_update('menu_example') - ->fields(array('menu_name' => $link['menu_name'])) - ->condition('mlid', $link['mlid']) - ->execute(); - } -} - -/** - * Inform modules that a menu link has been deleted. - * - * This hook is used to notify modules that menu items have been - * deleted. Contributed modules may use the information to perform - * actions based on the information entered into the menu system. - * - * @param $link - * Associative array defining a menu link as passed into menu_link_save(). - * - * @see hook_menu_link_insert() - * @see hook_menu_link_update() - */ -function hook_menu_link_delete($link) { - // Delete the record from our table. - db_delete('menu_example') - ->condition('mlid', $link['mlid']) - ->execute(); -} - -/** * Alter tabs and actions displayed on the page before they are rendered. * * This hook is invoked by menu_local_tasks(). The system-determined tabs and diff --git a/core/modules/system/system.install b/core/modules/system/system.install index b63ca46..baf4c0b 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -950,192 +950,6 @@ function system_schema() { 'primary key' => array('path'), ); - $schema['menu_links'] = array( - 'description' => '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, - ), - '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' => '', - ), - 'router_path' => array( - 'description' => 'For links corresponding to a Drupal path (external = 0), this connects the link to a {menu_router}.path for joins.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - ), - 'link_title' => array( - 'description' => 'The text displayed for the link, which may be modified by a title callback stored in {menu_router}.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'translatable' => TRUE, - ), - '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, - 'translatable' => TRUE, - ), - 'module' => array( - 'description' => 'The name of the module that generated this link.', - 'type' => 'varchar', - 'length' => 255, - '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', - ), - ), - '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'), - 'router_path' => array(array('router_path', 128)), - ), - 'primary key' => array('mlid'), - ); - $schema['queue'] = array( 'description' => 'Stores items in queues.', 'fields' => array( @@ -2195,6 +2009,42 @@ function system_update_8045() { } /** + * Enable the new Menu link module. + * + * Creates the langcode and UUID columns for menu links. + */ +function system_update_8046() { + // Enable the module without re-installing the schema. + update_module_enable(array('menu_link')); + + // Add the langcode column if it doesn't exist. + if (!db_field_exists('menu_links', 'langcode')) { + $column = array( + 'description' => 'The {language}.langcode of this entity.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + 'default' => '', + ); + db_add_field('menu_links', 'langcode', $column); + } + + // Add the UUID column. + $column = array( + 'description' => 'Unique Key: Universally unique identifier for this entity.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => FALSE, + ); + $keys = array( + 'unique keys' => array( + 'uuid' => array('uuid'), + ), + ); + db_add_field('menu_links', 'uuid', $column, $keys); +} + +/** * @} End of "defgroup updates-7.x-to-8.x". * The next series of updates should start at 9000. */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 336f84b..fe3fec4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2673,7 +2673,10 @@ function system_admin_menu_block($item) { } if (!isset($item['mlid'])) { - $item += db_query("SELECT mlid, menu_name FROM {menu_links} ml WHERE ml.router_path = :path AND module = 'system'", array(':path' => $item['path']))->fetchAssoc(); + $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => $item['path'], 'module' => 'system')); + $menu_link = reset($menu_links); + $item['mlid'] = $menu_link->id(); + $item['menu_name'] = $menu_link->menu_name; } if (isset($cache[$item['mlid']])) { @@ -2681,17 +2684,8 @@ function system_admin_menu_block($item) { } $content = array(); - $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); - $query->join('menu_router', 'm', 'm.path = ml.router_path'); - $query - ->fields('ml') - // Weight should be taken from {menu_links}, not {menu_router}. - ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight'))) - ->condition('ml.plid', $item['mlid']) - ->condition('ml.menu_name', $item['menu_name']) - ->condition('ml.hidden', 0); - - foreach ($query->execute() as $link) { + $menu_links = entity_load_multiple_by_properties('menu_link', 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 @@ -3456,18 +3450,8 @@ function system_get_module_admin_tasks($module, $info) { if (!isset($links)) { $links = array(); - $query = db_select('menu_links', 'ml', array('fetch' => PDO::FETCH_ASSOC)); - $query->join('menu_router', 'm', 'm.path = ml.router_path'); - $query - ->fields('ml') - // Weight should be taken from {menu_links}, not {menu_router}. - ->fields('m', array_diff(drupal_schema_fields_sql('menu_router'), array('weight'))) - ->condition('ml.link_path', 'admin/%', 'LIKE') - ->condition('ml.hidden', 0, '>=') - ->condition('ml.module', 'system') - ->condition('m.number_parts', 1, '>') - ->condition('m.page_callback', 'system_admin_menu_block_page', '<>'); - foreach ($query->execute() as $link) { + $menu_links = entity_get_controller('menu_link')->loadModuleAdminTasks(); + foreach ($menu_links as $link) { _menu_link_translate($link); if ($link['access']) { $links[$link['router_path']] = $link; @@ -3519,6 +3503,7 @@ function system_get_module_admin_tasks($module, $info) { $item['title'] = t('Configure @module permissions', array('@module' => $info['name'])); unset($item['description']); $item['localized_options']['fragment'] = 'module-' . $module; + $item = entity_create('menu_link', $item); $admin_tasks["admin/people/permissions#module-$module"] = $item; } } diff --git a/core/modules/toolbar/toolbar.info b/core/modules/toolbar/toolbar.info index a6fd841..16046ea 100644 --- a/core/modules/toolbar/toolbar.info +++ b/core/modules/toolbar/toolbar.info @@ -5,5 +5,6 @@ package = Core version = VERSION dependencies[] = breakpoint +dependencies[] = menu_link configure = admin/structure/toolbar diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index e543326..5802eb8 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -358,8 +358,13 @@ function($object) {return $object->mediaQuery;}, */ function toolbar_get_menu_tree() { $tree = array(); - $admin_link = db_query('SELECT * FROM {menu_links} WHERE menu_name = :menu_name AND module = :module AND link_path = :path', array(':menu_name' => 'admin', ':module' => 'system', ':path' => 'admin'))->fetchAssoc(); - if ($admin_link) { + $query = entity_query('menu_link') + ->condition('menu_name', 'admin') + ->condition('module', 'system') + ->condition('link_path', 'admin'); + $result = $query->execute(); + if (!empty($result)) { + $admin_link = menu_link_load(reset($result)); $tree = menu_build_tree('admin', array( 'expanded' => array($admin_link['mlid']), 'min_depth' => $admin_link['depth'] + 1, diff --git a/core/modules/user/user.module b/core/modules/user/user.module index 3082cde..6a7c757 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1125,13 +1125,13 @@ function user_menu_site_status_alter(&$menu_site_status, $path) { } /** - * Implements hook_menu_link_alter(). + * Implements hook_menu_link_presave(). */ -function user_menu_link_alter(&$link) { +function user_menu_link_presave($link) { // The path 'user' must be accessible for anonymous users, but only visible // for authenticated users. Authenticated users should see "My account", but // anonymous users should not see it at all. Therefore, invoke - // user_translated_menu_link_alter() to conditionally hide the link. + // user_menu_link_load() to conditionally hide the link. if ($link['link_path'] == 'user' && $link['module'] == 'system') { $link['options']['alter'] = TRUE; } @@ -1155,12 +1155,14 @@ function user_menu_breadcrumb_alter(&$active_trail, $item) { } /** - * Implements hook_translated_menu_link_alter(). + * Implements hook_menu_link_load(). */ -function user_translated_menu_link_alter(&$link) { +function user_menu_link_load($menu_links) { // Hide the "User account" link for anonymous users. - if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) { - $link['hidden'] = 1; + foreach ($menu_links as $link) { + if ($link['link_path'] == 'user' && $link['module'] == 'system' && !$GLOBALS['user']->uid) { + $link['hidden'] = 1; + } } } diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php index d329414..a93cbe1 100644 --- a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php @@ -37,6 +37,7 @@ protected function setUp() { parent::setUp(); $this->enableModules(array('system')); + $this->enableModules(array('menu_link')); } function viewsData() { diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php index f22afbe..6b74ddd 100644 --- a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php @@ -38,6 +38,7 @@ protected function setUp() { parent::setUp(); $this->enableModules(array('system')); + $this->enableModules(array('menu_link')); } function viewsData() { diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php index ee58a3b..d023554 100644 --- a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterNumericTest.php @@ -38,6 +38,7 @@ protected function setUp() { parent::setUp(); $this->enableModules(array('system')); + $this->enableModules(array('menu_link')); } function viewsData() { diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php index 0e9887a..c5f27f6 100644 --- a/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FilterStringTest.php @@ -37,6 +37,7 @@ protected function setUp() { parent::setUp(); $this->enableModules(array('system')); + $this->enableModules(array('menu_link')); } function viewsData() { diff --git a/core/profiles/minimal/minimal.info b/core/profiles/minimal/minimal.info index 545e85c..5276bb2 100644 --- a/core/profiles/minimal/minimal.info +++ b/core/profiles/minimal/minimal.info @@ -2,6 +2,7 @@ name = Minimal description = Build a custom site without pre-configured functionality. Suitable for advanced users. version = VERSION core = 8.x +dependencies[] = menu_link dependencies[] = node dependencies[] = block dependencies[] = dblog diff --git a/core/profiles/standard/standard.install b/core/profiles/standard/standard.install index 4d464cc..628d1ee 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -241,11 +241,11 @@ function standard_install() { ->execute(); // Create a Home link in the main menu. - $item = array( + $item = entity_create('menu_link', array( 'link_title' => st('Home'), 'link_path' => '', 'menu_name' => 'main', - ); + )); menu_link_save($item); // Enable the Contact link in the footer menu. @@ -255,16 +255,16 @@ function standard_install() { // Populate the default shortcut set. $shortcut_set = shortcut_set_load('default'); - $shortcut_set->links[] = array( + $shortcut_set->links[] = entity_create('menu_link', array( 'link_path' => 'node/add', 'link_title' => st('Add content'), 'weight' => -20, - ); - $shortcut_set->links[] = array( + )); + $shortcut_set->links[] = entity_create('menu_link', array( 'link_path' => 'admin/content', 'link_title' => st('Find content'), 'weight' => -19, - ); + )); $shortcut_set->save(); // Enable the admin theme. diff --git a/core/profiles/testing/testing.info b/core/profiles/testing/testing.info index fff3df2..fcbe479 100644 --- a/core/profiles/testing/testing.info +++ b/core/profiles/testing/testing.info @@ -3,5 +3,6 @@ description = Minimal profile for running tests. Includes absolutely required mo version = VERSION core = 8.x hidden = TRUE +dependencies[] = menu_link ; @todo Remove dependency on Node module. dependencies[] = node