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