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