alecpl
2011-05-20 3253b296c21c54df228de39ff3e4775974df81d5
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                                  |
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$
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.0
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 $cache         = array();
45     private $cache_keys    = array();
46     private $cache_changes = array();
47     private $cache_sums    = array();
48
49
50
51     /**
52      * Object constructor.
53      *
8edb3d 54      * @param string $type   Engine type ('db' or 'memcache' or 'apc')
5cf5ee 55      * @param int    $userid User identifier
A 56      * @param string $prefix Key name prefix
57      */
58     function __construct($type, $userid, $prefix='')
59     {
60         $rcmail = rcmail::get_instance();
8edb3d 61         $type   = strtolower($type);
5cf5ee 62     
8edb3d 63         if ($type == 'memcache') {
5cf5ee 64             $this->type = 'memcache';
A 65             $this->db   = $rcmail->get_memcache();
8edb3d 66         }
A 67         else if ($type == 'apc') {
68             $this->type = 'apc';
69             $this->db   = function_exists('apc_exists'); // APC 3.1.4 required
5cf5ee 70         }
A 71         else {
72             $this->type = 'db';
73             $this->db   = $rcmail->get_dbh();
74         }
75
76         $this->userid = (int) $userid;
77         $this->prefix = $prefix;
78     }
79
80
81     /**
82      * Returns cached value.
83      *
84      * @param string $key Cache key
85      *
86      * @return mixed Cached value
87      */
88     function get($key)
89     {
90         $key = $this->prefix.$key;
91     
92         if ($this->type == 'memcache') {
93             return $this->read_cache_record($key);
94         }
95
96         // read cache (if it was not read before)
97         if (!count($this->cache)) {
98             $do_read = true;
99         }
100         else if (isset($this->cache[$key])) {
101             $do_read = false;
102         }
103         // Find cache prefix, we'll load data for all keys
104         // with specified (class) prefix into internal cache (memory)
105         else if ($pos = strpos($key, '.')) {
106             $prefix = substr($key, 0, $pos);
107             $regexp = '/^' . preg_quote($prefix, '/') . '/';
108             if (!count(preg_grep($regexp, array_keys($this->cache_keys)))) {
109                 $do_read = true;
110             }
111         }
112
113         if ($do_read) {
114             return $this->read_cache_record($key);
115         }
116
117         return $this->cache[$key];
118     }
119
120
121     /**
122      * Sets (add/update) value in cache.
123      *
124      * @param string $key  Cache key
125      * @param mixed  $data Data
126      */
127     function set($key, $data)
128     {
129         $key = $this->prefix.$key;
130
131         $this->cache[$key]         = $data;
132         $this->cache_changed       = true;
133         $this->cache_changes[$key] = true;
134     }
135
136
137     /**
138      * Clears the cache.
139      *
140      * @param string  $key          Cache key name or pattern
141      * @param boolean $pattern_mode Enable it to clear all keys with name
142      *                              matching PREG pattern in $key
143      */
144     function remove($key=null, $pattern_mode=false)
145     {
146         if ($key === null) {
147             foreach (array_keys($this->cache) as $key)
148                 $this->clear_cache_record($key);
149
150             $this->cache         = array();
151             $this->cache_changed = false;
152             $this->cache_changes = array();
153         }
154         else if ($pattern_mode) {
7786ba 155             // add cache prefix into PCRE expression
A 156             if (preg_match('/^(.)([^a-z0-9]*).*/i', $key, $matches)) {
157                 $key = $matches[1] . $matches[2] . preg_quote($this->prefix, $matches[1])
158                     . substr($key, strlen($matches[1].$matches[2]));
159             }
160             else {
161                 $key = $this->prefix.$key;
162             }
5cf5ee 163
A 164             foreach (array_keys($this->cache) as $k) {
165                 if (preg_match($key, $k)) {
166                     $this->clear_cache_record($k);
167                     $this->cache_changes[$k] = false;
168                     unset($this->cache[$key]);
169                 }
170             }
171             if (!count($this->cache)) {
172                 $this->cache_changed = false;
173             }
174         }
175         else {
176             $key = $this->prefix.$key;
177
178             $this->clear_cache_record($key);
179             $this->cache_changes[$key] = false;
180             unset($this->cache[$key]);
181         }
182     }
183
184
185     /**
186      * Writes the cache back to the DB.
187      */
188     function close()
189     {
190         if (!$this->cache_changed) {
191             return;
192         }
193
194         foreach ($this->cache as $key => $data) {
195             // The key has been used
196             if ($this->cache_changes[$key]) {
197                 // Make sure we're not going to write unchanged data
198                 // by comparing current md5 sum with the sum calculated on DB read
199                 $data = serialize($data);
200                 if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
201                     $this->write_cache_record($key, $data);
202                 }
203             }
204         }
205     }
206
207
208     /**
209      * Returns cached entry.
210      *
211      * @param string $key Cache key
212      *
213      * @return mixed Cached value
214      * @access private
215      */
216     private function read_cache_record($key)
217     {
218         if (!$this->db) {
219             return null;
220         }
221
222         if ($this->type == 'memcache') {
8edb3d 223             $data = $this->db->get($this->ckey($key));
A 224             
225             if ($data) {
226                 $this->cache_sums[$key] = md5($data);
227                 $data = unserialize($data);
228             }
229             return $this->cache[$key] = $data;
230         }
231
232         if ($this->type == 'apc') {
233             $data = apc_fetch($this->ckey($key));
5cf5ee 234             
A 235             if ($data) {
236                 $this->cache_sums[$key] = md5($data);
237                 $data = unserialize($data);
238             }
239             return $this->cache[$key] = $data;
240         }
241
242         // Find cache prefix, we'll load data for all keys
243         // with specified (class) prefix into internal cache (memory)
244         if ($pos = strpos($key, '.')) {
245             $prefix = substr($key, 0, $pos);
246             $where = " AND cache_key LIKE '$prefix%'";
247         }
248         else {
249             $where = " AND cache_key = ".$this->db->quote($key);
250         }
251
252         // get cached data from DB
253         $sql_result = $this->db->query(
254             "SELECT cache_id, data, cache_key".
255             " FROM ".get_table_name('cache').
256             " WHERE user_id = ?".$where,
257             $this->userid);
258
259         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
260             $sql_key = $sql_arr['cache_key'];
261             $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
262             if (!isset($this->cache[$sql_key])) {
263                 $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
264                 $data   = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
265                 $this->cache[$sql_key]      = $data;
266                 $this->cache_sums[$sql_key] = $md5sum;
267             }
268         }
269
270         return $this->cache[$key];
271     }
272
273
274     /**
275      * Writes single cache record.
276      *
277      * @param string $key  Cache key
278      * @param mxied  $data Cache value
279      * @access private
280      */
281     private function write_cache_record($key, $data)
282     {
283         if (!$this->db) {
284             return false;
285         }
286
287         if ($this->type == 'memcache') {
8edb3d 288             $key = $this->ckey($key);
5cf5ee 289             $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED);
A 290             if (!$result)
291                 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED);
292             return $result;
8edb3d 293         }
A 294
295         if ($this->type == 'apc') {
296             $key = $this->ckey($key);
297             if (apc_exists($key))
298                 apc_delete($key);
299             return apc_store($key, $data);
5cf5ee 300         }
A 301
302         // update existing cache record
303         if ($this->cache_keys[$key]) {
304             $this->db->query(
305                 "UPDATE ".get_table_name('cache').
306                 " SET created = ". $this->db->now().", data = ?".
307                 " WHERE user_id = ?".
308                 " AND cache_key = ?",
309                 $data, $this->userid, $key);
310         }
311         // add new cache record
312         else {
313             $this->db->query(
314                 "INSERT INTO ".get_table_name('cache').
315                 " (created, user_id, cache_key, data)".
316                 " VALUES (".$this->db->now().", ?, ?, ?)",
317                 $this->userid, $key, $data);
318
319             // get cache entry ID for this key
320             $sql_result = $this->db->query(
321                 "SELECT cache_id".
322                 " FROM ".get_table_name('cache').
323                 " WHERE user_id = ?".
324                 " AND cache_key = ?",
325                 $this->userid, $key);
326
327             if ($sql_arr = $this->db->fetch_assoc($sql_result))
328                 $this->cache_keys[$key] = $sql_arr['cache_id'];
329         }
330     }
331
332
333     /**
334      * Clears cache for single record.
335      *
336      * @param string $key Cache key
337      * @access private
338      */
339     private function clear_cache_record($key)
340     {
341         if (!$this->db) {
342             return false;
343         }
344
345         if ($this->type == 'memcache') {
8edb3d 346             return $this->db->delete($this->ckey($key));
A 347         }
348
349         if ($this->type == 'apc') {
350             return apc_delete($this->ckey($key));
5cf5ee 351         }
A 352
353         $this->db->query(
354             "DELETE FROM ".get_table_name('cache').
355             " WHERE user_id = ?".
356             " AND cache_key = ?",
357             $this->userid, $key);
358
359         unset($this->cache_keys[$key]);
360     }
361
b5f836 362
A 363     /**
8edb3d 364      * Creates per-user cache key (for memcache and apc)
b5f836 365      *
A 366      * @param string $key Cache key
367      * @access private
368      */
8edb3d 369     private function ckey($key)
b5f836 370     {
A 371         return sprintf('[%d]%s', $this->userid, $key);
372     }
5cf5ee 373 }