Aleksander Machniak
2016-05-16 0b7e26c1bf6bc7a684eb3a214d92d3927306cd8a
commit | author | age
5cf5ee 1 <?php
A 2
a95874 3 /**
5cf5ee 4  +-----------------------------------------------------------------------+
A 5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2011, The Roundcube Dev Team                            |
7  | Copyright (C) 2011, Kolab Systems AG                                  |
7fe381 8  |                                                                       |
T 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.                     |
5cf5ee 12  |                                                                       |
A 13  | PURPOSE:                                                              |
14  |   Caching engine                                                      |
15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17  | Author: Aleksander Machniak <alec@alec.pl>                            |
18  +-----------------------------------------------------------------------+
19 */
20
21 /**
22  * Interface class for accessing Roundcube cache
23  *
9ab346 24  * @package    Framework
AM 25  * @subpackage Cache
5cf5ee 26  * @author     Thomas Bruederli <roundcube@gmail.com>
A 27  * @author     Aleksander Machniak <alec@alec.pl>
28  */
29 class rcube_cache
30 {
31     /**
0d94fd 32      * Instance of database handler
5cf5ee 33      *
0d94fd 34      * @var rcube_db|Memcache|bool
5cf5ee 35      */
A 36     private $db;
37     private $type;
38     private $userid;
39     private $prefix;
60b6d7 40     private $table;
7ad8e2 41     private $ttl;
c9f4e9 42     private $packed;
b9e42e 43     private $index;
44708e 44     private $debug;
b1e35a 45     private $index_changed = false;
5cf5ee 46     private $cache         = array();
A 47     private $cache_changes = array();
48     private $cache_sums    = array();
579330 49     private $max_packet    = -1;
44708e 50
5cf5ee 51
A 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') {
44708e 69             $this->type  = 'memcache';
AM 70             $this->db    = $rcube->get_memcache();
71             $this->debug = $rcube->config->get('memcache_debug');
8edb3d 72         }
A 73         else if ($type == 'apc') {
44708e 74             $this->type  = 'apc';
AM 75             $this->db    = function_exists('apc_exists'); // APC 3.1.4 required
76             $this->debug = $rcube->config->get('apc_debug');
5cf5ee 77         }
A 78         else {
60b6d7 79             $this->type  = 'db';
AM 80             $this->db    = $rcube->get_dbh();
34a090 81             $this->table = $this->db->table_name('cache', true);
5cf5ee 82         }
A 83
5d66a4 84         // convert ttl string to seconds
A 85         $ttl = get_offset_sec($ttl);
86         if ($ttl > 2592000) $ttl = 2592000;
87
c9f4e9 88         $this->userid    = (int) $userid;
5d66a4 89         $this->ttl       = $ttl;
c9f4e9 90         $this->packed    = $packed;
A 91         $this->prefix    = $prefix;
5cf5ee 92     }
A 93
94     /**
95      * Returns cached value.
96      *
b9e42e 97      * @param string $key Cache key name
5cf5ee 98      *
A 99      * @return mixed Cached value
100      */
101     function get($key)
102     {
b9e42e 103         if (!array_key_exists($key, $this->cache)) {
A 104             return $this->read_record($key);
105         }
5cf5ee 106
A 107         return $this->cache[$key];
108     }
109
110     /**
111      * Sets (add/update) value in cache.
112      *
b9e42e 113      * @param string $key  Cache key name
A 114      * @param mixed  $data Cache data
5cf5ee 115      */
A 116     function set($key, $data)
117     {
118         $this->cache[$key]         = $data;
119         $this->cache_changes[$key] = true;
c9f4e9 120     }
A 121
122     /**
123      * Returns cached value without storing it in internal memory.
124      *
125      * @param string $key Cache key name
126      *
127      * @return mixed Cached value
128      */
129     function read($key)
130     {
131         if (array_key_exists($key, $this->cache)) {
132             return $this->cache[$key];
133         }
134
135         return $this->read_record($key, true);
136     }
137
138     /**
139      * Sets (add/update) value in cache and immediately saves
140      * it in the backend, no internal memory will be used.
141      *
142      * @param string $key  Cache key name
143      * @param mixed  $data Cache data
144      *
145      * @param boolean True on success, False on failure
146      */
147     function write($key, $data)
148     {
a6b0ca 149         return $this->write_record($key, $this->serialize($data));
5cf5ee 150     }
A 151
152     /**
153      * Clears the cache.
154      *
ccc059 155      * @param string  $key         Cache key name or pattern
A 156      * @param boolean $prefix_mode Enable it to clear all keys starting
157      *                             with prefix specified in $key
5cf5ee 158      */
b9e42e 159     function remove($key=null, $prefix_mode=false)
5cf5ee 160     {
ccc059 161         // Remove all keys
5cf5ee 162         if ($key === null) {
A 163             $this->cache         = array();
164             $this->cache_changes = array();
83121e 165             $this->cache_sums    = array();
5cf5ee 166         }
ccc059 167         // Remove keys by name prefix
b9e42e 168         else if ($prefix_mode) {
5cf5ee 169             foreach (array_keys($this->cache) as $k) {
ccc059 170                 if (strpos($k, $key) === 0) {
A 171                     $this->cache[$k] = null;
5cf5ee 172                     $this->cache_changes[$k] = false;
83121e 173                     unset($this->cache_sums[$k]);
5cf5ee 174                 }
A 175             }
b9e42e 176         }
A 177         // Remove one key by name
178         else {
179             $this->cache[$key] = null;
180             $this->cache_changes[$key] = false;
83121e 181             unset($this->cache_sums[$key]);
5cf5ee 182         }
A 183
b9e42e 184         // Remove record(s) from the backend
A 185         $this->remove_record($key, $prefix_mode);
5cf5ee 186     }
A 187
188     /**
feb378 189      * Remove cache records older than ttl
T 190      */
191     function expunge()
192     {
00cb22 193         if ($this->type == 'db' && $this->db && $this->ttl) {
feb378 194             $this->db->query(
34a090 195                 "DELETE FROM {$this->table}".
AM 196                 " WHERE `user_id` = ?".
197                 " AND `cache_key` LIKE ?".
198                 " AND `expires` < " . $this->db->now(),
feb378 199                 $this->userid,
60b6d7 200                 $this->prefix.'.%');
feb378 201         }
60b6d7 202     }
AM 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
34a090 212         $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
feb378 213     }
T 214
215     /**
5cf5ee 216      * Writes the cache back to the DB.
A 217      */
218     function close()
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
a6b0ca 225                 $data = $this->serialize($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
b1e35a 233         if ($this->index_changed) {
AM 234             $this->write_index();
235         }
9335f9 236
AM 237         // reset internal cache index, thanks to this we can force index reload
238         $this->index = null;
ccc059 239     }
A 240
241     /**
b9e42e 242      * Reads cache entry.
ccc059 243      *
c9f4e9 244      * @param string  $key     Cache key name
A 245      * @param boolean $nostore Enable to skip in-memory store
b9e42e 246      *
A 247      * @return mixed Cached value
248      */
c9f4e9 249     private function read_record($key, $nostore=false)
b9e42e 250     {
A 251         if (!$this->db) {
252             return null;
253         }
254
03079a 255         if ($this->type != 'db') {
fff8e0 256             $this->load_index();
AM 257
258             // Consistency check (#1490390)
259             if (!in_array($key, $this->index)) {
260                 // we always check if the key exist in the index
261                 // to have data in consistent state. Keeping the index consistent
262                 // is needed for keys delete operation when we delete all keys or by prefix.
263             }
044c1a 264             else {
44708e 265                 $ckey = $this->ckey($key);
044c1a 266
AM 267                 if ($this->type == 'memcache') {
268                     $data = $this->db->get($ckey);
269                 }
270                 else if ($this->type == 'apc') {
271                     $data = apc_fetch($ckey);
272                 }
44708e 273
AM 274                 if ($this->debug) {
275                     $this->debug('get', $ckey, $data);
276                 }
413df0 277             }
c9f4e9 278
03079a 279             if ($data) {
A 280                 $md5sum = md5($data);
a6b0ca 281                 $data   = $this->unserialize($data);
03079a 282
A 283                 if ($nostore) {
284                     return $data;
285                 }
286
287                 $this->cache_sums[$key] = $md5sum;
288                 $this->cache[$key]      = $data;
289             }
130cdc 290             else {
03079a 291                 $this->cache[$key] = null;
A 292             }
b9e42e 293         }
03079a 294         else {
b9e42e 295             $sql_result = $this->db->limitquery(
34a090 296                 "SELECT `data`, `cache_key`".
AM 297                 " FROM {$this->table}".
298                 " WHERE `user_id` = ? AND `cache_key` = ?".
b9e42e 299                 // for better performance we allow more records for one key
A 300                 // get the newer one
34a090 301                 " ORDER BY `created` DESC",
b9e42e 302                 0, 1, $this->userid, $this->prefix.'.'.$key);
A 303
304             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
305                 $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
306                 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
c9f4e9 307                 if ($sql_arr['data']) {
a6b0ca 308                     $data = $this->unserialize($sql_arr['data']);
c9f4e9 309                 }
A 310
311                 if ($nostore) {
312                     return $data;
313                 }
314
b9e42e 315                 $this->cache[$key]      = $data;
413df0 316                 $this->cache_sums[$key] = $md5sum;
b9e42e 317             }
130cdc 318             else {
03079a 319                 $this->cache[$key] = null;
A 320             }
b9e42e 321         }
A 322
323         return $this->cache[$key];
324     }
325
326     /**
327      * Writes single cache record into DB.
328      *
329      * @param string $key  Cache key name
579330 330      * @param mixed  $data Serialized cache data
c9f4e9 331      *
A 332      * @param boolean True on success, False on failure
ccc059 333      */
A 334     private function write_record($key, $data)
335     {
336         if (!$this->db) {
579330 337             return false;
TB 338         }
339
340         // don't attempt to write too big data sets
341         if (strlen($data) > $this->max_packet_size()) {
342             trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
ccc059 343             return false;
5cf5ee 344         }
A 345
b9e42e 346         if ($this->type == 'memcache' || $this->type == 'apc') {
652e11 347             $result = $this->add_record($this->ckey($key), $data);
AM 348
349             // make sure index will be updated
b120d4 350             if ($result) {
AM 351                 if (!array_key_exists($key, $this->cache_sums)) {
352                     $this->cache_sums[$key] = true;
353                 }
354
355                 $this->load_index();
356
357                 if (!$this->index_changed && !in_array($key, $this->index)) {
358                     $this->index_changed = true;
359                 }
652e11 360             }
AM 361
362             return $result;
b9e42e 363         }
A 364
83121e 365         $key_exists = array_key_exists($key, $this->cache_sums);
b9e42e 366         $key        = $this->prefix . '.' . $key;
A 367
368         // Remove NULL rows (here we don't need to check if the record exist)
369         if ($data == 'N;') {
370             $this->db->query(
34a090 371                 "DELETE FROM {$this->table}".
AM 372                 " WHERE `user_id` = ? AND `cache_key` = ?",
b9e42e 373                 $this->userid, $key);
A 374
375             return true;
376         }
377
5cf5ee 378         // update existing cache record
b9e42e 379         if ($key_exists) {
c9f4e9 380             $result = $this->db->query(
34a090 381                 "UPDATE {$this->table}".
AM 382                 " SET `created` = " . $this->db->now().
383                     ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
384                     ", `data` = ?".
385                 " WHERE `user_id` = ?".
386                 " AND `cache_key` = ?",
b9e42e 387                 $data, $this->userid, $key);
5cf5ee 388         }
A 389         // add new cache record
390         else {
b9e42e 391             // for better performance we allow more records for one key
A 392             // so, no need to check if record exist (see rcube_cache::read_record())
c9f4e9 393             $result = $this->db->query(
34a090 394                 "INSERT INTO {$this->table}".
AM 395                 " (`created`, `expires`, `user_id`, `cache_key`, `data`)".
60b6d7 396                 " VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)",
b9e42e 397                 $this->userid, $key, $data);
A 398         }
399
c9f4e9 400         return $this->db->affected_rows($result);
b9e42e 401     }
A 402
403     /**
404      * Deletes the cache record(s).
405      *
406      * @param string  $key         Cache key name or pattern
407      * @param boolean $prefix_mode Enable it to clear all keys starting
408      *                             with prefix specified in $key
409      */
410     private function remove_record($key=null, $prefix_mode=false)
411     {
412         if (!$this->db) {
413             return;
414         }
415
416         if ($this->type != 'db') {
417             $this->load_index();
418
419             // Remove all keys
420             if ($key === null) {
421                 foreach ($this->index as $key) {
b120d4 422                     $this->delete_record($this->ckey($key));
b9e42e 423                 }
b120d4 424
b9e42e 425                 $this->index = array();
A 426             }
427             // Remove keys by name prefix
428             else if ($prefix_mode) {
b120d4 429                 foreach ($this->index as $idx => $k) {
b9e42e 430                     if (strpos($k, $key) === 0) {
b120d4 431                         $this->delete_record($this->ckey($k));
AM 432                         unset($this->index[$idx]);
b9e42e 433                     }
A 434                 }
435             }
436             // Remove one key by name
437             else {
b120d4 438                 $this->delete_record($this->ckey($key));
AM 439                 if (($idx = array_search($key, $this->index)) !== false) {
440                     unset($this->index[$idx]);
441                 }
b9e42e 442             }
b120d4 443
AM 444             $this->index_changed = true;
b9e42e 445
A 446             return;
447         }
448
449         // Remove all keys (in specified cache)
450         if ($key === null) {
34a090 451             $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.%');
b9e42e 452         }
A 453         // Remove keys by name prefix
454         else if ($prefix_mode) {
34a090 455             $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
b9e42e 456         }
A 457         // Remove one key by name
458         else {
34a090 459             $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
b9e42e 460         }
A 461
462         $this->db->query(
34a090 463             "DELETE FROM {$this->table} WHERE `user_id` = ?" . $where,
b9e42e 464             $this->userid);
A 465     }
466
467     /**
468      * Adds entry into memcache/apc DB.
c9f4e9 469      *
fff8e0 470      * @param string $key  Cache key name
AM 471      * @param mixed  $data Serialized cache data
c9f4e9 472      *
A 473      * @param boolean True on success, False on failure
b9e42e 474      */
fff8e0 475     private function add_record($key, $data)
b9e42e 476     {
A 477         if ($this->type == 'memcache') {
7ad8e2 478             $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
44708e 479
044c1a 480             if (!$result) {
AM 481                 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
44708e 482             }
b9e42e 483         }
c9f4e9 484         else if ($this->type == 'apc') {
044c1a 485             if (apc_exists($key)) {
b9e42e 486                 apc_delete($key);
c9f4e9 487             }
044c1a 488
AM 489             $result = apc_store($key, $data, $this->ttl);
490         }
491
492         if ($this->debug) {
493             $this->debug('set', $key, $data, $result);
c9f4e9 494         }
A 495
496         return $result;
5cf5ee 497     }
A 498
b5f836 499     /**
b9e42e 500      * Deletes entry from memcache/apc DB.
b120d4 501      *
AM 502      * @param string $key Cache key name
503      *
504      * @param boolean True on success, False on failure
b9e42e 505      */
b120d4 506     private function delete_record($key)
b9e42e 507     {
23557f 508         if ($this->type == 'memcache') {
AM 509             // #1488592: use 2nd argument
b120d4 510             $result = $this->db->delete($key, 0);
23557f 511         }
AM 512         else {
b120d4 513             $result = apc_delete($key);
044c1a 514         }
44708e 515
044c1a 516         if ($this->debug) {
b120d4 517             $this->debug('delete', $key, null, $result);
23557f 518         }
b9e42e 519
b120d4 520         return $result;
b9e42e 521     }
A 522
523     /**
524      * Writes the index entry into memcache/apc DB.
525      */
526     private function write_index()
527     {
044c1a 528         if (!$this->db || $this->type == 'db') {
b9e42e 529             return;
A 530         }
531
532         $this->load_index();
533
534         // Make sure index contains new keys
535         foreach ($this->cache as $key => $value) {
652e11 536             if ($value !== null && !in_array($key, $this->index)) {
AM 537                 $this->index[] = $key;
538             }
539         }
540
541         // new keys added using self::write()
542         foreach ($this->cache_sums as $key => $value) {
543             if ($value === true && !in_array($key, $this->index)) {
544                 $this->index[] = $key;
b9e42e 545             }
A 546         }
547
548         $data = serialize($this->index);
549         $this->add_record($this->ikey(), $data);
550     }
551
552     /**
553      * Gets the index entry from memcache/apc DB.
554      */
555     private function load_index()
556     {
044c1a 557         if (!$this->db || $this->type == 'db') {
b9e42e 558             return;
A 559         }
560
561         if ($this->index !== null) {
562             return;
563         }
564
565         $index_key = $this->ikey();
044c1a 566
b9e42e 567         if ($this->type == 'memcache') {
A 568             $data = $this->db->get($index_key);
569         }
570         else if ($this->type == 'apc') {
571             $data = apc_fetch($index_key);
044c1a 572         }
44708e 573
044c1a 574         if ($this->debug) {
AM 575             $this->debug('get', $index_key, $data);
b9e42e 576         }
A 577
578         $this->index = $data ? unserialize($data) : array();
579     }
580
581     /**
582      * Creates per-user cache key name (for memcache and apc)
583      *
584      * @param string $key Cache key name
b5f836 585      *
ccc059 586      * @return string Cache key
b5f836 587      */
b9e42e 588     private function ckey($key)
b5f836 589     {
b9e42e 590         return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
A 591     }
592
593     /**
594      * Creates per-user index cache key name (for memcache and apc)
595      *
596      * @return string Cache key
597      */
598     private function ikey()
599     {
600         // This way each cache will have its own index
601         return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
b5f836 602     }
a6b0ca 603
AM 604     /**
605      * Serializes data for storing
606      */
607     private function serialize($data)
608     {
609         if ($this->type == 'db') {
610             return $this->db->encode($data, $this->packed);
611         }
612
613         return $this->packed ? serialize($data) : $data;
614     }
615
616     /**
617      * Unserializes serialized data
618      */
619     private function unserialize($data)
620     {
621         if ($this->type == 'db') {
622             return $this->db->decode($data, $this->packed);
623         }
624
625         return $this->packed ? @unserialize($data) : $data;
626     }
579330 627
TB 628     /**
629      * Determine the maximum size for cache data to be written
630      */
631     private function max_packet_size()
632     {
633         if ($this->max_packet < 0) {
634             $this->max_packet = 2097152; // default/max is 2 MB
635
636             if ($this->type == 'db') {
2a31f6 637                 if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) {
AM 638                     $this->max_packet = $value;
639                 }
640                 $this->max_packet -= 2000;
579330 641             }
TB 642             else if ($this->type == 'memcache') {
643                 $stats = $this->db->getStats();
644                 $remaining = $stats['limit_maxbytes'] - $stats['bytes'];
645                 $this->max_packet = min($remaining / 5, $this->max_packet);
646             }
647             else if ($this->type == 'apc' && function_exists('apc_sma_info')) {
648                 $stats = apc_sma_info();
649                 $this->max_packet = min($stats['avail_mem'] / 5, $this->max_packet);
650             }
651         }
652
653         return $this->max_packet;
654     }
44708e 655
AM 656     /**
657      * Write memcache/apc debug info to the log
658      */
659     private function debug($type, $key, $data = null, $result = null)
660     {
11d5e7 661         $line = strtoupper($type) . ' ' . $key;
44708e 662
AM 663         if ($data !== null) {
664             $line .= ' ' . ($this->packed ? $data : serialize($data));
665         }
666
11d5e7 667         rcube::debug($this->type, $line, $result);
44708e 668     }
5cf5ee 669 }