diff --git a/core/includes/common.inc b/core/includes/common.inc index 0f252ca..42c7706 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -6172,8 +6172,6 @@ function drupal_sort_weight($a, $b) { /** * Sorts a structured array by 'title' key (no # prefix). * - * Callback for uasort() within system_admin_index(). - * * @param $a * First item for comparison. The compared items should be associative arrays * that optionally include a 'title' key. @@ -6191,6 +6189,31 @@ function drupal_sort_title($a, $b) { } /** + * Sorts a structured array using the 'label()' method of its elements. + * + * Callback for uasort() within system_admin_index(). + * + * @param Drupal\Core\Entity\EntityInterface $a + * First item for comparison. + * @param Drupal\Core\Entity\EntityInterface $b + * Second item for comparison. + * + * @return int + * An integer used by uasort(). + */ +function drupal_sort_label($a, $b) { + $b_label = $b->label(); + if (empty($b_label)) { + return -1; + } + $a_label = $a->label(); + if (empty($a_label)) { + return 1; + } + return strcasecmp($a_label, $b_label); +} + +/** * Checks if the key is a property. */ function element_property($key) { diff --git a/core/includes/menu.inc b/core/includes/menu.inc index 5a5e544..66df567 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\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; @@ -487,12 +493,12 @@ function menu_get_item($path = NULL, $router_item = NULL) { /** * Loads objects into the map as defined in the $item['load_functions']. * - * @param $item - * A menu router or menu link item - * @param $map + * @param array $item + * A menu router item. + * @param array $map * An array of path arguments (ex: array('node', '5')) * - * @return + * @return bool * Returns TRUE for success, FALSE if an object cannot be loaded. * Names of object loading functions are placed in $item['load_functions']. * Loaded objects are placed in $map[]; keys are the same as keys in the @@ -555,11 +561,11 @@ function _menu_load_objects(&$item, &$map) { } /** - * Checks access to a menu item using the access callback. + * Checks access to a menu router item using the access callback. * - * @param $item - * A menu router or menu link item - * @param $map + * @param array $item + * A menu router item. + * @param array $map * An array of path arguments (ex: array('node', '5')) * * @return @@ -594,14 +600,11 @@ function _menu_check_access(&$item, $map) { * by the current user. * * @param $item - * A menu router item or a menu link item. + * A menu router item. * @param $map * The path as an array with objects already replaced. E.g., for path * node/123 $map would be array('node', $node) where $node is the node * object for node 123. - * @param $link_translate - * TRUE if we are translating a menu link item; FALSE if we are - * translating a menu router item. * * @return * No return value. @@ -610,52 +613,41 @@ function _menu_check_access(&$item, $map) { * TRUE. * $item['description'] is computed using $item['description_callback'] if * specified; otherwise it is translated using t(). - * When doing link translation and the $item['options']['attributes']['title'] - * (link title attribute) matches the description, it is translated as well. */ -function _menu_item_localize(&$item, $map, $link_translate = FALSE) { +function _menu_item_localize(&$item, $map) { $title_callback = $item['title_callback']; $item['localized_options'] = $item['options']; // All 'class' attributes are assumed to be an array during rendering, but // links stored in the database may use an old string value. // @todo In order to remove this code we need to implement a database update // including unserializing all existing link options and running this code - // on them, as well as adding validation to menu_link_save(). + // on them, as well as adding validation to + // Drupal\menu_link\MenuLinkStorageController::save(). if (isset($item['options']['attributes']['class']) && is_string($item['options']['attributes']['class'])) { $item['localized_options']['attributes']['class'] = explode(' ', $item['options']['attributes']['class']); } - // If we are translating the title of a menu link, and its title is the same - // as the corresponding router item, then we can use the title information - // from the router. If it's customized, then we need to use the link title - // itself; can't localize. - // If we are translating a router item (tabs, page, breadcrumb), then we - // can always use the information from the router item. - if (!$link_translate || ($item['title'] == $item['link_title'])) { - // t() is a special case. Since it is used very close to all the time, - // we handle it directly instead of using indirect, slower methods. - if ($title_callback == 't') { - if (empty($item['title_arguments'])) { - $item['title'] = t($item['title']); - } - else { - $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map)); - } + + // t() is a special case. Since it is used very close to all the time, + // we handle it directly instead of using indirect, slower methods. + if ($title_callback == 't') { + if (empty($item['title_arguments'])) { + $item['title'] = t($item['title']); } - elseif ($title_callback) { - if (empty($item['title_arguments'])) { - $item['title'] = $title_callback($item['title']); - } - else { - $item['title'] = call_user_func_array($title_callback, menu_unserialize($item['title_arguments'], $map)); - } - // Avoid calling check_plain again on l() function. - if ($title_callback == 'check_plain') { - $item['localized_options']['html'] = TRUE; - } + else { + $item['title'] = t($item['title'], menu_unserialize($item['title_arguments'], $map)); } } - elseif ($link_translate) { - $item['title'] = $item['link_title']; + elseif ($title_callback) { + if (empty($item['title_arguments'])) { + $item['title'] = $title_callback($item['title']); + } + else { + $item['title'] = call_user_func_array($title_callback, menu_unserialize($item['title_arguments'], $map)); + } + // Avoid calling check_plain again on l() function. + if ($title_callback == 'check_plain') { + $item['localized_options']['html'] = TRUE; + } } // Translate description, see the motivation above. @@ -686,7 +678,7 @@ function _menu_item_localize(&$item, $map, $link_translate = FALSE) { } // If the title and description are the same, use the translated description // as a localized title. - if ($link_translate && isset($original_description) && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) { + if (isset($original_description) && isset($item['options']['attributes']['title']) && $item['options']['attributes']['title'] == $original_description) { $item['localized_options']['attributes']['title'] = $item['description']; } } @@ -991,7 +983,9 @@ function menu_tree_output($tree) { // Pull out just the menu links we are going to render so that we // get an accurate count for the first/last classes. foreach ($tree as $data) { - if ($data['link']['access'] && !$data['link']['hidden']) { + if ($data['link']->access() && !$data['link']->hidden) { + // @todo Remove this hack in the great localized_options conversion. + $data['link']->localized_options = $data['link']->options; $items[] = $data; } } @@ -1009,45 +1003,46 @@ function menu_tree_output($tree) { // Set a class for the
  • -tag. Since $data['below'] may contain local // tasks, only set 'expanded' class if the link also has children within // the current menu. - if ($data['link']['has_children'] && $data['below']) { + if ($data['link']->has_children && $data['below']) { $class[] = 'expanded'; } - elseif ($data['link']['has_children']) { + elseif ($data['link']->has_children) { $class[] = 'collapsed'; } else { $class[] = 'leaf'; } // Set a class if the link is in the active trail. - if ($data['link']['in_active_trail']) { + if ($data['link']->in_active_trail) { $class[] = 'active-trail'; - $data['link']['localized_options']['attributes']['class'][] = 'active-trail'; + $data['link']->localized_options['attributes']['class'][] = 'active-trail'; } // Normally, l() compares the href of every link with the current path and // sets the active class accordingly. But local tasks do not appear in menu // trees, so if the current path is a local task, and this link is its // tab root, then we have to set the class manually. - if ($data['link']['href'] == $router_item['tab_root_href'] && $data['link']['href'] != current_path()) { - $data['link']['localized_options']['attributes']['class'][] = 'active'; + if ($data['link']->href == $router_item['tab_root_href'] && $data['link']->href != current_path()) { + $data['link']->localized_options['attributes']['class'][] = 'active'; } // Allow menu-specific theme overrides. - $element['#theme'] = 'menu_link__' . strtr($data['link']['menu_name'], '-', '_'); + $element['#theme'] = 'menu_link__' . strtr($data['link']->menu_name, '-', '_'); $element['#attributes']['class'] = $class; - $element['#title'] = $data['link']['title']; - $element['#href'] = $data['link']['href']; - $element['#localized_options'] = !empty($data['link']['localized_options']) ? $data['link']['localized_options'] : array(); + $element['#title'] = $data['link']->label(); + $element['#href'] = $data['link']->href; + // @todo Deal with 'localized_options' later. + $element['#localized_options'] = !empty($data['link']->localized_options) ? $data['link']->localized_options : array(); $element['#below'] = $data['below'] ? menu_tree_output($data['below']) : $data['below']; $element['#original_link'] = $data['link']; - // Index using the link's unique mlid. - $build[$data['link']['mlid']] = $element; + // Index using the link's unique id. + $build[$data['link']->id()] = $element; } if ($build) { // Make sure drupal_render() does not re-order the links. $build['#sorted'] = TRUE; // Add the theme wrapper for outer markup. // Allow menu-specific theme overrides. - $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']['menu_name'], '-', '_'); + $build['#theme_wrappers'][] = 'menu_tree__' . strtr($data['link']->menu_name, '-', '_'); } return $build; @@ -1078,7 +1073,7 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { $language_interface = language(LANGUAGE_TYPE_INTERFACE); // Use $mlid as a flag for whether the data being loaded is for the whole tree. - $mlid = isset($link['mlid']) ? $link['mlid'] : 0; + $mlid = (isset($link) && $link->id()) ? $link->id() : 0; // Generate a cache ID (cid) specific for this $menu_name, $link, $language, and depth. $cid = 'links:' . $menu_name . ':all:' . $mlid . ':' . $language_interface->langcode . ':' . (int) $max_depth; @@ -1101,8 +1096,9 @@ function menu_tree_all_data($menu_name, $link = NULL, $max_depth = NULL) { // p columns and 0 (the top level) with the plid values of other links. $parents = array(0); for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { - if (!empty($link["p$i"])) { - $parents[] = $link["p$i"]; + $parent_property = "p$i"; + if (!empty($link->$parent_property)) { + $parents[] = $link->$parent_property; } } $tree_parameters['expanded'] = $parents; @@ -1237,12 +1233,13 @@ function menu_tree_page_data($menu_name, $max_depth = NULL, $only_active_trail = // active trail, if it resides in the requested menu. Otherwise, // we'd needlessly re-run _menu_build_tree() queries for every menu // on every page. - if ($active_link['menu_name'] == $menu_name) { + if ($active_link->menu_name == $menu_name) { // Use all the coordinates, except the last one because there // can be no child beyond the last column. for ($i = 1; $i < MENU_MAX_DEPTH; $i++) { - if ($active_link['p' . $i]) { - $active_trail[$active_link['p' . $i]] = $active_link['p' . $i]; + $parent_property = "p$i"; + if ($active_link->$parent_property) { + $active_trail[$active_link->$parent_property] = $active_link->$parent_property; } } // If we are asked to build links for the active trail only, skip @@ -1260,18 +1257,19 @@ 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')) - ->condition('menu_name', $menu_name) - ->condition('expanded', 1) - ->condition('has_children', 1) - ->condition('plid', $parents, 'IN') - ->condition('mlid', $parents, 'NOT IN') - ->execute(); + $query = menu_link_query() + ->propertyCondition('menu_name', $menu_name) + ->propertyCondition('expanded', 1) + ->propertyCondition('has_children', 1) + ->propertyCondition('plid', $parents, 'IN') + ->propertyCondition('mlid', $parents, 'NOT IN'); + $result = $query->execute(); $num_rows = FALSE; - foreach ($result as $item) { - $parents[$item['mlid']] = $item['mlid']; - $num_rows = TRUE; + if (!empty($result['menu_link'])) { + foreach ($result['menu_link'] as $item) { + $parents[$item->entity_id] = $item->entity_id; + $num_rows = TRUE; + } } } while ($num_rows); } @@ -1360,61 +1358,38 @@ 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 = menu_link_query(); for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { - $query->orderBy('p' . $i, 'ASC'); + $query->propertyOrderBy('p' . $i, 'ASC'); } - $query->condition('ml.menu_name', $menu_name); + $query->propertyCondition('menu_name', $menu_name); if (!empty($parameters['expanded'])) { - $query->condition('ml.plid', $parameters['expanded'], 'IN'); + $query->propertyCondition('plid', $parameters['expanded'], 'IN'); } elseif (!empty($parameters['only_active_trail'])) { - $query->condition('ml.mlid', $parameters['active_trail'], 'IN'); + $query->entityCondition('entity_id', $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->propertyCondition('depth', $min_depth, '>='); } if (isset($parameters['max_depth'])) { - $query->condition('ml.depth', $parameters['max_depth'], '<='); + $query->propertyCondition('depth', $parameters['max_depth'], '<='); } // Add custom query conditions, if any were passed. if (isset($parameters['conditions'])) { foreach ($parameters['conditions'] as $column => $value) { - $query->condition($column, $value); + $query->propertyCondition($column, $value); } } - // Build an ordered array of links using the query result object. - $links = array(); - foreach ($query->execute() as $item) { - $links[] = $item; + $result = $query->execute(); + + if (!empty($result['menu_link'])) { + $links = menu_link_load_multiple(array_keys($result['menu_link'])); } + $active_trail = (isset($parameters['active_trail']) ? $parameters['active_trail'] : array()); $data['tree'] = menu_tree_data($links, $active_trail, $min_depth); $data['node_links'] = array(); @@ -1438,11 +1413,11 @@ function _menu_build_tree($menu_name, array $parameters = array()) { */ function menu_tree_collect_node_links(&$tree, &$node_links) { foreach ($tree as $key => $v) { - if ($tree[$key]['link']['router_path'] == 'node/%') { - $nid = substr($tree[$key]['link']['link_path'], 5); + if ($tree[$key]['link']->router_path == 'node/%') { + $nid = substr($tree[$key]['link']->link_path, 5); if (is_numeric($nid)) { - $node_links[$nid][$tree[$key]['link']['mlid']] = &$tree[$key]['link']; - $tree[$key]['link']['access'] = FALSE; + $node_links[$nid][$tree[$key]['link']->id()] = &$tree[$key]['link']; + $tree[$key]['link']->access = FALSE; } } if ($tree[$key]['below']) { @@ -1471,7 +1446,7 @@ function menu_tree_check_access(&$tree, $node_links = array()) { $nids = $select->execute()->fetchCol(); foreach ($nids as $nid) { foreach ($node_links[$nid] as $mlid => $link) { - $node_links[$nid][$mlid]['access'] = TRUE; + $node_links[$nid][$mlid]->access = TRUE; } } } @@ -1485,15 +1460,13 @@ function _menu_tree_check_access(&$tree) { $new_tree = array(); foreach ($tree as $key => $v) { $item = &$tree[$key]['link']; - _menu_link_translate($item); - if ($item['access'] || ($item['in_active_trail'] && strpos($item['href'], '%') !== FALSE)) { + if ($item->access() || ($item->in_active_trail && strpos($item->href, '%') !== FALSE)) { if ($tree[$key]['below']) { _menu_tree_check_access($tree[$key]['below']); } // The weights are made a uniform 5 digits by adding 50000 as an offset. - // After _menu_link_translate(), $item['title'] has the localized link title. - // Adding the mlid to the end of the index insures that it is unique. - $new_tree[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $tree[$key]; + // Adding the entity id to the end of the index insures that it is unique. + $new_tree[(50000 + $item->weight) . ' ' . $item->label() . ' ' . $item->id()] = $tree[$key]; } } // Sort siblings in the tree based on the weights and localized titles. @@ -1541,27 +1514,27 @@ function menu_tree_data(array $links, array $parents = array(), $depth = 1) { */ function _menu_tree_data(&$links, $parents, $depth) { $tree = array(); - while ($item = array_pop($links)) { + while ($menu_link = array_pop($links)) { // We need to determine if we're on the path to root so we can later build // the correct active trail and breadcrumb. - $item['in_active_trail'] = in_array($item['mlid'], $parents); + $menu_link->in_active_trail = in_array($menu_link->id(), $parents); // Add the current link to the tree. - $tree[$item['mlid']] = array( - 'link' => $item, + $tree[$menu_link->id()] = array( + 'link' => $menu_link, 'below' => array(), ); // Look ahead to the next link, but leave it on the array so it's available // to other recursive function calls if we return or build a sub-tree. $next = end($links); // Check whether the next link is the first in a new sub-tree. - if ($next && $next['depth'] > $depth) { + if ($next && $next->depth > $depth) { // Recursively call _menu_tree_data to build the sub-tree. - $tree[$item['mlid']]['below'] = _menu_tree_data($links, $parents, $next['depth']); + $tree[$menu_link->id()]['below'] = _menu_tree_data($links, $parents, $next->depth); // Fetch next link after filling the sub-tree. $next = end($links); } // Determine if we should exit the loop and return. - if (!$next || $next['depth'] < $depth) { + if (!$next || $next->depth < $depth) { break; } } @@ -1813,7 +1786,7 @@ function menu_navigation_links($menu_name, $level = 0) { while ($level-- > 0 && $tree) { // Loop through the current level's items until we find one that is in trail. while ($item = array_shift($tree)) { - if ($item['link']['in_active_trail']) { + if ($item['link']->in_active_trail) { // If the item is in the active trail, we continue in the subtree. $tree = empty($item['below']) ? array() : $item['below']; break; @@ -1825,12 +1798,13 @@ function menu_navigation_links($menu_name, $level = 0) { $router_item = menu_get_item(); $links = array(); foreach ($tree as $item) { - if (!$item['link']['hidden']) { + if (!$item['link']->hidden) { $class = ''; - $l = $item['link']['localized_options']; - $l['href'] = $item['link']['href']; - $l['title'] = $item['link']['title']; - if ($item['link']['in_active_trail']) { + // @todo Deal with localized_options later. + $l = !empty($item['link']->localized_options) ? $item['link']->localized_options : $item['link']->options; + $l['href'] = $item['link']->href; + $l['title'] = $item['link']->label(); + if ($item['link']->in_active_trail) { $class = ' active-trail'; $l['attributes']['class'][] = 'active-trail'; } @@ -1838,11 +1812,11 @@ function menu_navigation_links($menu_name, $level = 0) { // sets the active class accordingly. But local tasks do not appear in // menu trees, so if the current path is a local task, and this link is // its tab root, then we have to set the class manually. - if ($item['link']['href'] == $router_item['tab_root_href'] && $item['link']['href'] != current_path()) { + if ($item['link']->href == $router_item['tab_root_href'] && $item['link']->href != current_path()) { $l['attributes']['class'][] = 'active'; } // Keyed with the unique mlid to generate classes in theme_links(). - $links['menu-' . $item['link']['mlid'] . $class] = $l; + $links['menu-' . $item['link']->id() . $class] = $l; } } return $links; @@ -2334,13 +2308,15 @@ function menu_set_active_trail($new_trail = NULL) { } elseif (!isset($trail)) { $trail = array(); - $trail[] = array( + $trail[] = entity_create('menu_link', array( 'title' => t('Home'), 'href' => '', 'link_path' => '', 'localized_options' => array(), - 'type' => 0, - ); + 'router_item' => array( + 'type' => 0, + ), + )); // Try to retrieve a menu link corresponding to the current path. If more // than one exists, the link from the most preferred menu is returned. @@ -2352,20 +2328,25 @@ function menu_set_active_trail($new_trail = NULL) { // Pass TRUE for $only_active_trail to make menu_tree_page_data() build // a stripped down menu tree containing the active trail only, in case // the given menu has not been built in this request yet. - $tree = menu_tree_page_data($preferred_link['menu_name'], NULL, TRUE); + $tree = menu_tree_page_data($preferred_link->menu_name, NULL, TRUE); list($key, $curr) = each($tree); } // There is no link for the current path. else { - $preferred_link = $current_item; + $preferred_link = entity_create('menu_link', array( + 'title' => $current_item['title'], + 'href' => $current_item['href'], + 'options' => array(), + 'router_item' => $current_item, + )); $curr = FALSE; } while ($curr) { $link = $curr['link']; - if ($link['in_active_trail']) { + if ($link->in_active_trail) { // Add the link to the trail, unless it links to its parent. - if (!($link['type'] & MENU_LINKS_TO_PARENT)) { + if (!($link->router_item['type'] & MENU_LINKS_TO_PARENT)) { // The menu tree for the active trail may contain additional links // that have not been translated yet, since they contain dynamic // argument placeholders (%). Such links are not contained in regular @@ -2373,11 +2354,11 @@ function menu_set_active_trail($new_trail = NULL) { // translation that happens here, so as to be able to display them in // the breadcumb for the current page. // @see _menu_tree_check_access() - // @see _menu_link_translate() - if (strpos($link['href'], '%') !== FALSE) { - _menu_link_translate($link, TRUE); + // @see Drupal\menu_link\MenuLink::translateCurrentPathMap() + if (strpos($link->href, '%') !== FALSE) { + $link->translateCurrentPathMap(); } - if ($link['access']) { + if ($link->access()) { $trail[] = $link; } } @@ -2389,7 +2370,7 @@ function menu_set_active_trail($new_trail = NULL) { // appending either the preferred link or the menu router item for the // current page. Exclude it if we are on the front page. $last = end($trail); - if ($preferred_link && $last['href'] != $preferred_link['href'] && !drupal_is_front_page()) { + if ($preferred_link && $last->href != $preferred_link->href && !drupal_is_front_page()) { $trail[] = $preferred_link; } } @@ -2450,22 +2431,15 @@ 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']; - $candidates[$candidate['link_path']][$candidate['menu_name']] = $candidate; + foreach ($menu_links as $candidate) { + $candidates[$candidate->link_path][$candidate->menu_name] = $candidate; // Add any menus not already in the menu name search list. - if (!in_array($candidate['menu_name'], $menu_names)) { - $menu_names[] = $candidate['menu_name']; + if (!in_array($candidate->menu_name, $menu_names)) { + $menu_names[] = $candidate->menu_name; } } @@ -2477,10 +2451,16 @@ function menu_link_get_preferred($path = NULL, $selected_menu = NULL) { if (empty($preferred_links[$path][$menu_name]) && isset($candidates[$link_path][$menu_name])) { $candidate_item = $candidates[$link_path][$menu_name]; $map = explode('/', $path); - _menu_translate($candidate_item, $map); - if ($candidate_item['access']) { + _menu_translate($candidate_item->router_item, $map); + if ($candidate_item->router_item['access']) { $preferred_links[$path][$menu_name] = $candidate_item; if (empty($preferred_links[$path][MENU_PREFERRED_LINK])) { + // Bring over some properties from the router item. + // @todo Clean-up this crap. + $candidate_item->href = $candidate_item->router_item['href']; + $candidate_item->options = $candidate_item->router_item['options']; + $candidate_item->localized_options = $candidate_item->router_item['localized_options']; + // Store the most specific link. $preferred_links[$path][MENU_PREFERRED_LINK] = $candidate_item; } @@ -2539,7 +2519,7 @@ function menu_get_active_breadcrumb() { // Don't show a link to the current page in the breadcrumb trail. $end = end($active_trail); - if ($item['href'] == $end['href']) { + if ($item['href'] == $end->href) { array_pop($active_trail); } @@ -2561,7 +2541,7 @@ function menu_get_active_breadcrumb() { } foreach ($active_trail as $parent) { - $breadcrumb[] = l($parent['title'], $parent['href'], $parent['localized_options']); + $breadcrumb[] = l($parent->label(), $parent->href, $parent->options); } } return $breadcrumb; @@ -2574,42 +2554,10 @@ function menu_get_active_title() { $active_trail = menu_get_active_trail(); foreach (array_reverse($active_trail) as $item) { - if (!(bool) ($item['type'] & MENU_IS_LOCAL_TASK)) { - return $item['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; + if (!(bool) ($item->router_item['type'] & MENU_IS_LOCAL_TASK)) { + return $item->label(); } } - return FALSE; } /** @@ -2734,156 +2682,92 @@ function menu_get_router() { } /** - * Builds a link from a router item. - */ -function _menu_link_build($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( - 'menu_name' => 'navigation', - '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); - - foreach ($menu_links as $key => $item) { - $existing_item = db_select('menu_links') - ->fields('menu_links') - ->condition('link_path', $item['path']) - ->condition('module', 'system') - ->execute()->fetchAssoc(); - if ($existing_item) { - $item['mlid'] = $existing_item['mlid']; + array_multisort($sort, SORT_NUMERIC, $router_items); + + foreach ($router_items as $key => $router_item) { + if ($existing_item = entity_load_multiple_by_properties('menu_link', array('link_path' => $router_item['path'], 'module' => 'system'))) { + $existing_item = reset($existing_item); + + $router_item['mlid'] = $existing_item->id(); // 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; + } + else { + $existing_item = NULL; } - if ($existing_item && $existing_item['customized']) { - $parent_candidates[$existing_item['mlid']] = $existing_item; + + if ($existing_item && $existing_item->customized) { + $parent_candidates[$existing_item->id()] = $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->save(); + $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 = entity_get_controller('menu_link')->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->save(); } } + // Find any item whose router path does not exist any more. - $result = db_select('menu_links') - ->fields('menu_links') - ->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); - } -} + $query = menu_link_query() + ->propertyCondition('router_path', $paths, 'NOT IN') + ->propertyCondition('external', 0) + ->propertyCondition('updated', 0) + ->propertyCondition('customized', 0) + ->propertyOrderBy('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'])) { + menu_link_delete_multiple(array_keys($result['menu_link']), TRUE); } - return $links; } /** @@ -2896,18 +2780,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(); + + $entity_query = menu_link_query() + ->propertyCondition('menu_name', $menu_name) // Order by weight so as to be helpful for menus that are only one level // deep. - ->orderBy('weight') - ->execute() - ->fetchAll(); + ->propertyOrderBy('weight'); + $result = $entity_query->execute(); - foreach ($links as &$link) { - $link['options'] = unserialize($link['options']); + if (!empty($result['menu_link'])) { + $links = menu_link_load_multiple(array_keys($result['menu_link'])); } + return $links; } @@ -2919,333 +2804,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_get_normal_path() 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 'navigation'. - * - 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' => 'navigation', - '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,169 +2880,6 @@ function _menu_find_router_path($link_path) { } /** - * Inserts, updates, or deletes an uncustomized menu link related to a module. - * - * @param $module - * The name of the module. - * @param $op - * Operation to perform: insert, update or delete. - * @param $link_path - * The path this link points to. - * @param $link_title - * Title of the link to insert or new title to update the link to. - * Unused for delete. - * - * @return - * The insert op returns the mlid of the new item. Others op return NULL. - */ -function menu_link_maintain($module, $op, $link_path, $link_title) { - switch ($op) { - case 'insert': - $menu_link = array( - 'link_title' => $link_title, - 'link_path' => $link_path, - 'module' => $module, - ); - return menu_link_save($menu_link); - break; - 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) { - $link['link_title'] = $link_title; - $link['options'] = unserialize($link['options']); - menu_link_save($link); - } - 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 047026a..4c85673 100644 --- a/core/modules/aggregator/aggregator.module +++ b/core/modules/aggregator/aggregator.module @@ -482,7 +482,7 @@ function aggregator_save_category($edit) { $op = 'insert'; } if (isset($op)) { - menu_link_maintain('aggregator', $op, $link_path, $edit['title']); + module_invoke('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 ae71799..a8ac6c5 100644 --- a/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php +++ b/core/modules/aggregator/lib/Drupal/aggregator/Tests/CategorizeFeedItemTest.php @@ -32,8 +32,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.admin.inc b/core/modules/book/book.admin.inc index fbd8dda..cb99679 100644 --- a/core/modules/book/book.admin.inc +++ b/core/modules/book/book.admin.inc @@ -153,7 +153,7 @@ function book_admin_edit_submit($form, &$form_state) { if ($row['plid']['#default_value'] != $values['plid'] || $row['weight']['#default_value'] != $values['weight']) { $row['#item']['plid'] = $values['plid']; $row['#item']['weight'] = $values['weight']; - menu_link_save($row['#item']); + $row['#item']->save(); } // Update the title if changed. diff --git a/core/modules/book/book.info b/core/modules/book/book.info index 1f2be6f..14fb146 100644 --- a/core/modules/book/book.info +++ b/core/modules/book/book.info @@ -3,6 +3,7 @@ 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 stylesheets[all][] = book.theme.css diff --git a/core/modules/book/book.module b/core/modules/book/book.module index 7c43e28..a20f05c 100644 --- a/core/modules/book/book.module +++ b/core/modules/book/book.module @@ -5,6 +5,7 @@ * Allows users to create and organize related content in an outline. */ +use Drupal\menu_link\MenuLinkStorageController; use Drupal\node\Node; use Drupal\Core\Template\Attribute; @@ -663,7 +664,7 @@ function _book_update_outline(Node $node) { } } - if (menu_link_save($node->book)) { + if ($node->book->save()) { if ($new) { // Insert new. db_insert('book') @@ -1031,7 +1032,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/help/help.admin.inc b/core/modules/help/help.admin.inc index 0a8e147..b086d9a 100644 --- a/core/modules/help/help.admin.inc +++ b/core/modules/help/help.admin.inc @@ -45,7 +45,7 @@ function help_page($name) { if (!empty($admin_tasks)) { $links = array(); foreach ($admin_tasks as $task) { - $links[] = l($task['title'], $task['link_path'], $task['localized_options']); + $links[] = l($task->label(), $task->link_path, $task->localized_options); } $output .= theme('item_list', array('items' => $links, 'title' => t('@module administration pages', array('@module' => $info[$name]['name'])))); } diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php index c3450a7..dfa81ce 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuNodeTest.php @@ -107,38 +107,38 @@ function testMenuNodeFormWidget() { $this->assertNoLink($node_title); // Add a menu link to the Management menu. - $item = array( + $item = entity_create('menu_link', array( 'link_path' => 'node/' . $node->nid, 'link_title' => $this->randomName(16), 'menu_name' => 'management', - ); - menu_link_save($item); + )); + $item->save(); // Assert that disabled Management menu is not shown on the node/$nid/edit page. $this->drupalGet('node/' . $node->nid . '/edit'); $this->assertText('Provide a menu link', 'Link in not allowed menu not shown in node edit form'); // Assert that the link is still in the management menu after save. $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); - $link = menu_link_load($item['mlid']); + $link = menu_link_load($item->id()); $this->assertTrue($link, 'Link in not allowed menu still exists after saving node'); // Move the menu link back to the Navigation menu. - $item['menu_name'] = 'navigation'; - menu_link_save($item); + $item->menu_name = 'navigation'; + $item->save(); // Create a second node. $child_node = $this->drupalCreateNode(array('type' => 'article')); // Assign a menu link to the second node, being a child of the first one. - $child_item = 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); + 'plid' => $item->id(), + )); + $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. - $this->assertNoOption('edit-menu-parent', 'navigation:'. $item['mlid']); - $this->assertNoOption('edit-menu-parent', 'navigation:'. $child_item['mlid']); + $this->assertNoOption('edit-menu-parent', 'navigation:'. $item->id()); + $this->assertNoOption('edit-menu-parent', 'navigation:'. $child_item->id()); // Assert that unallowed Management menu is not available in options. $this->assertNoOption('edit-menu-parent', 'management:0'); } diff --git a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php index b4cd7e7..c0eb4c7 100644 --- a/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php +++ b/core/modules/menu/lib/Drupal/menu/Tests/MenuTest.php @@ -60,7 +60,7 @@ function testMenu() { $this->drupalLogin($this->std_user); $this->verifyAccess(403); foreach ($this->items as $item) { - $node = node_load(substr($item['link_path'], 5)); // Paths were set as 'node/$nid'. + $node = node_load(substr($item->link_path, 5)); // Paths were set as 'node/$nid'. $this->verifyMenuLink($item, $node); } @@ -77,15 +77,15 @@ function testMenu() { // Modify and reset a standard menu link. $item = $this->getStandardMenuLink(); - $old_title = $item['link_title']; + $old_title = $item->link_title; $this->modifyMenuLink($item); - $item = menu_link_load($item['mlid']); + $item = menu_link_load($item->id()); // Verify that a change to the description is saved. $description = $this->randomName(16); - $item['options']['attributes']['title'] = $description; - menu_link_save($item); - $saved_item = menu_link_load($item['mlid']); - $this->assertEqual($description, $saved_item['options']['attributes']['title'], 'Saving an existing link updates the description (title attribute)'); + $item->options['attributes']['title'] = $description; + $item->save(); + $saved_item = menu_link_load($item->id()); + $this->assertEqual($description, $saved_item->options['attributes']['title'], 'Saving an existing link updates the description (title attribute)'); $this->resetMenuLink($item, $old_title); } @@ -202,7 +202,7 @@ function deleteCustomMenu($menu) { $this->assertRaw(t('The custom menu %title has been deleted.', array('%title' => $title)), '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.'); } @@ -220,11 +220,12 @@ function doMenuTests($menu_name = 'navigation') { // Add menu links. $item1 = $this->addMenuLink(0, 'node/' . $node1->nid, $menu_name); - $item2 = $this->addMenuLink($item1['mlid'], 'node/' . $node2->nid, $menu_name, FALSE); - $item3 = $this->addMenuLink($item2['mlid'], 'node/' . $node3->nid, $menu_name); - $this->assertMenuLink($item1['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => 0)); - $this->assertMenuLink($item2['mlid'], array('depth' => 2, 'has_children' => 1, 'p1' => $item1['mlid'], 'p2' => $item2['mlid'], 'p3' => 0)); - $this->assertMenuLink($item3['mlid'], array('depth' => 3, 'has_children' => 0, 'p1' => $item1['mlid'], 'p2' => $item2['mlid'], 'p3' => $item3['mlid'], 'p4' => 0)); + $item2 = $this->addMenuLink($item1->id(), 'node/' . $node2->nid, $menu_name, FALSE); + $item3 = $this->addMenuLink($item2->id(), 'node/' . $node3->nid, $menu_name); + entity_get_controller('menu_link')->resetCache(array($item1->id(), $item2->id(), $item3->id())); + $this->assertMenuLink($item1->id(), array('depth' => 1, 'has_children' => 1, 'p1' => $item1->id(), 'p2' => 0)); + $this->assertMenuLink($item2->id(), array('depth' => 2, 'has_children' => 1, 'p1' => $item1->id(), 'p2' => $item2->id(), 'p3' => 0)); + $this->assertMenuLink($item3->id(), array('depth' => 3, 'has_children' => 0, 'p1' => $item1->id(), 'p2' => $item2->id(), 'p3' => $item3->id(), 'p4' => 0)); // Verify menu links. $this->verifyMenuLink($item1, $node1); @@ -233,9 +234,10 @@ function doMenuTests($menu_name = 'navigation') { // Add more menu links. $item4 = $this->addMenuLink(0, 'node/' . $node4->nid, $menu_name); - $item5 = $this->addMenuLink($item4['mlid'], 'node/' . $node5->nid, $menu_name); - $this->assertMenuLink($item4['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => 0)); - $this->assertMenuLink($item5['mlid'], array('depth' => 2, 'has_children' => 0, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => 0)); + $item5 = $this->addMenuLink($item4->id(), 'node/' . $node5->nid, $menu_name); + entity_get_controller('menu_link')->resetCache(array($item4->id(), $item5->id())); + $this->assertMenuLink($item4->id(), array('depth' => 1, 'has_children' => 1, 'p1' => $item4->id(), 'p2' => 0)); + $this->assertMenuLink($item5->id(), array('depth' => 2, 'has_children' => 0, 'p1' => $item4->id(), 'p2' => $item5->id(), 'p3' => 0)); // Modify menu links. $this->modifyMenuLink($item1); @@ -246,12 +248,13 @@ function doMenuTests($menu_name = 'navigation') { $this->toggleMenuLink($item2); // Move link and verify that descendants are updated. - $this->moveMenuLink($item2, $item5['mlid'], $menu_name); - $this->assertMenuLink($item1['mlid'], array('depth' => 1, 'has_children' => 0, 'p1' => $item1['mlid'], 'p2' => 0)); - $this->assertMenuLink($item4['mlid'], array('depth' => 1, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => 0)); - $this->assertMenuLink($item5['mlid'], array('depth' => 2, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => 0)); - $this->assertMenuLink($item2['mlid'], array('depth' => 3, 'has_children' => 1, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => $item2['mlid'], 'p4' => 0)); - $this->assertMenuLink($item3['mlid'], array('depth' => 4, 'has_children' => 0, 'p1' => $item4['mlid'], 'p2' => $item5['mlid'], 'p3' => $item2['mlid'], 'p4' => $item3['mlid'], 'p5' => 0)); + $this->moveMenuLink($item2, $item5->id(), $menu_name); + entity_get_controller('menu_link')->resetCache(array($item1->id(), $item2->id(), $item3->id(), $item4->id(), $item5->id())); + $this->assertMenuLink($item1->id(), array('depth' => 1, 'has_children' => 0, 'p1' => $item1->id(), 'p2' => 0)); + $this->assertMenuLink($item4->id(), array('depth' => 1, 'has_children' => 1, 'p1' => $item4->id(), 'p2' => 0)); + $this->assertMenuLink($item5->id(), array('depth' => 2, 'has_children' => 1, 'p1' => $item4->id(), 'p2' => $item5->id(), 'p3' => 0)); + $this->assertMenuLink($item2->id(), array('depth' => 3, 'has_children' => 1, 'p1' => $item4->id(), 'p2' => $item5->id(), 'p3' => $item2->id(), 'p4' => 0)); + $this->assertMenuLink($item3->id(), array('depth' => 4, 'has_children' => 0, 'p1' => $item4->id(), 'p2' => $item5->id(), 'p3' => $item2->id(), 'p4' => $item3->id(), 'p5' => 0)); // Add 102 menu links with increasing weights, then make sure the last-added // item's weight doesn't get changed because of the old hardcoded delta=50 @@ -259,7 +262,7 @@ function doMenuTests($menu_name = 'navigation') { for ($i = -50; $i <= 51; $i++) { $items[$i] = $this->addMenuLink(0, 'node/' . $node1->nid, $menu_name, TRUE, strval($i)); } - $this->assertMenuLink($items[51]['mlid'], array('weight' => '51')); + $this->assertMenuLink($items[51]->id(), array('weight' => '51')); // Enable a link via the overview form. $this->disableMenuLink($item1); @@ -267,11 +270,12 @@ function doMenuTests($menu_name = 'navigation') { // Note in the UI the 'mlid:x[hidden]' form element maps to enabled, or // NOT hidden. - $edit['mlid:' . $item1['mlid'] . '[hidden]'] = TRUE; - $this->drupalPost('admin/structure/menu/manage/' . $item1['menu_name'], $edit, t('Save configuration')); + $edit['mlid:' . $item1->id() . '[hidden]'] = TRUE; + $this->drupalPost('admin/structure/menu/manage/' . $item1->menu_name, $edit, t('Save configuration')); + entity_get_controller('menu_link')->resetCache(array($item1->id())); // Verify in the database. - $this->assertMenuLink($item1['mlid'], array('hidden' => 0)); + $this->assertMenuLink($item1->id(), array('hidden' => 0)); // Save menu links for later tests. $this->items[] = $item1; @@ -288,13 +292,13 @@ function testMenuQueryAndFragment() { $path = 'node?arg1=value1&arg2=value2'; $item = $this->addMenuLink(0, $path); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); + $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); $this->assertFieldByName('link_path', $path, 'Path is found with both query and fragment.'); // Now change the path to something without query and fragment. $path = 'node'; - $this->drupalPost('admin/structure/menu/item/' . $item['mlid'] . '/edit', array('link_path' => $path), t('Save')); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); + $this->drupalPost('admin/structure/menu/item/' . $item->id() . '/edit', array('link_path' => $path), t('Save')); + $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); $this->assertFieldByName('link_path', $path, 'Path no longer has query or fragment.'); } @@ -305,7 +309,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\MenuLink $menu_link + * A menu link entity. */ function addMenuLink($plid = 0, $link = '', $menu_name = 'navigation', $expanded = TRUE, $weight = '0') { // View add menu link page. @@ -326,14 +332,14 @@ function addMenuLink($plid = 0, $link = '', $menu_name = 'navigation', $e // 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; } /** @@ -368,7 +374,7 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) // Verify parent menu link. if (isset($parent)) { // Verify menu link. - $title = $parent['link_title']; + $title = $parent->link_title; $this->assertLink($title, 0, 'Parent menu link was displayed'); // Verify menu link link. @@ -378,7 +384,8 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) } // Verify menu link. - $title = $item['link_title']; + $title = $item->link_title; + debug($title); $this->assertLink($title, 0, 'Menu link was displayed'); // Verify menu link link. @@ -391,7 +398,7 @@ function verifyMenuLink($item, $item_node, $parent = NULL, $parent_node = NULL) * Change the parent of a menu link using the menu module UI. */ function moveMenuLink($item, $plid, $menu_name) { - $mlid = $item['mlid']; + $mlid = $item->id(); $edit = array( 'parent' => $menu_name . ':' . $plid, @@ -406,10 +413,10 @@ function moveMenuLink($item, $plid, $menu_name) { * @param array $item Menu link passed by reference. */ function modifyMenuLink(&$item) { - $item['link_title'] = $this->randomName(16); + $item->link_title = $this->randomName(16); - $mlid = $item['mlid']; - $title = $item['link_title']; + $mlid = $item->id(); + $title = $item->link_title; // Edit menu link. $edit = array(); @@ -419,7 +426,7 @@ function modifyMenuLink(&$item) { // Unlike most other modules, there is no confirmation message displayed. // Verify menu link. - $this->drupalGet('admin/structure/menu/manage/' . $item['menu_name']); + $this->drupalGet('admin/structure/menu/manage/' . $item->menu_name); $this->assertText($title, 'Menu link was edited'); } @@ -430,13 +437,14 @@ function modifyMenuLink(&$item) { * @param string $old_title Original title for menu link. */ function resetMenuLink($item, $old_title) { - $mlid = $item['mlid']; - $title = $item['link_title']; + $mlid = $item->id(); + $title = $item->link_title; // Reset menu link. $this->drupalPost("admin/structure/menu/item/$mlid/reset", array(), t('Reset')); $this->assertResponse(200); $this->assertRaw(t('The menu link was reset to its default settings.'), 'Menu link was reset'); + entity_get_controller('menu_link')->resetCache(array($mlid)); // Verify menu link. $this->drupalGet(''); @@ -450,8 +458,8 @@ function resetMenuLink($item, $old_title) { * @param array $item Menu link. */ function deleteMenuLink($item) { - $mlid = $item['mlid']; - $title = $item['link_title']; + $mlid = $item->id(); + $title = $item->link_title; // Delete menu link. $this->drupalPost("admin/structure/menu/item/$mlid/delete", array(), t('Confirm')); @@ -474,12 +482,12 @@ function toggleMenuLink($item) { // Verify menu link is absent. $this->drupalGet(''); - $this->assertNoText($item['link_title'], 'Menu link was not displayed'); + $this->assertNoText($item->link_title, 'Menu link was not displayed'); $this->enableMenuLink($item); // Verify menu link is displayed. $this->drupalGet(''); - $this->assertText($item['link_title'], 'Menu link was displayed'); + $this->assertText($item->link_title, 'Menu link was displayed'); } /** @@ -489,9 +497,10 @@ function toggleMenuLink($item) { * Menu link. */ function disableMenuLink($item) { - $mlid = $item['mlid']; + $mlid = $item->id(); $edit['enabled'] = FALSE; $this->drupalPost("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); + entity_get_controller('menu_link')->resetCache(array($mlid)); // Unlike most other modules, there is no confirmation message displayed. // Verify in the database. @@ -505,9 +514,10 @@ function disableMenuLink($item) { * Menu link. */ function enableMenuLink($item) { - $mlid = $item['mlid']; + $mlid = $item->id(); $edit['enabled'] = TRUE; $this->drupalPost("admin/structure/menu/item/$mlid/edit", $edit, t('Save')); + entity_get_controller('menu_link')->resetCache(array($mlid)); // Verify in the database. $this->assertMenuLink($mlid, array('hidden' => 0)); @@ -524,16 +534,15 @@ 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']); - if (!empty($options['query'])) { - $item['link_path'] .= '?' . drupal_http_build_query($options['query']); + $item = menu_link_load($mlid); + if (!empty($item->options['query'])) { + $item->link_path .= '?' . drupal_http_build_query($item->options['query']); } - if (!empty($options['fragment'])) { - $item['link_path'] .= '#' . $options['fragment']; + if (!empty($item->options['fragment'])) { + $item->link_path .= '#' . $item->options['fragment']; } foreach ($expected_item as $key => $value) { - $this->assertEqual($item[$key], $value, format_string('Parameter %key had expected value.', array('%key' => $key))); + $this->assertEqual($item->{$key}, $value, format_string('Parameter %key had expected value.', array('%key' => $key))); } } @@ -541,8 +550,17 @@ 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 = menu_link_query() + ->propertyCondition('module', 'system') + ->propertyCondition('router_path', 'user/logout'); + $result = $query->execute(); + if (!empty($result['menu_link'])) { + $link = reset($result['menu_link']); + $mlid = $link->entity_id; + } + $this->assertTrue($mlid > 0, 'Standard menu link id was found'); // Load menu link. // Use api function so that link is translated for rendering. @@ -580,7 +598,7 @@ private function verifyAccess($response = 200) { // View menu edit node. $item = $this->getStandardMenuLink(); - $this->drupalGet('admin/structure/menu/item/' . $item['mlid'] . '/edit'); + $this->drupalGet('admin/structure/menu/item/' . $item->id() . '/edit'); $this->assertResponse($response); if ($response == 200) { $this->assertText(t('Edit menu item'), 'Menu edit node was displayed'); diff --git a/core/modules/menu/menu.admin.inc b/core/modules/menu/menu.admin.inc index eeb5c7f..3fa5525 100644 --- a/core/modules/menu/menu.admin.inc +++ b/core/modules/menu/menu.admin.inc @@ -6,6 +6,7 @@ */ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; +use Drupal\menu_link\MenuLink; /** * Menu callback which shows an overview page of all the custom menus and their descriptions. @@ -68,16 +69,18 @@ function theme_menu_admin_overview($variables) { 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['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); $links = array(); - foreach ($result as $item) { - $links[] = $item; + $query = menu_link_query() + ->propertyCondition('menu_name', $menu['menu_name']); + for ($i = 1; $i <= MENU_MAX_DEPTH; $i++) { + $query->propertyOrderBy('p' . $i, 'ASC'); } + $result = $query->execute(); + + if (!empty($result['menu_link'])) { + $links = menu_link_load_multiple(array_keys($result['menu_link'])); + } + $delta = max(count($links), 50); $tree = menu_tree_data($links); $node_links = array(); @@ -117,62 +120,62 @@ function _menu_overview_tree_form($tree, $delta = 50) { $title = ''; $item = $data['link']; // Don't show callbacks; these have $item['hidden'] < 0. - if ($item && $item['hidden'] >= 0) { - $mlid = 'mlid:' . $item['mlid']; + if ($item && $item->hidden >= 0) { + $mlid = 'mlid:' . $item->id(); $form[$mlid]['#item'] = $item; - $form[$mlid]['#attributes'] = $item['hidden'] ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); - $form[$mlid]['title']['#markup'] = l($item['title'], $item['href'], $item['localized_options']); - if ($item['hidden']) { + $form[$mlid]['#attributes'] = $item->hidden ? array('class' => array('menu-disabled')) : array('class' => array('menu-enabled')); + $form[$mlid]['title']['#markup'] = l($item->label(), $item->href, $item->options); + if ($item->hidden) { $form[$mlid]['title']['#markup'] .= ' (' . t('disabled') . ')'; } - elseif ($item['link_path'] == 'user' && $item['module'] == 'system') { + elseif ($item->link_path == 'user' && $item->module == 'system') { $form[$mlid]['title']['#markup'] .= ' (' . t('logged in users only') . ')'; } $form[$mlid]['hidden'] = array( '#type' => 'checkbox', - '#title' => t('Enable @title menu link', array('@title' => $item['title'])), + '#title' => t('Enable @title menu link', array('@title' => $item->label())), '#title_display' => 'invisible', - '#default_value' => !$item['hidden'], + '#default_value' => !$item->hidden, ); $form[$mlid]['weight'] = array( '#type' => 'weight', '#delta' => $delta, - '#default_value' => $item['weight'], + '#default_value' => $item->weight, '#title_display' => 'invisible', - '#title' => t('Weight for @title', array('@title' => $item['title'])), + '#title' => t('Weight for @title', array('@title' => $item->label())), ); $form[$mlid]['mlid'] = array( '#type' => 'hidden', - '#value' => $item['mlid'], + '#value' => $item->id(), ); $form[$mlid]['plid'] = array( '#type' => 'hidden', - '#default_value' => $item['plid'], + '#default_value' => $item->plid, ); // Build a list of operations. $operations = array(); $links = array(); $links['edit'] = array( 'title' => t('edit'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit', + 'href' => 'admin/structure/menu/item/' . $item->id() . '/edit', ); - $operations['edit'] = array('#type' => 'link', '#title' => t('edit'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/edit'); + $operations['edit'] = array('#type' => 'link', '#title' => t('edit'), '#href' => 'admin/structure/menu/item/' . $item->id() . '/edit'); // Only items created by the menu module can be deleted. - if ($item['module'] == 'menu' || $item['updated'] == 1) { + if ($item->module == 'menu' || $item->updated == 1) { $links['delete'] = array( 'title' => t('delete'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete', + 'href' => 'admin/structure/menu/item/' . $item->id() . '/delete', ); - $operations['delete'] = array('#type' => 'link', '#title' => t('delete'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/delete'); + $operations['delete'] = array('#type' => 'link', '#title' => t('delete'), '#href' => 'admin/structure/menu/item/' . $item->id() . '/delete'); } // Set the reset column. - elseif ($item['module'] == 'system' && $item['customized']) { + elseif ($item->module == 'system' && $item->customized) { $links['reset'] = array( 'title' => t('reset'), - 'href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset', + 'href' => 'admin/structure/menu/item/' . $item->id() . '/reset', ); - $operations['reset'] = array('#type' => 'link', '#title' => t('reset'), '#href' => 'admin/structure/menu/item/' . $item['mlid'] . '/reset'); + $operations['reset'] = array('#type' => 'link', '#title' => t('reset'), '#href' => 'admin/structure/menu/item/' . $item->id() . '/reset'); } $form[$mlid]['operations'] = array( '#type' => 'operations', @@ -213,23 +216,23 @@ function menu_overview_form_submit($form, &$form_state) { // Update any fields that have changed in this menu item. foreach ($fields as $field) { if ($element[$field]['#value'] != $element[$field]['#default_value']) { - $element['#item'][$field] = $element[$field]['#value']; + $element['#item']->{$field} = $element[$field]['#value']; $updated_items[$mlid] = $element['#item']; } } // Hidden is a special case, the value needs to be reversed. if ($element['hidden']['#value'] != $element['hidden']['#default_value']) { // Convert to integer rather than boolean due to PDO cast to string. - $element['#item']['hidden'] = $element['hidden']['#value'] ? 0 : 1; + $element['#item']->hidden = $element['hidden']['#value'] ? 0 : 1; $updated_items[$mlid] = $element['#item']; } } } // Save all our changed items to the database. - foreach ($updated_items as $item) { - $item['customized'] = 1; - menu_link_save($item); + foreach ($updated_items as $menu_link) { + $menu_link->customized = 1; + $menu_link->save(); } drupal_set_message(t('Your configuration has been saved.')); } @@ -270,7 +273,7 @@ function theme_menu_overview_form($variables) { $element['plid']['#type'] = 'hidden'; $row = array(); - $row[] = theme('indentation', array('size' => $element['#item']['depth'] - 1)) . drupal_render($element['title']); + $row[] = theme('indentation', array('size' => $element['#item']->depth - 1)) . drupal_render($element['title']); $row[] = array('data' => drupal_render($element['hidden']), 'class' => array('checkbox', 'menu-enabled')); $row[] = drupal_render($element['weight']) . drupal_render($element['plid']) . drupal_render($element['mlid']); $row[] = drupal_render($element['operations']); @@ -290,193 +293,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['menu_name'], '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 = 'navigation: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')); - - 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_get_normal_path($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; Build the form that handles the adding/editing of a custom menu. */ function menu_edit_menu($form, &$form_state, $type, $menu = array()) { @@ -567,7 +383,7 @@ function menu_delete_menu_page($menu) { function menu_delete_menu_confirm($form, &$form_state, $menu) { $form['#menu'] = $menu; $caption = ''; - $num_links = db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = :menu", array(':menu' => $menu['menu_name']))->fetchField(); + $num_links = entity_get_controller('menu_link')->countMenuLinks($menu['menu_name']); 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['title'])) . '

    '; } @@ -589,16 +405,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['menu_name']), 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['menu_name']), 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['menu_name']), 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['menu_name'])); + menu_link_delete_multiple(array_keys($menu_links)); // Delete the custom menu and all its menu links. menu_delete($menu); @@ -618,7 +434,7 @@ function menu_edit_menu_name_exists($value) { // 'menu-' is added to the menu name to avoid name-space conflicts. $value = 'menu-' . $value; $custom_exists = db_query_range('SELECT 1 FROM {menu_custom} WHERE menu_name = :menu', 0, 1, array(':menu' => $value))->fetchField(); - $link_exists = db_query_range("SELECT 1 FROM {menu_links} WHERE menu_name = :menu", 0, 1, array(':menu' => $value))->fetchField(); + $link_exists = menu_link_query()->propertyCondition('menu_name', $value)->range(0,1)->count()->execute(); return $custom_exists || $link_exists; } @@ -632,26 +448,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['menu_name'] = 'menu-' . $menu['menu_name']; - $link['link_title'] = $menu['title']; - $link['link_path'] = $path . $menu['menu_name']; - $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['title'], + 'link_path' => $path . $menu['menu_name'], + '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['menu_name']), array('fetch' => PDO::FETCH_ASSOC)); - foreach ($result as $m) { - $link = menu_link_load($m['mlid']); - $link['link_title'] = $menu['title']; - menu_link_save($link); + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $path . $menu['menu_name'])); + foreach ($menu_links as $menu_link) { + $menu_link->link_title = $menu['title']; + $menu_link->save(); } } drupal_set_message(t('Your configuration has been saved.')); @@ -659,53 +472,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 array $menu + * An array representing a custom menu. + * + * @return + * Returns the menu link submission form. */ -function menu_item_delete_page($item) { +function menu_link_add(array $menu) { + $menu_link = entity_create('menu_link', array( + 'mlid' => 0, + 'plid' => 0, + 'menu_name' => $menu['menu_name'], + )); + 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_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 cee4ba5..21bc7c0 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -14,6 +14,8 @@ use Drupal\node\Node; use Symfony\Component\HttpFoundation\JsonResponse; +use Drupal\menu_link\MenuLink; +use Drupal\menu_link\MenuLinkStorageController; /** * Maximum length of menu name as entered by the user. Database length is 32 @@ -113,9 +115,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', @@ -138,21 +140,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', @@ -183,23 +184,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', + )); + $result = db_query("SELECT * FROM {menu_custom}", array(), array('fetch' => PDO::FETCH_ASSOC)); foreach ($result 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['title']; - $link['link_path'] = 'admin/structure/menu/manage/' . $menu['menu_name']; - $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['title']; + $link->link_path = 'admin/structure/menu/manage/' . $menu['menu_name']; + + $query = menu_link_query() + ->propertyCondition('link_path', $link->link_path) + ->propertyCondition('plid', $link->plid); + $result = $query->execute(); + + if (empty($result['menu_link'])) { + $link->save(); } } menu_cache_clear_all(); @@ -337,24 +344,26 @@ function menu_delete($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\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 * 'menu_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 @@ -363,14 +372,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; } @@ -382,7 +389,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); } /** @@ -404,13 +411,14 @@ function menu_parent_options_js() { /** * Helper function to get the items of the given menu. */ -function _menu_get_options($menus, $available_menus, $item) { - // If the item has children, there is an added limit to the depth of valid parents. - if (isset($item['parent_depth_limit'])) { - $limit = $item['parent_depth_limit']; +function _menu_get_options($menus, $available_menus, MenuLink $menu_link) { + // If the menu link has children, there is an added limit to the depth of + // valid parents. + if (isset($menu_link->parent_depth_limit)) { + $limit = $menu_link->parent_depth_limit; } else { - $limit = _menu_parent_depth_limit($item); + $limit = _menu_parent_depth_limit($menu_link); } $options = array(); @@ -418,7 +426,7 @@ function _menu_get_options($menus, $available_menus, $item) { if (isset($available_menus[$menu_name])) { $tree = menu_tree_all_data($menu_name, NULL); $options[$menu_name . ':0'] = '<' . $title . '>'; - _menu_parents_recurse($tree, $menu_name, '--', $options, $item['mlid'], $limit); + _menu_parents_recurse($tree, $menu_name, '--', $options, $menu_link->id(), $limit); } } return $options; @@ -429,16 +437,16 @@ function _menu_get_options($menus, $available_menus, $item) { */ function _menu_parents_recurse($tree, $menu_name, $indent, &$options, $exclude, $depth_limit) { foreach ($tree as $data) { - if ($data['link']['depth'] > $depth_limit) { + if ($data['link']->depth > $depth_limit) { // Don't iterate through any links on this level. break; } - if ($data['link']['mlid'] != $exclude && $data['link']['hidden'] >= 0) { - $title = $indent . ' ' . truncate_utf8($data['link']['title'], 30, TRUE, FALSE); - if ($data['link']['hidden']) { + if ($data['link']->id() != $exclude && $data['link']->hidden >= 0) { + $title = $indent . ' ' . truncate_utf8($data['link']->label(), 30, TRUE, FALSE); + if ($data['link']->hidden) { $title .= ' (' . t('disabled') . ')'; } - $options[$menu_name . ':' . $data['link']['mlid']] = $title; + $options[$menu_name . ':' . $data['link']->id()] = $title; if ($data['below']) { _menu_parents_recurse($data['below'], $menu_name, $indent . '--', $options, $exclude, $depth_limit); } @@ -447,26 +455,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_info(). */ function menu_block_info() { @@ -527,27 +515,25 @@ function menu_node_update(Node $node) { * Helper for hook_node_insert() and hook_node_update(). */ function menu_node_save(Node $node) { - if (isset($node->menu)) { - $link = &$node->menu; - if (empty($link['enabled'])) { - if (!empty($link['mlid'])) { - menu_link_delete($link['mlid']); + if (isset($node->menu_link)) { + $menu_link = $node->menu_link; + if (empty($menu_link->enabled)) { + if (!$menu_link->isNew()) { + menu_link_delete($menu_link->id()); } } - elseif (trim($link['link_title'])) { - $link['link_title'] = trim($link['link_title']); - $link['link_path'] = "node/$node->nid"; - if (trim($link['description'])) { - $link['options']['attributes']['title'] = trim($link['description']); + elseif (trim($menu_link->link_title)) { + $menu_link->link_title = trim($menu_link->link_title); + $menu_link->link_path = "node/$node->nid"; + if (trim($menu_link->description)) { + $menu_link->options['attributes']['title'] = trim($menu_link->description); } else { // If the description field was left empty, remove the title attribute // from the menu link. - unset($link['options']['attributes']['title']); - } - if (!menu_link_save($link)) { - drupal_set_message(t('There was an error saving the menu link.'), 'error'); + unset($menu_link->options['attributes']['title']); } + $menu_link->save(); } } } @@ -557,9 +543,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 = menu_link_query() + ->propertyCondition('link_path', 'node/' . $node->nid) + ->propertyCondition('module', 'menu'); + $result = $query->execute(); + + if (!empty($result['menu_link'])) { + menu_link_delete_multiple(array_keys($result['menu_link'])); } } @@ -567,57 +557,63 @@ function menu_node_predelete(Node $node) { * Implements hook_node_prepare(). */ function menu_node_prepare(Node $node) { - if (empty($node->menu)) { - // Prepare the node for the edit form so that $node->menu always exists. + if (empty($node->menu_link)) { + // Prepare the node for the edit form so that $node->menu_link always exists. $menu_name = strtok(variable_get('menu_parent_' . $node->type, 'main-menu: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-menu' => 'main-menu')); 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 = menu_link_query() + ->propertyCondition('link_path', 'node/' . $node->nid) + ->propertyCondition('menu_name', $menu_name) + ->propertyCondition('module', 'menu') + ->propertyOrderBy('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result['menu_link'])) ? key($result['menu_link']) : 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 = menu_link_query() + ->propertyCondition('link_path', 'node/' . $node->nid) + ->propertyCondition('menu_name', array_values($type_menus), 'IN') + ->propertyCondition('module', 'menu') + ->propertyOrderBy('mlid', 'ASC') + ->range(0, 1); + $result = $query->execute(); + + $mlid = (!empty($result['menu_link'])) ? key($result['menu_link']) : 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_link = $menu_link; } // Find the depth limit for the parent select. - if (!isset($node->menu['parent_depth_limit'])) { - $node->menu['parent_depth_limit'] = _menu_parent_depth_limit($node->menu); + if (!isset($node->menu_link->parent_depth_limit)) { + $node->menu_link->parent_depth_limit = _menu_parent_depth_limit($node->menu_link); } } /** * 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); +function _menu_parent_depth_limit(MenuLink $menu_link) { + return MENU_MAX_DEPTH - 1 - (($menu_link->id() && $menu_link->has_children) ? entity_get_controller('menu_link')->findChildrenRelativeDepth($menu_link) : 0); } /** @@ -631,12 +627,9 @@ function menu_form_node_form_alter(&$form, $form_state) { // Generate a list of possible parents (not including this link or descendants). // @todo This must be handled in a #process handler. $node = $form_state['controller']->getEntity($form_state); - $link = $node->menu; + $menu_link = $node->menu_link; $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(), $menu_link, $type); // If no possible parent menu items were found, there is nothing to display. if (empty($options)) { return; @@ -647,7 +640,7 @@ function menu_form_node_form_alter(&$form, $form_state) { '#title' => t('Menu settings'), '#access' => user_access('administer menu'), '#collapsible' => TRUE, - '#collapsed' => !$link['link_title'], + '#collapsed' => !$menu_link->link_title, '#group' => 'additional_settings', '#attached' => array( 'library' => array(array('menu', 'drupal.menu')), @@ -659,7 +652,7 @@ function menu_form_node_form_alter(&$form, $form_state) { $form['menu']['enabled'] = array( '#type' => 'checkbox', '#title' => t('Provide a menu link'), - '#default_value' => (int) (bool) $link['mlid'], + '#default_value' => (int) (bool) $menu_link->id(), ); $form['menu']['link'] = array( '#type' => 'container', @@ -673,24 +666,24 @@ function menu_form_node_form_alter(&$form, $form_state) { // Populate the element with the link data. foreach (array('mlid', 'module', 'hidden', 'has_children', 'customized', 'options', 'expanded', 'hidden', 'parent_depth_limit') as $key) { - $form['menu']['link'][$key] = array('#type' => 'value', '#value' => $link[$key]); + $form['menu']['link'][$key] = array('#type' => 'value', '#value' => $menu_link->{$key}); } $form['menu']['link']['link_title'] = array( '#type' => 'textfield', '#title' => t('Menu link title'), - '#default_value' => $link['link_title'], + '#default_value' => $menu_link->link_title, ); $form['menu']['link']['description'] = array( '#type' => 'textarea', '#title' => t('Description'), - '#default_value' => isset($link['options']['attributes']['title']) ? $link['options']['attributes']['title'] : '', + '#default_value' => isset($menu_link->options['attributes']['title']) ? $menu_link->options['attributes']['title'] : '', '#rows' => 1, '#description' => t('Shown when hovering over the menu link.'), ); - $default = ($link['mlid'] ? $link['menu_name'] . ':' . $link['plid'] : variable_get('menu_parent_' . $type, 'main-menu:0')); + $default = ($menu_link->id() ? $menu_link->menu_name . ':' . $menu_link->plid : variable_get('menu_parent_' . $type, 'main-menu:0')); // If the current parent menu item is not present in options, use the first // available option as default value. // @todo User should not be allowed to access menu link settings in such a @@ -708,13 +701,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($menu_link->menu_name); if ($delta < 50) { // Old hardcoded value $delta = 50; @@ -723,7 +710,7 @@ function menu_form_node_form_alter(&$form, $form_state) { '#type' => 'weight', '#title' => t('Weight'), '#delta' => $delta, - '#default_value' => $link['weight'], + '#default_value' => $menu_link->weight, '#description' => t('Menu links with smaller weights are displayed before links with larger weights.'), ); } @@ -733,11 +720,12 @@ function menu_form_node_form_alter(&$form, $form_state) { * * @see menu_form_node_form_alter() */ -function menu_node_submit(Node $node, $form, $form_state) { +function menu_node_submit(Node &$node, $form, $form_state) { + $node->menu_link = 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'])) { - list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + list($node->menu_link->menu_name, $node->menu_link->plid) = explode(':', $form_state['values']['menu']['parent']); } } @@ -770,7 +758,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/MenuLink.php b/core/modules/menu_link/lib/Drupal/menu_link/MenuLink.php new file mode 100644 index 0000000..ab00f13 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLink.php @@ -0,0 +1,360 @@ +mlid; + } + + /** + * Overrides Drupal\Entity\Entity::label(). + */ + public function label($langcode = NULL) { + $label = NULL; + + // Pass-through already translated labels. + // @see menu_set_active_trail() + if (isset($this->title)) { + return $this->title; + } + + // Don't do anything special for external and customized links. + if ($this->external || $this->module != 'system' || ($this->module == 'system' && $this->customized)) { + $label = $this->get('link_title', $langcode); + } + // For performance, don't localize a link the user can't access. + elseif ($this->access()) { + // Quick hack until we move the localization code to the MenuLink class. + // @todo Remove the hack, obviously :) + if (!isset($this->router_item['localized'])) { + $this->router_item['options'] = $this->options; + _menu_item_localize($this->router_item, $this->map); + $this->localized_options = $this->router_item['localized_options']; + $this->description = $this->router_item['description']; + $this->router_item['localized'] = TRUE; + } + + $label = $this->router_item['title']; + } + + return $label; + } + + /** + * Overrides Drupal\entity\Entity::createDuplicate(). + */ + public function createDuplicate() { + $duplicate = parent::createDuplicate(); + $duplicate->plid = NULL; + return $duplicate; + } + + /** + * Overrides Drupal\entity\Entity::access(). + */ + public function access(User $account = NULL) { + if ($this->external) { + $this->access = TRUE; + } + else { + // Skip links containing untranslated arguments. + if (strpos($this->href, '%') !== FALSE) { + $this->access = FALSE; + return FALSE; + } + + // menu_tree_check_access() may set this ahead of time for links to nodes. + if (!isset($this->access)) { + if (!empty($this->router_item['load_functions']) && !_menu_load_objects($this->router_item, $this->map)) { + // An error occurred loading an object. + $this->access = FALSE; + return FALSE; + } + _menu_check_access($this->router_item, $this->map); + $this->access = $this->router_item['access']; + } + } + + return $this->access; + } + + /** + * @todo Document. + */ + public function translateCurrentPathMap() { + // Try to derive the path argument map from the current router item, + // if this link's path is within the router item's path. This means + // that if we are on the current path 'foo/%/bar/%/baz', then + // menu_get_item() will have translated the menu router item for the + // current path, and we can take over the argument map for a link like + // 'foo/%/bar'. This inheritance is only valid for breadcrumb links. + // @see _menu_tree_check_access() + // @see menu_get_active_breadcrumb() + if (strpos($this->link_path, '%') !== FALSE && ($current_router_item = menu_get_item())) { + // Only translate paths within the current path. + if (strpos($current_router_item['path'], $this->link_path) === 0) { + $count = count($this->map); + $this->map = array_slice($current_router_item['original_map'], 0, $count); + $this->router_item['original_map'] = $this->map; + if (isset($current_router_item['map'])) { + $this->router_item['map'] = array_slice($current_router_item['map'], 0, $count); + } + // Reset access to check it (for the first time). + $this->access = NULL; + + $this->href = implode('/', $this->map); + } + } + } + + + /** + * 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); + } +} 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..44d67cd --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkFormController.php @@ -0,0 +1,223 @@ +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 = 'navigation: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['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_get_normal_path($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..b2d18e9 --- /dev/null +++ b/core/modules/menu_link/lib/Drupal/menu_link/MenuLinkStorageController.php @@ -0,0 +1,596 @@ +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); + + // Move all router item info to a separate $router_item property. + foreach (self::$routerItemFields as $field) { + $menu_link->router_item[$field] = $menu_link->$field; + unset($menu_link->$field); + } + + // Use the weight property from the menu link. + $menu_link->router_item['weight'] = $menu_link->weight; + + // Complete the path of the menu link with elements from the current path, + // if it contains dynamic placeholders (%). + $menu_link->map = explode('/', $menu_link->link_path); + if (strpos($menu_link->link_path, '%') !== FALSE) { + // Invoke registered to_arg callbacks. + if (!empty($menu_link->router_item['to_arg_functions'])) { + _menu_link_map_translate($menu_link->map, $menu_link->router_item['to_arg_functions']); + } + } + $menu_link->href = implode('/', $menu_link->map); + } + + 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' => 'navigation'))->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(); + } + + public function deleteHelper($ids, $force = FALSE, $prevent_reparenting = FALSE) { + $entities = $ids ? $this->load($ids) : FALSE; + if (!$entities) { + // If no IDs or invalid IDs were passed, do nothing. + return; + } + + // Activate the reparenting prevention switch. + $this->preventReparenting = $prevent_reparenting; + + // Bail out links that can't be deleted. + if (!$force) { + foreach ($entities as $entity) { + if ($entity->module == 'system' || !$entity->updated) { + unset($ids[$entity->{$this->idKey}]); + } + } + } + + parent::delete($ids); + } + + /** + * 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['entity 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['entity 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 = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', $this->entityType) + ->propertyCondition('menu_name', $entity->menu_name) + ->propertyCondition('hidden', 0) + ->propertyCondition('plid', $entity->plid) + ->count(); + + if ($exclude) { + $query->entityCondition('entity_id', $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') { + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', $this->entityType) + ->entityCondition('entity_id', $entity->id(), '<>') + ->propertyCondition('module', 'system') + // We always respect the link's 'menu_name'; inheritance for router + // items is ensured in _menu_router_build(). + ->propertyCondition('menu_name', $entity->menu_name); + + // Find the parent - it must be unique. + $parent_path = $entity->link_path; + do { + $parent = FALSE; + $parent_path = substr($parent_path, 0, strrpos($parent_path, '/')); + $new_query = clone $query; + $new_query->propertyCondition('link_path', $parent_path); + + $count_query = clone $new_query; + // Only valid if we get a unique result. + if ($count_query->count()->execute() == 1) { + $result = $new_query->execute(); + $parent = $this->load(array(key($result[$this->entityType]))); + $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) { + $entity_query = new EntityFieldQuery(); + $entity_query + ->entityCondition('entity_type', $this->entityType) + ->propertyCondition('menu_name', $menu_name) + ->count(); + return $entity_query->execute(); + } +} 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..5fe0fa5 --- /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\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\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\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\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..fd5eabe --- /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 'navigation') 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..2145027 --- /dev/null +++ b/core/modules/menu_link/menu_link.module @@ -0,0 +1,216 @@ + array( + 'label' => t('Menu link'), + 'entity class' => 'Drupal\menu_link\MenuLink', + 'controller class' => 'Drupal\menu_link\MenuLinkStorageController', + 'form controller class' => array( + 'default' => 'Drupal\menu_link\MenuLinkFormController', + ), + 'base table' => 'menu_links', + // @todo Investigate why this cache breaks things like drupal_get_title(). + 'static cache' => FALSE, + 'uri callback' => 'menu_link_uri', + 'entity keys' => array( + 'id' => 'mlid', + 'label' => 'link_title', + 'uuid' => 'uuid', + ), + 'bundles' => array( + 'menu_link' => array( + 'label' => t('Menu link'), + ), + ), + ), + ); + + return $return; +} + +/** + * Entity URI callback. + * + * @param Drupal\menu_link\MenuLink $menu_link + * A menu link entity. + */ +function menu_link_uri(MenuLink $menu_link) { + return array( + 'path' => $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\MenuLink|false + * A menu link entity, with $item->access filled and link translated for + * rendering. + */ +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 + * An array of menu link entities indexed by mlid. + * + * @see menu_link_load() + * @see entity_load_multiple() + * @see Drupal\entity\EntityFieldQuery + */ +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 + * A 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) { + entity_get_controller('menu_link')->deleteHelper($mlids, $force, $prevent_reparenting); +} + +/** + * Saves a menu link. + * + * After calling this function, rebuild the menu cache using + * menu_cache_clear_all(). + * + * @param Drupal\menu_link\MenuLink $menu_link + * The menu link entity to be saved. + */ +function menu_link_save(MenuLink $menu_link) { + $menu_link->save(); +} + +/** + * Returns a new EntityFieldQuery object with the entity type set to menu_link. + * + * @return Drupal\Core\Entity\EntityFieldQuery + * A new EntityFieldQuery object. + * + * @todo Remove when we have entity_query(). + */ +function menu_link_query() { + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'menu_link'); + + return $query; +} + +/** + * 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, or deletes an uncustomized menu link related to a module. + * + * @param string $module + * The name of the module. + * @param string $op + * Operation to perform: insert, update or delete. + * @param string $link_path + * The path this link points to. + * @param string $link_title + * Title of the link to insert or new title to update the link to. + * Unused for delete. + * + * @return bool|null + * The insert operation returns SAVED_NEW if the operation succeeded. Other + * operations return NULL. + */ +function menu_link_maintain($module, $op, $link_path, $link_title) { + 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(); + break; + 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->link_title = $link_title; + $menu_link->save(); + } + break; + case 'delete': + $menu_links = entity_load_multiple_by_properties('menu_link', array('link_path' => $link_path)); + menu_link_delete_multiple(array_keys($menu_links)); + break; + } +} diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php index 113cc3d..7034ef6 100644 --- a/core/modules/node/node.api.php +++ b/core/modules/node/node.api.php @@ -818,7 +818,7 @@ function hook_node_submit(Drupal\node\Node $node, $form, &$form_state) { // Decompose the selected menu parent option into 'menu_name' and 'plid', if // the form used the default parent selection widget. if (!empty($form_state['values']['menu']['parent'])) { - list($node->menu['menu_name'], $node->menu['plid']) = explode(':', $form_state['values']['menu']['parent']); + list($node->menu_link->menu_name, $node->menu_link->plid) = explode(':', $form_state['values']['menu']['parent']); } } diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php index 54eeab4..ef3c05a 100644 --- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php +++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php @@ -66,7 +66,7 @@ function testShortcutQuickLink() { theme_enable(array('seven')); variable_set('admin_theme', 'seven'); variable_set('node_admin_theme', TRUE); - $this->drupalGet($this->set->links[0]['link_path']); + $this->drupalGet($this->set->links[0]->link_path); $this->assertRaw(t('Remove from %title shortcuts', array('%title' => $this->set->title)), '"Add to shortcuts" link properly switched to "Remove from shortcuts".'); } @@ -79,7 +79,7 @@ function testShortcutLinkRename() { // Attempt to rename shortcut link. $new_link_name = $this->randomName(10); - $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'], array('shortcut_link[link_title]' => $new_link_name, 'shortcut_link[link_path]' => $set->links[0]['link_path']), t('Save')); + $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]->id(), array('shortcut_link[link_title]' => $new_link_name, 'shortcut_link[link_path]' => $set->links[0]->link_path), t('Save')); $saved_set = shortcut_set_load($set->set_name); $titles = $this->getShortcutInformation($saved_set, 'link_title'); $this->assertTrue(in_array($new_link_name, $titles), 'Shortcut renamed: ' . $new_link_name); @@ -95,7 +95,7 @@ function testShortcutLinkChangePath() { // Tests changing a shortcut path. $new_link_path = 'admin/config'; - $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'], array('shortcut_link[link_title]' => $set->links[0]['link_title'], 'shortcut_link[link_path]' => $new_link_path), t('Save')); + $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]->id(), array('shortcut_link[link_title]' => $set->links[0]->link_title, 'shortcut_link[link_path]' => $new_link_path), t('Save')); $saved_set = shortcut_set_load($set->set_name); $paths = $this->getShortcutInformation($saved_set, 'link_path'); $this->assertTrue(in_array($new_link_path, $paths), 'Shortcut path changed: ' . $new_link_path); @@ -108,10 +108,10 @@ function testShortcutLinkChangePath() { function testShortcutLinkDelete() { $set = $this->set; - $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]['mlid'] . '/delete', array(), 'Delete'); + $this->drupalPost('admin/config/user-interface/shortcut/link/' . $set->links[0]->id() . '/delete', array(), 'Delete'); $saved_set = shortcut_set_load($set->set_name); $mlids = $this->getShortcutInformation($saved_set, 'mlid'); - $this->assertFalse(in_array($set->links[0]['mlid'], $mlids), 'Successfully deleted a shortcut.'); + $this->assertFalse(in_array($set->links[0]->id(), $mlids), 'Successfully deleted a shortcut.'); } /** diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php index 8c94435..ddd346d 100644 --- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php +++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php @@ -52,16 +52,16 @@ function setUp() { // Populate the default shortcut set. $shortcut_set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME); - $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($shortcut_set); } @@ -75,6 +75,8 @@ function setUp() { // Log in as admin and grab the default shortcut set. $this->drupalLogin($this->admin_user); $this->set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME); + // Reset the keys of the links array. + $this->set->links = array_values($this->set->links); shortcut_set_assign_user($this->set, $this->admin_user); } @@ -104,10 +106,10 @@ function generateShortcutSet($title = '', $default_links = TRUE, $set_name = '') * Creates a generic shortcut link. */ function generateShortcutLink($path, $title = '') { - $link = array( + $link = entity_create('menu_link', array( 'link_path' => $path, 'link_title' => !empty($title) ? $title : $this->randomName(10), - ); + )); return $link; } @@ -129,7 +131,7 @@ function generateShortcutLink($path, $title = '') { function getShortcutInformation($set, $key) { $info = array(); foreach ($set->links as $link) { - $info[] = $link[$key]; + $info[] = $link->{$key}; } return $info; } diff --git a/core/modules/shortcut/shortcut.admin.inc b/core/modules/shortcut/shortcut.admin.inc index e805acf..1b63fea 100644 --- a/core/modules/shortcut/shortcut.admin.inc +++ b/core/modules/shortcut/shortcut.admin.inc @@ -120,7 +120,7 @@ function shortcut_set_switch_submit($form, &$form_state) { $default_set = shortcut_default_set($account); $set = (object) array( 'title' => $form_state['values']['new'], - 'links' => menu_links_clone($default_set->links), + 'links' => menu_link_clone($default_set->links), ); shortcut_set_save($set); $replacements = array( @@ -241,7 +241,7 @@ function shortcut_set_add_form_submit($form, &$form_state) { $default_set = shortcut_default_set(); $set = (object) array( 'title' => $form_state['values']['new'], - 'links' => menu_links_clone($default_set->links), + 'links' => menu_link_clone($default_set->links), ); shortcut_set_save($set); drupal_set_message(t('The %set_name shortcut set has been created. You can edit it from this page.', array('%set_name' => $set->title))); @@ -273,13 +273,13 @@ function shortcut_set_customize($form, &$form_state, $shortcut_set) { ); foreach ($shortcut_set->links as $link) { - $mlid = $link['mlid']; - $form['shortcuts']['links'][$mlid]['name']['#markup'] = l($link['link_title'], $link['link_path']); + $mlid = $link->id(); + $form['shortcuts']['links'][$mlid]['name']['#markup'] = l($link->link_title, $link->link_path); $form['shortcuts']['links'][$mlid]['weight'] = array( '#type' => 'weight', '#title' => t('Weight'), '#delta' => 50, - '#default_value' => $link['weight'], + '#default_value' => $link->weight, '#attributes' => array('class' => array('shortcut-weight')), ); @@ -316,7 +316,7 @@ function shortcut_set_customize_submit($form, &$form_state) { foreach ($form_state['values']['shortcuts']['links'] as $mlid => $data) { $link = menu_link_load($mlid); $link['weight'] = $data['weight']; - menu_link_save($link); + $link->save(); } drupal_set_message(t('The shortcut set has been updated.')); } @@ -403,7 +403,7 @@ function shortcut_link_add($form, &$form_state, $shortcut_set) { * @see shortcut_link_edit_submit() */ function shortcut_link_edit($form, &$form_state, $shortcut_link) { - drupal_set_title(t('Editing @shortcut', array('@shortcut' => $shortcut_link['link_title']))); + drupal_set_title(t('Editing @shortcut', array('@shortcut' => $shortcut_link->link_title))); $form['original_shortcut_link'] = array( '#type' => 'value', '#value' => $shortcut_link, @@ -424,13 +424,13 @@ 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_get_path_alias($shortcut_link['link_path']); + $shortcut_link->link_path = ($shortcut_link->link_path == '') ? '' : drupal_get_path_alias($shortcut_link->link_path); } $form['shortcut_link']['#tree'] = TRUE; @@ -439,7 +439,7 @@ function _shortcut_link_form_elements($shortcut_link = NULL) { '#title' => t('Name'), '#size' => 40, '#maxlength' => 255, - '#default_value' => $shortcut_link['link_title'], + '#default_value' => $shortcut_link->link_title, '#required' => TRUE, ); @@ -449,7 +449,7 @@ function _shortcut_link_form_elements($shortcut_link = NULL) { '#size' => 40, '#maxlength' => 255, '#field_prefix' => url(NULL, array('absolute' => TRUE)), - '#default_value' => $shortcut_link['link_path'], + '#default_value' => $shortcut_link->link_path, ); $form['#validate'][] = 'shortcut_link_edit_validate'; @@ -483,11 +483,14 @@ 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->set($key, $value); + } - menu_link_save($shortcut_link); - $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link['menu_name']; - drupal_set_message(t('The shortcut %link has been updated.', array('%link' => $shortcut_link['link_title']))); + $shortcut_link->save(); + $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link->menu_name; + drupal_set_message(t('The shortcut %link has been updated.', array('%link' => $shortcut_link->link_title))); } /** @@ -496,12 +499,12 @@ 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['menu_name'] = $shortcut_set->set_name; + $shortcut_link = entity_create('menu_link', $form_state['values']['shortcut_link']); + $shortcut_link->menu_name = $shortcut_set->set_name; shortcut_admin_add_link($shortcut_link, $shortcut_set); shortcut_set_save($shortcut_set); - $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link['menu_name']; - drupal_set_message(t('Added a shortcut for %title.', array('%title' => $shortcut_link['link_title']))); + $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link->menu_name; + drupal_set_message(t('Added a shortcut for %title.', array('%title' => $shortcut_link->link_title))); } /** @@ -517,9 +520,9 @@ function shortcut_link_add_submit($form, &$form_state) { */ function shortcut_admin_add_link($shortcut_link, &$shortcut_set) { // Normalize the path in case it is an alias. - $shortcut_link['link_path'] = drupal_get_normal_path($shortcut_link['link_path']); - if (empty($shortcut_link['link_path'])) { - $shortcut_link['link_path'] = ''; + $shortcut_link->link_path = drupal_get_normal_path($shortcut_link->link_path); + if (empty($shortcut_link->link_path)) { + $shortcut_link->link_path = ''; } // Add the link to the end of the list. @@ -677,8 +680,8 @@ function shortcut_link_delete($form, &$form_state, $shortcut_link) { return confirm_form( $form, - t('Are you sure you want to delete the shortcut %title?', array('%title' => $shortcut_link['link_title'])), - 'admin/config/user-interface/shortcut/' . $shortcut_link['menu_name'], + t('Are you sure you want to delete the shortcut %title?', array('%title' => $shortcut_link->link_title)), + 'admin/config/user-interface/shortcut/' . $shortcut_link->menu_name, t('This action cannot be undone.'), t('Delete'), t('Cancel') @@ -690,9 +693,9 @@ function shortcut_link_delete($form, &$form_state, $shortcut_link) { */ function shortcut_link_delete_submit($form, &$form_state) { $shortcut_link = $form_state['values']['shortcut_link']; - menu_link_delete($shortcut_link['mlid']); - $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link['menu_name']; - drupal_set_message(t('The shortcut %title has been deleted.', array('%title' => $shortcut_link['link_title']))); + menu_link_delete($shortcut_link->id()); + $form_state['redirect'] = 'admin/config/user-interface/shortcut/' . $shortcut_link->menu_name; + drupal_set_message(t('The shortcut %title has been deleted.', array('%title' => $shortcut_link->link_title))); } /** @@ -713,10 +716,10 @@ function shortcut_link_add_inline($shortcut_set) { ); shortcut_admin_add_link($link, $shortcut_set); if (shortcut_set_save($shortcut_set)) { - drupal_set_message(t('Added a shortcut for %title.', array('%title' => $link['link_title']))); + drupal_set_message(t('Added a shortcut for %title.', array('%title' => $link->link_title))); } else { - drupal_set_message(t('Unable to add a shortcut for %title.', array('%title' => $link['link_title']))); + drupal_set_message(t('Unable to add a shortcut for %title.', array('%title' => $link->link_title))); } drupal_goto(); } 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/shortcut/shortcut.install b/core/modules/shortcut/shortcut.install index c4023d7..c2b7158 100644 --- a/core/modules/shortcut/shortcut.install +++ b/core/modules/shortcut/shortcut.install @@ -40,7 +40,7 @@ function shortcut_schema() { 'length' => 32, 'not null' => TRUE, 'default' => '', - 'description' => "Primary Key: The {menu_links}.menu_name under which the set's links are stored.", + 'description' => "Primary Key: The menu link's menu_name under which the set's links are stored.", ), 'title' => array( 'type' => 'varchar', diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module index 9bbb12a..6452e07 100644 --- a/core/modules/shortcut/shortcut.module +++ b/core/modules/shortcut/shortcut.module @@ -293,7 +293,7 @@ function shortcut_set_switch_access($account = NULL) { function shortcut_link_access($menu_link) { // The link must belong to a shortcut set that the current user has access // to edit. - if ($shortcut_set = shortcut_set_load($menu_link['menu_name'])) { + if ($shortcut_set = shortcut_set_load($menu_link->menu_name)) { return shortcut_set_edit_access($shortcut_set); } return FALSE; @@ -340,13 +340,13 @@ function shortcut_set_load($set_name) { * saved): * - 'link_path': The Drupal path or external path that the link points to. * - 'link_title': The title of the link. - * Any other keys accepted by menu_link_save() may also be provided. + * Any other fields of a the menu link entity may also be provided. * * @return * A constant which is either SAVED_NEW or SAVED_UPDATED depending on whether * a new set was created or an existing one was updated. * - * @see menu_link_save() + * @see Drupal\menu_link\MenuLinkStorageController::save() */ function shortcut_set_save(&$shortcut_set) { // First save the shortcut set itself. @@ -366,9 +366,9 @@ function shortcut_set_save(&$shortcut_set) { // Do not specifically associate these links with the shortcut module, // since other modules may make them editable via the menu system. // However, we do need to specify the correct menu name. - $link['menu_name'] = $shortcut_set->set_name; - $link['plid'] = 0; - menu_link_save($link); + $link->menu_name = $shortcut_set->set_name; + $link->plid = 0; + $link->save(); } // Make sure that we have a return value, since if the links were updated // but the shortcut set was not, the call to drupal_write_record() above @@ -428,7 +428,7 @@ function shortcut_set_delete($shortcut_set) { function shortcut_set_reset_link_weights(&$shortcut_set) { $weight = -50; foreach ($shortcut_set->links as &$link) { - $link['weight'] = $weight; + $link->weight = $weight; $weight++; } } @@ -563,7 +563,7 @@ function shortcut_set_get_unique_name() { * Returns the name of a shortcut set, based on a provided number. * * All shortcut sets have names like "shortcut-set-N" so that they can be - * matched with a properly-namespaced entry in the {menu_links} table. + * matched with properly-namespaced menu link entities. * * @param $number * A number representing the shortcut set whose name should be retrieved. @@ -675,8 +675,8 @@ function shortcut_preprocess_page(&$variables) { // Check if $link is already a shortcut and set $link_mode accordingly. foreach ($shortcut_set->links as $shortcut) { - if ($link == $shortcut['link_path']) { - $mlid = $shortcut['mlid']; + if ($link == $shortcut->link_path) { + $mlid = $shortcut->id(); break; } } 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 5c0ab5f..db13075 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/BreadcrumbTest.php @@ -202,26 +202,26 @@ function testBreadCrumbs() { $node2 = $this->drupalCreateNode(array( 'type' => $type, 'title' => $title, - 'menu' => array( + 'menu_link' => entity_create('menu_link', array( 'enabled' => 1, 'link_title' => 'Parent ' . $title, 'description' => '', 'menu_name' => $menu, 'plid' => 0, - ), + )), )); $nid2 = $node2->nid; $trail = $home; $tree = array( - "node/$nid2" => $node2->menu['link_title'], + "node/$nid2" => $node2->menu_link->label(), ); $this->assertBreadcrumb("node/$nid2", $trail, $node2->title, $tree); // The node itself should not be contained in the breadcrumb on the // default local task, since there is no difference between both pages. $this->assertBreadcrumb("node/$nid2/view", $trail, $node2->title, $tree); $trail += array( - "node/$nid2" => $node2->menu['link_title'], + "node/$nid2" => $node2->menu_link->label(), ); $this->assertBreadcrumb("node/$nid2/edit", $trail); @@ -230,13 +230,13 @@ function testBreadCrumbs() { $node3 = $this->drupalCreateNode(array( 'type' => $type, 'title' => $title, - 'menu' => array( + 'menu_link' => entity_create('menu_link', array( 'enabled' => 1, 'link_title' => 'Child ' . $title, 'description' => '', 'menu_name' => $menu, - 'plid' => $node2->menu['mlid'], - ), + 'plid' => $node2->menu_link->id(), + )), )); $nid3 = $node3->nid; @@ -245,10 +245,10 @@ function testBreadCrumbs() { // default local task, since there is no difference between both pages. $this->assertBreadcrumb("node/$nid3/view", $trail, $node3->title, $tree, FALSE); $trail += array( - "node/$nid3" => $node3->menu['link_title'], + "node/$nid3" => $node3->menu_link->label(), ); $tree += array( - "node/$nid3" => $node3->menu['link_title'], + "node/$nid3" => $node3->menu_link->label(), ); $this->assertBreadcrumb("node/$nid3/edit", $trail); @@ -270,25 +270,27 @@ 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(); + $link = entity_load_multiple_by_properties('menu_link', array('link_title' => 'Root')); + debug($link); + $link = reset($link); $edit = array( - 'menu[parent]' => $link['menu_name'] . ':' . $link['mlid'], + 'menu[parent]' => $link->menu_name . ':' . $link->id(), ); $this->drupalPost("node/{$parent->nid}/edit", $edit, t('Save')); $expected = array( - "node" => $link['link_title'], + "node" => $link->label(), ); $trail = $home + $expected; $tree = $expected + array( - "node/{$parent->nid}" => $parent->menu['link_title'], + "node/{$parent->nid}" => $parent->menu_link->label(), ); $this->assertBreadcrumb(NULL, $trail, $parent->title, $tree); $trail += array( - "node/{$parent->nid}" => $parent->menu['link_title'], + "node/{$parent->nid}" => $parent->menu_link->label(), ); $tree += array( - "node/{$child->nid}" => $child->menu['link_title'], + "node/{$child->nid}" => $child->menu_link->label(), ); $this->assertBreadcrumb("node/{$child->nid}", $trail, $child->title, $tree); @@ -328,12 +330,10 @@ 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(); - $tags[$name]['link']['link_path'] = $edit['link_path']; - $parent_mlid = $tags[$name]['link']['mlid']; + $link = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $tags[$name]['link'] = reset($link); + $tags[$name]['link']->link_path = $edit['link_path']; + $parent_mlid = $tags[$name]['link']->id(); } // Verify expected breadcrumbs for menu links. @@ -344,9 +344,9 @@ function testBreadCrumbs() { $link = $data['link']; $tree += array( - $link['link_path'] => $link['link_title'], + $link->link_path => $link->label(), ); - $this->assertBreadcrumb($link['link_path'], $trail, $term->name, $tree); + $this->assertBreadcrumb($link->link_path, $trail, $term->name, $tree); $this->assertRaw(check_plain($parent->title), 'Tagged node found.'); // Additionally make sure that this link appears only once; i.e., the @@ -355,14 +355,14 @@ function testBreadCrumbs() { // other than the breadcrumb trail. $elements = $this->xpath('//div[@id=:menu]/descendant::a[@href=:href]', array( ':menu' => 'block-system-navigation', - ':href' => url($link['link_path']), + ':href' => url($link->link_path), )); - $this->assertTrue(count($elements) == 1, "Link to {$link['link_path']} appears only once."); + $this->assertTrue(count($elements) == 1, "Link to {$link->link_path} appears only once."); // Next iteration should expect this tag as parent link. // Note: Term name, not link name, due to taxonomy_term_page(). $trail += array( - $link['link_path'] => $term->name, + $link->link_path => $term->name, ); } @@ -429,64 +429,60 @@ 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(); + $link_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $link_user = reset($link_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(); + $link_admin_user = entity_load_multiple_by_properties('menu_link', array('link_title' => $edit['link_title'], 'link_path' => $edit['link_path'])); + $link_admin_user = reset($link_admin_user); // Verify expected breadcrumbs for the two separate links. $this->drupalLogout(); $trail = $home; $tree = array( - $link_user['link_path'] => $link_user['link_title'], + $link_user->link_path => $link_user->label(), ); - $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $this->assertBreadcrumb('user', $trail, $link_user->label(), $tree); $tree = array( - $link_admin_user['link_path'] => $link_admin_user['link_title'], + $link_admin_user->link_path => $link_admin_user->label(), ); - $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user->label(), $tree); $this->drupalLogin($this->admin_user); $trail += array( - $link_admin_user['link_path'] => $link_admin_user['link_title'], + $link_admin_user->link_path => $link_admin_user->label(), ); - $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user->label(), $tree, FALSE); // Move 'user/%' below 'user' and verify again. $edit = array( - 'parent' => "$menu:{$link_user['mlid']}", + 'parent' => "$menu:{$link_user->id()}", ); - $this->drupalPost("admin/structure/menu/item/{$link_admin_user['mlid']}/edit", $edit, t('Save')); + $this->drupalPost("admin/structure/menu/item/{$link_admin_user->id()}/edit", $edit, t('Save')); $this->drupalLogout(); $trail = $home; $tree = array( - $link_user['link_path'] => $link_user['link_title'], + $link_user->link_path => $link_user->label(), ); - $this->assertBreadcrumb('user', $trail, $link_user['link_title'], $tree); + $this->assertBreadcrumb('user', $trail, $link_user->label(), $tree); $trail += array( - $link_user['link_path'] => $link_user['link_title'], + $link_user->link_path => $link_user->label(), ); $tree += array( - $link_admin_user['link_path'] => $link_admin_user['link_title'], + $link_admin_user->link_path => $link_admin_user->label(), ); - $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user['link_title'], $tree); + $this->assertBreadcrumb('user/' . $this->admin_user->uid, $trail, $link_admin_user->label(), $tree); $this->drupalLogin($this->admin_user); $trail += array( - $link_admin_user['link_path'] => $link_admin_user['link_title'], + $link_admin_user->link_path => $link_admin_user->label(), ); - $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user['link_title'], $tree, FALSE); + $this->assertBreadcrumb('user/' . $this->admin_user->uid . '/edit', $trail, $link_admin_user->label(), $tree, FALSE); // Create an only slightly privileged user being able to access site reports // but not administration pages. 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..590a728 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'], + 'plid' => $links['parent']->id(), ); - 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'], + 'plid' => $links['child-1']->id(), ); - 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'], + 'plid' => $links['child-1']->id(), ); - 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'], + 'plid' => $links['parent']->id(), ); - menu_link_save($links['child-2']); + $links['child-2'] = entity_create('menu_link', $links['child-2']); + $links['child-2']->save(); return $links; } @@ -77,12 +83,12 @@ function createLinkHierarchy($module = 'menu_test') { */ function assertMenuLinkParents($links, $expected_hierarchy) { foreach ($expected_hierarchy as $child => $parent) { - $mlid = $links[$child]['mlid']; - $plid = $parent ? $links[$parent]['mlid'] : 0; + $mlid = $links[$child]->id(); + $plid = $parent ? $links[$parent]->id() : 0; $menu_link = menu_link_load($mlid); - menu_link_save($menu_link); - $this->assertEqual($menu_link['plid'], $plid, format_string('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link['plid'], '%expected_plid' => $plid))); + $menu_link->save(); + $this->assertEqual($menu_link->plid, $plid, format_string('Menu link %mlid has parent of %plid, expected %expected_plid.', array('%mlid' => $mlid, '%plid' => $menu_link->plid, '%expected_plid' => $plid))); } } @@ -105,8 +111,8 @@ function testMenuLinkReparenting($module = 'menu_test') { // Start over, and move child-1 under child-2, and check that all the // childs of child-1 have been moved too. $links = $this->createLinkHierarchy($module); - $links['child-1']['plid'] = $links['child-2']['mlid']; - menu_link_save($links['child-1']); + $links['child-1']->plid = $links['child-2']->id(); + $links['child-1']->save(); $expected_hierarchy = array( 'parent' => FALSE, @@ -123,7 +129,7 @@ function testMenuLinkReparenting($module = 'menu_test') { // test in that case. if ($module != 'system') { $links = $this->createLinkHierarchy($module); - menu_link_delete($links['child-1']['mlid']); + menu_link_delete($links['child-1']->id()); $expected_hierarchy = array( 'parent' => FALSE, @@ -141,7 +147,7 @@ function testMenuLinkReparenting($module = 'menu_test') { $links = $this->createLinkHierarchy($module); // Don't do that at home. db_delete('menu_links') - ->condition('mlid', $links['child-1']['mlid']) + ->condition('mlid', $links['child-1']->id()) ->execute(); $expected_hierarchy = array( @@ -157,10 +163,11 @@ function testMenuLinkReparenting($module = 'menu_test') { $links = $this->createLinkHierarchy($module); // Don't do that at home. db_delete('menu_links') - ->condition('mlid', $links['parent']['mlid']) + ->condition('mlid', $links['parent']->id()) ->execute(); $expected_hierarchy = array( + 'child-1' => FALSE, 'child-1-1' => 'child-1', 'child-1-2' => 'child-1', 'child-2' => FALSE, @@ -181,8 +188,8 @@ function testMenuLinkRouterReparenting() { // Move child-1-2 has a child of child-2, making the link hierarchy // inconsistent with the path hierarchy. - $links['child-1-2']['plid'] = $links['child-2']['mlid']; - menu_link_save($links['child-1-2']); + $links['child-1-2']->plid = $links['child-2']->id(); + $links['child-1-2']->save(); // Check the new hierarchy. $expected_hierarchy = array( @@ -199,8 +206,9 @@ function testMenuLinkRouterReparenting() { // top-level. // Don't do that at home. db_delete('menu_links') - ->condition('mlid', $links['parent']['mlid']) + ->condition('mlid', $links['parent']->id()) ->execute(); + entity_get_controller('menu_link')->resetCache(array($links['parent']->id())); $expected_hierarchy = array( 'child-1' => FALSE, 'child-1-1' => 'child-1', @@ -214,8 +222,9 @@ function testMenuLinkRouterReparenting() { // path. // Don't do that at home. db_delete('menu_links') - ->condition('mlid', $links['child-2']['mlid']) + ->condition('mlid', $links['child-2']->id()) ->execute(); + entity_get_controller('menu_link')->resetCache(array($links['child-2']->id())); $expected_hierarchy = array( 'child-1' => FALSE, 'child-1-1' => 'child-1', diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php index 094961c..627485c 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/RouterTest.php @@ -304,30 +304,33 @@ function testMenuName() { $admin_user = $this->drupalCreateUser(array('administer site configuration')); $this->drupalLogin($admin_user); - $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; - $name = db_query($sql)->fetchField(); - $this->assertEqual($name, 'original', 'Menu name is "original".'); + $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test')); + $menu_link = reset($menu_links); + $this->assertEqual($menu_link->menu_name, 'original', 'Menu name is "original".'); // Change the menu_name parameter in menu_test.module, then force a menu // rebuild. menu_test_menu_name('changed'); menu_router_rebuild(); - $sql = "SELECT menu_name FROM {menu_links} WHERE router_path = 'menu_name_test'"; - $name = db_query($sql)->fetchField(); - $this->assertEqual($name, 'changed', 'Menu name was successfully changed after rebuild.'); + $menu_links = entity_load_multiple_by_properties('menu_link', array('router_path' => 'menu_name_test')); + $menu_link = reset($menu_links); + $this->assertEqual($menu_link->menu_name, 'changed', 'Menu name was successfully changed after rebuild.'); } /** * Tests for menu hierarchy. */ function testMenuHierarchy() { - $parent_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent'))->fetchAssoc(); - $child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child'))->fetchAssoc(); - $unattached_child_link = db_query('SELECT * FROM {menu_links} WHERE link_path = :link_path', array(':link_path' => 'menu-test/hierarchy/parent/child2/child'))->fetchAssoc(); - - $this->assertEqual($child_link['plid'], $parent_link['mlid'], 'The parent of a directly attached child is correct.'); - $this->assertEqual($unattached_child_link['plid'], $parent_link['mlid'], 'The parent of a non-directly attached child is correct.'); + $parent_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent')); + $parent_link = reset($parent_link); + $child_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child')); + $child_link = reset($child_link); + $unattached_child_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'menu-test/hierarchy/parent/child2/child')); + $unattached_child_link = reset($unattached_child_link); + + $this->assertEqual($child_link->plid, $parent_link->id(), 'The parent of a directly attached child is correct.'); + $this->assertEqual($unattached_child_link->plid, $parent_link->id(), 'The parent of a non-directly attached child is correct.'); } /** @@ -335,88 +338,96 @@ function testMenuHierarchy() { */ function testMenuHidden() { // Verify links for one dynamic argument. - $links = db_select('menu_links', 'ml') - ->fields('ml') - ->condition('ml.router_path', 'menu-test/hidden/menu%', 'LIKE') - ->orderBy('ml.router_path') - ->execute() - ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC); + $query = menu_link_query() + ->propertyCondition('router_path', 'menu-test/hidden/menu', 'STARTS_WITH') + ->propertyOrderBy('router_path'); + $result = $query->execute(); + $menu_links = menu_link_load_multiple(array_keys($result['menu_link'])); + + $links = array(); + foreach ($menu_links as $menu_link) { + $links[$menu_link->router_path] = $menu_link; + } $parent = $links['menu-test/hidden/menu']; - $depth = $parent['depth'] + 1; - $plid = $parent['mlid']; + $depth = $parent->depth + 1; + $plid = $parent->id(); $link = $links['menu-test/hidden/menu/list']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/add']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/settings']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $parent = $links['menu-test/hidden/menu/manage/%']; - $depth = $parent['depth'] + 1; - $plid = $parent['mlid']; + $depth = $parent->depth + 1; + $plid = $parent->id(); $link = $links['menu-test/hidden/menu/manage/%/list']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/add']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/edit']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/menu/manage/%/delete']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); // Verify links for two dynamic arguments. - $links = db_select('menu_links', 'ml') - ->fields('ml') - ->condition('ml.router_path', 'menu-test/hidden/block%', 'LIKE') - ->orderBy('ml.router_path') - ->execute() - ->fetchAllAssoc('router_path', PDO::FETCH_ASSOC); + $query = menu_link_query() + ->propertyCondition('router_path', 'menu-test/hidden/block', 'STARTS_WITH') + ->propertyOrderBy('router_path'); + $result = $query->execute(); + $menu_links = menu_link_load_multiple(array_keys($result['menu_link'])); + + $links = array(); + foreach ($menu_links as $menu_link) { + $links[$menu_link->router_path] = $menu_link; + } $parent = $links['menu-test/hidden/block']; - $depth = $parent['depth'] + 1; - $plid = $parent['mlid']; + $depth = $parent->depth + 1; + $plid = $parent->id(); $link = $links['menu-test/hidden/block/list']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/block/add']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/block/manage/%/%']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $parent = $links['menu-test/hidden/block/manage/%/%']; - $depth = $parent['depth'] + 1; - $plid = $parent['mlid']; + $depth = $parent->depth + 1; + $plid = $parent->id(); $link = $links['menu-test/hidden/block/manage/%/%/configure']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); $link = $links['menu-test/hidden/block/manage/%/%/delete']; - $this->assertEqual($link['depth'], $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link['router_path'], '@link_depth' => $link['depth'], '@depth' => $depth))); - $this->assertEqual($link['plid'], $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link['router_path'], '@link_plid' => $link['plid'], '@plid' => $plid))); + $this->assertEqual($link->depth, $depth, format_string('%path depth @link_depth is equal to @depth.', array('%path' => $link->router_path, '@link_depth' => $link->depth, '@depth' => $depth))); + $this->assertEqual($link->plid, $plid, format_string('%path plid @link_plid is equal to @plid.', array('%path' => $link->router_path, '@link_plid' => $link->plid, '@plid' => $plid))); } /** @@ -464,7 +475,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', @@ -476,8 +487,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/TrailTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php index eac8546..e1c518e 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/TrailTest.php @@ -184,11 +184,11 @@ function testCustom403And404Pages() { '@found' => count($initial_trail), ))); foreach (array_keys($expected_trail[$status_code]['initial']) as $index => $path) { - $this->assertEqual($initial_trail[$index]['href'], $path, format_string('Element number @number of the initial active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + $this->assertEqual($initial_trail[$index]->href, $path, format_string('Element number @number of the initial active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( '@number' => $index + 1, '@status_code' => $status_code, '@expected' => $path, - '@found' => $initial_trail[$index]['href'], + '@found' => $initial_trail[$index]->href, ))); } @@ -201,11 +201,11 @@ function testCustom403And404Pages() { '@found' => count($final_trail), ))); foreach (array_keys($expected_trail[$status_code]['final']) as $index => $path) { - $this->assertEqual($final_trail[$index]['href'], $path, format_string('Element number @number of the final active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( + $this->assertEqual($final_trail[$index]->href, $path, format_string('Element number @number of the final active trail for a @status_code page contains the correct path (expected: @expected, found: @found)', array( '@number' => $index + 1, '@status_code' => $status_code, '@expected' => $path, - '@found' => $final_trail[$index]['href'], + '@found' => $final_trail[$index]->href, ))); } 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..3f7f96a 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\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. @@ -60,6 +63,6 @@ function testMenuTreeData() { * TRUE if the assertion succeeded, FALSE otherwise. */ protected function assertSameLink($link1, $link2, $message = '') { - return $this->assert($link1['mlid'] == $link2['mlid'], $message ?: 'First link is identical to second link'); + return $this->assert($link1->id() == $link2->id(), $message ?: 'First link is identical to second link'); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/TreeOutputTest.php index e1b46ad..c78846c 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\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 c55289c..ab2a1ac 100644 --- a/core/modules/system/system.admin.inc +++ b/core/modules/system/system.admin.inc @@ -18,33 +18,40 @@ 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) { - _menu_link_translate($item); - if (!$item['access']) { - continue; - } - // The link description, either derived from 'description' in hook_menu() - // or customized via menu module is used as title attribute. - if (!empty($item['localized_options']['attributes']['title'])) { - $item['description'] = $item['localized_options']['attributes']['title']; - unset($item['localized_options']['attributes']['title']); - } - $block = $item; - $block['content'] = ''; - $block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($item))); - if (!empty($block['content'])) { - $block['show'] = TRUE; - } + if ($system_link = entity_load_multiple_by_properties('menu_link', array('link_path' => 'admin/config', 'module' => 'system'))) { + $system_link = reset($system_link); + $query = menu_link_query() + ->propertyCondition('link_path', 'admin/help', '<>') + ->propertyCondition('menu_name', $system_link->menu_name) + ->propertyCondition('plid', $system_link->id()) + ->propertyCondition('hidden', 0); + $result = $query->execute(); + + if (!empty($result['menu_link'])) { + $menu_links = menu_link_load_multiple(array_keys($result['menu_link'])); + foreach ($menu_links as $menu_link) { + if (!$menu_link->access()) { + continue; + } + // The link description, either derived from 'description' in hook_menu() + // or customized via menu module is used as title attribute. + // @todo Cleanup localized_options. +// if (!empty($menu_link->localized_options['attributes']['title'])) { +// $menu_link->description = $menu_link->localized_options['attributes']['title']; +// unset($menu_link->localized_options['attributes']['title']); +// } + $block = $menu_link->router_item; + $block['content'] = ''; + // @todo Convert this to a render array. + $block['content'] .= theme('admin_block_content', array('content' => system_admin_menu_block($menu_link->router_item))); + if (!empty($block['content'])) { + $block['show'] = TRUE; + } - // Prepare for sorting as in function _menu_tree_check_access(). - // The weight is offset so it is always positive, with a uniform 5-digits. - $blocks[(50000 + $item['weight']) . ' ' . $item['title'] . ' ' . $item['mlid']] = $block; + // Prepare for sorting as in function _menu_tree_check_access(). + // The weight is offset so it is always positive, with a uniform 5-digits. + $blocks[(50000 + $menu_link->weight) . ' ' . $menu_link->label() . ' ' . $menu_link->id()] = $block; + } } } if ($blocks) { @@ -94,7 +101,7 @@ function system_admin_index() { // Only display a section if there are any available tasks. if ($admin_tasks = system_get_module_admin_tasks($module, $info->info)) { // Sort links by title. - uasort($admin_tasks, 'drupal_sort_title'); + uasort($admin_tasks, 'drupal_sort_label'); // Move 'Configure permissions' links to the bottom of each section. $permission_key = "admin/people/permissions#module-$module"; if (isset($admin_tasks[$permission_key])) { @@ -2314,9 +2321,9 @@ function theme_admin_block_content($variables) { } $output .= '
    '; foreach ($content as $item) { - $output .= '
    ' . l($item['title'], $item['href'], $item['localized_options']) . '
    '; - if (!$compact && isset($item['description'])) { - $output .= '
    ' . filter_xss_admin($item['description']) . '
    '; + $output .= '
    ' . l($item->label(), $item->href, $item->localized_options) . '
    '; + if (!$compact && isset($item->description)) { + $output .= '
    ' . filter_xss_admin($item->description) . '
    '; } } $output .= '
    '; diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 0f6d668..630abf5 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -947,127 +947,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 9026310..3194220 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1029,192 +1029,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 'navigation') 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( @@ -2186,6 +2000,51 @@ function system_update_8030() { } /** + * Enable the Menu link module. + * + * Creates the langcode and UUID columns for menu links and adds the + * 'serialize' property to the options column. + */ +function system_update_8031() { + // Enable the module without re-installing the schema. + update_module_enable(array('menu_link')); + + // Add the langcode column. + $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); + + // Add the 'serialize' property to the options column. + $column = 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, + ); + db_change_field('menu_links', 'options', 'options', $column); +} + +/** * @} 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 ea3dadc..085a797 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2598,7 +2598,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']])) { @@ -2606,29 +2609,20 @@ 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_link_translate($link); - if ($link['access']) { + $menu_links = entity_load_multiple_by_properties('menu_link', array('plid' => $item['mlid'], 'menu_name' => $item['menu_name'], 'hidden' => 0)); + foreach ($menu_links as $menu_link) { + if ($menu_link->access()) { // The link description, either derived from 'description' in // hook_menu() or customized via menu module is used as title attribute. - if (!empty($link['localized_options']['attributes']['title'])) { - $link['description'] = $link['localized_options']['attributes']['title']; - unset($link['localized_options']['attributes']['title']); - } + // @todo Cleanup localized_options. +// if (!empty($menu_link->localized_options['attributes']['title'])) { +// $menu_link->description = $menu_link->localized_options['attributes']['title']; +// unset($menu_link->localized_options['attributes']['title']); +// } // Prepare for sorting as in function _menu_tree_check_access(). // The weight is offset so it is always positive, with a uniform 5-digits. - $key = (50000 + $link['weight']) . ' ' . drupal_strtolower($link['title']) . ' ' . $link['mlid']; - $content[$key] = $link; + $key = (50000 + $menu_link->weight) . ' ' . drupal_strtolower($menu_link->label()) . ' ' . $menu_link->id(); + $content[$key] = $menu_link; } } ksort($content); @@ -3389,21 +3383,10 @@ 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_link_translate($link); - if ($link['access']) { - $links[$link['router_path']] = $link; + $menu_links = entity_get_controller('menu_link')->loadModuleAdminTasks(); + foreach ($menu_links as $menu_link) { + if ($menu_link->access()) { + $links[$menu_link->router_path] = $menu_link; } } } @@ -3416,27 +3399,28 @@ function system_get_module_admin_tasks($module, $info) { $task = $links[$path]; // The link description, either derived from 'description' in // hook_menu() or customized via menu module is used as title attribute. - if (!empty($task['localized_options']['attributes']['title'])) { - $task['description'] = $task['localized_options']['attributes']['title']; - unset($task['localized_options']['attributes']['title']); - } + // @todo Cleanup localized_options. +// if (!empty($task['localized_options']['attributes']['title'])) { +// $task['description'] = $task['localized_options']['attributes']['title']; +// unset($task['localized_options']['attributes']['title']); +// } // Check the admin tasks for duplicate names. If one is found, // append the parent menu item's title to differentiate. - $duplicate_path = array_search($task['title'], $titles); + $duplicate_path = array_search($task->label(), $titles); if ($duplicate_path !== FALSE) { - if ($parent = menu_link_load($task['plid'])) { + if ($parent = menu_link_load($task->plid)) { // Append the parent item's title to this task's title. - $task['title'] = t('@original_title (@parent_title)', array('@original_title' => $task['title'], '@parent_title' => $parent['title'])); + $task->title = t('@original_title (@parent_title)', array('@original_title' => $task->label(), '@parent_title' => $parent->label())); } - if ($parent = menu_link_load($admin_tasks[$duplicate_path]['plid'])) { + if ($parent = menu_link_load($admin_tasks[$duplicate_path]->plid)) { // Append the parent item's title to the duplicated task's title. // We use $links[$duplicate_path] in case there are triplicates. - $admin_tasks[$duplicate_path]['title'] = t('@original_title (@parent_title)', array('@original_title' => $links[$duplicate_path]['title'], '@parent_title' => $parent['title'])); + $admin_tasks[$duplicate_path]->title = t('@original_title (@parent_title)', array('@original_title' => $links[$duplicate_path]->label(), '@parent_title' => $parent->label())); } } else { - $titles[$path] = $task['title']; + $titles[$path] = $task->label(); } $admin_tasks[$path] = $task; @@ -3451,7 +3435,9 @@ function system_get_module_admin_tasks($module, $info) { $item['link_path'] = $item['href']; $item['title'] = t('Configure @module permissions', array('@module' => $info['name'])); unset($item['description']); + // @todo Cleanup localized_options. $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 758dc9c..bc7a75f 100644 --- a/core/modules/toolbar/toolbar.info +++ b/core/modules/toolbar/toolbar.info @@ -3,3 +3,4 @@ description = Provides a toolbar that shows the top-level administration menu it core = 8.x package = Core version = VERSION +dependencies[] = menu_link diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module index 3717d7b..3b5a561 100644 --- a/core/modules/toolbar/toolbar.module +++ b/core/modules/toolbar/toolbar.module @@ -278,12 +278,17 @@ function toolbar_view() { */ 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' => 'management', ':module' => 'system', ':path' => 'admin'))->fetchAssoc(); - if ($admin_link) { + $query = menu_link_query() + ->propertyCondition('menu_name', 'management') + ->propertyCondition('module', 'system') + ->propertyCondition('link_path', 'admin'); + $result = $query->execute(); + if (!empty($result['menu_link'])) { + $admin_link = menu_link_load(key($result['menu_link'])); $tree = menu_build_tree('management', array( - 'expanded' => array($admin_link['mlid']), - 'min_depth' => $admin_link['depth'] + 1, - 'max_depth' => $admin_link['depth'] + 1, + 'expanded' => array($admin_link->id()), + 'min_depth' => $admin_link->depth + 1, + 'max_depth' => $admin_link->depth + 1, )); } @@ -302,28 +307,28 @@ function toolbar_get_menu_tree() { function toolbar_menu_navigation_links($tree) { $links = array(); foreach ($tree as $item) { - if (!$item['link']['hidden'] && $item['link']['access']) { + if (!$item['link']->hidden && $item['link']->access()) { // Make sure we have a path specific ID in place, so we can attach icons // and behaviors to the items. - $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['link']['href']); + $id = str_replace(array('/', '<', '>'), array('-', '', ''), $item['link']->href); - $link = $item['link']['localized_options']; - $link['href'] = $item['link']['href']; + $link = $item['link']->options; + $link['href'] = $item['link']->href; // Add icon placeholder. - $link['title'] = '' . check_plain($item['link']['title']); + $link['title'] = '' . check_plain($item['link']->label()); // Add admin link ID. $link['attributes'] = array('id' => 'toolbar-link-' . $id); - if (!empty($item['link']['description'])) { - $link['title'] .= ' (' . $item['link']['description'] . ')'; - $link['attributes']['title'] = $item['link']['description']; + if (!empty($item['link']->description)) { + $link['title'] .= ' (' . $item['link']->description . ')'; + $link['attributes']['title'] = $item['link']->description; } $link['html'] = TRUE; $class = ' path-' . $id; - if (toolbar_in_active_trail($item['link']['href'])) { + if (toolbar_in_active_trail($item['link']->href)) { $class .= ' active-trail'; } - $links['menu-' . $item['link']['mlid'] . $class] = $link; + $links['menu-' . $item['link']->id() . $class] = $link; } } return $links; @@ -349,8 +354,8 @@ function toolbar_in_active_trail($path) { $active_paths = array(); $trail = menu_get_active_trail(); foreach ($trail as $item) { - if (!empty($item['href'])) { - $active_paths[] = $item['href']; + if (!empty($item->href)) { + $active_paths[] = $item->href; } } } diff --git a/core/modules/user/user.module b/core/modules/user/user.module index c064427..acdee03 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1400,21 +1400,21 @@ 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($menu_link) { // The path 'user' must be accessible for anonymous users, but only visible // for authenticated users. Authenticated users should see "My account", but // anonymous users should not see it at all. Therefore, invoke - // user_translated_menu_link_alter() to conditionally hide the link. - if ($link['link_path'] == 'user' && $link['module'] == 'system') { - $link['options']['alter'] = TRUE; + // user_menu_link_load() to conditionally hide the link. + if ($menu_link->link_path == 'user' && $menu_link->module == 'system') { + $menu_link->options['alter'] = TRUE; } // Force the Logout link to appear on the top-level of 'user-menu' menu by // default (i.e., unless it has been customized). - if ($link['link_path'] == 'user/logout' && $link['module'] == 'system' && empty($link['customized'])) { - $link['plid'] = 0; + if ($menu_link->link_path == 'user/logout' && $menu_link->module == 'system' && empty($menu_link->customized)) { + $menu_link->plid = 0; } } @@ -1424,18 +1424,20 @@ function user_menu_link_alter(&$link) { function user_menu_breadcrumb_alter(&$active_trail, $item) { // Remove "My account" from the breadcrumb when $item is descendant-or-self // of system path user/%. - if (isset($active_trail[1]['module']) && $active_trail[1]['module'] == 'system' && $active_trail[1]['link_path'] == 'user' && strpos($item['path'], 'user/%') === 0) { + if (isset($active_trail[1]->module) && $active_trail[1]->module == 'system' && $active_trail[1]->link_path == 'user' && strpos($item['path'], 'user/%') === 0) { array_splice($active_trail, 1, 1); } } /** - * 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 $menu_link) { + if ($menu_link->link_path == 'user' && $menu_link->module == 'system' && !$GLOBALS['user']->uid) { + $menu_link->hidden = 1; + } } } 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 4eeb8d6..c28447d 100644 --- a/core/profiles/standard/standard.install +++ b/core/profiles/standard/standard.install @@ -394,25 +394,25 @@ function standard_install() { ->execute(); // Create a Home link in the main menu. - $item = array( + $menu_link = entity_create('menu_link', array( 'link_title' => st('Home'), 'link_path' => '', 'menu_name' => 'main-menu', - ); - menu_link_save($item); + )); + $menu_link->save(); // Populate the default shortcut set. $shortcut_set = shortcut_set_load(SHORTCUT_DEFAULT_SET_NAME); - $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($shortcut_set); // Update the menu router information. 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 diff --git a/core/themes/seven/template.php b/core/themes/seven/template.php index a8643ab..3295faa 100644 --- a/core/themes/seven/template.php +++ b/core/themes/seven/template.php @@ -71,9 +71,11 @@ function seven_admin_block_content($variables) { $output = system_admin_compact_mode() ? '
      ' : '
        '; foreach ($content as $item) { $output .= '
      • '; - $output .= l($item['title'], $item['href'], $item['localized_options']); - if (isset($item['description']) && !system_admin_compact_mode()) { - $output .= '
        ' . filter_xss_admin($item['description']) . '
        '; + $output .= l($item->label(), $item->href, $item->options); + // @todo Figure out what to do with description. Should probably turn into + // a field. + if (isset($item->description) && !system_admin_compact_mode()) { + $output .= '
        ' . filter_xss_admin($item->description) . '
        '; } $output .= '
      • '; }