Index: includes/menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.166
diff -u -p -r1.166 menu.inc
--- includes/menu.inc	16 May 2007 13:45:16 -0000	1.166
+++ includes/menu.inc	19 May 2007 19:36:20 -0000
@@ -149,11 +149,6 @@ define('MENU_SITE_OFFLINE', 4);
  * @} End of "Menu status codes".
  */
 
-
-/**
- * @} End of "Menu operations."
- */
-
 /**
  * @Name Menu tree parameters
  * @{
@@ -511,13 +506,13 @@ function _menu_link_translate(&$item) {
       $item->access = FALSE;
       return FALSE;
     }
-    if (!_menu_load_objects($item, $map)) {
-      // An error occured loading an object
-      $item->access = FALSE;
-      return FALSE;
-    }
     // TODO: menu_tree may set this ahead of time for links to nodes
     if (!isset($item->access)) {
+      if (!_menu_load_objects($item, $map)) {
+        // An error occured loading an object
+        $item->access = FALSE;
+        return FALSE;
+      }
       _menu_check_access($item, $map);
     }
     // If the link title matches that of a router item, localize it.
@@ -578,6 +573,49 @@ function menu_tree_output($tree) {
 }
 
 /**
+ * Get the full data structure representing a named menu tree. Since this can be
+ * the full tree with no access checks done and including hidden items, the 
+ * data returned may be used for generating an an admin interface or a select.
+ *
+ * @param $menu_name
+ *   The named menu links to return
+ * @param $item
+ *    A fully loaded menu link object, or NULL.  If an object is supplied, the
+ *    path to root will be set in the returned tree.
+ * @param $show_hidden
+ * @param $check_access
+ * @return
+ *   An array of menu links, in the order they should be rendered. 
+ */
+function menu_tree_all_data($menu_name = 'navigation', $item = NULL, $show_hidden = FALSE, $check_access = TRUE) {
+  static $tree = array();
+
+  $mlid = isset($item->mild) ? $item->mild : 0;
+  $key = $menu_name .':'. $mlid .':'. (int)$check_access .':'. (int)$show_hidden;
+
+  if (!isset($tree[$key])) {
+    if ($mlid) {
+      $parents = array($item->p1, $item->p2, $item->p3, $item->p4, $item->p5);
+    }
+    else {
+      $parents = array();
+    }
+    if (!$show_hidden) {
+      $where = ' AND ml.hidden = 0';
+    }
+    else {
+      $where = ' AND ml.hidden != '.MENU_CALLBACK;
+    }
+    list(, $tree[$key]) = _menu_tree_data(db_query("
+      SELECT *, ml.weight + 50000 AS weight FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path
+      WHERE ml.menu_name = '%s'". $where ."
+      ORDER BY p1 ASC, p2 ASC, p3 ASC, p4 ASC, p5 ASC", $menu_name), $parents);
+  }
+
+  return $tree[$key];
+}
+
+/**
  * 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 invidual
  * field, see http://drupal.org/node/141866 for more.
@@ -628,7 +666,7 @@ function menu_tree_data($menu_name = 'na
       // No need to order by p6 - there is a sort by weight later.
       list(, $tree[$menu_name]) = _menu_tree_data(db_query("
         SELECT *, ml.weight + 50000 AS weight 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 .")
+        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);
 
       // TODO: cache_set() for the untranslated links
@@ -898,6 +936,25 @@ function menu_secondary_local_tasks() {
   return menu_local_tasks(1);
 }
 
+/**
+ * Returns the rendered local tasks. The default implementation renders
+ * them as tabs.
+ *
+ * @ingroup themeable
+ */
+function theme_menu_local_tasks() {
+ $output = '';
+
+ if ($primary = menu_primary_local_tasks()) {
+   $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
+ }
+ if ($secondary = menu_secondary_local_tasks()) {
+   $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
+ }
+
+ return $output;
+}
+
 function menu_set_active_menu_name($menu_name = NULL) {
   static $active;
 
@@ -1004,36 +1061,14 @@ function menu_get_active_title() {
 function menu_get_item_by_mlid($mlid) {
   if ($item = db_fetch_object(db_query("SELECT * FROM {menu_links} ml LEFT JOIN {menu_router} m ON m.path = ml.router_path WHERE mlid = %d", $mlid))) {
     _menu_link_translate($item);
-    if ($item->access) {
-      return $item;
-    }
+    return $item;
   }
   return FALSE;
 }
 
-/**
- * Returns the rendered local tasks. The default implementation renders
- * them as tabs.
- *
- * @ingroup themeable
- */
-function theme_menu_local_tasks() {
- $output = '';
-
- if ($primary = menu_primary_local_tasks()) {
-   $output .= "<ul class=\"tabs primary\">\n". $primary ."</ul>\n";
- }
- if ($secondary = menu_secondary_local_tasks()) {
-   $output .= "<ul class=\"tabs secondary\">\n". $secondary ."</ul>\n";
- }
-
- return $output;
-}
-
 function menu_cache_clear($menu_name = 'navigation') {
-  // TODO: starting stub.  This will be called whenever an item is added to or
-  // moved from a named menu
 
+  cache_clear_all($menu_name .':', 'cache_menu', TRUE);
 }
 
 /**
@@ -1041,8 +1076,7 @@ function menu_cache_clear($menu_name = '
  * router items or menu links.
  */
 function menu_cache_clear_all() {
-  cache_clear_all('*', 'menu_links', TRUE);
-  cache_clear_all('*', 'menu_router', TRUE);
+  cache_clear_all('*', 'cache_menu', TRUE);
 }
 
 /**
@@ -1074,15 +1108,17 @@ function _menu_navigation_links_rebuild(
     if ($item['type'] == MENU_CALLBACK || $item['type'] == MENU_SUGGESTED_ITEM) {
       $item['hidden'] = $item['type'];
     }
+    // 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',
+      'menu_name' => 'navigation',
       'link_title' => $item['title'],
       'href' => $path,
-      'module' => 'system',
       'hidden' => 0,
     );
     // We add nonexisting items.
-    if ($item['_visible'] && !db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $item['menu name'], $item['href']))) {
+    if ($item['_visible'] && !db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $item['menu_name'], $item['href']))) {
       $menu_links[$path] = $item;
       $sort[$path] = $item['_number_parts'];
     }
@@ -1101,12 +1137,34 @@ function _menu_navigation_links_rebuild(
 }
 
 /**
+ * Delete a menu link.
+ *
+ * @param $mlid
+ *   A valid menu link mlid
+ */
+function menu_link_delete($mlid) {
+
+  $item = menu_get_item_by_mlid($mlid);
+  
+  // System-created items get automatically deleted, but only on menu rebuild.
+  if ($item && $item->module != 'system') {
+
+    if ($item->has_children) {
+      // TODO - reparent children?
+    }
+    db_query('DELETE FROM {menu_links} WHERE mild = %d', $mlid);  
+    menu_cache_clear($item->menu_name);
+  }
+}
+
+
+/**
  * Save a menu link.
  *
  * @param $item
  *   An array representing a menu link item. The only mandatory keys are href
  *   and link_title. Possible keys are
- *     menu name   default is navigation
+ *     menu_name   default is navigation
  *     weight      default is 0
  *     expanded    whether the item is expanded.
  *     options     An array of options, @see l for more.
@@ -1130,7 +1188,7 @@ function menu_link_save(&$item, $_menu =
   $item['_external'] = menu_path_is_external($item['href']);
   // Load defaults.
   $item += array(
-    'menu name' => 'navigation',
+    'menu_name' => 'navigation',
     'weight' => 0,
     'link_title' => '',
     'hidden' => 0,
@@ -1138,26 +1196,24 @@ function menu_link_save(&$item, $_menu =
     'expanded' => 0,
     'options' => empty($item['description']) ? array() : array('attributes' => array('title' => $item['description'])),
   );
-  $existing_item = array();
+  $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 href = '%s'", $item['menu name'], $item['href']));
+    $existing_item = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE menu_name = '%s' AND href = '%s'", $menu_name, $item['href']));
   }
 
-  if (empty($existing_item)) {
+  if (!$existing_item) {
     $item['mlid'] = db_next_id('{menu_links}_mlid');
   }
 
-  $menu_name = $item['menu name'];
-  $new_path = !$existing_item || ($existing_item['href'] != $item['href']);
-
   // Find the parent.
   if (isset($item['plid'])) {
     $parent = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $item['plid']));
   }
-  else { //
+  else {
     $parent_path = $item['href'];
     do {
       $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
@@ -1180,21 +1236,24 @@ function menu_link_save(&$item, $_menu =
   }
   else {
     // Cannot add beyond the maximum depth.
-    if ($parent['depth'] >= (MENU_MAX_DEPTH)) {
+    if ($item['has_children'] && $existing_item) {
+      $limit = MENU_MAX_DEPTH - menu_children_relative_depth($existing_item) - 1;
+    }
+    else {
+      $limit = MENU_MAX_DEPTH - 1;
+    }
+    if ($parent['depth'] > $limit) {
       return FALSE;
     }
     $item['depth'] = $parent['depth'] + 1;
-    _menu_parents_copy($item, $parent);
-    $item['p'. $item['depth']] = $item['mlid'];
+    _menu_link_parents_set($item, $parent);
   }
-
-  if ($item['plid'] != $existing_item['plid']) {
-
-    // TODO: UPDATE the parents of the children of the current item
-    // TODO: check the has_children status of the previous 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 callback.
-  if (empty($item['router_path']) || $new_path) {
+  if (empty($item['router_path']) || !$existing_item || ($existing_item['href'] != $item['href'])) {
     if ($item['_external']) {
       $item['router_path'] = '';
     }
@@ -1213,13 +1272,13 @@ function menu_link_save(&$item, $_menu =
       }
     }
   }
-  if (!empty($existing_item)) {
+  if ($existing_item) {
     db_query("UPDATE {menu_links} SET menu_name = '%s', plid = %d, href = '%s',
       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",
-      $item['menu name'], $item['plid'], $item['href'],
+      $item['menu_name'], $item['plid'], $item['href'],
       $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'],
@@ -1237,18 +1296,19 @@ function menu_link_save(&$item, $_menu =
       %d, %d, %d,
       %d, %d, %d, %d, %d, %d,
       '%s', '%s', '%s')",
-      $item['menu name'], $item['mlid'], $item['plid'], $item['href'],
+      $item['menu_name'], $item['mlid'], $item['plid'], $item['href'],
       $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']));
- }
-
-  if ($item['plid'] && !$item['hidden']) {
-    db_query("UPDATE {menu_links} SET has_children = 1 WHERE mlid = %d", $item['plid']);
+  }
+  // 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']));
+    db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $item['plid']);
   }
 
-  // Keep track of which menus have expanded items
+  // Keep track of which menus have expanded items.
   $names = array();
   $result = db_query("SELECT menu_name FROM {menu_links} WHERE expanded != 0 GROUP BY menu_name");
   while ($n = db_fetch_array($result)) {
@@ -1258,13 +1318,94 @@ function menu_link_save(&$item, $_menu =
   return TRUE;
 }
 
-function _menu_parents_copy(&$dest, $source, $offset = 0){
-  $i = 1 + $offset;
-  $depth = 0;
-  for ($j = 1; $i <= MENU_MAX_DEPTH; $i++) {
-    $dest['p'. $i] = $source['p'. $j];
-    $j++;
-  }
+/**
+ * Find 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) {
+  $i = 1;
+  $match = '';
+  $args[] = $item['menu_name'];
+  $p = 'p1';
+  while ($i <= MENU_MAX_DEPTH && $item[$p]) {
+    $match .= " AND $p = %d";
+    $args[] = $item[$p];
+    $p = 'p'. ++$i;
+  }
+  
+  $max_depth = db_result(db_query_range("SELECT depth FROM {menu_links} WHERE menu_name = '%s'". $match ." ORDER BY depth DESC", $args, 0, 1));
+  
+  return ($max_depth > $item['depth']) ? $max_depth - $item['depth'] : 0;
+}
+
+/** 
+ * Update the menu name, parents (p1 - p6), and depth for the children of 
+ * a menu link that's being moved in the tree and check the has_children status
+ * of the previous parent. 
+ */
+function _menu_link_move_children($item, $existing_item) {
+
+  $args[] = $item['menu_name'];
+  $set = '';
+  $shift = $item['depth'] - $existing_item['depth'];
+  if ($shift < 0) {
+    $args[] = -$shift;
+    $set = ', depth = depth - %d';
+  }
+  elseif ($shift > 0) {
+    $args[] = $shift;
+    $set = ', depth = depth + %d';
+  }
+  $i = 1;
+  while ($i <= $item['depth']) {
+    $p = 'p'. $i++;  
+    $set .= ", $p = %d";
+    $args[] = $item[$p];    
+  }
+  $j = $existing_item['depth'] + 1;
+  while ($i <= MENU_MAX_DEPTH && $j <= MENU_MAX_DEPTH) {
+    $set .= ', p'. $i++ .' = p'. $j++;
+  }
+  while ($i <= MENU_MAX_DEPTH) {
+    $set .= ', p'. $i++ .' = 0';
+  }
+
+  $args[] = $existing_item['menu_name'];
+  $i = 1;
+  while ($i <= MENU_MAX_DEPTH && $existing_item[$p]) {
+    $p = 'p'. $i++;
+    $match .= " AND $p = %d";
+    $args[] = $existing_item[$p];
+  }
+  
+  db_query("UPDATE {menu_links} SET menu_name = '%s'". $set ." WHERE menu_name = '%s'". $match, $args);
+  
+  if ($existing_item['plid']) {
+    $parent_has_children = (bool)db_result(db_query("SELECT COUNT(*) FROM {menu_links} WHERE plid = %d AND hidden = 0 AND mlid != %d",$existing_item['plid'], $existing_item['mlid']));
+    db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", $parent_has_children, $existing_item['plid']);
+  }
+}
+
+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 - p6) corresponding to the depth always equals the mlid.
+  $item[$p] = $item['mlid'];
+  while ($i <= MENU_MAX_DEPTH) {
+    $p = 'p'. $i++;
+    $item[$p] = 0;
+  }  
 }
 
 function _menu_router_build($callbacks) {
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.112
diff -u -p -r1.112 system.install
--- modules/system/system.install	18 May 2007 07:03:21 -0000	1.112
+++ modules/system/system.install	19 May 2007 19:36:21 -0000
@@ -265,6 +265,17 @@ function system_install() {
         INDEX expire (expire)
       ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
 
+      db_query("CREATE TABLE {cache_menu} (
+        cid varchar(255) BINARY NOT NULL default '',
+        data longblob,
+        expire int NOT NULL default '0',
+        created int NOT NULL default '0',
+        headers text,
+        serialized int(1) NOT NULL default '0',
+        PRIMARY KEY (cid),
+        INDEX expire (expire)
+      ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
+      
       db_query("CREATE TABLE {comments} (
         cid int NOT NULL auto_increment,
         pid int NOT NULL default '0',
@@ -398,7 +409,9 @@ function system_install() {
         link_title varchar(255) NOT NULL default '',
         options text,
         PRIMARY KEY (mlid),
-        KEY parents (plid, p1, p2, p3, p4, p5),
+        KEY router_path (router_path),
+        KEY plid (plid),
+        KEY parents (p1, p2, p3, p4, p5),
         KEY menu_name_path (menu_name, href),
         KEY menu_expanded_children (expanded, has_children)
       ) /*!40100 DEFAULT CHARACTER SET UTF8 */ ");
@@ -780,10 +793,19 @@ function system_install() {
         serialized smallint NOT NULL default '0',
         PRIMARY KEY (cid)
       )");
-      db_query("CREATE INDEX {cache}_expire_idx ON {cache} (expire)");
+      db_query("CREATE TABLE {cache_menu} (
+        cid varchar(255) NOT NULL default '',
+        data bytea,
+        expire int NOT NULL default '0',
+        created int NOT NULL default '0',
+        headers text,
+        serialized smallint NOT NULL default '0',
+        PRIMARY KEY (cid)
+      )");      db_query("CREATE INDEX {cache}_expire_idx ON {cache} (expire)");
       db_query("CREATE INDEX {cache_filter}_expire_idx ON {cache_filter} (expire)");
       db_query("CREATE INDEX {cache_page}_expire_idx ON {cache_page} (expire)");
       db_query("CREATE INDEX {cache_form}_expire_idx ON {cache_form} (expire)");
+      db_query("CREATE INDEX {cache_menu}_expire_idx ON {cache_form} (expire)");
 
       db_query("CREATE TABLE {comments} (
         cid serial,
@@ -919,7 +941,10 @@ function system_install() {
         options text,
         PRIMARY KEY (mlid)
       )");
-      db_query("CREATE INDEX {menu_links}_parents_idx ON {menu_links} (plid, p1, p2, p3, p4, p5)");
+
+      db_query("CREATE INDEX {menu_links}_router_path_idx ON {menu_links} (router_path)");
+      db_query("CREATE INDEX {menu_links}_plid_idx ON {menu_links} (plid)");
+      db_query("CREATE INDEX {menu_links}_parents_idx ON {menu_links} (p1, p2, p3, p4, p5)");
       db_query("CREATE INDEX {menu_links}_menu_name_idx ON {menu_links} (menu_name, href)");
       db_query("CREATE INDEX {menu_links}_expanded_children_idx ON {menu_links} (expanded, has_children)");
 
@@ -3919,14 +3944,15 @@ function system_update_6012() {
       db_add_column($ret, 'cache', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE));
       db_add_column($ret, 'cache_filter', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE));
       db_add_column($ret, 'cache_page', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE));
+      db_add_column($ret, 'cache_menu', 'serialized', 'smallint', array('default' => "'0'", 'not null' => TRUE));
       break;
     case 'mysql':
     case 'mysqli':
       $ret[] = update_sql("ALTER TABLE {cache} ADD serialized int(1) NOT NULL default '0'");
       $ret[] = update_sql("ALTER TABLE {cache_filter} ADD serialized int(1) NOT NULL default '0'");
       $ret[] = update_sql("ALTER TABLE {cache_page} ADD serialized int(1) NOT NULL default '0'");
+      $ret[] = update_sql("ALTER TABLE {cache_menu} ADD serialized int(1) NOT NULL default '0'");
       break;
-
   }
 
   return $ret;
