Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.387
diff -u -p -r1.387 bootstrap.inc
--- includes/bootstrap.inc	16 May 2010 09:39:31 -0000	1.387
+++ includes/bootstrap.inc	18 May 2010 01:10:40 -0000
@@ -1058,6 +1058,10 @@ function drupal_page_header() {
  * response is sent.
  */
 function drupal_serve_page_from_cache(stdClass $cache) {
+  // Restore the metadata that was cached with the page.
+  $_GET['q'] = $cache->data['path'];
+  drupal_set_title($cache->data['title'], PASS_THROUGH);
+
   // Negotiate whether to use compression.
   $page_compression = variable_get('page_compression', TRUE) && extension_loaded('zlib');
   $return_compressed = $page_compression && isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE;
@@ -1069,14 +1073,14 @@ function drupal_serve_page_from_cache(st
   // drupal_add_http_headers(). Keys are mixed-case.
   $default_headers = array();
 
-  foreach ($cache->headers as $name => $value) {
+  foreach ($cache->data['headers'] as $name => $value) {
     // In the case of a 304 response, certain headers must be sent, and the
     // remaining may not (see RFC 2616, section 10.3.5). Do not override
     // headers set in hook_boot().
     $name_lower = strtolower($name);
     if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($hook_boot_headers[$name_lower])) {
       drupal_add_http_header($name, $value);
-      unset($cache->headers[$name]);
+      unset($cache->data['headers'][$name]);
     }
   }
 
@@ -1106,7 +1110,7 @@ function drupal_serve_page_from_cache(st
   }
 
   // Send the remaining headers.
-  foreach ($cache->headers as $name => $value) {
+  foreach ($cache->data['headers'] as $name => $value) {
     drupal_add_http_header($name, $value);
   }
 
@@ -1134,7 +1138,7 @@ function drupal_serve_page_from_cache(st
     header('Vary: Accept-Encoding', FALSE);
     // If page_compression is enabled, the cache contains gzipped data.
     if ($return_compressed) {
-      // $cache->data is already gzip'ed, so make sure zlib.output_compression
+      // $cache->data['body'] is already gzip'ed, so make sure zlib.output_compression
       // does not compress it once more.
       ini_set('zlib.output_compression', '0');
       header('Content-Encoding: gzip');
@@ -1142,11 +1146,12 @@ function drupal_serve_page_from_cache(st
     else {
       // The client does not support compression, so unzip the data in the
       // cache. Strip the gzip header and run uncompress.
-      $cache->data = gzinflate(substr(substr($cache->data, 10), 0, -8));
+      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
     }
   }
 
-  print $cache->data;
+  // Print the page.
+  print $cache->data['body'];
 }
 
 /**
@@ -1639,6 +1644,48 @@ function drupal_get_messages($type = NUL
 }
 
 /**
+ * Get the title of the current page, for display on the page and in the title bar.
+ *
+ * @return
+ *   The current page's title.
+ */
+function drupal_get_title() {
+  $title = drupal_set_title();
+
+  // During a bootstrap, menu.inc is not included and thus we cannot provide a title.
+  if (!isset($title) && function_exists('menu_get_active_title')) {
+    $title = check_plain(menu_get_active_title());
+  }
+
+  return $title;
+}
+
+/**
+ * Set the title of the current page, for display on the page and in the title bar.
+ *
+ * @param $title
+ *   Optional string value to assign to the page title; or if set to NULL
+ *   (default), leaves the current title unchanged.
+ * @param $output
+ *   Optional flag - normally should be left as CHECK_PLAIN. Only set to
+ *   PASS_THROUGH if you have already removed any possibly dangerous code
+ *   from $title using a function like check_plain() or filter_xss(). With this
+ *   flag the string will be passed through unchanged.
+ *
+ * @return
+ *   The updated title of the current page.
+ */
+function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
+  $stored_title = &drupal_static(__FUNCTION__);
+
+  if (isset($title)) {
+    $stored_title = ($output == PASS_THROUGH) ? $title : check_plain($title);
+  }
+
+  return $stored_title;
+}
+
+/**
  * Check to see if an IP address has been blocked.
  *
  * Blocked IP addresses are stored in the database by default. However for
Index: includes/cache-install.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/cache-install.inc,v
retrieving revision 1.8
diff -u -p -r1.8 cache-install.inc
--- includes/cache-install.inc	11 Apr 2010 17:16:45 -0000	1.8
+++ includes/cache-install.inc	18 May 2010 01:10:40 -0000
@@ -24,7 +24,7 @@ class DrupalFakeCache extends DrupalData
     return array();
   }
 
-  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) {
+  function set($cid, $data, $expire = CACHE_PERMANENT) {
   }
 
   function clear($cid = NULL, $wildcard = FALSE) {
Index: includes/cache.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/cache.inc,v
retrieving revision 1.47
diff -u -p -r1.47 cache.inc
--- includes/cache.inc	7 Mar 2010 07:26:13 -0000	1.47
+++ includes/cache.inc	18 May 2010 01:10:40 -0000
@@ -133,11 +133,9 @@ function cache_get_multiple(array &$cids
  *     general cache wipe.
  *   - A Unix timestamp: Indicates that the item should be kept at least until
  *     the given time, after which it behaves like CACHE_TEMPORARY.
- * @param $headers
- *   A string containing HTTP header information for cached pages.
  */
-function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT, array $headers = NULL) {
-  return _cache_get_object($bin)->set($cid, $data, $expire, $headers);
+function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
+  return _cache_get_object($bin)->set($cid, $data, $expire);
 }
 
 /**
@@ -263,10 +261,8 @@ interface DrupalCacheInterface {
    *     general cache wipe.
    *   - A Unix timestamp: Indicates that the item should be kept at least until
    *     the given time, after which it behaves like CACHE_TEMPORARY.
-   * @param $headers
-   *   A string containing HTTP header information for cached pages.
    */
-  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL);
+  function set($cid, $data, $expire = CACHE_PERMANENT);
 
 
   /**
@@ -312,7 +308,7 @@ class DrupalDatabaseCache implements Dru
     try {
       // Garbage collection necessary when enforcing a minimum cache lifetime.
       $this->garbageCollection($this->bin);
-      $cache = db_query("SELECT data, created, headers, expire, serialized FROM {" . $this->bin . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
+      $cache = db_query("SELECT data, created, expire, serialized FROM {" . $this->bin . "} WHERE cid = :cid", array(':cid' => $cid))->fetchObject();
       return $this->prepareItem($cache);
     }
     catch (Exception $e) {
@@ -327,7 +323,7 @@ class DrupalDatabaseCache implements Dru
       // Garbage collection necessary when enforcing a minimum cache lifetime.
       $this->garbageCollection($this->bin);
       $query = db_select($this->bin);
-      $query->fields($this->bin, array('cid', 'data', 'created', 'headers', 'expire', 'serialized'));
+      $query->fields($this->bin, array('cid', 'data', 'created', 'expire', 'serialized'));
       $query->condition($this->bin . '.cid', $cids, 'IN');
       $result = $query->execute();
       $cache = array();
@@ -401,19 +397,15 @@ class DrupalDatabaseCache implements Dru
     if ($cache->serialized) {
       $cache->data = unserialize($cache->data);
     }
-    if (isset($cache->headers)) {
-      $cache->headers = unserialize($cache->headers);
-    }
 
     return $cache;
   }
 
-  function set($cid, $data, $expire = CACHE_PERMANENT, array $headers = NULL) {
+  function set($cid, $data, $expire = CACHE_PERMANENT) {
     $fields = array(
       'serialized' => 0,
       'created' => REQUEST_TIME,
       'expire' => $expire,
-      'headers' => isset($headers) ? serialize($headers) : NULL,
     );
     if (!is_string($data)) {
       $fields['data'] = serialize($data);
Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.1164
diff -u -p -r1.1164 common.inc
--- includes/common.inc	16 May 2010 19:21:45 -0000	1.1164
+++ includes/common.inc	18 May 2010 01:10:41 -0000
@@ -4518,28 +4518,32 @@ function drupal_page_set_cache() {
   if (drupal_page_is_cacheable()) {
     $cache = (object) array(
       'cid' => $base_root . request_uri(),
-      'data' => ob_get_clean(),
+      'data' => array(
+        'path' => $_GET['q'],
+        'body' => ob_get_clean(),
+        'title' => drupal_get_title(),
+        'headers' => array(),
+      ),
       'expire' => CACHE_TEMPORARY,
       'created' => REQUEST_TIME,
-      'headers' => array(),
     );
 
     // Restore preferred header names based on the lower-case names returned
     // by drupal_get_http_header().
     $header_names = _drupal_set_preferred_header_name();
     foreach (drupal_get_http_header() as $name_lower => $value) {
-      $cache->headers[$header_names[$name_lower]] = $value;
+      $cache->data['headers'][$header_names[$name_lower]] = $value;
       if ($name_lower == 'expires') {
         // Use the actual timestamp from an Expires header if available.
         $cache->expire = strtotime($value);
       }
     }
 
-    if ($cache->data) {
+    if ($cache->data['body']) {
       if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) {
-        $cache->data = gzencode($cache->data, 9, FORCE_GZIP);
+        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
       }
-      cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire, $cache->headers);
+      cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire);
     }
     return $cache;
   }
Index: includes/path.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/path.inc,v
retrieving revision 1.62
diff -u -p -r1.62 path.inc
--- includes/path.inc	24 Apr 2010 15:11:27 -0000	1.62
+++ includes/path.inc	18 May 2010 01:10:41 -0000
@@ -283,48 +283,6 @@ function arg($index = NULL, $path = NULL
 }
 
 /**
- * Get the title of the current page, for display on the page and in the title bar.
- *
- * @return
- *   The current page's title.
- */
-function drupal_get_title() {
-  $title = drupal_set_title();
-
-  // During a bootstrap, menu.inc is not included and thus we cannot provide a title.
-  if (!isset($title) && function_exists('menu_get_active_title')) {
-    $title = check_plain(menu_get_active_title());
-  }
-
-  return $title;
-}
-
-/**
- * Set the title of the current page, for display on the page and in the title bar.
- *
- * @param $title
- *   Optional string value to assign to the page title; or if set to NULL
- *   (default), leaves the current title unchanged.
- * @param $output
- *   Optional flag - normally should be left as CHECK_PLAIN. Only set to
- *   PASS_THROUGH if you have already removed any possibly dangerous code
- *   from $title using a function like check_plain() or filter_xss(). With this
- *   flag the string will be passed through unchanged.
- *
- * @return
- *   The updated title of the current page.
- */
-function drupal_set_title($title = NULL, $output = CHECK_PLAIN) {
-  $stored_title = &drupal_static(__FUNCTION__);
-
-  if (isset($title)) {
-    $stored_title = ($output == PASS_THROUGH) ? $title : check_plain($title);
-  }
-
-  return $stored_title;
-}
-
-/**
  * Check if the current page is the front page.
  *
  * @return
Index: modules/statistics/statistics.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.module,v
retrieving revision 1.334
diff -u -p -r1.334 statistics.module
--- modules/statistics/statistics.module	13 May 2010 08:22:31 -0000	1.334
+++ modules/statistics/statistics.module	18 May 2010 01:10:45 -0000
@@ -51,9 +51,23 @@ function statistics_help($path, $arg) {
 function statistics_exit() {
   global $user;
 
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
+  // If this is running for a cached page using the page_cache_without_database
+  // configuration, then we may not have bootstrapped the system variables yet.
+  // Websites running non-database page caches will almost certainly not have
+  // the statistics module enabled, but just in case, bootstrap the system
+  // variables. This wipes out most if not all of the performance gain from a
+  // non-database page cache, but if the statistics module is enabled, then it
+  // is also likely to have the 'statistics_enable_access_log' variable set, in
+  // which case, we'll be bootstrapping to the session phase anyway. In summary,
+  // we do not invoke an expensive full bootstrap, but for any site that does
+  // not want to bootstrap beyond DRUPAL_BOOTSTRAP_PAGE_CACHE for cached pages,
+  // do not enable the statistics module. 
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_VARIABLES);
 
   if (variable_get('statistics_count_content_views', 0)) {
+    // We need this to use the arg() function.
+    require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
+
     // We are counting content views.
     if ((arg(0) == 'node') && is_numeric(arg(1)) && arg(2) == '') {
       // A node has been viewed, so update the node's counters.
@@ -70,6 +84,7 @@ function statistics_exit() {
     }
   }
   if (variable_get('statistics_enable_access_log', 0)) {
+    drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
     // Log this page access.
     db_insert('accesslog')
       ->fields(array(
Index: modules/statistics/statistics.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/statistics/statistics.test,v
retrieving revision 1.17
diff -u -p -r1.17 statistics.test
--- modules/statistics/statistics.test	20 Apr 2010 09:48:06 -0000	1.17
+++ modules/statistics/statistics.test	18 May 2010 01:10:45 -0000
@@ -34,6 +34,69 @@ class StatisticsTestCase extends DrupalW
 }
 
 /**
+ * Tests that logging via statistics_exit() works for cached and uncached pages.
+ *
+ * Subclass DrupalWebTestCase rather than StatisticsTestCase, because we want
+ * to test requests from an anonymous user.
+ */
+class StatisticsLoggingTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Statistics logging tests',
+      'description' => 'Tests request logging for cached and uncached pages.',
+      'group' => 'Statistics'
+    );
+  }
+
+  function setUp() {
+    parent::setUp('statistics');
+
+    // Ensure we have a node page to access.
+    $this->node = $this->drupalCreateNode();
+
+    // Enable page caching.
+    variable_set('cache', TRUE);
+
+    // Enable access logging.
+    variable_set('statistics_enable_access_log', 1);
+    variable_set('statistics_count_content_views', 1);
+
+    // Clear the logs.
+    db_truncate('accesslog');
+    db_truncate('node_counter');
+  }
+
+  /**
+   * Verifies request logging for cached and uncached pages.
+   */
+  function testLogging() {
+    $path = 'node/' . $this->node->nid;
+    $expected = array(
+      'title' => $this->node->title,
+      'path' => $path,
+    );
+
+    // Verify logging of an uncached page.
+    $this->drupalGet($path);
+    $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', t('Testing an uncached page.'));
+    $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC);
+    $this->assertTrue(is_array($log) && count($log) == 1, t('Page request was logged.'));
+    $this->assertEqual(array_intersect_key($log[0], $expected), $expected);
+    $node_counter = statistics_get($this->node->nid);
+    $this->assertIdentical($node_counter['totalcount'], '1');
+
+    // Verify logging of a cached page.
+    $this->drupalGet($path);
+    $this->assertIdentical($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', t('Testing a cached page.'));
+    $log = db_query('SELECT * FROM {accesslog}')->fetchAll(PDO::FETCH_ASSOC);
+    $this->assertTrue(is_array($log) && count($log) == 2, t('Page request was logged.'));
+    $this->assertEqual(array_intersect_key($log[1], $expected), $expected);
+    $node_counter = statistics_get($this->node->nid);
+    $this->assertIdentical($node_counter['totalcount'], '2');
+  }
+}
+
+/**
  * Tests that report pages render properly, and that access logging works.
  */
 class StatisticsReportsTestCase extends StatisticsTestCase {
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.467
diff -u -p -r1.467 system.install
--- modules/system/system.install	16 May 2010 20:08:38 -0000	1.467
+++ modules/system/system.install	18 May 2010 01:10:45 -0000
@@ -634,11 +634,6 @@ function system_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
-      'headers' => array(
-        'description' => 'Any custom HTTP headers to be added to cached data.',
-        'type' => 'text',
-        'not null' => FALSE,
-      ),
       'serialized' => array(
         'description' => 'A flag to indicate whether content is serialized (1) or not (0).',
         'type' => 'int',
@@ -2382,6 +2377,16 @@ function system_update_7053() {
 }
 
 /**
+ * Remove {cache_*}.headers columns.
+ */
+function system_update_7054() {
+  $cache_tables = array('cache', 'cache_bootstrap', 'cache_filter', 'cache_form', 'cache_menu', 'cache_page', 'cache_path');
+  foreach ($cache_tables as $table) {
+    db_drop_field($table, 'headers');
+  }
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
Index: modules/update/update.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/update/update.install,v
retrieving revision 1.16
diff -u -p -r1.16 update.install
--- modules/update/update.install	15 Jan 2010 10:12:36 -0000	1.16
+++ modules/update/update.install	18 May 2010 01:10:45 -0000
@@ -167,3 +167,10 @@ function update_update_7000() {
   $queue = DrupalQueue::get('update_fetch_tasks');
   $queue->createQueue();
 }
+
+/**
+ * Remove {cache_update}.headers columns.
+ */
+function update_update_7001() {
+  db_drop_field('cache_update', 'headers');
+}
\ No newline at end of file
Index: modules/update/update.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/update/update.module,v
retrieving revision 1.66
diff -u -p -r1.66 update.module
--- modules/update/update.module	6 May 2010 15:21:25 -0000	1.66
+++ modules/update/update.module	18 May 2010 01:10:45 -0000
@@ -653,9 +653,6 @@ function theme_update_last_check($variab
 /**
  * Store data in the private update status cache table.
  *
- * Note: this function completely ignores the {cache_update}.headers field
- * since that is meaningless for the kinds of data we're caching.
- *
  * @param $cid
  *   The cache ID to save the data with.
  * @param $data
@@ -671,7 +668,6 @@ function _update_cache_set($cid, $data, 
   $fields = array(
     'created' => REQUEST_TIME,
     'expire' => $expire,
-    'headers' => NULL,
   );
   if (!is_string($data)) {
     $fields['data'] = serialize($data);
