Index: includes/menu.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/menu.inc,v
retrieving revision 1.255.2.37
diff -u -p -r1.255.2.37 menu.inc
--- includes/menu.inc	4 Nov 2010 09:58:34 -0000	1.255.2.37
+++ includes/menu.inc	30 Nov 2010 16:02:48 -0000
@@ -1690,7 +1690,7 @@ function menu_cache_clear_all() {
  * menu_execute_active_handler(), because the maintenance page environment
  * is different and leaves stale data in the menu tables.
  */
-function menu_rebuild() {
+function menu_rebuild($full = FALSE) {
   if (!lock_acquire('menu_rebuild')) {
     // Wait for another request that is already doing this work.
     // We choose to block here since otherwise the router item may not 
@@ -1699,12 +1699,17 @@ function menu_rebuild() {
     return FALSE;
   }
 
-  $menu = menu_router_build(TRUE);
+  $menu = menu_router_build(TRUE, $full);
   _menu_navigation_links_rebuild($menu);
   // Clear the menu, page and block caches.
   menu_cache_clear_all();
   _menu_clear_page_cache();
   
+  $checksums = _menu_router_build_checksum_cache();
+  if (!empty($checksums)) {
+    cache_set('menu-rebuild:router-checksums', $checksums, 'cache_menu');
+  }
+
   if (defined('MAINTENANCE_MODE')) {
     variable_set('menu_rebuild_needed', TRUE);
   }
@@ -1716,9 +1721,22 @@ function menu_rebuild() {
 }
 
 /**
+ * Helper function to store the cached menu checksums.
+ */
+function _menu_router_build_checksum_cache($checksums = NULL) {
+  static $checksum_cache = array();
+
+  if (isset($checksums)) {
+    $checksum_cache = $checksums;
+  }
+
+  return $checksum_cache;
+}
+
+/**
  * Collect, alter and store the menu definitions.
  */
-function menu_router_build($reset = FALSE) {
+function menu_router_build($reset = FALSE, $full = FALSE) {
   static $menu;
 
   if (!isset($menu) || $reset) {
@@ -1737,6 +1755,212 @@ function menu_router_build($reset = FALS
     // Alter the menu as defined in modules, keys are like user/%user.
     drupal_alter('menu', $callbacks);
     $menu = _menu_router_build($callbacks);
+
+    // We no longer need the callbacks.
+    unset($callbacks, $router_items, $module, $path);
+
+    // Get the old menu from the cache if we can, otherwise assume an initial build
+    // and empty menu_router.
+    $menu_old_checksums = array(
+      'router' => array(),
+      'links' => array(),
+    );
+    $flag_crud = TRUE;
+    if (!$full && $data = cache_get('menu-rebuild:router-checksums', 'cache_menu')) {
+      $menu_old_checksums = $data->data;
+
+      // Unset $data to save on memory usage.
+      unset($data);
+
+      // Empty the cache immediatly in case there is an error whilst processing changes
+      // so we don't get left in a indeterminant state.
+      cache_clear_all('menu-rebuild:router-checksums', 'cache_menu');
+    }
+    else {
+      db_query('TRUNCATE TABLE {menu_router}');
+
+      // We can only do CRUD flagging when using the cached router entries.
+      $flag_crud = FALSE;
+    }
+
+    // Set up a database map.
+    $db_map = array(
+      'path' => 'path',
+      'load_functions' => 'load_functions',
+      'to_arg_functions' => 'to_arg_functions',
+      'access callback' => 'access_callback',
+      'access arguments' => 'access_arguments',
+      'page callback' => 'page_callback',
+      'page arguments' => 'page_arguments',
+      '_fit' => 'fit',
+      '_number_parts' => 'number_parts',
+      'tab_parent' => 'tab_parent',
+      'tab_root' => 'tab_root',
+      'title' => 'title',
+      'title callback' => 'title_callback',
+      'title arguments' => 'title_arguments',
+      'type' => 'type',
+      'block callback' => 'block_callback',
+      'description' => 'description',
+      'position' => 'position',
+      'weight' => 'weight',
+      'include file' => 'file',
+    );
+
+    // Create the new menu checksums.
+    $menu_checksums = array();
+    $menu_checksums_full = array();
+    foreach (array_keys($menu) as $path) {
+      // Only include the items we care about.
+      $menu_checksums[$path] = md5(serialize(array(
+        'path' => $menu[$path]['path'],
+        'load_functions' => $menu[$path]['load_functions'],
+        'to_arg_functions' => $menu[$path]['to_arg_functions'],
+        'access callback' => $menu[$path]['access callback'],
+        'access arguments' => $menu[$path]['access arguments'],
+        'page callback' => $menu[$path]['page callback'],
+        'page arguments' => $menu[$path]['page arguments'],
+        '_fit' => $menu[$path]['_fit'],
+        '_number_parts' => $menu[$path]['_number_parts'],
+        'tab_parent' => $menu[$path]['tab_parent'],
+        'tab_root' => $menu[$path]['tab_root'],
+        'title' => $menu[$path]['title'],
+        'title callback' => $menu[$path]['title callback'],
+        'title arguments' => $menu[$path]['title arguments'],
+        'type' => $menu[$path]['type'],
+        'block callback' => $menu[$path]['block callback'],
+        'description' => $menu[$path]['description'],
+        'position' => $menu[$path]['position'],
+        'weight' => $menu[$path]['weight'],
+        'include file' => $menu[$path]['include file'],
+      )));
+
+      // Sort the item to preserve key order and keep the md5 as clean as possible.
+      ksort($menu[$path]);
+      $menu_checksums_full[$path] = md5(serialize($menu[$path]));
+
+      // Handle CRUD flagging.
+      $menu[$path]['_crud'] = $flag_crud;
+    }
+
+    // Cacluate the changes.
+    $changes_insert = array_diff(array_keys($menu_checksums), array_keys($menu_old_checksums['router']));
+    $changes_delete = array_diff(array_keys($menu_old_checksums['router']), array_keys($menu_checksums));
+
+    // To calculate the update changes, we must first remove any inserted keys.
+    // We do this on a copy so that we can cache the original later.
+    $menu_checksums_copy = $menu_checksums;
+    foreach ($changes_insert as $path) {
+      unset($menu_checksums_copy[$path]);
+    }
+
+    $changes_update = array_keys(array_diff_assoc($menu_checksums_copy, $menu_old_checksums['router']));
+
+    // Check for any paths that are not to be inserted, updated or deleted, that might have changed outside of
+    // the router. This will signify paths that need to be updated in the navigation links rebuild.
+    $menu_checksums_full_copy = $menu_checksums_full;
+    foreach (array_merge($changes_insert, $changes_delete, $changes_update) as $path) {
+      unset($menu_checksums_full_copy[$path]);
+    }
+    $changes_full_update = array_keys(array_diff_assoc($menu_checksums_full_copy, $menu_old_checksums['links']));
+    foreach ($changes_full_update as $path) {
+      $menu[$path]['_updated_link'] = TRUE;
+    }
+
+    // We no longer need $menu_old_checksumes or the copy.
+    unset($menu_old_checksums, $menu_checksums_copy, $menu_checksums_full_copy, $changes_full_update);
+
+    // Perform the inserts.
+    if (!empty($changes_insert)) {
+      // The SQL is always the same, only the values change.
+
+      // Map fields from the router build to the db equivelant.
+      $fields = array_values($db_map);
+      $value_fields = array_keys($db_map);
+
+      $tokens = db_placeholders($fields, 'varchar');
+      $fields = implode(', ', $fields);
+      $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)";
+
+      foreach ($changes_insert as $path) {
+        // Map values from the router build to the db equivalent.
+        $values = array();
+        foreach ($value_fields as $mfield) {
+          $value = $menu[$path][$mfield];
+          if ($mfield == 'access arguments' || $mfield == 'page arguments') {
+            $values[] = serialize($value);
+          }
+          elseif ($mfield == 'title arguments') {
+            $values[] = $value ? serialize($value) : '';
+          }
+          elseif ($mfield == 'path') {
+            // Make sure that we insert the correct path.
+            $values[] = $path;
+          }
+          else {
+            $values[] = $value;
+          }
+        }
+
+        db_query($sql, $values);
+
+        // Flag this item as being inserted.
+        $menu[$path]['_inserted'] = TRUE;
+      }
+    }
+
+    // We no longer need the insert changes.
+    unset($changes_insert, $fields, $value_fields, $tokens, $sql, $path, $mfield, $value, $values);
+
+    // Perform the updates.
+    foreach ($changes_update as $path) {
+      $set = $args = array();
+      foreach ($menu[$path] as $mfield => $value) {
+        if (isset($db_map[$mfield]) && $mfield != 'path') {
+          $set[] = $db_map[$mfield] ." = '%s'";
+          if ($mfield == 'access arguments' || $mfield == 'page arguments') {
+            $args[] = serialize($value);
+          }
+          elseif ($mfield == 'title arguments') {
+            $args[] = $value ? serialize($value) : '';
+          }
+          else {
+            $args[] = $value;
+          }
+        }
+      }
+
+      if (!empty($set)) {
+        $set = implode(', ', $set);
+        $sql = "UPDATE {menu_router} SET $set WHERE path = '%s'";
+
+        $args[] = $path;
+        db_query($sql, $args);
+
+        // Flag this item as being updated.
+        $menu[$path]['_updated'] = TRUE;
+      }
+    }
+
+    // We no longer need the update changes.
+    unset($changes_update, $path, $set, $args, $db_map, $mfield, $value, $sql);
+
+    // Perform the deletes.
+    $sql = "DELETE FROM {menu_router} WHERE path = '%s'";
+    foreach ($changes_delete as $path) {
+      db_query($sql, $path);
+
+      // Flag this item as being deleted.
+      $menu[$path]['_deleted'] = TRUE;
+    }
+
+    // We no longer need the delete changes.
+    unset($changes_delete, $path, $sql);
+
+    // Store the new rows in a cache for use as old rows on next rebuild.
+    // We delay the storage of this cache until after the main menu cache is cleared.
+    _menu_router_build_checksum_cache(array('router' => $menu_checksums, 'links' => $menu_checksums_full));
+
     _menu_router_cache($menu);
   }
   return $menu;
@@ -1782,19 +2006,42 @@ function _menu_link_build($item) {
  */
 function _menu_navigation_links_rebuild($menu) {
   // Add normal and suggested items as links.
-  $menu_links = array();
+  $menu_links = $inserted = $updated = $deleted = array();
+  $crud = FALSE;
   foreach ($menu as $path => $item) {
+    // Check if we can do CRUD actions.
+    if ($item['_crud']) {
+      $crud = TRUE;
+    }
+
     if ($item['_visible']) {
       $item = _menu_link_build($item);
       $menu_links[$path] = $item;
       $sort[$path] = $item['_number_parts'];
+
+      if (!empty($item['_inserted'])) {
+        $inserted[] = $path;
+      }
+      elseif (!empty($item['_updated']) || !empty($item['_updated_link'])) {
+        $updated[$path] = $item['_number_parts'];
+      }
+      elseif (!empty($item['_deleted'])) {
+        $deleted[] = $path;
+      }
     }
   }
-  if ($menu_links) {
+
+  if ($crud) {
+    // Only router items that have changed should be affected.
+    foreach ($inserted as $path) {
+      menu_link_save($menu_links[$path]);
+    }
+
     // Make sure no child comes before its parent.
-    array_multisort($sort, SORT_NUMERIC, $menu_links);
+    asort($updated, SORT_NUMERIC);
 
-    foreach ($menu_links as $item) {
+    foreach ($updated as $path => $number_parts) {
+      $item = $menu_links[$path];
       $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system'));
       if ($existing_item) {
         $item['mlid'] = $existing_item['mlid'];
@@ -1811,6 +2058,29 @@ function _menu_navigation_links_rebuild(
       }
     }
   }
+  else {
+    if ($menu_links) {
+      // Make sure no child comes before its parent.
+      array_multisort($sort, SORT_NUMERIC, $menu_links);
+
+      foreach ($menu_links as $item) {
+        $existing_item = db_fetch_array(db_query("SELECT mlid, menu_name, plid, customized, has_children, updated FROM {menu_links} WHERE link_path = '%s' AND module = '%s'", $item['link_path'], 'system'));
+        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']) {
+          menu_link_save($item);
+        }
+      }
+    }
+  }
   $placeholders = db_placeholders($menu, 'varchar');
   $paths = array_keys($menu);
   // Updated and customized items whose router paths are gone need new ones.
@@ -2358,8 +2628,7 @@ function _menu_router_build($callbacks) 
     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];
@@ -2439,24 +2708,6 @@ function _menu_router_build($callbacks) 
       $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);
