From 015a9fba7a82e2fb8393941f0cc838ec126d1675 Mon Sep 17 00:00:00 2001
From: Bob Vincent <bobvin@pillars.net>
Date: Sun, 2 Oct 2011 03:54:13 -0400
Subject: [PATCH] Issue #1015946: Fix per-user cache expiration.

---
 includes/bootstrap.inc        |    1 -
 includes/cache.inc            |  101 ++++++++++++++++++++++++++---------------
 includes/session.inc          |    1 -
 modules/system/system.install |   13 +++---
 4 files changed, 72 insertions(+), 44 deletions(-)

diff --git a/includes/bootstrap.inc b/includes/bootstrap.inc
index 545beba21638014f886713bd07f04d4ffcb55165..66bef65e37c14ad63529d55170a264c83cf6dea8 100644
--- a/includes/bootstrap.inc
+++ b/includes/bootstrap.inc
@@ -2012,7 +2012,6 @@ function drupal_anonymous_user() {
   $user->hostname = ip_address();
   $user->roles = array();
   $user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
-  $user->cache = 0;
   return $user;
 }
 
diff --git a/includes/cache.inc b/includes/cache.inc
index 8666874ac6250efd71ec4ab873e6f0b39ada09b0..152f8c3f5a0a77d02de0c0d05f3f52fdc2450627 100644
--- a/includes/cache.inc
+++ b/includes/cache.inc
@@ -1,7 +1,7 @@
 <?php
 
 /**
- * Get the cache object for a cache bin.
+ * Returns the cache object for a cache bin.
  *
  * By default, this returns an instance of the DrupalDatabaseCache class.
  * Classes implementing DrupalCacheInterface can register themselves both as a
@@ -11,6 +11,7 @@
  *
  * @param $bin
  *   The cache bin for which the cache object should be returned.
+ *
  * @return DrupalCacheInterface
  *   The cache object associated with the specified bin.
  */
@@ -29,7 +30,7 @@ function _cache_get_object($bin) {
 }
 
 /**
- * Return data from the persistent cache
+ * Returns data from the persistent cache.
  *
  * Data may be stored as either plain text or as serialized data. cache_get
  * will automatically return unserialized objects and arrays.
@@ -50,13 +51,14 @@ function cache_get($cid, $bin = 'cache') {
 }
 
 /**
- * Return data from the persistent cache when given an array of cache IDs.
+ * Returns data from the persistent cache when given an array of cache IDs.
  *
  * @param $cids
  *   An array of cache IDs for the data to retrieve. This is passed by
  *   reference, and will have the IDs successfully returned from cache removed.
  * @param $bin
  *   The cache bin where the data is stored.
+ *
  * @return
  *   An array of the items successfully returned from cache indexed by cid.
  */
@@ -138,7 +140,7 @@ function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
 }
 
 /**
- * Expire data from the cache.
+ * Expires data from the cache.
  *
  * If called without arguments, expirable entries will be cleared from the
  * cache_page and cache_block bins.
@@ -170,15 +172,16 @@ function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
 }
 
 /**
- * Check if a cache bin is empty.
+ * Checks whether a cache bin is empty.
  *
  * A cache bin is considered empty if it does not contain any valid data for any
  * cache ID.
  *
  * @param $bin
  *   The cache bin to check.
+ *
  * @return
- *   TRUE if the cache bin specified is empty.
+ *   TRUE if the cache bin specified is empty; FALSE otherwise.
  */
 function cache_is_empty($bin) {
   return _cache_get_object($bin)->isEmpty();
@@ -227,13 +230,16 @@ interface DrupalCacheInterface {
    *
    * @param $bin
    *   The cache bin for which the object is created.
+   *
+   * @return
+   *   The newly-created DrupalCacheInterface object.
    */
   function __construct($bin);
 
   /**
-   * Return data from the persistent cache. Data may be stored as either plain
-   * text or as serialized data. cache_get will automatically return
-   * unserialized objects and arrays.
+   * Returns data from the persistent cache. Data may be stored as either plain
+   * text or as serialized data. The cache_get() function will automatically
+   * return unserialized objects and arrays.
    *
    * @param $cid
    *   The cache ID of the data to retrieve.
@@ -243,19 +249,20 @@ interface DrupalCacheInterface {
   function get($cid);
 
   /**
-   * Return data from the persistent cache when given an array of cache IDs.
+   * Returns data from the persistent cache when given an array of cache IDs.
    *
    * @param $cids
    *   An array of cache IDs for the data to retrieve. This is passed by
    *   reference, and will have the IDs successfully returned from cache
    *   removed.
+   *
    * @return
    *   An array of the items successfully returned from cache indexed by cid.
    */
    function getMultiple(&$cids);
 
   /**
-   * Store data in the persistent cache.
+   * Stores data in the persistent cache.
    *
    * @param $cid
    *   The cache ID of the data to store.
@@ -276,8 +283,10 @@ interface DrupalCacheInterface {
 
 
   /**
-   * Expire data from the cache. If called without arguments, expirable
-   * entries will be cleared from the cache_page and cache_block bins.
+   * Expires data from the cache.
+   *
+   * If called without arguments, expirable entries will be cleared from the
+   * cache_page and cache_block bins.
    *
    * @param $cid
    *   If set, the cache ID to delete. Otherwise, all cache entries that can
@@ -290,13 +299,13 @@ interface DrupalCacheInterface {
   function clear($cid = NULL, $wildcard = FALSE);
 
   /**
-   * Check if a cache bin is empty.
+   * Checks whether a cache bin is empty.
    *
    * A cache bin is considered empty if it does not contain any valid data for
    * any cache ID.
    *
    * @return
-   *   TRUE if the cache bin specified is empty.
+   *   TRUE if the cache bin specified is empty; FALSE otherwise.
    */
   function isEmpty();
 }
@@ -351,18 +360,40 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
   }
 
   /**
-   * Garbage collection for get() and getMultiple().
+   * Performs garbage collection for get() and getMultiple().
    *
    * @param $bin
    *   The bin being requested.
    */
   protected function garbageCollection() {
-    global $user;
+    $cache_lifetime = variable_get('cache_lifetime', 0);
 
-    // Garbage collection necessary when enforcing a minimum cache lifetime.
+    // Clean-up the per-user cache expiration session data, so that the session
+    // handler can properly clean-up the session data for anonymous users.
+    if (isset($_SESSION['cache_expiration'])) {
+      $expire = REQUEST_TIME - $cache_lifetime;
+      foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
+        if ($timestamp < $expire) {
+          unset($_SESSION['cache_expiration'][$bin]);
+        }
+      }
+      if (!$_SESSION['cache_expiration']) {
+        unset($_SESSION['cache_expiration']);
+      }
+    }
+
+    // Garbage collection of temporary items is only necessary when enforcing
+    // a minimum cache lifetime.
+    $cache_lifetime = variable_get('cache_lifetime', 0);
+    if (!$cache_lifetime) {
+      return;
+    }
+    // When cache lifetime is in force, avoid running garbage collection too
+    // often since this will remove temporary cache items indiscriminately.
     $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
     if ($cache_flush && ($cache_flush + variable_get('cache_lifetime', 0) <= REQUEST_TIME)) {
-      // Reset the variable immediately to prevent a meltdown in heavy load situations.
+      // Reset the variable immediately to prevent a meltdown in heavy load
+      // situations.
       variable_set('cache_flush_' . $this->bin, 0);
       // Time to flush old cache data
       db_delete($this->bin)
@@ -373,13 +404,14 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
   }
 
   /**
-   * Prepare a cached item.
+   * Prepares a cached item.
    *
-   * Checks that items are either permanent or did not expire, and unserializes
-   * data as appropriate.
+   * Checks whether items are either permanent or did not expire, and
+   * unserializes data as appropriate.
    *
    * @param $cache
    *   An item loaded from cache_get() or cache_get_multiple().
+   *
    * @return
    *   The item with data unserialized as appropriate or FALSE if there is no
    *   valid item to load.
@@ -390,17 +422,15 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
     if (!isset($cache->data)) {
       return FALSE;
     }
-    // If enforcing a minimum cache lifetime, validate that the data is
-    // currently valid for this user before we return it by making sure the cache
-    // entry was created before the timestamp in the current session's cache
-    // timer. The cache variable is loaded into the $user object by _drupal_session_read()
-    // in session.inc. If the data is permanent or we're not enforcing a minimum
-    // cache lifetime always return the cached data.
-    if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && $user->cache > $cache->created) {
-      // This cache data is too old and thus not valid for us, ignore it.
+    // If the cached data is temporary and subject to a per-user minimum
+    // lifetime, compare the cache entry timestamp with the user session
+    // cache_expiration timestamp. If the cache entry is too old, ignore it.
+    if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
+      // This cached data is too old; ignore it.
       return FALSE;
     }
-
+    // If the data is permanent or not subject to a minimum cache lifetime,
+    // unserialize and return the cached data.
     if ($cache->serialized) {
       $cache->data = unserialize($cache->data);
     }
@@ -439,11 +469,10 @@ class DrupalDatabaseCache implements DrupalCacheInterface {
 
     if (empty($cid)) {
       if (variable_get('cache_lifetime', 0)) {
-        // We store the time in the current user's $user->cache variable which
-        // will be saved into the sessions bin by _drupal_session_write(). We then
-        // simulate that the cache was flushed for this user by not returning
-        // cached data that was cached before the timestamp.
-        $user->cache = REQUEST_TIME;
+        // We store the time in the current user's session. We then simulate
+        // that the cache was flushed for this user by not returning cached
+        // data that was cached before the timestamp.
+        $_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
 
         $cache_flush = variable_get('cache_flush_' . $this->bin, 0);
         if ($cache_flush == 0) {
diff --git a/includes/session.inc b/includes/session.inc
index fd04de8753b45e8c83cc0acd43a1fc013735cbf2..6af4643d695f408da4f3569e933c7cee8b48942f 100644
--- a/includes/session.inc
+++ b/includes/session.inc
@@ -176,7 +176,6 @@ function _drupal_session_write($sid, $value) {
       // Either ssid or sid or both will be added from $key below.
       $fields = array(
         'uid' => $user->uid,
-        'cache' => isset($user->cache) ? $user->cache : 0,
         'hostname' => ip_address(),
         'session' => $value,
         'timestamp' => REQUEST_TIME,
diff --git a/modules/system/system.install b/modules/system/system.install
index 219f067d12ca98c2c93ed9a9171b6eb4eef9009c..29330fee4d0192bdbc2119df0a9f3e76a82b287f 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -1479,12 +1479,6 @@ function system_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
-      'cache' => array(
-        'description' => "The time of this user's last post. This is used when the site has specified a minimum_cache_lifetime. See cache_get().",
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
       'session' => array(
         'description' => 'The serialized contents of $_SESSION, an array of name/value pairs that persists across page requests by this session ID. Drupal loads $_SESSION from here at the start of each request and saves it at the end.',
         'type' => 'blob',
@@ -2974,6 +2968,13 @@ function system_update_7071() {
 }
 
 /**
+ * Remove the obsolete {session}.cache column.
+ */
+function system_update_7072() {
+  db_drop_field('session', 'cache');
+}
+
+/**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
  */
-- 
1.7.5.4

