Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_cache.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
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 | | |
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 | 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 * | |
24 * @package Framework | |
25 * @subpackage Cache | |
26 * @author Thomas Bruederli <roundcube@gmail.com> | |
27 * @author Aleksander Machniak <alec@alec.pl> | |
28 */ | |
29 class rcube_cache | |
30 { | |
31 /** | |
32 * Instance of database handler | |
33 * | |
34 * @var rcube_db|Memcache|bool | |
35 */ | |
36 private $db; | |
37 private $type; | |
38 private $userid; | |
39 private $prefix; | |
40 private $table; | |
41 private $ttl; | |
42 private $packed; | |
43 private $index; | |
44 private $debug; | |
45 private $index_changed = false; | |
46 private $cache = array(); | |
47 private $cache_changes = array(); | |
48 private $cache_sums = array(); | |
49 private $max_packet = -1; | |
50 | |
51 | |
52 /** | |
53 * Object constructor. | |
54 * | |
55 * @param string $type Engine type ('db' or 'memcache' or 'apc') | |
56 * @param int $userid User identifier | |
57 * @param string $prefix Key name prefix | |
58 * @param string $ttl Expiration time of memcache/apc items | |
59 * @param bool $packed Enables/disabled data serialization. | |
60 * It's possible to disable data serialization if you're sure | |
61 * stored data will be always a safe string | |
62 */ | |
63 function __construct($type, $userid, $prefix='', $ttl=0, $packed=true) | |
64 { | |
65 $rcube = rcube::get_instance(); | |
66 $type = strtolower($type); | |
67 | |
68 if ($type == 'memcache') { | |
69 $this->type = 'memcache'; | |
70 $this->db = $rcube->get_memcache(); | |
71 $this->debug = $rcube->config->get('memcache_debug'); | |
72 } | |
73 else if ($type == 'apc') { | |
74 $this->type = 'apc'; | |
75 $this->db = function_exists('apc_exists'); // APC 3.1.4 required | |
76 $this->debug = $rcube->config->get('apc_debug'); | |
77 } | |
78 else { | |
79 $this->type = 'db'; | |
80 $this->db = $rcube->get_dbh(); | |
81 $this->table = $this->db->table_name('cache', true); | |
82 } | |
83 | |
84 // convert ttl string to seconds | |
85 $ttl = get_offset_sec($ttl); | |
86 if ($ttl > 2592000) $ttl = 2592000; | |
87 | |
88 $this->userid = (int) $userid; | |
89 $this->ttl = $ttl; | |
90 $this->packed = $packed; | |
91 $this->prefix = $prefix; | |
92 } | |
93 | |
94 /** | |
95 * Returns cached value. | |
96 * | |
97 * @param string $key Cache key name | |
98 * | |
99 * @return mixed Cached value | |
100 */ | |
101 function get($key) | |
102 { | |
103 if (!array_key_exists($key, $this->cache)) { | |
104 return $this->read_record($key); | |
105 } | |
106 | |
107 return $this->cache[$key]; | |
108 } | |
109 | |
110 /** | |
111 * Sets (add/update) value in cache. | |
112 * | |
113 * @param string $key Cache key name | |
114 * @param mixed $data Cache data | |
115 */ | |
116 function set($key, $data) | |
117 { | |
118 $this->cache[$key] = $data; | |
119 $this->cache_changes[$key] = true; | |
120 } | |
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 { | |
149 return $this->write_record($key, $this->serialize($data)); | |
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_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 * Remove cache records older than ttl | |
190 */ | |
191 function expunge() | |
192 { | |
193 if ($this->type == 'db' && $this->db && $this->ttl) { | |
194 $this->db->query( | |
195 "DELETE FROM {$this->table}". | |
196 " WHERE `user_id` = ?". | |
197 " AND `cache_key` LIKE ?". | |
198 " AND `expires` < " . $this->db->now(), | |
199 $this->userid, | |
200 $this->prefix.'.%'); | |
201 } | |
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', true) . " WHERE `expires` < " . $db->now()); | |
213 } | |
214 | |
215 /** | |
216 * Writes the cache back to the DB. | |
217 */ | |
218 function close() | |
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->serialize($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 if ($this->index_changed) { | |
234 $this->write_index(); | |
235 } | |
236 | |
237 // reset internal cache index, thanks to this we can force index reload | |
238 $this->index = null; | |
239 $this->index_changed = false; | |
240 $this->cache = array(); | |
241 $this->cache_sums = array(); | |
242 $this->cache_changes = array(); | |
243 } | |
244 | |
245 /** | |
246 * Reads cache entry. | |
247 * | |
248 * @param string $key Cache key name | |
249 * @param boolean $nostore Enable to skip in-memory store | |
250 * | |
251 * @return mixed Cached value | |
252 */ | |
253 private function read_record($key, $nostore=false) | |
254 { | |
255 if (!$this->db) { | |
256 return null; | |
257 } | |
258 | |
259 if ($this->type != 'db') { | |
260 $this->load_index(); | |
261 | |
262 // Consistency check (#1490390) | |
263 if (!in_array($key, $this->index)) { | |
264 // we always check if the key exist in the index | |
265 // to have data in consistent state. Keeping the index consistent | |
266 // is needed for keys delete operation when we delete all keys or by prefix. | |
267 } | |
268 else { | |
269 $ckey = $this->ckey($key); | |
270 | |
271 if ($this->type == 'memcache') { | |
272 $data = $this->db->get($ckey); | |
273 } | |
274 else if ($this->type == 'apc') { | |
275 $data = apc_fetch($ckey); | |
276 } | |
277 | |
278 if ($this->debug) { | |
279 $this->debug('get', $ckey, $data); | |
280 } | |
281 } | |
282 | |
283 if ($data !== false) { | |
284 $md5sum = md5($data); | |
285 $data = $this->unserialize($data); | |
286 | |
287 if ($nostore) { | |
288 return $data; | |
289 } | |
290 | |
291 $this->cache_sums[$key] = $md5sum; | |
292 $this->cache[$key] = $data; | |
293 } | |
294 else { | |
295 $this->cache[$key] = null; | |
296 } | |
297 } | |
298 else { | |
299 $sql_result = $this->db->query( | |
300 "SELECT `data`, `cache_key` FROM {$this->table}" | |
301 . " WHERE `user_id` = ? AND `cache_key` = ?", | |
302 $this->userid, $this->prefix.'.'.$key); | |
303 | |
304 if ($sql_arr = $this->db->fetch_assoc($sql_result)) { | |
305 if (strlen($sql_arr['data']) > 0) { | |
306 $md5sum = md5($sql_arr['data']); | |
307 $data = $this->unserialize($sql_arr['data']); | |
308 } | |
309 | |
310 $this->db->reset(); | |
311 | |
312 if ($nostore) { | |
313 return $data; | |
314 } | |
315 | |
316 $this->cache[$key] = $data; | |
317 $this->cache_sums[$key] = $md5sum; | |
318 } | |
319 else { | |
320 $this->cache[$key] = null; | |
321 } | |
322 } | |
323 | |
324 return $this->cache[$key]; | |
325 } | |
326 | |
327 /** | |
328 * Writes single cache record into DB. | |
329 * | |
330 * @param string $key Cache key name | |
331 * @param mixed $data Serialized cache data | |
332 * | |
333 * @param boolean True on success, False on failure | |
334 */ | |
335 private function write_record($key, $data) | |
336 { | |
337 if (!$this->db) { | |
338 return false; | |
339 } | |
340 | |
341 // don't attempt to write too big data sets | |
342 if (strlen($data) > $this->max_packet_size()) { | |
343 trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); | |
344 return false; | |
345 } | |
346 | |
347 if ($this->type == 'memcache' || $this->type == 'apc') { | |
348 $result = $this->add_record($this->ckey($key), $data); | |
349 | |
350 // make sure index will be updated | |
351 if ($result) { | |
352 if (!array_key_exists($key, $this->cache_sums)) { | |
353 $this->cache_sums[$key] = true; | |
354 } | |
355 | |
356 $this->load_index(); | |
357 | |
358 if (!$this->index_changed && !in_array($key, $this->index)) { | |
359 $this->index_changed = true; | |
360 } | |
361 } | |
362 | |
363 return $result; | |
364 } | |
365 | |
366 $db_key = $this->prefix . '.' . $key; | |
367 | |
368 // Remove NULL rows (here we don't need to check if the record exist) | |
369 if ($data == 'N;') { | |
370 $result = $this->db->query( | |
371 "DELETE FROM {$this->table}". | |
372 " WHERE `user_id` = ? AND `cache_key` = ?", | |
373 $this->userid, $db_key); | |
374 | |
375 return !$this->db->is_error($result); | |
376 } | |
377 | |
378 $key_exists = array_key_exists($key, $this->cache_sums); | |
379 $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL'; | |
380 | |
381 if (!$key_exists) { | |
382 // Try INSERT temporarily ignoring "duplicate key" errors | |
383 $this->db->set_option('ignore_key_errors', true); | |
384 | |
385 $result = $this->db->query( | |
386 "INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)" | |
387 . " VALUES ($expires, ?, ?, ?)", | |
388 $this->userid, $db_key, $data); | |
389 | |
390 $this->db->set_option('ignore_key_errors', false); | |
391 } | |
392 | |
393 // otherwise try UPDATE | |
394 if (!isset($result) || !($count = $this->db->affected_rows($result))) { | |
395 $result = $this->db->query( | |
396 "UPDATE {$this->table} SET `expires` = $expires, `data` = ?" | |
397 . " WHERE `user_id` = ? AND `cache_key` = ?", | |
398 $data, $this->userid, $db_key); | |
399 | |
400 $count = $this->db->affected_rows($result); | |
401 } | |
402 | |
403 return $count > 0; | |
404 } | |
405 | |
406 /** | |
407 * Deletes the cache record(s). | |
408 * | |
409 * @param string $key Cache key name or pattern | |
410 * @param boolean $prefix_mode Enable it to clear all keys starting | |
411 * with prefix specified in $key | |
412 */ | |
413 private function remove_record($key=null, $prefix_mode=false) | |
414 { | |
415 if (!$this->db) { | |
416 return; | |
417 } | |
418 | |
419 if ($this->type != 'db') { | |
420 $this->load_index(); | |
421 | |
422 // Remove all keys | |
423 if ($key === null) { | |
424 foreach ($this->index as $key) { | |
425 $this->delete_record($this->ckey($key)); | |
426 } | |
427 | |
428 $this->index = array(); | |
429 } | |
430 // Remove keys by name prefix | |
431 else if ($prefix_mode) { | |
432 foreach ($this->index as $idx => $k) { | |
433 if (strpos($k, $key) === 0) { | |
434 $this->delete_record($this->ckey($k)); | |
435 unset($this->index[$idx]); | |
436 } | |
437 } | |
438 } | |
439 // Remove one key by name | |
440 else { | |
441 $this->delete_record($this->ckey($key)); | |
442 if (($idx = array_search($key, $this->index)) !== false) { | |
443 unset($this->index[$idx]); | |
444 } | |
445 } | |
446 | |
447 $this->index_changed = true; | |
448 | |
449 return; | |
450 } | |
451 | |
452 // Remove all keys (in specified cache) | |
453 if ($key === null) { | |
454 $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.%'); | |
455 } | |
456 // Remove keys by name prefix | |
457 else if ($prefix_mode) { | |
458 $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); | |
459 } | |
460 // Remove one key by name | |
461 else { | |
462 $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key); | |
463 } | |
464 | |
465 $this->db->query( | |
466 "DELETE FROM {$this->table} WHERE `user_id` = ?" . $where, | |
467 $this->userid); | |
468 } | |
469 | |
470 /** | |
471 * Adds entry into memcache/apc DB. | |
472 * | |
473 * @param string $key Cache key name | |
474 * @param mixed $data Serialized cache data | |
475 * | |
476 * @param boolean True on success, False on failure | |
477 */ | |
478 private function add_record($key, $data) | |
479 { | |
480 if ($this->type == 'memcache') { | |
481 $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); | |
482 | |
483 if (!$result) { | |
484 $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); | |
485 } | |
486 } | |
487 else if ($this->type == 'apc') { | |
488 if (apc_exists($key)) { | |
489 apc_delete($key); | |
490 } | |
491 | |
492 $result = apc_store($key, $data, $this->ttl); | |
493 } | |
494 | |
495 if ($this->debug) { | |
496 $this->debug('set', $key, $data, $result); | |
497 } | |
498 | |
499 return $result; | |
500 } | |
501 | |
502 /** | |
503 * Deletes entry from memcache/apc DB. | |
504 * | |
505 * @param string $key Cache key name | |
506 * | |
507 * @param boolean True on success, False on failure | |
508 */ | |
509 private function delete_record($key) | |
510 { | |
511 if ($this->type == 'memcache') { | |
512 // #1488592: use 2nd argument | |
513 $result = $this->db->delete($key, 0); | |
514 } | |
515 else { | |
516 $result = apc_delete($key); | |
517 } | |
518 | |
519 if ($this->debug) { | |
520 $this->debug('delete', $key, null, $result); | |
521 } | |
522 | |
523 return $result; | |
524 } | |
525 | |
526 /** | |
527 * Writes the index entry into memcache/apc DB. | |
528 */ | |
529 private function write_index() | |
530 { | |
531 if (!$this->db || $this->type == 'db') { | |
532 return; | |
533 } | |
534 | |
535 $this->load_index(); | |
536 | |
537 // Make sure index contains new keys | |
538 foreach ($this->cache as $key => $value) { | |
539 if ($value !== null && !in_array($key, $this->index)) { | |
540 $this->index[] = $key; | |
541 } | |
542 } | |
543 | |
544 // new keys added using self::write() | |
545 foreach ($this->cache_sums as $key => $value) { | |
546 if ($value === true && !in_array($key, $this->index)) { | |
547 $this->index[] = $key; | |
548 } | |
549 } | |
550 | |
551 $data = serialize($this->index); | |
552 $this->add_record($this->ikey(), $data); | |
553 } | |
554 | |
555 /** | |
556 * Gets the index entry from memcache/apc DB. | |
557 */ | |
558 private function load_index() | |
559 { | |
560 if (!$this->db || $this->type == 'db') { | |
561 return; | |
562 } | |
563 | |
564 if ($this->index !== null) { | |
565 return; | |
566 } | |
567 | |
568 $index_key = $this->ikey(); | |
569 | |
570 if ($this->type == 'memcache') { | |
571 $data = $this->db->get($index_key); | |
572 } | |
573 else if ($this->type == 'apc') { | |
574 $data = apc_fetch($index_key); | |
575 } | |
576 | |
577 if ($this->debug) { | |
578 $this->debug('get', $index_key, $data); | |
579 } | |
580 | |
581 $this->index = $data ? unserialize($data) : array(); | |
582 } | |
583 | |
584 /** | |
585 * Creates per-user cache key name (for memcache and apc) | |
586 * | |
587 * @param string $key Cache key name | |
588 * | |
589 * @return string Cache key | |
590 */ | |
591 private function ckey($key) | |
592 { | |
593 return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key); | |
594 } | |
595 | |
596 /** | |
597 * Creates per-user index cache key name (for memcache and apc) | |
598 * | |
599 * @return string Cache key | |
600 */ | |
601 private function ikey() | |
602 { | |
603 // This way each cache will have its own index | |
604 return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX'); | |
605 } | |
606 | |
607 /** | |
608 * Serializes data for storing | |
609 */ | |
610 private function serialize($data) | |
611 { | |
612 if ($this->type == 'db') { | |
613 return $this->db->encode($data, $this->packed); | |
614 } | |
615 | |
616 return $this->packed ? serialize($data) : $data; | |
617 } | |
618 | |
619 /** | |
620 * Unserializes serialized data | |
621 */ | |
622 private function unserialize($data) | |
623 { | |
624 if ($this->type == 'db') { | |
625 return $this->db->decode($data, $this->packed); | |
626 } | |
627 | |
628 return $this->packed ? @unserialize($data) : $data; | |
629 } | |
630 | |
631 /** | |
632 * Determine the maximum size for cache data to be written | |
633 */ | |
634 private function max_packet_size() | |
635 { | |
636 if ($this->max_packet < 0) { | |
637 $this->max_packet = 2097152; // default/max is 2 MB | |
638 | |
639 if ($this->type == 'db') { | |
640 if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) { | |
641 $this->max_packet = $value; | |
642 } | |
643 $this->max_packet -= 2000; | |
644 } | |
645 else { | |
646 $max_packet = rcube::get_instance()->config->get($this->type . '_max_allowed_packet'); | |
647 $this->max_packet = parse_bytes($max_packet) ?: $this->max_packet; | |
648 } | |
649 } | |
650 | |
651 return $this->max_packet; | |
652 } | |
653 | |
654 /** | |
655 * Write memcache/apc debug info to the log | |
656 */ | |
657 private function debug($type, $key, $data = null, $result = null) | |
658 { | |
659 $line = strtoupper($type) . ' ' . $key; | |
660 | |
661 if ($data !== null) { | |
662 $line .= ' ' . ($this->packed ? $data : serialize($data)); | |
663 } | |
664 | |
665 rcube::debug($this->type, $line, $result); | |
666 } | |
667 } |