source: subversion/trunk/roundcubemail/program/include/rcube_cache.php @ 5787

Last change on this file since 5787 was 5787, checked in by thomasb, 16 months ago

Changed license to GNU GPLv3+ with exceptions for skins and plugins

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