Aleksander Machniak
2012-11-17 9ab34604d94270f6c1795c7dd7b615273d05db0c
commit | author | age
5cf5ee 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_cache.php                                       |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2011, The Roundcube Dev Team                            |
9  | Copyright (C) 2011, Kolab Systems AG                                  |
7fe381 10  |                                                                       |
T 11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
5cf5ee 14  |                                                                       |
A 15  | PURPOSE:                                                              |
16  |   Caching engine                                                      |
17  |                                                                       |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20  | Author: Aleksander Machniak <alec@alec.pl>                            |
21  +-----------------------------------------------------------------------+
22 */
23
24
25 /**
26  * Interface class for accessing Roundcube cache
27  *
9ab346 28  * @package    Framework
AM 29  * @subpackage Cache
5cf5ee 30  * @author     Thomas Bruederli <roundcube@gmail.com>
A 31  * @author     Aleksander Machniak <alec@alec.pl>
32  */
33 class rcube_cache
34 {
35     /**
0d94fd 36      * Instance of database handler
5cf5ee 37      *
0d94fd 38      * @var rcube_db|Memcache|bool
5cf5ee 39      */
A 40     private $db;
41     private $type;
42     private $userid;
43     private $prefix;
7ad8e2 44     private $ttl;
c9f4e9 45     private $packed;
b9e42e 46     private $index;
5cf5ee 47     private $cache         = array();
A 48     private $cache_changes = array();
49     private $cache_sums    = array();
50
51
52     /**
53      * Object constructor.
54      *
8edb3d 55      * @param string $type   Engine type ('db' or 'memcache' or 'apc')
5cf5ee 56      * @param int    $userid User identifier
A 57      * @param string $prefix Key name prefix
5d66a4 58      * @param string $ttl    Expiration time of memcache/apc items
c9f4e9 59      * @param bool   $packed Enables/disabled data serialization.
A 60      *                       It's possible to disable data serialization if you're sure
61      *                       stored data will be always a safe string
5cf5ee 62      */
c9f4e9 63     function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
5cf5ee 64     {
be98df 65         $rcube = rcube::get_instance();
A 66         $type  = strtolower($type);
7ad8e2 67
8edb3d 68         if ($type == 'memcache') {
5cf5ee 69             $this->type = 'memcache';
be98df 70             $this->db   = $rcube->get_memcache();
8edb3d 71         }
A 72         else if ($type == 'apc') {
73             $this->type = 'apc';
74             $this->db   = function_exists('apc_exists'); // APC 3.1.4 required
5cf5ee 75         }
A 76         else {
77             $this->type = 'db';
be98df 78             $this->db   = $rcube->get_dbh();
5cf5ee 79         }
A 80
5d66a4 81         // convert ttl string to seconds
A 82         $ttl = get_offset_sec($ttl);
83         if ($ttl > 2592000) $ttl = 2592000;
84
c9f4e9 85         $this->userid    = (int) $userid;
5d66a4 86         $this->ttl       = $ttl;
c9f4e9 87         $this->packed    = $packed;
A 88         $this->prefix    = $prefix;
5cf5ee 89     }
A 90
91
92     /**
93      * Returns cached value.
94      *
b9e42e 95      * @param string $key Cache key name
5cf5ee 96      *
A 97      * @return mixed Cached value
98      */
99     function get($key)
100     {
b9e42e 101         if (!array_key_exists($key, $this->cache)) {
A 102             return $this->read_record($key);
103         }
5cf5ee 104
A 105         return $this->cache[$key];
106     }
107
108
109     /**
110      * Sets (add/update) value in cache.
111      *
b9e42e 112      * @param string $key  Cache key name
A 113      * @param mixed  $data Cache data
5cf5ee 114      */
A 115     function set($key, $data)
116     {
117         $this->cache[$key]         = $data;
118         $this->cache_changed       = true;
119         $this->cache_changes[$key] = true;
c9f4e9 120     }
A 121
122
123     /**
124      * Returns cached value without storing it in internal memory.
125      *
126      * @param string $key Cache key name
127      *
128      * @return mixed Cached value
129      */
130     function read($key)
131     {
132         if (array_key_exists($key, $this->cache)) {
133             return $this->cache[$key];
134         }
135
136         return $this->read_record($key, true);
137     }
138
139
140     /**
141      * Sets (add/update) value in cache and immediately saves
142      * it in the backend, no internal memory will be used.
143      *
144      * @param string $key  Cache key name
145      * @param mixed  $data Cache data
146      *
147      * @param boolean True on success, False on failure
148      */
149     function write($key, $data)
150     {
151         return $this->write_record($key, $this->packed ? serialize($data) : $data);
5cf5ee 152     }
A 153
154
155     /**
156      * Clears the cache.
157      *
ccc059 158      * @param string  $key         Cache key name or pattern
A 159      * @param boolean $prefix_mode Enable it to clear all keys starting
160      *                             with prefix specified in $key
5cf5ee 161      */
b9e42e 162     function remove($key=null, $prefix_mode=false)
5cf5ee 163     {
ccc059 164         // Remove all keys
5cf5ee 165         if ($key === null) {
A 166             $this->cache         = array();
167             $this->cache_changed = false;
168             $this->cache_changes = array();
83121e 169             $this->cache_sums    = array();
5cf5ee 170         }
ccc059 171         // Remove keys by name prefix
b9e42e 172         else if ($prefix_mode) {
5cf5ee 173             foreach (array_keys($this->cache) as $k) {
ccc059 174                 if (strpos($k, $key) === 0) {
A 175                     $this->cache[$k] = null;
5cf5ee 176                     $this->cache_changes[$k] = false;
83121e 177                     unset($this->cache_sums[$k]);
5cf5ee 178                 }
A 179             }
b9e42e 180         }
A 181         // Remove one key by name
182         else {
183             $this->cache[$key] = null;
184             $this->cache_changes[$key] = false;
83121e 185             unset($this->cache_sums[$key]);
5cf5ee 186         }
A 187
b9e42e 188         // Remove record(s) from the backend
A 189         $this->remove_record($key, $prefix_mode);
5cf5ee 190     }
A 191
192
193     /**
feb378 194      * Remove cache records older than ttl
T 195      */
196     function expunge()
197     {
198         if ($this->type == 'db' && $this->db) {
199             $this->db->query(
0c2596 200                 "DELETE FROM ".$this->db->table_name('cache').
feb378 201                 " WHERE user_id = ?".
T 202                 " AND cache_key LIKE ?".
203                 " AND " . $this->db->unixtimestamp('created')." < ?",
204                 $this->userid,
205                 $this->prefix.'.%',
206                 time() - $this->ttl);
207         }
208     }
209
210
211     /**
5cf5ee 212      * Writes the cache back to the DB.
A 213      */
214     function close()
215     {
216         if (!$this->cache_changed) {
217             return;
8edb3d 218         }
A 219
ccc059 220         foreach ($this->cache as $key => $data) {
A 221             // The key has been used
222             if ($this->cache_changes[$key]) {
223                 // Make sure we're not going to write unchanged data
224                 // by comparing current md5 sum with the sum calculated on DB read
c9f4e9 225                 $data = $this->packed ? serialize($data) : $data;
ccc059 226
A 227                 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
228                     $this->write_record($key, $data);
229                 }
230             }
231         }
232
b9e42e 233         $this->write_index();
ccc059 234     }
A 235
236
237     /**
b9e42e 238      * Reads cache entry.
ccc059 239      *
c9f4e9 240      * @param string  $key     Cache key name
A 241      * @param boolean $nostore Enable to skip in-memory store
b9e42e 242      *
A 243      * @return mixed Cached value
244      */
c9f4e9 245     private function read_record($key, $nostore=false)
b9e42e 246     {
A 247         if (!$this->db) {
248             return null;
249         }
250
03079a 251         if ($this->type != 'db') {
A 252             if ($this->type == 'memcache') {
253                 $data = $this->db->get($this->ckey($key));
c9f4e9 254             }
03079a 255             else if ($this->type == 'apc') {
A 256                 $data = apc_fetch($this->ckey($key));
413df0 257             }
c9f4e9 258
03079a 259             if ($data) {
A 260                 $md5sum = md5($data);
261                 $data   = $this->packed ? unserialize($data) : $data;
262
263                 if ($nostore) {
264                     return $data;
265                 }
266
267                 $this->cache_sums[$key] = $md5sum;
268                 $this->cache[$key]      = $data;
269             }
130cdc 270             else {
03079a 271                 $this->cache[$key] = null;
A 272             }
b9e42e 273         }
03079a 274         else {
b9e42e 275             $sql_result = $this->db->limitquery(
83121e 276                 "SELECT data, cache_key".
0c2596 277                 " FROM ".$this->db->table_name('cache').
b9e42e 278                 " WHERE user_id = ?".
A 279                 " AND cache_key = ?".
280                 // for better performance we allow more records for one key
281                 // get the newer one
282                 " ORDER BY created DESC",
283                 0, 1, $this->userid, $this->prefix.'.'.$key);
284
285             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
286                 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
287                 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
c9f4e9 288                 if ($sql_arr['data']) {
A 289                     $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
290                 }
291
292                 if ($nostore) {
293                     return $data;
294                 }
295
b9e42e 296                 $this->cache[$key]      = $data;
413df0 297                 $this->cache_sums[$key] = $md5sum;
b9e42e 298             }
130cdc 299             else {
03079a 300                 $this->cache[$key] = null;
A 301             }
b9e42e 302         }
A 303
304         return $this->cache[$key];
305     }
306
307
308     /**
309      * Writes single cache record into DB.
310      *
311      * @param string $key  Cache key name
312      * @param mxied  $data Serialized cache data 
c9f4e9 313      *
A 314      * @param boolean True on success, False on failure
ccc059 315      */
A 316     private function write_record($key, $data)
317     {
318         if (!$this->db) {
319             return false;
5cf5ee 320         }
A 321
b9e42e 322         if ($this->type == 'memcache' || $this->type == 'apc') {
A 323             return $this->add_record($this->ckey($key), $data);
324         }
325
83121e 326         $key_exists = array_key_exists($key, $this->cache_sums);
b9e42e 327         $key        = $this->prefix . '.' . $key;
A 328
329         // Remove NULL rows (here we don't need to check if the record exist)
330         if ($data == 'N;') {
331             $this->db->query(
0c2596 332                 "DELETE FROM ".$this->db->table_name('cache').
b9e42e 333                 " WHERE user_id = ?".
A 334                 " AND cache_key = ?",
335                 $this->userid, $key);
336
337             return true;
338         }
339
5cf5ee 340         // update existing cache record
b9e42e 341         if ($key_exists) {
c9f4e9 342             $result = $this->db->query(
0c2596 343                 "UPDATE ".$this->db->table_name('cache').
5cf5ee 344                 " SET created = ". $this->db->now().", data = ?".
A 345                 " WHERE user_id = ?".
346                 " AND cache_key = ?",
b9e42e 347                 $data, $this->userid, $key);
5cf5ee 348         }
A 349         // add new cache record
350         else {
b9e42e 351             // for better performance we allow more records for one key
A 352             // so, no need to check if record exist (see rcube_cache::read_record())
c9f4e9 353             $result = $this->db->query(
0c2596 354                 "INSERT INTO ".$this->db->table_name('cache').
5cf5ee 355                 " (created, user_id, cache_key, data)".
A 356                 " VALUES (".$this->db->now().", ?, ?, ?)",
b9e42e 357                 $this->userid, $key, $data);
A 358         }
359
c9f4e9 360         return $this->db->affected_rows($result);
b9e42e 361     }
A 362
363
364     /**
365      * Deletes the cache record(s).
366      *
367      * @param string  $key         Cache key name or pattern
368      * @param boolean $prefix_mode Enable it to clear all keys starting
369      *                             with prefix specified in $key
370      *
371      */
372     private function remove_record($key=null, $prefix_mode=false)
373     {
374         if (!$this->db) {
375             return;
376         }
377
378         if ($this->type != 'db') {
379             $this->load_index();
380
381             // Remove all keys
382             if ($key === null) {
383                 foreach ($this->index as $key) {
384                     $this->delete_record($key, false);
385                 }
386                 $this->index = array();
387             }
388             // Remove keys by name prefix
389             else if ($prefix_mode) {
390                 foreach ($this->index as $k) {
391                     if (strpos($k, $key) === 0) {
392                         $this->delete_record($k);
393                     }
394                 }
395             }
396             // Remove one key by name
397             else {
398                 $this->delete_record($key);
399             }
400
401             return;
402         }
403
404         // Remove all keys (in specified cache)
405         if ($key === null) {
406             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
407         }
408         // Remove keys by name prefix
409         else if ($prefix_mode) {
410             $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
411         }
412         // Remove one key by name
413         else {
414             $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
415         }
416
417         $this->db->query(
0c2596 418             "DELETE FROM ".$this->db->table_name('cache').
b9e42e 419             " WHERE user_id = ?" . $where,
A 420             $this->userid);
421     }
422
423
424     /**
425      * Adds entry into memcache/apc DB.
c9f4e9 426      *
A 427      * @param string  $key   Cache key name
428      * @param mxied   $data  Serialized cache data
429      * @param bollean $index Enables immediate index update
430      *
431      * @param boolean True on success, False on failure
b9e42e 432      */
c9f4e9 433     private function add_record($key, $data, $index=false)
b9e42e 434     {
A 435         if ($this->type == 'memcache') {
7ad8e2 436             $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
b9e42e 437             if (!$result)
7ad8e2 438                 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
b9e42e 439         }
c9f4e9 440         else if ($this->type == 'apc') {
b9e42e 441             if (apc_exists($key))
A 442                 apc_delete($key);
c9f4e9 443             $result = apc_store($key, $data, $this->ttl);
5cf5ee 444         }
c9f4e9 445
A 446         // Update index
447         if ($index && $result) {
448             $this->load_index();
449
450             if (array_search($key, $this->index) === false) {
451                 $this->index[] = $key;
452                 $data = serialize($this->index);
453                 $this->add_record($this->ikey(), $data);
454             }
455         }
456
457         return $result;
5cf5ee 458     }
A 459
b5f836 460
A 461     /**
b9e42e 462      * Deletes entry from memcache/apc DB.
A 463      */
9d195d 464     private function delete_record($key, $index=true)
b9e42e 465     {
23557f 466         if ($this->type == 'memcache') {
AM 467             // #1488592: use 2nd argument
468             $this->db->delete($this->ckey($key), 0);
469         }
470         else {
b9e42e 471             apc_delete($this->ckey($key));
23557f 472         }
b9e42e 473
A 474         if ($index) {
475             if (($idx = array_search($key, $this->index)) !== false) {
476                 unset($this->index[$idx]);
477             }
478         }
479     }
480
481
482     /**
483      * Writes the index entry into memcache/apc DB.
484      */
485     private function write_index()
486     {
487         if (!$this->db) {
488             return;
489         }
490
491         if ($this->type == 'db') {
492             return;
493         }
494
495         $this->load_index();
496
497         // Make sure index contains new keys
498         foreach ($this->cache as $key => $value) {
499             if ($value !== null) {
500                 if (array_search($key, $this->index) === false) {
501                     $this->index[] = $key;
502                 }
503             }
504         }
505
506         $data = serialize($this->index);
507         $this->add_record($this->ikey(), $data);
508     }
509
510
511     /**
512      * Gets the index entry from memcache/apc DB.
513      */
514     private function load_index()
515     {
516         if (!$this->db) {
517             return;
518         }
519
520         if ($this->index !== null) {
521             return;
522         }
523
524         $index_key = $this->ikey();
525         if ($this->type == 'memcache') {
526             $data = $this->db->get($index_key);
527         }
528         else if ($this->type == 'apc') {
529             $data = apc_fetch($index_key);
530         }
531
532         $this->index = $data ? unserialize($data) : array();
533     }
534
535
536     /**
537      * Creates per-user cache key name (for memcache and apc)
538      *
539      * @param string $key Cache key name
b5f836 540      *
ccc059 541      * @return string Cache key
b5f836 542      */
b9e42e 543     private function ckey($key)
b5f836 544     {
b9e42e 545         return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
A 546     }
547
548
549     /**
550      * Creates per-user index cache key name (for memcache and apc)
551      *
552      * @return string Cache key
553      */
554     private function ikey()
555     {
556         // This way each cache will have its own index
557         return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
b5f836 558     }
5cf5ee 559 }