Index: admin_menu.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.js,v
retrieving revision 1.7.2.7.2.4
diff -u -p -r1.7.2.7.2.4 admin_menu.js
--- admin_menu.js	26 Mar 2009 23:11:23 -0000	1.7.2.7.2.4
+++ admin_menu.js	29 Mar 2009 03:32:56 -0000
@@ -1,20 +1,47 @@
 /* $Id: admin_menu.js,v 1.7.2.7.2.4 2009/03/26 23:11:23 sun Exp $ */
 
-Drupal.adminMenu = Drupal.adminMenu || {};
+Drupal.admin = Drupal.admin || { behaviors: {} };
 
 /**
  * Core behavior for Administration menu.
  *
- * This tests whether there is an administration menu is in the output and
- * executes all registered behaviors.
+ * Test whether there is an administration menu is in the output and execute all
+ * registered behaviors.
  */
 Drupal.behaviors.adminMenu = function (context) {
-  var $adminMenu = $('#admin-menu');
-  if ($adminMenu.size()) {
-    $.each(Drupal.adminMenu, function() {
-      this(context, $adminMenu);
+  // Initialize settings.
+  Drupal.settings.admin_menu = $.extend({
+    suppress: false,
+    margin_top: false,
+    position_fixed: false,
+    tweak_modules: false,
+    tweak_tabs: false,
+    destination: '',
+    basePath: Drupal.settings.basePath,
+    hash: 0
+  }, Drupal.settings.admin_menu || {});
+  // Check whether administration menu should be suppressed.
+  if (Drupal.settings.admin_menu.suppress) {
+    return;
+  }
+  var $adminMenu = $('#admin-menu:not(.admin-menu-processed)', context);
+  // Client-side caching; if administration menu is not in the output, it is
+  // fetched from the server and cached in the browser.
+  if (!$adminMenu.length && Drupal.settings.admin_menu.hash) {
+    Drupal.admin.getCache(Drupal.settings.admin_menu.hash, function (response) {
+      if (typeof response == 'string' && response.length > 0) {
+        $('body', context).prepend(response);
+      }
+      var $adminMenu = $('#admin-menu:not(.admin-menu-processed)', context);
+      // Apply our behaviors.
+      Drupal.admin.attachBehaviors(context, $adminMenu);
     });
   }
+  // If the menu is in the output already, this means there is a new version.
+  else {
+    // Apply our behaviors.
+    Drupal.admin.attachBehaviors(context, $adminMenu);
+  }
 };
 
 /**
@@ -23,25 +50,65 @@ Drupal.behaviors.adminMenu = function (c
  * For why multiple selectors see #111719.
  */
 Drupal.behaviors.adminMenuCollapseModules = function (context) {
-  if (Drupal.settings.admin_menu && Drupal.settings.admin_menu.tweak_modules) {
+  if (Drupal.settings.admin_menu.tweak_modules) {
     $('#system-modules fieldset:not(.collapsed), #system-modules-1 fieldset:not(.collapsed)', context).addClass('collapsed');
   }
 };
 
 /**
- * Apply 'margin-top'; directly applying marginTop does not work in IE.
+ * Apply margin to page.
+ *
+ * Note that directly applying marginTop does not work in IE. To prevent
+ * flickering/jumping page content with client-side caching, this is a regular
+ * Drupal behavior.
  */
-Drupal.adminMenu.marginTop = function (context, $adminMenu) {
-  if (Drupal.settings.admin_menu && Drupal.settings.admin_menu.margin_top) {
+Drupal.behaviors.adminMenuMarginTop = function (context) {
+  if (Drupal.settings.admin_menu.margin_top) {
     $('body', context).addClass('admin-menu');
   }
 };
 
 /**
+ * Retrieve content from client-side cache.
+ *
+ * @param hash
+ *   The md5 hash of the content to retrieve.
+ * @param onSuccess
+ *   A callback function invoked when the cache request was successful.
+ */
+Drupal.admin.getCache = function (hash, onSuccess) {
+  $.ajax({
+    cache: true,
+    type: 'GET',
+    dataType: 'text', // Prevent auto-evaluation of response.
+    global: false, // Do not trigger global AJAX events.
+    url: Drupal.settings.admin_menu.basePath + 'js/admin_menu/cache/' + hash,
+    success: onSuccess
+  });
+}
+
+/**
+ * @defgroup admin_behaviors Administration behaviors.
+ * @{
+ */
+
+/**
+ * Attach administrative behaviors.
+ */
+Drupal.admin.attachBehaviors = function (context, $adminMenu) {
+  if ($adminMenu.length) {
+    $adminMenu.addClass('admin-menu-processed');
+    $.each(Drupal.admin.behaviors, function() {
+      this(context, $adminMenu);
+    });
+  }
+};
+
+/**
  * Apply 'position: fixed'.
  */
-Drupal.adminMenu.positionFixed = function (context, $adminMenu) {
-  if (Drupal.settings.admin_menu && Drupal.settings.admin_menu.position_fixed) {
+Drupal.admin.behaviors.positionFixed = function (context, $adminMenu) {
+  if (Drupal.settings.admin_menu.position_fixed) {
     $adminMenu.css('position', 'fixed');
   }
 };
@@ -49,8 +116,8 @@ Drupal.adminMenu.positionFixed = functio
 /**
  * Move page tabs into administration menu.
  */
-Drupal.adminMenu.pageTabs = function (context, $adminMenu) {
-  if (Drupal.settings.admin_menu && Drupal.settings.admin_menu.tweak_tabs) {
+Drupal.admin.behaviors.pageTabs = function (context, $adminMenu) {
+  if (Drupal.settings.admin_menu.tweak_tabs) {
     $('ul.tabs.primary li', context).addClass('admin-menu-tab').appendTo('#admin-menu > ul');
     $('ul.tabs.secondary', context).appendTo('#admin-menu > ul > li.admin-menu-tab.active');
     $('ul.tabs.primary', context).remove();
@@ -60,8 +127,8 @@ Drupal.adminMenu.pageTabs = function (co
 /**
  * Inject destination query strings for current page.
  */
-Drupal.adminMenu.destination = function (context, $adminMenu) {
-  if (Drupal.settings.admin_menu && Drupal.settings.admin_menu.destination) {
+Drupal.admin.behaviors.destination = function (context, $adminMenu) {
+  if (Drupal.settings.admin_menu.destination) {
     $('.admin-menu-destination', $adminMenu).each(function() {
       this.search += (!this.search.length ? '?' : '&') + Drupal.settings.admin_menu.destination;
     });
@@ -74,7 +141,7 @@ Drupal.adminMenu.destination = function 
  * @todo This has to run last.  If another script registers additional behaviors
  *   it will not run last.
  */
-Drupal.adminMenu.hover = function (context, $adminMenu) {
+Drupal.admin.behaviors.hover = function (context, $adminMenu) {
   // Hover emulation for IE 6.
   if ($.browser.msie && parseInt(jQuery.browser.version) == 6) {
     $('li', $adminMenu).hover(function() {
@@ -101,3 +168,7 @@ Drupal.adminMenu.hover = function (conte
   });
 };
 
+/**
+ * @} End of "defgroup admin_behaviors".
+ */
+
Index: admin_menu.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/admin_menu/admin_menu.module,v
retrieving revision 1.43.2.17.2.2
diff -u -p -r1.43.2.17.2.2 admin_menu.module
--- admin_menu.module	8 Mar 2009 02:01:15 -0000	1.43.2.17.2.2
+++ admin_menu.module	29 Mar 2009 03:36:38 -0000
@@ -50,6 +50,13 @@ function admin_menu_theme() {
  * Implementation of hook_menu().
  */
 function admin_menu_menu() {
+  // AJAX callback.
+  $items['js/admin_menu/cache'] = array(
+    'page callback' => 'admin_menu_js_cache',
+    'access arguments' => array('access administration menu'),
+    'type' => MENU_CALLBACK,
+  );
+  // Module settings.
   $items['admin/settings/admin_menu'] = array(
     'title' => 'Administration menu',
     'description' => 'Adjust administration menu settings.',
@@ -58,6 +65,7 @@ function admin_menu_menu() {
     'access arguments' => array('administer site configuration'),
     'file' => 'admin_menu.inc',
   );
+  // Menu link callbacks.
   $items['admin_menu/toggle-modules'] = array(
     'page callback' => 'admin_menu_toggle_modules',
     'access arguments' => array('administer site configuration'),
@@ -82,28 +90,43 @@ function admin_menu_menu() {
  * all pages via hook_init().
  */
 function admin_menu_init() {
-  if (user_access('access administration menu')) {
-    $path = drupal_get_path('module', 'admin_menu');
-    drupal_add_css($path . '/admin_menu.css', 'module', 'all', FALSE);
-    // Performance: Defer execution.
-    drupal_add_js($path . '/admin_menu.js', 'module', 'header', TRUE);
+  if (!user_access('access administration menu')) {
+    return;
+  }
+  global $user, $language;
 
-    // The destination query string for the current page is applied via JS.
-    drupal_add_js(array('admin_menu' => array('destination' => drupal_get_destination())), 'setting');
+  $path = drupal_get_path('module', 'admin_menu');
+  drupal_add_css($path . '/admin_menu.css', 'module', 'all', FALSE);
+  // Performance: Defer execution.
+  drupal_add_js($path . '/admin_menu.js', 'module', 'header', TRUE);
 
-    if ($setting = variable_get('admin_menu_margin_top', 1)) {
-      drupal_add_js(array('admin_menu' => array('margin_top' => $setting)), 'setting');
-    }
-    if ($setting = variable_get('admin_menu_position_fixed', 0)) {
-      drupal_add_js(array('admin_menu' => array('position_fixed' => $setting)), 'setting');
-    }
-    if ($setting = variable_get('admin_menu_tweak_tabs', 0)) {
-      drupal_add_js(array('admin_menu' => array('tweak_tabs' => $setting)), 'setting');
-    }
-    if ($_GET['q'] == 'admin/build/modules') {
-      drupal_add_js(array('admin_menu' => array('tweak_modules' => variable_get('admin_menu_tweak_modules', 0))), 'setting');
-    }
+  // Destination query strings are applied via JS.
+  $settings['destination'] = drupal_get_destination();
+
+  // Hash for client-side HTTP/AJAX caching.
+  $hash_key = 'admin_menu_hash_' . $language->language;
+  if (!empty($_COOKIE['has_js']) && !empty($GLOBALS['user']->{$hash_key})) {
+    $settings['hash'] = $GLOBALS['user']->{$hash_key};
+    // The base path to use for cache requests depends on whether clean URLs
+    // are enabled and on the language system configuration. url() provides us
+    // the proper path.
+    $settings['basePath'] = url('') . '/';
+  }
+
+  if ($setting = variable_get('admin_menu_margin_top', 1)) {
+    $settings['margin_top'] = $setting;
+  }
+  if ($setting = variable_get('admin_menu_position_fixed', 0)) {
+    $settings['position_fixed'] = $setting;
   }
+  if ($setting = variable_get('admin_menu_tweak_tabs', 0)) {
+    $settings['tweak_tabs'] = $setting;
+  }
+  if ($_GET['q'] == 'admin/build/modules') {
+    $settings['tweak_modules'] = variable_get('admin_menu_tweak_modules', 0);
+  }
+
+  drupal_add_js(array('admin_menu' => $settings), 'setting');
 }
 
 /**
@@ -120,8 +143,10 @@ function admin_menu_init() {
  */
 function admin_menu_suppress($set = TRUE) {
   static $suppress = FALSE;
-  if (!empty($set)) {
+  // drupal_add_js() must only be invoked once.
+  if (!empty($set) && $suppress === FALSE) {
     $suppress = TRUE;
+    drupal_add_js(array('admin_menu' => array('suppress' => 1)), 'setting');
   }
   return $suppress;
 }
@@ -139,34 +164,102 @@ function admin_menu_footer($main = 0) {
   }
   global $user, $language;
 
+  // Determine whether we need to rebuild.
+  $rebuild = variable_get('admin_menu_rebuild_links', FALSE);
+  $hash_key = 'admin_menu_hash_' . $language->language;
+
+  // Do nothing at all here if the client supports client-side caching, no
+  // rebuild is needed, the user has a hash, and is NOT requesting the cache
+  // update path.
+  // @todo Implement a sanity-check to prevent permanent double requests; i.e.
+  //   what if the client-side cache fails for any reason and performs a second
+  //   request on every page?
+  if (!empty($_COOKIE['has_js']) && !$rebuild && !empty($user->{$hash_key}) && strpos($_GET['q'], 'js/admin_menu/cache') !== 0) {
+    return;
+  }
+
   $cid = 'admin_menu:' . $user->uid . ':' . $language->language;
 
-  // Check for the flag indicating that we need to rebuild.
-  if (variable_get('admin_menu_rebuild_links', FALSE)) {
+  // Check for the flag indicating that we need to rebuild the menu.
+  if ($rebuild) {
     module_load_include('inc', 'admin_menu');
     _admin_menu_rebuild_links();
     variable_del('admin_menu_rebuild_links');
   }
-  // Try to load and output administration menu from cache.
+  // Try to load and output administration menu from server-side cache.
   else {
     $cache = cache_get($cid, 'cache_menu');
     if ($cache && isset($cache->data)) {
-      return $cache->data;
+      $content = $cache->data;
     }
   }
 
   // Rebuild the output.
-  $content  = '<div id="admin-menu">';
-  $content .= admin_menu_tree_output(menu_tree_all_data('admin_menu'));
-  $content .= '</div>';
+  if (!isset($content)) {
+    $content  = '<div id="admin-menu">';
+    $content .= admin_menu_tree_output(menu_tree_all_data('admin_menu'));
+    $content .= '</div>';
+  
+    // Cache the menu for this user.
+    cache_set($cid, $content, 'cache_menu');
+  }
 
-  // Cache the menu for this user.
-  cache_set($cid, $content, 'cache_menu');
+  // Store the new hash for this user.
+  // @todo Use an own storage for our hashs, so we can cache sub-menus as well.
+  if (!empty($_COOKIE['has_js'])) {
+    user_save($user, array($hash_key => md5($content)));
+  }
 
   return $content;
 }
 
 /**
+ * Implementation of hook_js().
+ */
+function admin_menu_js() {
+  return array(
+    'cache' => array(
+      'callback' => 'admin_menu_js_cache',
+      'dependencies' => array('user'),
+    ),
+  );
+}
+
+/**
+ * Menu callback; Output administration menu for HTTP caching via AJAX request.
+ */
+function admin_menu_js_cache($hash = NULL) {
+  // Fetch the menu.
+  $content = admin_menu_footer();
+
+  // Determine if the client accepts gzipped data.
+  if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && function_exists('gzencode')) {
+    if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) {
+      $encoding = 'gzip';
+    }
+    elseif (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== FALSE) {
+      $encoding = 'x-gzip';
+    }
+    if (!empty($encoding)) {
+      header('Vary: Accept-Encoding');
+      header('Content-Encoding: ' . $encoding);
+      $content = gzencode($content, 9, FORCE_GZIP);
+    }
+  }
+
+  $expires = time() + (3600 * 24 * 365);
+  header('Expires: ' . gmdate('D, d M Y H:i:s', $expires) . ' GMT');
+  header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
+  header('Cache-Control: max-age=' . $expires);
+  header('Content-Length: ' . strlen($content));
+
+  // Suppress Devel module.
+  $GLOBALS['devel_shutdown'] = FALSE;
+  echo $content;
+  exit;
+}
+
+/**
  * Return a rendered menu tree.
  *
  * @param $tree
