Index: menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.255.2.27
diff -u -r1.255.2.27 menu.inc
--- menu.inc	10 Nov 2008 17:30:39 -0000	1.255.2.27
+++ menu.inc	13 Jul 2009 19:13:49 -0000
@@ -1652,10 +1652,13 @@
  */
 function menu_rebuild() {
   variable_del('menu_rebuild_needed');
+  $changes = new stdClass;
+  $menu = menu_router_build(TRUE, $changes);
+  require_once 'includes/menu.rebuild_links.inc';
+  // Right now the info in $changes is not actually used...
+  _menu_links_build_rebuild($menu, $changes);
+  // Clear the menu, page and block caches.
   menu_cache_clear_all();
-  $menu = menu_router_build(TRUE);
-  _menu_navigation_links_rebuild($menu);
-  // Clear the page and block caches.
   _menu_clear_page_cache();
   if (defined('MAINTENANCE_MODE')) {
     variable_set('menu_rebuild_needed', TRUE);
@@ -1666,29 +1669,12 @@
  * Collect, alter and store the menu definitions.
  */
 function menu_router_build($reset = FALSE) {
+  require_once 'includes/menu.rebuild_router.inc';
   static $menu;
 
   if (!isset($menu) || $reset) {
-    if (!$reset && ($cache = cache_get('router:', 'cache_menu')) && isset($cache->data)) {
-      $menu = $cache->data;
-    }
-    else {
-      // We need to manually call each module so that we can know which module
-      // a given item came from.
-      $callbacks = array();
-      foreach (module_implements('menu') as $module) {
-        $router_items = call_user_func($module .'_menu');
-        if (isset($router_items) && is_array($router_items)) {
-          foreach (array_keys($router_items) as $path) {
-            $router_items[$path]['module'] = $module;
-          }
-          $callbacks = array_merge($callbacks, $router_items);
-        }
-      }
-      // Alter the menu as defined in modules, keys are like user/%user.
-      drupal_alter('menu', $callbacks);
-      $menu = _menu_router_build($callbacks);
-    }
+    $menu = _menu_router_build($changes);
+    _menu_router_cache($menu);
   }
   return $menu;
 }
@@ -2193,205 +2179,47 @@
   }
 }
 
+
 /**
  * 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.
-  $menu = array();
-  foreach ($callbacks as $path => $item) {
-    $load_functions = array();
-    $to_arg_functions = array();
-    $fit = 0;
-    $move = FALSE;
-
-    $parts = explode('/', $path, MENU_MAX_PARTS);
-    $number_parts = count($parts);
-    // We store the highest index of parts here to save some work in the fit
-    // calculation loop.
-    $slashes = $number_parts - 1;
-    // Extract load and to_arg functions.
-    foreach ($parts as $k => $part) {
-      $match = FALSE;
-      // Look for wildcards in the form allowed to be used in PHP functions,
-      // because we are using these to construct the load function names.
-      // See http://php.net/manual/en/language.functions.php for reference.
-      if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $part, $matches)) {
-        if (empty($matches[1])) {
-          $match = TRUE;
-          $load_functions[$k] = NULL;
-        }
-        else {
-          if (function_exists($matches[1] .'_to_arg')) {
-            $to_arg_functions[$k] = $matches[1] .'_to_arg';
-            $load_functions[$k] = NULL;
-            $match = TRUE;
-          }
-          if (function_exists($matches[1] .'_load')) {
-            $function = $matches[1] .'_load';
-            // Create an array of arguments that will be passed to the _load
-            // function when this menu path is checked, if 'load arguments'
-            // exists.
-            $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
-            $match = TRUE;
-          }
-        }
-      }
-      if ($match) {
-        $parts[$k] = '%';
-      }
-      else {
-        $fit |=  1 << ($slashes - $k);
-      }
-    }
-    if ($fit) {
-      $move = TRUE;
-    }
-    else {
-      // If there is no %, it fits maximally.
-      $fit = (1 << $number_parts) - 1;
-    }
-    $masks[$fit] = 1;
-    $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
-    $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
-    $item += array(
-      'title' => '',
-      'weight' => 0,
-      'type' => MENU_NORMAL_ITEM,
-      '_number_parts' => $number_parts,
-      '_parts' => $parts,
-      '_fit' => $fit,
-    );
-    $item += array(
-      '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
-      '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK),
-    );
-    if ($move) {
-      $new_path = implode('/', $item['_parts']);
-      $menu[$new_path] = $item;
-      $sort[$new_path] = $number_parts;
-    }
-    else {
-      $menu[$path] = $item;
-      $sort[$path] = $number_parts;
-    }
-  }
-  array_multisort($sort, SORT_NUMERIC, $menu);
-
+ * To be called by menu_router_build()
+ * 
+ * @param $changes object to collect $insert, $update, $delete arrays
+ */
+function _menu_router_build($changes = NULL) {
+  
+  $callbacks = _menu_router_build_calc_callbacks();
+  list($menu, $masks) = _menu_router_build_calc_menu($callbacks);
+  
   if (!$menu) {
     // We must have a serious error - there is no data to save.
     watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR);
     return array();
   }
-  // Delete the existing router since we have some data to replace it.
-  db_query('DELETE FROM {menu_router}');
-  // Apply inheritance rules.
-  foreach ($menu as $path => $v) {
-    $item = &$menu[$path];
-    if (!$item['_tab']) {
-      // Non-tab items.
-      $item['tab_parent'] = '';
-      $item['tab_root'] = $path;
-    }
-    for ($i = $item['_number_parts'] - 1; $i; $i--) {
-      $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
-      if (isset($menu[$parent_path])) {
-
-        $parent = $menu[$parent_path];
-
-        if (!isset($item['tab_parent'])) {
-          // Parent stores the parent of the path.
-          $item['tab_parent'] = $parent_path;
-        }
-        if (!isset($item['tab_root']) && !$parent['_tab']) {
-          $item['tab_root'] = $parent_path;
-        }
-        // If an access callback is not found for a default local task we use
-        // the callback from the parent, since we expect them to be identical.
-        // In all other cases, the access parameters must be specified.
-        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
-          $item['access callback'] = $parent['access callback'];
-          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
-            $item['access arguments'] = $parent['access arguments'];
-          }
-        }
-        // Same for page callbacks.
-        if (!isset($item['page callback']) && isset($parent['page callback'])) {
-          $item['page callback'] = $parent['page callback'];
-          if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
-            $item['page arguments'] = $parent['page arguments'];
-          }
-          if (!isset($item['file']) && isset($parent['file'])) {
-            $item['file'] = $parent['file'];
-          }
-          if (!isset($item['file path']) && isset($parent['file path'])) {
-            $item['file path'] = $parent['file path'];
-          }
-        }
-      }
-    }
-    if (!isset($item['access callback']) && isset($item['access arguments'])) {
-      // Default callback.
-      $item['access callback'] = 'user_access';
-    }
-    if (!isset($item['access callback']) || empty($item['page callback'])) {
-      $item['access callback'] = 0;
-    }
-    if (is_bool($item['access callback'])) {
-      $item['access callback'] = intval($item['access callback']);
-    }
-
-    $item += array(
-      'access arguments' => array(),
-      'access callback' => '',
-      'page arguments' => array(),
-      'page callback' => '',
-      'block callback' => '',
-      'title arguments' => array(),
-      'title callback' => 't',
-      'description' => '',
-      'position' => '',
-      'tab_parent' => '',
-      'tab_root' => $path,
-      'path' => $path,
-      'file' => '',
-      'file path' => '',
-      'include file' => '',
-    );
-
-    // Calculate out the file to be included for each callback, if any.
-    if ($item['file']) {
-      $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
-      $item['include file'] = $file_path .'/'. $item['file'];
-    }
-
-    $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : '';
-    db_query("INSERT INTO {menu_router}
-      (path, load_functions, to_arg_functions, access_callback,
-      access_arguments, page_callback, page_arguments, fit,
-      number_parts, tab_parent, tab_root,
-      title, title_callback, title_arguments,
-      type, block_callback, description, position, weight, file)
-      VALUES ('%s', '%s', '%s', '%s',
-      '%s', '%s', '%s', %d,
-      %d, '%s', '%s',
-      '%s', '%s', '%s',
-      %d, '%s', '%s', '%s', %d, '%s')",
-      $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'],
-      serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'],
-      $item['_number_parts'], $item['tab_parent'], $item['tab_root'],
-      $item['title'], $item['title callback'], $title_arguments,
-      $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']);
-  }
-  // Sort the masks so they are in order of descending fit, and store them.
-  $masks = array_keys($masks);
-  rsort($masks);
+  
+  // calculate new table contents
+  $rows_new = _menu_router_build_calc_rows($menu);
+  
+  // compare with old table contents
+  if (!is_object($changes)) {
+    $changes = new stdClass;
+  }
+  $rows_old = _menu_router_build_load_rows();
+  _menu_router_build_calc_changes($rows_old, $rows_new, $changes);
+  
+  // save changes
+  _menu_router_build_save_insert($changes->insert);
+  _menu_router_build_save_update($changes->update);
+  _menu_router_build_save_delete($changes->delete);
+  
+  // save the masks
   variable_set('menu_masks', $masks);
-  cache_set('router:', $menu, 'cache_menu');
+  
   return $menu;
 }
 
+
+
 /**
  * Returns TRUE if a path is external (e.g. http://example.com).
  */
--- menu.rebuild_links.inc
+++ menu.rebuild_links.inc
@@ -0,0 +1,416 @@
+<?php
+
+
+/**
+ * Build menu links for the items in the menu router.
+ * The information in $changes allows a more fine-grained update.
+ * 
+ * TODO: Use the info in $changes, update only what has changed
+ * 
+ * @param $menu array
+ * @param $changes object with $insert, $update, $delete arrays
+ */
+function _menu_links_build_rebuild($menu, $changes = NULL) {
+  // Add normal and suggested items as links.
+  $menu_links = array();
+  $link_paths = array();
+  foreach ($menu as $path => $item) {
+    if ($item['_visible']) {
+      $item = _menu_links_build_build_item($item);
+      $menu_links[$path] = $item;
+      $sort[$path] = $item['_number_parts'];
+      $link_paths[] = $item['link_path'];
+    }
+  }
+  // simulating OOP is ugly...
+  $cache = _menu_links_build_load_rows($link_paths);
+  $cache->menu = $menu;
+  $parent_update_buffer = array();
+  if ($menu_links) {
+    // Make sure no child comes before its parent.
+    array_multisort($sort, SORT_NUMERIC, $menu_links);
+
+    foreach ($menu_links as $path => $item) {
+      _menu_links_build_save_item($item, $cache, $parent_update_buffer);
+      // $sql = "SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'";
+    }
+  }
+  
+  // update has_children column
+  _menu_links_build_multi_update_parental_status($parent_update_buffer, $cache);
+  
+  // Updated and customized items whose router paths are gone need new ones.
+  $router_paths = array_keys($menu);
+  _menu_links_build_add_missing_router_paths($router_paths);
+  
+  // Find any item whose router path does not exist any more.
+  _menu_links_build_remove_orphan_items($router_paths);
+}
+
+
+function _menu_links_build_add_missing_router_paths($paths) {
+  $placeholders = db_placeholders($paths, 'varchar');
+  $sql = "SELECT ml.link_path, ml.mlid, ml.router_path, ml.updated FROM {menu_links} ml WHERE ml.updated = 1 OR (router_path NOT IN ($placeholders) AND external = 0 AND customized = 1)";
+  $result = db_query($sql, $paths);
+  while ($item = db_fetch_array($result)) {
+    $router_path = _menu_find_router_path($item['link_path']);
+    if (!empty($router_path) && ($router_path != $item['router_path'] || $item['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_query("UPDATE {menu_links} SET router_path = '%s', updated = %d WHERE mlid = %d", $router_path, $updated, $item['mlid']);
+    }
+  }
+}
+
+
+function _menu_links_build_remove_orphan_items($paths) {
+  $placeholders = db_placeholders($paths, 'varchar');
+  $sql = "SELECT * FROM {menu_links} WHERE router_path NOT IN ($placeholders) AND external = 0 AND updated = 0 AND customized = 0 ORDER BY depth DESC";
+  $result = db_query($sql, $paths);
+  // Remove all such items. Starting from those with the greatest depth will
+  // minimize the amount of re-parenting done by menu_link_delete().
+  while ($item = db_fetch_array($result)) {
+    _menu_delete_item($item, TRUE);
+  }
+}
+
+
+/**
+ * Update parental status for a bunch of parent menu links
+ * 
+ * @param $parent_update_buffer
+ * @param $cache
+ * @return unknown_type
+ */
+function _menu_links_build_multi_update_parental_status($parent_update_buffer, $cache) {
+  foreach ($parent_update_buffer as $plid => $children) {
+    $has_children = false;
+    foreach ($children as $mlid => $hidden) {
+      if (!$hidden) {
+        $has_children = true;
+        break;
+      }
+    }
+    if ($has_children) {
+      if (!$cache->rows[$plid]['has_children']) {
+        db_query("UPDATE {menu_links} SET has_children = %d WHERE mlid = %d", 1, $plid);
+        $cache->rows[$plid]['has_children'] = 1;
+      }
+    } else {
+      // leave as is
+      // TODO: can it ever happen that children are removed?
+    }
+  }
+}
+
+
+/**
+ * Builds a link from a router item.
+ */
+function _menu_links_build_build_item($item) {
+  if ($item['type'] == MENU_CALLBACK) {
+    $item['hidden'] = -1;
+  }
+  elseif ($item['type'] == MENU_SUGGESTED_ITEM) {
+    $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;
+}
+
+
+/**
+ * Save a menu link.
+ *
+ * @param $item array
+ *   An array representing a menu link item.
+ *   drupal_alter has already run.
+ * @param $existing_item array
+ *   Existing menu link with same mlid,
+ *   or FALSE if an existing item does not exist.
+ * @return
+ *   The mlid of the saved menu link, or FALSE if the menu link could not be 
+ *   saved.
+ */
+function _menu_links_build_save_item(&$item, $cache, &$parent_update_buffer) {
+  $existing_item = _menu_links_build_find_existing($item['link_path'], $cache);
+  if ($existing_item) {
+    $item['mlid'] = $existing_item['mlid'];
+    // 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'];
+    }
+    $item['has_children'] = $existing_item['has_children'];
+    $item['updated'] = $existing_item['updated'];
+  }
+  if ($existing_item && $existing_item['customized']) {
+    return;
+  }
+  
+  drupal_alter('menu_link', $item, $menu);
+  
+  $parent = _menu_links_build_find_parent($item, $cache);
+
+  // 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,
+    'link_title' => '',
+    'hidden' => 0,
+    'has_children' => 0,
+    'expanded' => 0,
+    'options' => array(),
+    'module' => 'menu',
+    'customized' => 0,
+    'updated' => 0,
+  );
+  if (is_array($parent)) {
+    $item['menu_name'] = $parent['menu_name'];
+  }
+  // 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.
+  if (empty($parent['mlid']) || $parent['hidden'] < 0) {
+    $item['plid'] =  0;
+  } else {
+    $item['plid'] = $parent['mlid'];
+  }
+  
+  $menu_name = $item['menu_name'];
+
+  if (!$existing_item) {
+    // insert a row
+    $row = _menu_links_build_calc_row_from_item($item);
+    $item['mlid'] = _menu_links_build_db_insert_row($row);
+    $row['mlid'] = $item['mlid'];
+    $cache->mlids[$item['link_path']][] = $item['mlid'];
+    $cache->rows[$item['mlid']] = $row;
+  }
+
+  if (!$item['plid']) {
+    $item['p1'] = $item['mlid'];
+    for ($i = 2; $i <= MENU_MAX_DEPTH; $i++) {
+      $item["p$i"] = 0;
+    }
+    $item['depth'] = 1;
+  } else {
+    // Cannot add beyond the maximum depth.
+    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);
+  }
+  
+  // move_children is never necessary, fortunately!!
+  
+  // Find the callback. During the menu update we store empty paths to be
+  // fixed later, so we skip this.
+  if (!isset($_SESSION['system_update_6021']) && (
+    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_links_build_find_router_path($item['link_path'], $cache);
+    }
+  }
+  // update row
+  $row = _menu_links_build_calc_row_from_item($item);
+  if ($row != $cache->rows[$row['mlid']]) {
+    // only update if something changed
+    _menu_links_build_db_update_row($row);
+    $cache->rows[$row['mlid']] = $row;
+  }
+  
+  // Check the has_children status of the parent.
+  // If plid == 0, there is nothing to update.
+  if ($item['plid']) {
+    $parent_update_buffer[$item['plid']][$item['mlid']] = $item['hidden'];
+  }
+  menu_cache_clear($menu_name);
+  if ($existing_item && $menu_name != $existing_item['menu_name']) {
+    menu_cache_clear($existing_item['menu_name']);
+  }
+
+  _menu_clear_page_cache();
+  return $item['mlid'];
+}
+
+
+/**
+ * Find an existing menu item to the given $link_path
+ * 
+ * @param $link_path
+ * @param $cache
+ * @return unknown_type
+ */
+function _menu_links_build_find_existing($link_path, $cache) {
+  if (isset($cache->mlids[$link_path])) {
+    $mlid = end($cache->mlids[$link_path]);
+    return $cache->rows[$mlid];
+  }
+  return FALSE;
+}
+
+
+/**
+ * Calculate a database row for a given item
+ * 
+ * @param $item
+ * @return unknown_type
+ */
+function _menu_links_build_calc_row_from_item($item) {
+  $row = $item;
+  $row['options'] = serialize($item['options']);
+  $row['external'] = $item['_external'];
+  unset($row['_external']);
+  return $row;
+}
+
+
+function _menu_links_build_db_insert_row($row) {
+  $fields = array_keys($row);
+  $fields = implode(', ', $fields);
+  $placeholders = db_placeholders($args);
+  $args = array_values($row);
+  db_query("INSERT INTO {menu_links} ($fields) ($placeholders)", $args);
+  return db_last_insert_id('menu_links', 'mlid');
+}
+
+
+function _menu_links_build_db_update_row($row) {
+  $mlid = $row['mlid'];
+  unset($row['mlid']);
+  foreach ($row as $k => $v) {
+    if (is_numeric($v)) {
+      $set[] = "$k = '%s'";
+    } else {
+      $set[] = "$k = %s";
+    }
+    $args[] = $v;
+  }
+  $set = implode(', ', $set);
+  $args[] = $mlid;
+  db_query("UPDATE {menu_links} SET $set WHERE mlid = %s", $args);
+}
+
+
+
+/**
+ * Find a parent menu item based on $plid and/or $link_path
+ * 
+ * @param $item
+ * @return array parent menu item
+ */
+function _menu_links_build_find_parent($item, $cache) {
+  if ($item['plid']) {
+    $plid = $item['plid'];
+    if (isset($cache->rows[$plid])) {
+      return $cache->rows[$plid];
+    } else {
+      $row = db_fetch_array(db_query("SELECT * FROM {menu_links} WHERE mlid = %d", $plid));
+      $cache->rows[$plid] = $row;
+    }
+  }
+  $parent_path = $item['link_path'];
+  do {
+    $parent_path = substr($parent_path, 0, strrpos($parent_path, '/'));
+    if (!$parent_path) {
+      break;
+    }
+    if (isset($cache->mlids[$parent_path])) {
+      $mlids = $cache->mlids[$parent_path];
+      if (count($mlids == 1)) {
+        return $cache->rows[end($mlids)];
+      }
+    }
+  } while (TRUE);
+  return FALSE;
+}
+
+
+/**
+ * Load all the rows that are relevant to the update.
+ * 
+ * @return array of rows
+ */
+function _menu_links_build_load_rows($link_paths) {
+  $prefix_lookup = array();
+  foreach ($link_paths as $path) {
+    while ($path) {
+      $prefix_lookup[$path] = true;
+      $path = substr($path, 0, strrpos($parent_path, '/'));
+    }
+  }
+  $cache = new stdClass;
+  $cache->rows = array();
+  $cache->mlids = array();
+  $children = array();
+  $result = db_query("SELECT * FROM {menu_links} WHERE module = 'system' ORDER BY depth DESC");
+  while ($row = db_fetch_array($result)) {
+    if (array_key_exists($row['link_path'], $prefix_lookup) || array_key_exists($children[$row['mlid']])) {
+      $cache->rows[$row['mlid']] = $row;
+      $cache->mlids[$row['link_path']][] = $row['mlid'];
+      if ($row['plid']) {
+        $children[$row['plid']][] = $row['mlid'];
+      }
+    }
+  }
+  return $cache;
+}
+
+
+
+/**
+ * Find the router path which will serve this path.
+ *
+ * @param $link_path
+ *  The path for we are looking up its router path.
+ * @return
+ *  A path from $menu keys or empty if $link_path points to a nonexisting
+ *  place.
+ */
+function _menu_links_build_find_router_path($link_path, $cache) {
+
+  $router_path = $link_path;
+  $parts = explode('/', $link_path, MENU_MAX_PARTS);
+  list($ancestors, $placeholders) = menu_get_ancestors($parts);
+  
+  if (!isset($cache->menu[$router_path])) {
+    // Add an empty path as a fallback.
+    $ancestors[] = '';
+    foreach ($ancestors as $key => $router_path) {
+      if (isset($cache->menu[$router_path])) {
+        // Exit the loop leaving $router_path as the first match.
+        break;
+      }
+    }
+    // If we did not find the path, $router_path will be the empty string
+    // at the end of $ancestors.
+  }
+  return $router_path;
+}
+

--- menu.rebuild_router.inc
+++ menu.rebuild_router.inc
@@ -0,0 +1,351 @@
+<?php
+
+
+/**
+ * Calculate the new content for the menu_router table
+ * 
+ * @param $menu array as generated by _menu_router_build_calc_menu()
+ * @return array the rows, as in db_fetch_array().
+ */
+function _menu_router_build_calc_rows(array $menu) {
+  
+  // Apply inheritance rules.
+  $rows_new = array();
+  foreach ($menu as $path => $v) {
+    $item = &$menu[$path];
+    if (!$item['_tab']) {
+      // Non-tab items.
+      $item['tab_parent'] = '';
+      $item['tab_root'] = $path;
+    }
+    for ($i = $item['_number_parts'] - 1; $i; $i--) {
+      $parent_path = implode('/', array_slice($item['_parts'], 0, $i));
+      if (isset($menu[$parent_path])) {
+
+        $parent = $menu[$parent_path];
+
+        if (!isset($item['tab_parent'])) {
+          // Parent stores the parent of the path.
+          $item['tab_parent'] = $parent_path;
+        }
+        if (!isset($item['tab_root']) && !$parent['_tab']) {
+          $item['tab_root'] = $parent_path;
+        }
+        // If an access callback is not found for a default local task we use
+        // the callback from the parent, since we expect them to be identical.
+        // In all other cases, the access parameters must be specified.
+        if (($item['type'] == MENU_DEFAULT_LOCAL_TASK) && !isset($item['access callback']) && isset($parent['access callback'])) {
+          $item['access callback'] = $parent['access callback'];
+          if (!isset($item['access arguments']) && isset($parent['access arguments'])) {
+            $item['access arguments'] = $parent['access arguments'];
+          }
+        }
+        // Same for page callbacks.
+        if (!isset($item['page callback']) && isset($parent['page callback'])) {
+          $item['page callback'] = $parent['page callback'];
+          if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
+            $item['page arguments'] = $parent['page arguments'];
+          }
+          if (!isset($item['file']) && isset($parent['file'])) {
+            $item['file'] = $parent['file'];
+          }
+          if (!isset($item['file path']) && isset($parent['file path'])) {
+            $item['file path'] = $parent['file path'];
+          }
+        }
+      }
+    }
+    if (!isset($item['access callback']) && isset($item['access arguments'])) {
+      // Default callback.
+      $item['access callback'] = 'user_access';
+    }
+    if (!isset($item['access callback']) || empty($item['page callback'])) {
+      $item['access callback'] = 0;
+    }
+    if (is_bool($item['access callback'])) {
+      $item['access callback'] = intval($item['access callback']);
+    }
+
+    $item += array(
+      'access arguments' => array(),
+      'access callback' => '',
+      'page arguments' => array(),
+      'page callback' => '',
+      'block callback' => '',
+      'title arguments' => array(),
+      'title callback' => 't',
+      'description' => '',
+      'position' => '',
+      'tab_parent' => '',
+      'tab_root' => $path,
+      'path' => $path,
+      'file' => '',
+      'file path' => '',
+      'include file' => '',
+    );
+
+    // Calculate out the file to be included for each callback, if any.
+    if ($item['file']) {
+      $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']);
+      $item['include file'] = $file_path .'/'. $item['file'];
+    }
+
+    $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : '';
+    $rows[$path] = array(
+      'path' => $path,
+      'load_functions' => $item['load_functions'],
+      'to_arg_functions' => $item['to_arg_functions'],
+      'access_callback' => $item['access callback'],
+      'access_arguments' => serialize($item['access arguments']),
+      'page_callback' => $item['page callback'],
+      'page_arguments' => serialize($item['page arguments']),
+      'fit' => $item['_fit'],
+      'number_parts' => $item['_number_parts'],
+      'tab_parent' => $item['tab_parent'],
+      'tab_root' => $item['tab_root'],
+      'title' => $item['title'],
+      'title_callback' => $item['title callback'],
+      'title_arguments' => $item['title arguments'] ? serialize($item['title arguments']) : '',
+      'type' => $item['type'],
+      'block_callback' => $item['block callback'],
+      'description' => $item['description'],
+      'position' => $item['position'],
+      'weight' => $item['weight'],
+      'file' => $item['include file'],
+    );
+  }
+
+  return $rows;
+}
+
+
+/**
+ * Get callbacks from hook_menu implementations
+ * 
+ * @return array callbacks
+ */
+function _menu_router_build_calc_callbacks() {
+  // We need to manually call each module so that we can know which module
+  // a given item came from.
+  $callbacks = array();
+  foreach (module_implements('menu') as $module) {
+    $router_items = call_user_func($module .'_menu');
+    if (isset($router_items) && is_array($router_items)) {
+      foreach (array_keys($router_items) as $path) {
+        $router_items[$path]['module'] = $module;
+      }
+      $callbacks = array_merge($callbacks, $router_items);
+    }
+  }
+  // Alter the menu as defined in modules, keys are like user/%user.
+  drupal_alter('menu', $callbacks);
+  return $callbacks;
+}
+
+/**
+ * Calculate the menu from given callbacks
+ * 
+ * @param $callbacks array collected from hook_menu implementations
+ * @return array($menu, $masks)
+ */
+function _menu_router_build_calc_menu($callbacks) {
+  // First pass: separate callbacks from paths, making paths ready for
+  // matching. Calculate fitness, and fill some default values.
+  $menu = array();
+  foreach ($callbacks as $path => $item) {
+    $load_functions = array();
+    $to_arg_functions = array();
+    $fit = 0;
+    $move = FALSE;
+
+    $parts = explode('/', $path, MENU_MAX_PARTS);
+    $number_parts = count($parts);
+    // We store the highest index of parts here to save some work in the fit
+    // calculation loop.
+    $slashes = $number_parts - 1;
+    // Extract load and to_arg functions.
+    foreach ($parts as $k => $part) {
+      $match = FALSE;
+      // Look for wildcards in the form allowed to be used in PHP functions,
+      // because we are using these to construct the load function names.
+      // See http://php.net/manual/en/language.functions.php for reference.
+      if (preg_match('/^%(|[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)$/', $part, $matches)) {
+        if (empty($matches[1])) {
+          $match = TRUE;
+          $load_functions[$k] = NULL;
+        }
+        else {
+          if (function_exists($matches[1] .'_to_arg')) {
+            $to_arg_functions[$k] = $matches[1] .'_to_arg';
+            $load_functions[$k] = NULL;
+            $match = TRUE;
+          }
+          if (function_exists($matches[1] .'_load')) {
+            $function = $matches[1] .'_load';
+            // Create an array of arguments that will be passed to the _load
+            // function when this menu path is checked, if 'load arguments'
+            // exists.
+            $load_functions[$k] = isset($item['load arguments']) ? array($function => $item['load arguments']) : $function;
+            $match = TRUE;
+          }
+        }
+      }
+      if ($match) {
+        $parts[$k] = '%';
+      }
+      else {
+        $fit |=  1 << ($slashes - $k);
+      }
+    }
+    if ($fit) {
+      $move = TRUE;
+    }
+    else {
+      // If there is no %, it fits maximally.
+      $fit = (1 << $number_parts) - 1;
+    }
+    $masks[$fit] = 1;
+    $item['load_functions'] = empty($load_functions) ? '' : serialize($load_functions);
+    $item['to_arg_functions'] = empty($to_arg_functions) ? '' : serialize($to_arg_functions);
+    $item += array(
+      'title' => '',
+      'weight' => 0,
+      'type' => MENU_NORMAL_ITEM,
+      '_number_parts' => $number_parts,
+      '_parts' => $parts,
+      '_fit' => $fit,
+    );
+    $item += array(
+      '_visible' => (bool)($item['type'] & MENU_VISIBLE_IN_BREADCRUMB),
+      '_tab' => (bool)($item['type'] & MENU_IS_LOCAL_TASK),
+    );
+    if ($move) {
+      $new_path = implode('/', $item['_parts']);
+      $menu[$new_path] = $item;
+      $sort[$new_path] = $number_parts;
+    }
+    else {
+      $menu[$path] = $item;
+      $sort[$path] = $number_parts;
+    }
+  }
+  array_multisort($sort, SORT_NUMERIC, $menu);
+  
+  // Sort the masks so they are in order of descending fit.
+  $masks = array_keys($masks);
+  rsort($masks);
+  
+  return array($menu, $masks);
+}
+
+
+/**
+ * Find the changes to be stored in the menu_router table
+ * 
+ * @param $rows_old
+ * @param $rows_new
+ * @param $changes object to collect $insert, $update, $delete arrays
+ * @return $changes object
+ */
+function _menu_router_build_calc_changes($rows_old, $rows_new, $changes = NULL) {
+  if (!is_object($changes)) {
+    $changes = new stdClass;
+  }
+  $changes->insert = array();
+  $changes->update = array();
+  $changes->delete = array();
+  foreach ($rows_new as $path => $row_new) {
+    if (!isset($rows_old[$path])) {
+      $changes->insert[$path] = $row_new;
+    } else {
+      $row_old = $rows_old[$path];
+      $changes = array();
+      foreach ($row_new as $key => $value) {
+        if ($value != $row_old[$key]) {
+          $changes[$key] = $value;
+        }
+      }
+      if (!empty($changes)) {
+        $changes->update[$path] = $changes;
+      }
+      unset($rows_old[$path]);
+    }
+  }
+  // delete remaining rows
+  foreach ($rows_old as $path => $row_old) {
+    $changes->delete[$path] = $path;
+  }
+  return $changes;
+}
+
+
+/**
+ * Load the old contents of menu_router
+ * 
+ * @return array rows as in db_fetch_array, keyed by $path
+ */
+function _menu_router_build_load_rows() {
+  $rows = array();
+  $result = db_query("SELECT * FROM {menu_router}");
+  while ($row = db_fetch_array($result)) {
+    $rows[$row['path']] = $row; 
+  }
+  return $rows;
+}
+
+/**
+ * Execute INSERT queries
+ * 
+ * @param $insert array of rows to insert
+ */
+function _menu_router_build_save_insert($insert) {
+  // TODO: ON DUPLICATE KEY UPDATE ?
+  if (count($insert)) {
+    // the $sql is always the same
+    $example_row = end($insert);
+    foreach ($example_row as $k => $v) {
+      $fields[] = $k;
+      $tokens[] = "'%s'";
+    }
+    $fields = implode(', ', $fields);
+    $tokens = implode(', ', $tokens);
+    $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)";
+    // insert the rows
+    foreach ($insert as $row) {
+      $args = array_values($row);
+      array_unshift($args, $sql);
+      call_user_func_array('db_query', $args);
+    }
+  }
+}
+
+/**
+ * Execute UPDATE queries
+ * 
+ * @param $update array of rows with modifications
+ */
+function _menu_router_build_save_update($update) {
+  foreach ($update as $changes) {
+    foreach ($changes as $k => $v) {
+      $set[] = "$k = %s";
+    }
+    $set = implode(', ', $set);
+    $sql = "UPDATE {menu_router} SET $set";
+    $args = array_values($changes);
+    array_unshift($args, $sql);
+    call_user_func_array('db_query', $args);
+  }
+}
+
+/**
+ * Execute DELETE queries
+ * 
+ * @param $delete array of paths (PRIMARY KEY) to delete
+ */
+function _menu_router_build_save_delete($delete) {
+  $sql = "DELETE FROM {menu_router} WHERE path = '%s'";
+  foreach ($delete as $path) {
+    db_query($sql, $path);
+  }
+}
+


