From e0fe1ae8eb5dd4e5d5442b8b48afe2c47a166d2b Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Wed, 4 Jul 2007 15:49:44 +0000
Subject: [PATCH] - Patch #151583 by pwolanin, Shakur, webernet, Arancaytar et
 al: various menu module fixes.

---
 includes/menu.inc                | 242 ++++++++++----
 modules/menu/menu.module         | 557 ++++++++++++++++++-------------
 modules/system/system.install    |   1 +
 modules/system/system.schema     |   1 +
 profiles/default/default.profile |   2 +-
 5 files changed, 496 insertions(+), 307 deletions(-)

diff --git a/includes/menu.inc b/includes/menu.inc
index f96641273a99..ec3e2e93f233 100644
--- a/includes/menu.inc
+++ b/includes/menu.inc
@@ -67,7 +67,7 @@
  *
  * Everything described so far is stored in the menu_router table. The
  * menu_links table holds the visible menu links. By default these are
- * derived from the same hook_menu definitions, however you are free to
+ * derived from the same hook_menu definitons, however you are free to
  * add more with menu_link_save().
  */
 
@@ -297,7 +297,6 @@ function menu_get_item($path = NULL) {
 /**
  * Execute the page callback associated with the current path
  */
-
 function menu_execute_active_handler($path = NULL) {
   if (_menu_site_is_offline()) {
     return MENU_SITE_OFFLINE;
@@ -378,6 +377,9 @@ function _menu_check_access(&$item, $map) {
   }
 }
 
+/**
+ * Localize the item title using t() or another callback.
+ */
 function _menu_item_localize(&$item) {
   // Translate the title to allow storage of English title strings
   // in the database, yet display of them in the language required
@@ -514,15 +516,15 @@ function _menu_link_translate(&$item) {
     _menu_link_map_translate($map, $item['to_arg_functions']);
     $item['href'] = implode('/', $map);
 
-    // Note- skip callbacks without real values for their arguments
+    // Note- skip callbacks without real values for their arguments.
     if (strpos($item['href'], '%') !== FALSE) {
       $item['access'] = FALSE;
       return FALSE;
     }
-    // TODO: menu_tree_data may set this ahead of time for links to nodes
+    // menu_tree_check_access() may set this ahead of time for links to nodes.
     if (!isset($item['access'])) {
       if (!_menu_load_objects($item, $map)) {
-        // An error occurred loading an object
+        // An error occured loading an object.
         $item['access'] = FALSE;
         return FALSE;
       }
@@ -606,16 +608,22 @@ function menu_tree_output($tree) {
 function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidden = FALSE) {
   static $tree = array();
 
+  // Use $mlid as a flag for whether the data being loaded is for the whole tree.
   $mlid = isset($item['mlid']) ? $item['mlid'] : 0;
+  // Generate the cache ID.
   $cid = 'links:'. $menu_name .':all:'. $mlid .':'. (int)$show_hidden;
 
   if (!isset($tree[$cid])) {
+    // If the static variable doesn't have the data, check {cache_menu}.
     $cache = cache_get($cid, 'cache_menu');
     if ($cache && isset($cache->data)) {
       $tree[$cid] = $cache->data;
     }
     else {
+      // Build and run the query, and build the tree.
       if ($mlid) {
+        // The tree is for a single item, so we need to match the values in its
+        // p columns and 0 (the top level) with the plid values of other links.
         $args = array(0, $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5']);
         $args = array_unique($args);
         $placeholders = implode(', ', array_fill(0, count($args), '%d'));
@@ -624,26 +632,28 @@ function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidde
         $parents[] = $item['mlid'];
       }
       else {
+        // Get all links in this menu.
         $where = '';
         $args = array();
         $parents = array();
       }
-      if (!$show_hidden) {
-        $where .= ' AND ml.hidden = 0';
-      }
-      else {
-        $where .= ' AND ml.hidden > 0';
-      }
       array_unshift($args, $menu_name);
-      list(, $tree[$cid]) = _menu_tree_data(db_query("
+      // 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.
+      // We need to select links that are visible or hidden (ml.hidden >= 0), but
+      // not callbacks (ml.hidden < 0), so that we can later exclude all the
+      // children of a hidden item.
+      // No need to order by p6 - there is a sort by weight later.
+      $tree[$cid] = menu_tree_data(db_query("
         SELECT m.*, ml.menu_name, ml.mlid, ml.plid, ml.link_path, ml.router_path, ml.hidden, ml.external, ml.has_children, ml.expanded, ml.weight + 50000 AS weight, ml.depth, ml.p1, ml.p2, ml.p3, ml.p4, ml.p5, ml.p6, ml.module, ml.link_title, ml.options
         FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
-        WHERE ml.menu_name = '%s'". $where ."
+        WHERE ml.menu_name = '%s'". $where ." AND ml.hidden >= 0
         ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $args), $parents);
+      // Cache the data.
       cache_set($cid, $tree[$cid], 'cache_menu');
     }
-    // TODO: special case node links and access check via db_rewite_sql()
-    _menu_tree_check_access($tree[$cid]);
+    // Check access for the current user to each item in the tree.
+    menu_tree_check_access($tree[$cid], $show_hidden);
   }
 
   return $tree[$cid];
@@ -651,7 +661,7 @@ function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidde
 
 /**
  * Get the data structure representing a named menu tree, based on the current
- * page. The tree order is maintained by storing each parent in an individual
+ * page. The tree order is maintained by storing each parent in an invidual
  * field, see http://drupal.org/node/141866 for more.
  *
  * @param $menu_name
@@ -666,28 +676,38 @@ function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidde
 function menu_tree_page_data($menu_name = 'navigation') {
   static $tree = array();
 
+  // Load the menu item corresponding to the current page.
   if ($item = menu_get_item()) {
+    // Generate the cache ID.
     $cid = 'links:'. $menu_name .':page:'. $item['href'] .':'. (int)$item['access'];
 
     if (!isset($tree[$cid])) {
+      // If the static variable doesn't have the data, check {cache_menu}.
       $cache = cache_get($cid, 'cache_menu');
       if ($cache && isset($cache->data)) {
         $tree[$cid] = $cache->data;
       }
       else {
+        // Build and run the query, and build the tree.
         if ($item['access']) {
+          // Check whether a menu link exists that corresponds to the current path.
           $parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6 FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item['href']));
-          // We may be on a local task that's not in the links
-          // TODO how do we handle the case like a local task on a specific node in the menu?
+
           if (empty($parents)) {
+            // If no link exists, we may be on a local task that's not in the links.
+            // TODO: handle the case like a local task on a specific node in the menu.
             $parents = db_fetch_array(db_query("SELECT p1, p2, p3, p4, p5, p6 FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item['tab_root']));
           }
+          // We always want all the top-level links with plid == 0.
           $parents[] = '0';
 
           $args = $parents = array_unique($parents);
           $placeholders = implode(', ', array_fill(0, count($args), '%d'));
           $expanded = variable_get('menu_expanded', array());
+          // Check whether the current menu has any links set to be expanded.
           if (in_array($menu_name, $expanded)) {
+            // Collect all the links set to be expanded, and then add all their
+            // children to the list also.
             do {
               $result = db_query("SELECT mlid FROM {menu_links} WHERE expanded != 0 AND has_children != 0 AND menu_name = '%s' AND plid IN (". $placeholders .') AND mlid NOT IN ('. $placeholders .')', array_merge(array($menu_name), $args, $args));
               while ($item = db_fetch_array($result)) {
@@ -698,23 +718,28 @@ function menu_tree_page_data($menu_name = 'navigation') {
           }
           array_unshift($args, $menu_name);
         }
-        // Show the root menu for access denied.
         else {
-          $args = array('navigation', '0');
+          // Show only the top-level menu items when access is denied.
+          $args = array($menu_name, '0');
           $placeholders = '%d';
           $parents = array();
         }
+        // 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.
+        // We need to select links that are visible or hidden (ml.hidden >= 0), but
+        // not callbacks (ml.hidden < 0), so that we can later exclude all the
+        // children of a hidden item.
         // No need to order by p6 - there is a sort by weight later.
-        list(, $tree[$cid]) = _menu_tree_data(db_query("
+        $tree[$cid] = menu_tree_data(db_query("
           SELECT m.*, ml.menu_name, ml.mlid, ml.plid, ml.link_path, ml.router_path, ml.hidden, ml.external, ml.has_children, ml.expanded, ml.weight + 50000 AS weight, ml.depth, ml.p1, ml.p2, ml.p3, ml.p4, ml.p5, ml.p6, ml.module, ml.link_title, ml.options
           FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
-          WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") AND ml.hidden = 0
+          WHERE ml.menu_name = '%s' AND ml.plid IN (". $placeholders .") AND ml.hidden >= 0
           ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $args), $parents);
+        // Cache the data.
         cache_set($cid, $tree[$cid], 'cache_menu');
       }
-      // TODO: special case node links and access check via db_rewite_sql()
-      _menu_tree_check_access($tree[$cid]);
+      // Check access for the current user to each item in the tree.
+      menu_tree_check_access($tree[$cid]);
     }
     return $tree[$cid];
   }
@@ -722,15 +747,34 @@ function menu_tree_page_data($menu_name = 'navigation') {
   return array();
 }
 
-function _menu_tree_check_access(&$tree) {
+/**
+ * Check access and perform other dynamic operations for each link in the tree.
+ */
+function menu_tree_check_access(&$tree, $show_hidden = FALSE) {
+  // TODO: special case node links and access check via db_rewite_sql().
+  // TODO: move sorting of siblings in the tree here so that we can sort based on
+  // the localized title of each link.  Currently the dorting is done in
+  // function _menu_tree_data().
+  _menu_tree_check_access($tree, $show_hidden);
+}
+
+/**
+ * Recursive helper function for menu_tree_check_access()
+ */
+function _menu_tree_check_access(&$tree, $show_hidden) {
   foreach ($tree as $key => $v) {
     $item = &$tree[$key]['link'];
-    _menu_link_translate($item);
+    if (!$item['hidden'] || $show_hidden) {
+      _menu_link_translate($item);
+    }
+    else {
+      $item['access'] = FALSE;
+    }
     if (!$item['access']) {
       unset($tree[$key]);
     }
     elseif ($tree[$key]['below']) {
-      _menu_tree_check_access($tree[$key]['below']);
+      _menu_tree_check_access($tree[$key]['below'], $show_hidden);
     }
   }
 }
@@ -738,10 +782,6 @@ function _menu_tree_check_access(&$tree) {
 /**
  * Build the data representing a menu tree.
  *
- * The function is a bit complex because the rendering of an item depends on
- * the next menu item. So we are always rendering the element previously
- * processed not the current one.
- *
  * @param $result
  *   The database result.
  * @param $parents
@@ -749,12 +789,23 @@ function _menu_tree_check_access(&$tree) {
  *   to the root of the menu tree.
  * @param $depth
  *   The depth of the current menu tree.
- * @param $previous_element
- *   The previous menu link in the current menu tree.
  * @return
- *   See menu_tree_data for a description of the data structure.
+ *   See menu_tree_page_data for a description of the data structure.
+ */
+function menu_tree_data($result = NULL, $parents = array(), $depth = 1) {
+
+  list(, $tree) = _menu_tree_data($result, $parents, $depth);
+  return $tree;
+}
+
+/**
+ * Recursive helper function to build the data representing a menu tree.
+ *
+ * The function is a bit complex because the rendering of an item depends on
+ * the next menu item. So we are always rendering the element previously
+ * processed not the current one.
  */
-function _menu_tree_data($result = NULL, $parents = array(), $depth = 1, $previous_element = '') {
+function _menu_tree_data($result, $parents, $depth, $previous_element = '') {
   $remnant = NULL;
   $tree = array();
   while ($item = db_fetch_array($result)) {
@@ -763,7 +814,7 @@ function _menu_tree_data($result = NULL, $parents = array(), $depth = 1, $previo
     $item['in_active_trail'] = in_array($item['mlid'], $parents);
     // The weights are uniform 5 digits because of the 50000 offset in the
     // query. We add mlid at the end of the index to insure uniqueness.
-    $index = $previous_element ? ($previous_element['weight'] .' '. $previous_element['title'] . $previous_element['mlid']) : '';
+    $index = $previous_element ? ($previous_element['weight'] .' '. drupal_strtolower($previous_element['link_title']) . $previous_element['mlid']) : '';
     // The current item is the first in a new submenu.
     if ($item['depth'] > $depth) {
       // _menu_tree returns an item and the menu tree structure.
@@ -798,8 +849,8 @@ function _menu_tree_data($result = NULL, $parents = array(), $depth = 1, $previo
     }
   }
   if ($previous_element) {
-    // We have one more link dangling.
-    $tree[$previous_element['weight'] .' '. $previous_element['title'] .' '. $previous_element['mlid']] = array(
+    // We have one more link dangling
+    $tree[$previous_element['weight'] .' '. drupal_strtolower($previous_element['link_title']) .' '. $previous_element['mlid']] = array(
       'link' => $previous_element,
       'below' => '',
     );
@@ -833,6 +884,9 @@ function theme_menu_item($link, $has_children, $menu = '', $in_active_trail = FA
   return '<li class="'. $class .'">'. $link . $menu .'</li>'."\n";
 }
 
+/**
+ * Generate the HTML output for a single local task link.
+ */
 function theme_menu_local_task($link, $active = FALSE) {
   return '<li '. ($active ? 'class="active" ' : '') .'>'. $link .'</li>';
 }
@@ -886,6 +940,9 @@ function menu_get_names($reset = FALSE) {
   return $names;
 }
 
+/**
+ * Return an array of links to be rendered as the Primary links.
+ */
 function menu_primary_links() {
   $tree = menu_tree_page_data('primary-links');
   $links = array();
@@ -898,6 +955,9 @@ function menu_primary_links() {
   return $links;
 }
 
+/**
+ * Return an array of links to be rendered as the Secondary links.
+ */
 function menu_secondary_links() {
   $tree = menu_tree_page_data('secondary-links');
   $links = array();
@@ -941,7 +1001,7 @@ function menu_local_tasks($level = 0, $return_root = FALSE) {
     while ($item = db_fetch_array($result)) {
       _menu_translate($item, $map, TRUE);
       if ($item['tab_parent']) {
-        // All tabs, but not the root page.
+        // All tabs, but not the root page
         $children[$item['tab_parent']][$item['path']] = $item;
       }
       // Store the translated item for later use.
@@ -976,7 +1036,7 @@ function menu_local_tasks($level = 0, $return_root = FALSE) {
       $tabs[$item['number_parts']]['output'] = $tabs_current;
     }
 
-    // Find all tabs at the same level or above the current one
+    // Find all tabs at the same level or above the current one.
     $parent = $router_item['tab_parent'];
     $path = $router_item['path'];
     $current = $router_item;
@@ -1032,10 +1092,16 @@ function menu_local_tasks($level = 0, $return_root = FALSE) {
   }
 }
 
+/**
+ * Returns the rendered local tasks at the top level.
+ */
 function menu_primary_local_tasks() {
   return menu_local_tasks(0);
 }
 
+/**
+ * Returns the rendered local tasks at the second level.
+ */
 function menu_secondary_local_tasks() {
   return menu_local_tasks(1);
 }
@@ -1065,6 +1131,9 @@ function theme_menu_local_tasks() {
  return $output;
 }
 
+/**
+ * Set (or get) the active menu for the current page - determines the active trail.
+ */
 function menu_set_active_menu_name($menu_name = NULL) {
   static $active;
 
@@ -1077,6 +1146,9 @@ function menu_set_active_menu_name($menu_name = NULL) {
   return $active;
 }
 
+/**
+ * Get the active menu for the current page - determines the active trail.
+ */
 function menu_get_active_menu_name() {
   return menu_set_active_menu_name();
 }
@@ -1084,6 +1156,9 @@ function menu_get_active_menu_name() {
 function menu_set_active_item() {
 }
 
+/**
+ * Set (or get) the active trail for the current page - the path to root in the menu tree..
+ */
 function menu_set_active_trail($new_trail = NULL) {
   static $trail;
 
@@ -1121,6 +1196,9 @@ function menu_set_active_trail($new_trail = NULL) {
   return $trail;
 }
 
+/**
+ * Get the active trail for the current page - the path to root in the menu tree..
+ */
 function menu_get_active_trail() {
   return menu_set_active_trail();
 }
@@ -1128,6 +1206,9 @@ function menu_get_active_trail() {
 function menu_set_location() {
 }
 
+/**
+ * Get the breadcrumb for the current page, as determined by the active trail.
+ */
 function menu_get_active_breadcrumb() {
   $breadcrumb = array();
   $item = menu_get_item();
@@ -1147,6 +1228,9 @@ function menu_get_active_breadcrumb() {
   return $breadcrumb;
 }
 
+/**
+ * Get the title of the current page, as determined by the active trail.
+ */
 function menu_get_active_title() {
   $active_trail = menu_get_active_trail();
 
@@ -1168,20 +1252,23 @@ function menu_get_active_title() {
  *   rendering.
  */
 function menu_link_load($mlid) {
-  if ($item = db_fetch_array(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) {
+  if (is_numeric($mlid) && $item = db_fetch_array(db_query("SELECT m.*, ml.* FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE ml.mlid = %d", $mlid))) {
     _menu_link_translate($item);
     return $item;
   }
   return FALSE;
 }
 
+/**
+ * Clears the cached cached data for a single named menu.
+ */
 function menu_cache_clear($menu_name = 'navigation') {
   cache_clear_all('links:'. $menu_name .':', 'cache_menu', TRUE);
 }
 
 /**
- * This should be called any time broad changes might have been made to the
- * router items or menu links.
+ * Clears all cached menu data.  This should be called any time broad changes
+ * might have been made to the router items or menu links.
  */
 function menu_cache_clear_all() {
   cache_clear_all('*', 'cache_menu', TRUE);
@@ -1253,13 +1340,15 @@ function _menu_link_build($item) {
   return $item;
 }
 
+/**
+ * Helper function to build 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) {
-    $item = _menu_link_build($item);
-    // We add nonexisting items.
-    if ($item['_visible'] && !db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $item['menu_name'], $item['link_path']))) {
+    if ($item['_visible']) {
+      $item = _menu_link_build($item);
       $menu_links[$path] = $item;
       $sort[$path] = $item['_number_parts'];
     }
@@ -1269,7 +1358,13 @@ function _menu_navigation_links_rebuild($menu) {
     array_multisort($sort, SORT_NUMERIC, $menu_links);
 
     foreach ($menu_links as $item) {
-      menu_link_save($item);
+      $existing_item = db_fetch_array(db_query("SELECT mlid, customized FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s' AND module = 'system'", $item['menu_name'], $item['link_path']));
+      if ($existing_item) {
+        $item['mlid'] = $existing_item['mlid'];
+      }
+      if (!$existing_item || !$existing_item['customized']) {
+        menu_link_save($item);
+      }
     }
   }
   $placeholders = implode(', ', array_fill(0, count($menu), "'%s'"));
@@ -1297,6 +1392,9 @@ function menu_link_delete($mlid, $path = NULL) {
   }
 }
 
+/**
+ * Helper function for menu_link_delete; deletes a single menu link.
+ */
 function _menu_delete_item($item) {
   // System-created items get automatically deleted, but only on menu rebuild.
   if ($item && $item['module'] != 'system') {
@@ -1331,8 +1429,7 @@ function _menu_delete_item($item) {
  *     weight      default is 0
  *     expanded    whether the item is expanded.
  *     options     An array of options, @see l for more.
- *     mlid        If it's an existing item, this comes from the database.
- *                 Never set by hand.
+ *     mlid        Set to an existing value, or 0 or NULL to insert a new link.
  *     plid        The mlid of the parent.
  *     router_path The path of the relevant router item.
  */
@@ -1341,8 +1438,10 @@ function menu_link_save(&$item) {
 
   drupal_alter('menu_link', $item, $menu);
 
-  $item['_external'] = menu_path_is_external($item['link_path']);
-  // Load defaults.
+  // This is the easiest way to handle the unique internal path '<front>',
+  // since a path marked as external does not need to match a router path.
+  $item['_external'] = menu_path_is_external($item['link_path'])  || $item['link_path'] == '<front>';
+  // Load defaults
   $item += array(
     'menu_name' => 'navigation',
     'weight' => 0,
@@ -1352,19 +1451,13 @@ function menu_link_save(&$item) {
     'expanded' => 0,
     'options' => array(),
     'module' => 'menu',
+    'customized' => 0,
   );
   $menu_name = $item['menu_name'];
   $existing_item = FALSE;
   if (isset($item['mlid'])) {
     $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['mlid']));
   }
-  else {
-    $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE menu_name = '%s' AND link_path = '%s'", $menu_name, $item['link_path']));
-  }
-
-  if ($existing_item) {
-    $item['mlid'] = $existing_item['mlid'];
-  }
 
   // Find the parent - it must be in the same menu.
   if (isset($item['plid'])) {
@@ -1378,7 +1471,7 @@ function menu_link_save(&$item) {
     } while ($parent === FALSE && $parent_path);
   }
   // Menu callbacks need to be in the links table for breadcrumbs, but can't
-  // be parents if they are generated directly from a router item
+  // be parents if they are generated directly from a router item.
   if (empty($parent['mlid']) || $parent['hidden'] < 0) {
     $item['plid'] =  0;
   }
@@ -1391,15 +1484,15 @@ function menu_link_save(&$item) {
        menu_name, plid, link_path,
       hidden, external, has_children,
       expanded, weight,
-      module, link_title, options) VALUES (
+      module, link_title, options, customized) VALUES (
       '%s', %d, '%s',
       %d, %d, %d,
       %d, %d,
-      '%s', '%s', '%s')",
+      '%s', '%s', '%s', %d)",
       $item['menu_name'], $item['plid'], $item['link_path'],
       $item['hidden'], $item['_external'], $item['has_children'],
       $item['expanded'], $item['weight'],
-      $item['module'],  $item['link_title'], serialize($item['options']));
+      $item['module'],  $item['link_title'], serialize($item['options']), $item['customized']);
     $item['mlid'] = db_last_insert_id('menu_links', 'mlid');
   }
 
@@ -1409,7 +1502,7 @@ function menu_link_save(&$item) {
     $item['depth'] = 1;
   }
   else {
-    // Cannot add beyond the maximum depth.
+    // Cannot add beyond the maximum depth
     if ($item['has_children'] && $existing_item) {
       $limit = MENU_MAX_DEPTH - menu_link_children_relative_depth($existing_item) - 1;
     }
@@ -1450,12 +1543,12 @@ function menu_link_save(&$item) {
     router_path = '%s', hidden = %d, external = %d, has_children = %d,
     expanded = %d, weight = %d,  depth = %d,
     p1 = %d, p2 = %d, p3 = %d, p4 = %d, p5 = %d, p6 = %d,
-    module = '%s', link_title = '%s', options = '%s' WHERE mlid = %d",
+    module = '%s', link_title = '%s', options = '%s', customized = %d WHERE mlid = %d",
     $item['menu_name'], $item['plid'], $item['link_path'],
     $item['router_path'], $item['hidden'], $item['_external'], $item['has_children'],
     $item['expanded'], $item['weight'],  $item['depth'],
     $item['p1'], $item['p2'], $item['p3'], $item['p4'], $item['p5'], $item['p6'],
-    $item['module'],  $item['link_title'], serialize($item['options']), $item['mlid']);
+    $item['module'],  $item['link_title'], serialize($item['options']), $item['customized'], $item['mlid']);
   // Check the has_children status of the parent.
   if ($item['plid']) {
     $parent_has_children = (bool)db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE plid = %d AND hidden = 0", $item['plid']));
@@ -1552,6 +1645,9 @@ function _menu_link_move_children($item, $existing_item) {
   }
 }
 
+/**
+ * Helper function that sets the p1..p6 values for a menu link being saved.
+ */
 function _menu_link_parents_set(&$item, $parent) {
   $i = 1;
   while ($i < $item['depth']) {
@@ -1567,6 +1663,9 @@ function _menu_link_parents_set(&$item, $parent) {
   }
 }
 
+/**
+ * Helper function to build the router table based on the data from hook_menu.
+ */
 function _menu_router_build($callbacks) {
   // First pass: separate callbacks from paths, making paths ready for
   // matching. Calculate fitness, and fill some default values.
@@ -1582,7 +1681,7 @@ function _menu_router_build($callbacks) {
     // We store the highest index of parts here to save some work in the fit
     // calculation loop.
     $slashes = $number_parts - 1;
-    // extract functions
+    // Extract load and to_arg functions.
     foreach ($parts as $k => $part) {
       $match = FALSE;
       if (preg_match('/^%([a-z_]*)$/', $part, $matches)) {
@@ -1646,7 +1745,7 @@ function _menu_router_build($callbacks) {
   foreach ($menu as $path => $v) {
     $item = &$menu[$path];
     if (!isset($item['access callback']) && isset($item['access arguments'])) {
-      $item['access callback'] = 'user_access'; // Default callback
+      $item['access callback'] = 'user_access'; // Default callback.
     }
     if (!$item['_tab']) {
       // Non-tab items
@@ -1740,6 +1839,9 @@ function _menu_router_build($callbacks) {
   return $menu;
 }
 
+/**
+ * Returns TRUE if a path is external (e.g. http://example.com).
+ */
 function menu_path_is_external($path) {
   $colonpos = strpos($path, ':');
   return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path);
@@ -1749,11 +1851,11 @@ function menu_path_is_external($path) {
  * Returns TRUE if the site is off-line for maintenance.
  */
 function _menu_site_is_offline() {
-  // Check if site is set to off-line mode
+  // Check if site is set to off-line mode.
   if (variable_get('site_offline', 0)) {
-    // Check if the user has administration privileges
+    // Check if the user has administration privileges.
     if (!user_access('administer site configuration')) {
-      // Check if this is an attempt to login
+      // Check if this is an attempt to login.
       if (drupal_get_normal_path($_GET['q']) != 'user') {
         return TRUE;
       }
diff --git a/modules/menu/menu.module b/modules/menu/menu.module
index 7f3491ad6b1b..f0dc2e783a06 100644
--- a/modules/menu/menu.module
+++ b/modules/menu/menu.module
@@ -24,7 +24,7 @@ function menu_help($path, $arg) {
       return $output;
     case 'admin/build/menu':
       return '<p>'. t('Menus are a collection of links (menu items) used to navigate a website. The list(s) below display the currently available menus along with their menu items. Select an operation from the list to manage each menu or menu item.', array('@admin-settings-menus' => url('admin/build/menu/settings'), '@admin-block' => url('admin/build/block'))) .'</p>';
-    case 'admin/build/menu/menu/add':
+    case 'admin/build/menu/add':
       return '<p>'. t('Enter the name for your new menu. Remember to enable the newly created block in the <a href="@blocks">blocks administration page</a>.', array('@blocks' => url('admin/build/block'))) .'</p>';
     case 'admin/build/menu/item/add':
       return '<p>'. t('Enter the title, path, position and the weight for your new menu item.') .'</p>';
@@ -45,150 +45,169 @@ function menu_menu() {
   $items['admin/build/menu'] = array(
     'title' => 'Menus',
     'description' => "Control your site's navigation menu, primary links and secondary links. as well as rename and reorganize menu items.",
-    'page callback' => 'system_admin_menu_block_page',
-    'file' => 'system.admin.inc',
-    'file path' => drupal_get_path('module', 'system'),
+    'page callback' => 'menu_overview_page',
     'access callback' => 'user_access',
     'access arguments' => array('administer menu'),
   );
-  $result = db_query('SELECT * FROM {menu_custom} ORDER BY title');
-  while ($menu = db_fetch_object($result)) {
-    $items['admin/build/menu/'. $menu->menu_name] = array(
-      'title' => $menu->title,
-      'page callback' => 'menu_overview',
-      'page arguments' => array($menu->menu_name),
-      'description' => $menu->description,
-    );
-    $items['admin/build/menu/'. $menu->menu_name .'/list'] = array(
-      'title' => 'List items',
-      'weight' => -10,
-      'type' => MENU_DEFAULT_LOCAL_TASK,
-    );
-    $items['admin/build/menu/'. $menu->menu_name .'/add'] = array(
-      'title' => 'Add item',
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array('menu_edit_item', 'add', $menu->menu_name),
-      'type' => MENU_LOCAL_TASK);
-    $items['admin/build/menu/'. $menu->menu_name .'/edit'] = array(
-      'title' => 'Edit menu',
-      'page callback' => 'drupal_get_form',
-      'page arguments' => array('menu_edit_menu', 'edit', $menu->menu_name),
-      'type' => MENU_LOCAL_TASK);
-  }
 
   $items['admin/build/menu/list'] = array(
     'title' => 'List menus',
     'type' => MENU_DEFAULT_LOCAL_TASK,
-    'weight' => -10,
-  );
-  $items['admin/build/menu/menu/add'] = array(
+    'weight' => -10);
+  $items['admin/build/menu/add'] = array(
     'title' => 'Add menu',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('menu_edit_menu', 'add'),
     'type' => MENU_LOCAL_TASK);
-  $items['admin/build/menu/item/disable'] = array(
+  $items['admin/build/menu/settings'] = array(
+    'title' => 'Settings',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('menu_configure'),
+    'type' => MENU_LOCAL_TASK,
+    'weight' => 5);
+
+  $items['admin/build/menu-customize/%menu'] = array(
+    'title' => 'Customize menu',
+    'page callback' => 'menu_overview',
+    'page arguments' => array(3),
+    'access arguments' => array('administer menu'),
+    'type' => MENU_CALLBACK);
+  $items['admin/build/menu-customize/%menu/list'] = array(
+    'title' => 'List items',
+    'weight' => -10,
+    'type' => MENU_DEFAULT_LOCAL_TASK);
+  $items['admin/build/menu-customize/%menu/add'] = array(
+    'title' => 'Add item',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('menu_edit_item', 'add', NULL, 3),
+    'type' => MENU_LOCAL_TASK);
+  $items['admin/build/menu-customize/%menu/edit'] = array(
+    'title' => 'Edit menu',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('menu_edit_menu', 'edit', 3),
+    'type' => MENU_LOCAL_TASK);
+  $items['admin/build/menu/item/%menu_link/disable'] = array(
     'title' => 'Disable menu item',
     'page callback' => 'menu_flip_item',
-    'page arguments' => array(TRUE),
-    'type' => MENU_CALLBACK,
-  );
-  $items['admin/build/menu/item/enable'] = array(
+    'page arguments' => array(TRUE, 4),
+    'type' => MENU_CALLBACK);
+  $items['admin/build/menu/item/%menu_link/enable'] = array(
     'title' => 'Enable menu item',
     'page callback' => 'menu_flip_item',
-    'page arguments' => array(FALSE),
-    'type' => MENU_CALLBACK,
-  );
-  $items['admin/build/menu/item/edit'] = array(
+    'page arguments' => array(FALSE, 4),
+    'type' => MENU_CALLBACK);
+  $items['admin/build/menu/item/%menu_link/edit'] = array(
     'title' => 'Edit menu item',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_edit_item', 'edit', 5),
+    'page arguments' => array('menu_edit_item', 'edit', 4, NULL),
     'type' => MENU_CALLBACK);
-  $items['admin/build/menu/item/reset'] = array(
+  $items['admin/build/menu/item/%menu_link/reset'] = array(
     'title' => 'Reset menu item',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_reset_item'),
+    'page arguments' => array('menu_reset_item', 4),
     'type' => MENU_CALLBACK);
-  $items['admin/build/menu/item/delete'] = array(
+  $items['admin/build/menu/item/%menu_link/delete'] = array(
     'title' => 'Delete menu item',
     'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_item_delete_form'),
-    'type' => MENU_CALLBACK);
-
-  $items['admin/build/menu/menu/edit'] = array(
-    'title' => 'Edit menu',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_edit_menu', 'edit'),
-    'type' => MENU_CALLBACK);
-  $items['admin/build/menu/menu/delete'] = array(
-    'title' => 'Delete menu',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_item_delete_form'),
+    'page arguments' => array('menu_item_delete_form', 4),
     'type' => MENU_CALLBACK);
 
-  $items['admin/build/menu/settings'] = array(
-    'title' => 'Settings',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('menu_configure'),
-    'type' => MENU_LOCAL_TASK,
-    'weight' => 5,
-  );
-
   return $items;
+}
 
+/**
+ * Implementation of hook_enable()
+ *
+ *  Add a link for each custom menu.
+ */
+function menu_enable() {
+  menu_rebuild();
+  $result = db_query("SELECT * FROM {menu_custom}");
+  $link['module'] = 'menu';
+  $link['plid'] = db_result(db_query("SELECT mlid from {menu_links} WHERE menu_name = 'navigation' AND link_path = 'admin/build/menu'"));
+  $link['router_path'] = 'admin/build/menu-customize/%';
+
+  while ($menu = db_fetch_array($result)) {
+    $link['mlid'] = 0;
+    $link['link_title'] = $menu['title'];
+    $link['link_path'] = 'admin/build/menu-customize/'. $menu['menu_name'];
+    menu_link_save($link);
+  }
 }
 
+/**
+ * Load the data for a single custom menu.
+ */
+function menu_load($menu_name) {
+  return db_fetch_array(db_query("SELECT * FROM {menu_custom} WHERE menu_name = '%s'", $menu_name));
+}
+
+/**
+ * Menu callback which shows an overview page of all the custom menus and their descriptions.
+ */
+function menu_overview_page() {
+  $result = db_query("SELECT * FROM {menu_custom}");
+  $content = array();
+  while ($menu = db_fetch_array($result)) {
+    $menu['href'] = 'admin/build/menu-customize/'. $menu['menu_name'];
+    $menu['options'] = array();
+    $content[] = $menu;
+  }
+  return theme('admin_block_content', $content);
+}
 
 /**
  * Menu callback which displays every menu element accessible to the current
  * user and the relevant operations.
  */
-function menu_overview($menu_name) {
+function menu_overview($menu) {
+
   $header = array(t('Menu item'), t('Expanded'), array('data' => t('Operations'), 'colspan' => '3'));
   $sql ="
-    SELECT *, ml.weight + 50000 AS weight FROM {menu_links} ml
-    LEFT JOIN {menu_router} m ON m.path = ml.router_path
-    WHERE menu_name = '%s' AND hidden >= 0
+    SELECT m.*, ml.menu_name, ml.mlid, ml.plid, ml.link_path, ml.router_path, ml.hidden, ml.external, ml.has_children, ml.expanded, ml.weight + 50000 AS weight, ml.depth, ml.customized, ml.module, ml.link_title, ml.options
+    FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
+    WHERE ml.menu_name = '%s' AND ml.hidden >= 0
     ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC";
   $sql_count = "SELECT COUNT(*) FROM {menu_links} ml WHERE menu_name = '%s' AND hidden >= 0";
-  $result = pager_query($sql, 200, 0, $sql_count, $menu_name);
-  list(, $tree) = _menu_tree_data($result);
+  $result = pager_query($sql, 200, 0, $sql_count, $menu['menu_name']);
+  $tree = menu_tree_data($result);
+  menu_tree_check_access($tree, TRUE);
   $rows = _menu_overview_tree($tree);
   $output = theme('table', $header, $rows);
   $output .= theme('pager', NULL, 200, 0);
   return $output;
 }
 
+/**
+ * Recursive helper function for menu_overview().
+ */
 function _menu_overview_tree($tree) {
   static $rows = array();
   foreach ($tree as $data) {
     $title = '';
     if ($item = $data['link']) {
-      _menu_link_translate($item);
-      if (!$item['access']) {
-        continue;
-      }
       $title = str_repeat('&nbsp;&nbsp;', $item['depth'] - 1) . ($item['depth'] > 1 ? '-&nbsp;' : '');
       $title .= l($item['link_title'], $item['href'], $item['options']);
       // Populate the operations field.
       $operations = array();
       // Set the edit column.
-      $operations[] = array('data' => l(t('edit'), 'admin/build/menu/item/edit/'. $item['mlid']));
+      $operations[] = array('data' => l(t('edit'), 'admin/build/menu/item/'. $item['mlid'] .'/edit'));
       if ($item['hidden']) {
         $title .= ' ('. t('disabled') .')';
         $class = 'menu-disabled';
-        $operations[] = array('data' => l(t('enable'), 'admin/build/menu/item/enable/'. $item['mlid']));
+        $operations[] = array('data' => l(t('enable'), 'admin/build/menu/item/'. $item['mlid'] .'/enable'));
       }
       else {
         $class = 'menu-enabled';
-        $operations[] = array('data' => l(t('disable'), 'admin/build/menu/item/disable/'. $item['mlid']));
+        $operations[] = array('data' => l(t('disable'), 'admin/build/menu/item/'. $item['mlid'] .'/disable'));
       }
       // Only items created by the menu module can be deleted.
       if ($item['module'] == 'menu') {
-        $operations[] = array('data' => l(t('delete'), 'admin/build/menu/item/delete/'. $item['mlid']));
+        $operations[] = array('data' => l(t('delete'), 'admin/build/menu/item/'. $item['mlid'] .'/delete'));
       }
       // Set the reset column.
-      else if ($item['module'] == 'system') {
-        $operations[] = array('data' => l(t('reset'), 'admin/build/menu/item/reset/'. $item['mlid']));
+      elseif ($item['module'] == 'system' && $item['customized']) {
+        $operations[] = array('data' => l(t('reset'), 'admin/build/menu/item/'. $item['mlid'] .'/reset'));
       }
       else {
         $operations[] = array('data' => '');
@@ -211,55 +230,39 @@ function _menu_overview_tree($tree) {
 }
 
 /**
- * Menu callback; enable/disable a menu item.
+ * Menu callback; enable/disable a menu link.
  *
  * @param $hide
  *   TRUE to not show in the menu tree. FALSE to make the item and its children
  *   reappear in menu tree.
- * @param $mlid
- *    mlid of the menu item.
+ * @param $item
+ *    The menu item.
  */
-function menu_flip_item($hide, $mlid) {
-  if (!($item = menu_link_load($mlid))) {
-    drupal_not_found();
-    return;
-  }
+function menu_flip_item($hide, $item) {
+
   $item['hidden'] = (bool)$hide;
+  $item['customized'] = 1;
   menu_link_save($item);
   drupal_set_message($hide ? t('The menu item has been disabled.') : t('The menu item has been enabled.'));
-  drupal_goto('admin/build/menu/'. $item['menu_name']);
+  drupal_goto('admin/build/menu-customize/'. $item['menu_name']);
 }
 
 /**
- * Menu callback; present the menu item editing form.
+ * Menu callback; Build the menu link editing form.
  */
-function menu_edit_item(&$form_state, $type, $id = 0) {
-  if ($type == 'edit') {
-    if (!($item = menu_link_load($id))) {
-      drupal_not_found();
-      return;
-    }
-  }
-  else {
-    // This is an add form.
-    // The mlid argument (if set) will be the default pid to use.
-    $item = array('mlid' => 0, 'plid' => 0, 'menu_name' => $id, 'weight' => 0, 'link_title' => '', 'link_path' => '', 'options' => array(), 'module' => 'menu', 'expanded' => 0, 'hidden' => 0, 'has_children' => 0);
+function menu_edit_item(&$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);
   }
-  foreach (array('link_path', 'mlid', 'module', 'hidden', 'menu_name', 'has_children') as $key) {
+  foreach (array('link_path', 'mlid', 'module', 'hidden', 'menu_name', '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);
 
-  $form['link_title'] = array('#type' => 'textfield',
-    '#title' => t('Title'),
-    '#default_value' => $item['link_title'],
-    '#description' => t('The name of the menu item.'),
-    '#required' => TRUE,
-  );
-  $form['description'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Description'),
-    '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
-  );
   if ($item['module'] == 'menu') {
     $form['link_path'] = array(
       '#type' => 'textfield',
@@ -276,8 +279,19 @@ function menu_edit_item(&$form_state, $type, $id = 0) {
       '#description' => l($item['link_title'], $item['href'], $item['options']),
     );
   }
-  $form['original_item'] = array('#type' => 'value', '#value' => $item);
-
+  $form['link_title'] = array('#type' => 'textfield',
+    '#title' => t('Menu link title'),
+    '#default_value' => $item['link_title'],
+    '#description' => t('The link text corresponding to this item that should appear in the menu.'),
+    '#required' => TRUE,
+  );
+  $form['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Description'),
+    '#default_value' => isset($item['options']['attributes']['title']) ? $item['options']['attributes']['title'] : '',
+    '#rows' => 1,
+    '#description' => t('The description displayed when hovering over a menu item.'),
+  );
   $form['expanded'] = array(
     '#type' => 'checkbox',
     '#title' => t('Expanded'),
@@ -286,7 +300,7 @@ function menu_edit_item(&$form_state, $type, $id = 0) {
   );
 
   // Generate a list of possible parents (not including this item or descendants).
-  $options = menu_parent_options($item['mlid'], $item['menu_name']);
+  $options = menu_parent_options($item['menu_name'], $item);
   $form['plid'] = array(
     '#type' => 'select',
     '#title' => t('Parent item'),
@@ -296,7 +310,7 @@ function menu_edit_item(&$form_state, $type, $id = 0) {
   $form['weight'] = array(
     '#type' => 'weight',
     '#title' => t('Weight'),
-    '#default_value' => isset($item['weight']) ? $item['weight'] : 0,
+    '#default_value' => $item['weight'],
     '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
   );
   $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
@@ -304,14 +318,13 @@ function menu_edit_item(&$form_state, $type, $id = 0) {
   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'];
-  if (isset($item['link_path']) && !menu_path_is_external($item['link_path'])) {
-    $path = $item['link_path'];
-    $item = menu_get_item($path);
-    if (!$item || !$item['access']) {
-      form_set_error('path', t('This path is either invalid or you do not have access to it'));
-    }
+  if (!trim($item['link_path']) || !menu_valid_path($item)) {
+    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'])));
   }
 }
 
@@ -319,83 +332,56 @@ function menu_edit_item_validate($form, &$form_state) {
  * Process menu and menu item add/edit form submissions.
  */
 function menu_edit_item_submit($form, &$form_state) {
+  $form_state['values']['options']['attributes']['title'] = $form_state['values']['description'];
   menu_link_save($form_state['values']);
-  $form_state['redirect'] = 'admin/build/menu/'. $form_state['values']['menu_name'];
+  $form_state['redirect'] = 'admin/build/menu-customize/'. $form_state['values']['menu_name'];
 }
 
 /**
  * Return a list of menu items that are valid possible parents for the
  * given menu item. The list excludes the given item and its children.
  *
- * @param $mlid
- *   The menu item id for which to generate a list of parents.
- *   If $mlid == 0 then the complete tree is returned.
  * @param $menu_name
  *   The name of the menu.
- * @param $plid
- *   The menu link item id of the menu item at which to start the tree.
- *   If $pid > 0 then this item will be included in the tree.
- * @param $depth
- *   The current depth in the tree - used when recursing to indent the tree.
+ * @param $item
+ *   The menu item for which to generate a list of parents.
+ *   If $item['mlid'] == 0 or NULL then the complete tree is returned.
  * @return
- *   An array of menu titles keyed on the mlid.
+ *   An array of menu link titles keyed on the mlid.
  */
-function menu_parent_options($mlid, $menu_name, $plid = 0, $depth = 0) {
-  $options = array(0 => t('Root'));
-  // Exclude $mlid and its children from the list unless $mlid is 0.
-  if ($mlid && $mlid == $plid) {
-    return $options;
-  }
+function menu_parent_options($menu_name, $item) {
 
-  $sql = "SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} mr ON ml.router_path = mr.path WHERE menu_name = '%s' AND hidden >= 0";
-  $params = array($menu_name);
-  if ($mlid && ($item = menu_link_load($mlid))) {
-    $parents = array();
-    for ($i = 1; $i <= 6; $i++) {
-      $key = "p$i";
-      $value = $item[$key];
-      if ($value) {
-        $parents[]= "$key != %d";
-        $params[] = $value;
-      }
-      else {
-        break;
-      }
-    }
-    $sql .= ' AND (' . implode(' OR ', $parents) .')';
-  }
+  $tree = menu_tree_all_data($item['menu_name'], NULL, TRUE);
+  $options = array(0 => '<'. t('root') .'>');
+  _menu_parents_recurse($tree, '--', $options, $item['mlid']);
 
-  $sql .= ' ORDER BY p1, p2, p3, p4, p5';
-  $result = db_query($sql, $params);
-
-  while ($item = db_fetch_array($result)) {
-    _menu_link_translate($item);
-    if (!$item['access']) {
-      continue;
-    }
-    $title = str_repeat('--', $item['depth']) .' '. $item['link_title'];
-    if ($item['hidden']) {
-      $title .= ' ('. t('disabled') .')';
-    }
-    $options[$item['mlid']] = $title;
-  }
   return $options;
 }
 
 /**
- * Remove the menu item.
+ * Recursive helper function for menu_parent_options().
  */
-function menu_node_form_delete($node) {
-  menu_link_delete(NULL, 'node/'. $node->nid);
+function _menu_parents_recurse($tree, $indent, &$options, $exclude) {
+  foreach ($tree as $data) {
+    if ($data['link']['mlid'] != $exclude) {
+      $title = $indent .' '. truncate_utf8($data['link']['title'], 30, TRUE, FALSE);
+      if ($data['link']['hidden']) {
+        $title .= ' ('. t('disabled') .')';
+      }
+      $options[$data['link']['mlid']] = $title;
+      if ($data['below'] && $data['link']['depth'] < MENU_MAX_DEPTH - 1) {
+         _menu_parents_recurse($data['below'], $indent .'--', $options, $exclude);
+      }
+    }
+  }
 }
 
 /**
- * Menu callback; handle the adding/editing of a new menu.
+ * Menu callback; Build the form that handles the adding/editing of a custom menu.
  */
-function menu_edit_menu(&$form_state, $type, $menu_name = '') {
+function menu_edit_menu(&$form_state, $type, $menu = array()) {
   if ($type == 'edit') {
-    $menu = db_fetch_array(db_query("SELECT * FROM {menu_custom} WHERE menu_name = '%s'", $menu_name));
-    $form['menu_name'] = array('#type' => 'value', '#value' => $menu_name);
+    $form['menu_name'] = array('#type' => 'value', '#value' => $menu['menu_name']);
     $form['#insert'] = FALSE;
   }
   else {
@@ -403,7 +389,7 @@ function menu_edit_menu(&$form_state, $type, $menu_name = '') {
     $form['menu_name'] = array(
       '#type' => 'textfield',
       '#title' => t('Menu name'),
-      '#description' => t('The machine-readable name of this menu. This text will be used for constructing the URL of the <em>menu overwrite</em> page for this menu. This name may consist of only of lowercase letters, numbers and hyphens and must be unique.'),
+      '#description' => t('The machine-readable name of this menu. This text will be used for constructing the URL of the <em>menu overview</em> page for this menu. This name may consist of only of lowercase letters, numbers and hypens and must be unique.'),
       '#required' => TRUE,
     );
     $form['#insert'] = TRUE;
@@ -428,10 +414,13 @@ function menu_edit_menu(&$form_state, $type, $menu_name = '') {
   return $form;
 }
 
+/**
+ * Validates the human and machine-readable names when adding or editing a menu.
+ */
 function menu_edit_menu_validate($form, &$form_state) {
   $item = $form_state['values'];
   if (preg_match('/[^a-z0-9-]/', $item['menu_name'])) {
-    form_set_error('menu_name', t('Menu name may consist of only of lowercase letters, numbers and hyphens.'));
+    form_set_error('menu_name', t('Menu name may consist only of lowercase letters, numbers and hypens.'));
   }
   if ($form['#insert'] &&
       (db_result(db_query("SELECT menu_name FROM {menu_custom} WHERE menu_name = '%s'", $item['menu_name'])) ||
@@ -440,31 +429,44 @@ function menu_edit_menu_validate($form, &$form_state) {
   }
 }
 
+/**
+ * Submit function for adding or editing a custom menu.
+ */
 function menu_edit_menu_submit($form, &$form_state) {
   $menu = $form_state['values'];
-  $redirect = 'admin/build/menu/'. $menu['menu_name'];
+  // Append 'menu' to the menu name to help avoid name-space conflicts.
+  $menu['menu_name'] = 'menu-'. $menu['menu_name'];
+  $redirect = 'admin/build/menu-customize/'. $menu['menu_name'];
+  $link['link_title'] = $menu['title'];
+  $link['router_path'] = 'admin/build/menu-customize/%';
+  $link['module'] = 'menu';
+  $link['link_path'] = $redirect;
   if ($form['#insert']) {
+    $link['plid'] = db_result(db_query("SELECT mlid from {menu_links} WHERE menu_name = 'navigation' AND link_path = 'admin/build/menu'"));
+    menu_link_save($link);
     db_query("INSERT INTO {menu_custom} (menu_name, title, description) VALUES ('%s', '%s', '%s')", $menu['menu_name'], $menu['title'], $menu['description']);
   }
   else {
     db_query("UPDATE {menu_custom} SET title = '%s', description = '%s' WHERE menu_name = '%s'", $menu['title'], $menu['description'], $menu['menu_name']);
-    db_query("UPDATE {menu_links} SET link_title = '%s' WHERE link_title = '%s' AND router_path = '%s'", $menu['title'], $form['#title'], $redirect);
+    $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = '%s'", $link['link_path']);
+    while ($m = db_fetch_array($result)) {
+      $link = menu_link_load($m['mlid']);
+      $link['link_title'] = $menu['title'];
+      menu_link_save($link);
+    }
   }
-  menu_rebuild();
   $form_state['redirect'] = $redirect;
 }
 
 /**
- * Menu callback; delete a single custom item.
+ * Menu callback; Build a confirm form for deletion of a single menu link.
  */
-function menu_item_delete_form(&$form_state, $mlid) {
-  if (!$item = menu_link_load($mlid)) {
-    drupal_not_found();
-    return;
+function menu_item_delete_form(&$form_state, $item) {
+  if ($item['module'] == 'system') {
+    drupal_access_denied();
   }
   $form['#item'] = $item;
-
-  return confirm_form($form, t('Are you sure you want to delete the custom menu item %item?', array('%item' => $item['link_title'])), 'admin/build/menu/'. $item['menu_name'], t('This action cannot be undone.'), t('Delete'));
+  return confirm_form($form, t('Are you sure you want to delete the custom menu item %item?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name']);
 }
 
 /**
@@ -476,40 +478,37 @@ function menu_item_delete_form_submit($form, &$form_state) {
   $t_args = array('%title' => $item['link_title']);
   drupal_set_message(t('The menu item %title has been deleted.', $t_args));
   watchdog('menu', 'Deleted menu item %title.', $t_args, WATCHDOG_NOTICE);
-  $form_state['redirect'] = 'admin/build/menu/'. $item['menu_name'];
+  $form_state['redirect'] = 'admin/build/menu-customize/'. $item['menu_name'];
 }
 
 /**
  * Menu callback; reset a single modified item.
  */
-function menu_reset_item(&$form_state, $mlid) {
-  if (isset($mlid) && $item = db_fetch_array(db_query('SELECT router_path, link_title FROM {menu_links} WHERE mlid = %d', $mlid))) {
-    $form['#router_path'] = $item['router_path'];
-
-    $options = array(
-      'description' => t('Any customizations will be lost. This action cannot be undone.'),
-      'yes' => t('Reset')
-    );
+function menu_reset_item(&$form_state, $item) {
+  $form['item'] = array('#type' => 'value', '#value' => $item);
 
-    return confirm_form($form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => $item['link_title'])), 'admin/build/menu', $options);
-  }
-  else {
-    drupal_not_found();
-  }
+  $options = array(
+    'description' => t('Any customizations will be lost. This action cannot be undone.'),
+    'yes' => t('Reset')
+  );
+  return confirm_form($form, t('Are you sure you want to reset the item %item to its default values?', array('%item' => $item['link_title'])), 'admin/build/menu-customize/'. $item['menu_name'], $options);
 }
 
 /**
  * Process menu reset item form submissions.
  */
 function menu_reset_item_submit($form, &$form_state) {
-  $new_item = db_fetch_array(db_query("SELECT * FROM {menu_router} WHERE path = '%s'", $form['#router_path']));
-  menu_link_save(_menu_link_build($new_item));
+  $item = $form_state['values']['item'];
+  $router = menu_router_build();
+  $new_item = _menu_link_build($router[$item['router_path']]);
+  foreach (array('mlid', 'has_children') as $key) {
+    $new_item[$key] = $item[$key];
+  }
+  menu_link_save($new_item);
   drupal_set_message(t('The menu item was reset to its default settings.'));
-  $form_state['redirect'] = 'admin/build/menu/navigation';
+  $form_state['redirect'] = 'admin/build/menu-customize/'. $new_item['menu_name'];
 }
 
-// Conversion ends here.
-
 /**
  * Implementation of hook_block().
  */
@@ -533,24 +532,47 @@ function menu_block($op = 'list', $delta = 0) {
 /**
  * Implementation of hook_nodeapi().
  */
-function menu_nodeapi($node, $op) {
-  if (user_access('administer menu') && isset($node->menu)) {
-    $item = $node->menu;
-    switch ($op) {
-      case 'delete':
-        $item['delete'] = 1;
-        // Deliberate no break.
-      case 'insert':
-      case 'update':
-        if ($item['delete']) {
-          _menu_delete_item($item);
+function menu_nodeapi(&$node, $op) {
+  switch ($op) {
+    case 'insert':
+    case 'update':
+      if (isset($node->menu)) {
+        $item = $node->menu;
+        if (!empty($item['delete'])) {
+          menu_link_delete($item['mlid']);
         }
-        else {
+        elseif (trim($item['link_title'])) {
+          $item['link_title'] = trim($item['link_title']);
           $item['link_path'] = "node/$node->nid";
+          if (!$item['customized']) {
+            $item['options']['attributes']['title'] = trim($node->title);
+          }
           menu_link_save($item);
         }
-        break;
-    }
+      }
+      break;
+    case 'delete':
+      // Delete all menu module links that point to this node.
+      $result = db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND module = 'menu'", $node->nid);
+      while ($m = db_fetch_array($result)) {
+        menu_link_delete($m['mlid']);
+      }
+      break;
+    case 'prepare':
+      if (empty($node->menu)) {
+        // Prepare the node for the edit form so that $node->menu always exists.
+        $menu_name = variable_get('menu_parent_items', 'navigation');
+        $item = array();
+        if (isset($node->nid)) {
+          $mlid = db_result(db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s' AND module = 'menu' ORDER BY mlid ASC", $node->nid, $menu_name));
+          if ($mlid) {
+            $item = menu_link_load($mlid);
+          }
+        }
+        // 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);
+      }
+      break;
   }
 }
 
@@ -559,7 +581,10 @@ function menu_nodeapi($node, $op) {
  * Add menu item fields to the node form.
  */
 function menu_form_alter(&$form, $form_state, $form_id) {
-  if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
+  if (isset($form['#node']) && $form['#node']->type .'_node_form' == $form_id) {
+    // Note - doing this to make sure the delete checkbox stays in the form.
+    $form['#cache'] = TRUE;
+
     $form['menu'] = array(
       '#type' => 'fieldset',
       '#title' => t('Menu settings'),
@@ -567,27 +592,58 @@ function menu_form_alter(&$form, $form_state, $form_id) {
       '#collapsible' => TRUE,
       '#collapsed' => FALSE,
       '#tree' => TRUE,
-      '#weight' => 30,
+      '#weight' => -2,
     );
-    $form_state = array();
-    if ($mlid = db_result(db_query("SELECT mlid FROM {menu_links} WHERE link_path = 'node/%d' AND menu_name = '%s'", $form['nid']['#value'], variable_get('menu_parent_items', 'navigation')))) {
-      $menu_form = drupal_retrieve_form('menu_edit_item', $form_state, 'edit', $mlid);
-      $menu_form['delete'] = array(
+    $item = $form['#node']->menu;
+
+    if ($item['mlid']) {
+      // There is an existing link
+      $form['menu']['delete'] = array(
         '#type' => 'checkbox',
-        '#title' => t('Check to delete this menu item.'),
+        '#title' => t('Check to remove this item from the menu.'),
       );
     }
-    else {
-      $menu_form = drupal_retrieve_form('menu_edit_item', $form_state, 'add', variable_get('menu_parent_items', 'navigation'));
-      unset($menu_form['link_path']);
-      $menu_form['link_title']['#required'] = FALSE;
+    if (!$item['link_title']) {
       $form['menu']['#collapsed'] = TRUE;
     }
-    unset($menu_form['submit']);
-    $form['menu'] += $menu_form;
+
+    foreach (array('mlid', 'module', 'hidden', 'menu_name', 'has_children', 'customized', 'options', 'expanded', 'hidden') as $key) {
+      $form['menu'][$key] = array('#type' => 'value', '#value' => $item[$key]);
+    }
+
+    $form['menu']['link_title'] = array('#type' => 'textfield',
+      '#title' => t('Menu link title'),
+      '#default_value' => $item['link_title'],
+      '#description' => t('The link text corresponding to this item that should appear in the menu. Leave blank if you do not wish to add this post to the menu.'),
+      '#required' => FALSE,
+    );
+    // Generate a list of possible parents (not including this item or descendants).
+    $options = menu_parent_options($item['menu_name'], $item);
+    $form['menu']['plid'] = array(
+      '#type' => 'select',
+      '#title' => t('Parent item'),
+      '#default_value' => $item['plid'],
+      '#options' => $options,
+    );
+    $form['menu']['weight'] = array(
+      '#type' => 'weight',
+      '#title' => t('Weight'),
+      '#default_value' => $item['weight'],
+      '#description' => t('Optional. In the menu, the heavier items will sink and the lighter items will be positioned nearer the top.'),
+    );
   }
 }
 
+/**
+ * Return an associative array of the custom menus names.
+ *
+ * @param $all
+ *   If FALSE return only user-added menus, or if TRUE also include
+ *   the menus defined by the system.
+ * @return
+ *   An array with the machine-readable names as the keys, and human-readable
+ *   titles as the values.
+ */
 function menu_get_menus($all = FALSE) {
   $sql = 'SELECT * FROM {menu_custom}'. ($all ? '' : " WHERE menu_name NOT IN ('navigation', 'primary-links', 'secondary-links')") . ' ORDER BY title';
   $result = db_query($sql);
@@ -600,7 +656,7 @@ function menu_get_menus($all = FALSE) {
 }
 
 /**
- * Menu callback; presents menu configuration options.
+ * Menu callback; Build the form presenting menu configuration options.
  */
 function menu_configure() {
   $form['intro'] = array(
@@ -619,3 +675,32 @@ function menu_configure() {
 
   return system_settings_form($form);
 }
+
+/**
+ * Validates the path of a menu link being created or edited.
+ *
+ * @return
+ *   TRUE if it is a valid path AND the current user has access permission,
+ *   FALSE otherwise.
+ */
+function menu_valid_path($form_item) {
+  $item = array();
+  $path = $form_item['link_path'];
+  if ($path == '<front>' || menu_path_is_external($path)) {
+    $item = array('access' => TRUE);
+  }
+  elseif (preg_match('/\/\%/', $path)) {
+    // Path is dynamic (ie 'user/%'), so check directly against menu_router table.
+    if ($item = db_fetch_array(db_query("SELECT * FROM {menu_router} where path = '%s' ", $path))) {
+      $item['link_path']  = $form_item['link_path'];
+      $item['link_title'] = $form_item['link_title'];
+      $item['external']   = FALSE;
+      $item['options'] = '';
+      _menu_link_translate($item);
+    }
+  }
+  else {
+    $item = menu_get_item($path);
+  }
+  return $item && $item['access'];
+}
diff --git a/modules/system/system.install b/modules/system/system.install
index 7aa12ff8b1d3..525d66ceb49a 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -3339,6 +3339,7 @@ function system_update_6020() {
       'expanded'     => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
       'weight'       => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
       'depth'        => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
+      'customized'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
       'p1'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
       'p2'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
       'p3'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
diff --git a/modules/system/system.schema b/modules/system/system.schema
index a1ea98b8c408..544b4f1b2a97 100644
--- a/modules/system/system.schema
+++ b/modules/system/system.schema
@@ -110,6 +110,7 @@ function system_schema() {
       'expanded'     => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
       'weight'       => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
       'depth'        => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
+      'customized'   => array('type' => 'int', 'not null' => TRUE, 'default' => 0, 'size' => 'small'),
       'p1'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
       'p2'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
       'p3'           => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
diff --git a/profiles/default/default.profile b/profiles/default/default.profile
index 7a2a6b14046d..518b377c15c4 100644
--- a/profiles/default/default.profile
+++ b/profiles/default/default.profile
@@ -8,7 +8,7 @@
  *  An array of modules to be enabled.
  */
 function default_profile_modules() {
-  return array('color', 'comment', 'help', 'taxonomy', 'dblog');
+  return array('color', 'comment', 'help', 'menu', 'taxonomy', 'dblog');
 }
 
 /**
-- 
GitLab