PageRenderTime 47ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/program/include/rcube_cache.php

https://github.com/netconstructor/roundcubemail
PHP | 559 lines | 318 code | 76 blank | 165 comment | 86 complexity | 4c91c2cc79605da2c2d017eb0ad098c8 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_cache.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2011, The Roundcube Dev Team |
  8. | Copyright (C) 2011, Kolab Systems AG |
  9. | |
  10. | Licensed under the GNU General Public License version 3 or |
  11. | any later version with exceptions for skins & plugins. |
  12. | See the README file for a full license statement. |
  13. | |
  14. | PURPOSE: |
  15. | Caching engine |
  16. | |
  17. +-----------------------------------------------------------------------+
  18. | Author: Thomas Bruederli <roundcube@gmail.com> |
  19. | Author: Aleksander Machniak <alec@alec.pl> |
  20. +-----------------------------------------------------------------------+
  21. */
  22. /**
  23. * Interface class for accessing Roundcube cache
  24. *
  25. * @package Framework
  26. * @subpackage Cache
  27. * @author Thomas Bruederli <roundcube@gmail.com>
  28. * @author Aleksander Machniak <alec@alec.pl>
  29. */
  30. class rcube_cache
  31. {
  32. /**
  33. * Instance of database handler
  34. *
  35. * @var rcube_db|Memcache|bool
  36. */
  37. private $db;
  38. private $type;
  39. private $userid;
  40. private $prefix;
  41. private $ttl;
  42. private $packed;
  43. private $index;
  44. private $cache = array();
  45. private $cache_changes = array();
  46. private $cache_sums = array();
  47. /**
  48. * Object constructor.
  49. *
  50. * @param string $type Engine type ('db' or 'memcache' or 'apc')
  51. * @param int $userid User identifier
  52. * @param string $prefix Key name prefix
  53. * @param string $ttl Expiration time of memcache/apc items
  54. * @param bool $packed Enables/disabled data serialization.
  55. * It's possible to disable data serialization if you're sure
  56. * stored data will be always a safe string
  57. */
  58. function __construct($type, $userid, $prefix='', $ttl=0, $packed=true)
  59. {
  60. $rcube = rcube::get_instance();
  61. $type = strtolower($type);
  62. if ($type == 'memcache') {
  63. $this->type = 'memcache';
  64. $this->db = $rcube->get_memcache();
  65. }
  66. else if ($type == 'apc') {
  67. $this->type = 'apc';
  68. $this->db = function_exists('apc_exists'); // APC 3.1.4 required
  69. }
  70. else {
  71. $this->type = 'db';
  72. $this->db = $rcube->get_dbh();
  73. }
  74. // convert ttl string to seconds
  75. $ttl = get_offset_sec($ttl);
  76. if ($ttl > 2592000) $ttl = 2592000;
  77. $this->userid = (int) $userid;
  78. $this->ttl = $ttl;
  79. $this->packed = $packed;
  80. $this->prefix = $prefix;
  81. }
  82. /**
  83. * Returns cached value.
  84. *
  85. * @param string $key Cache key name
  86. *
  87. * @return mixed Cached value
  88. */
  89. function get($key)
  90. {
  91. if (!array_key_exists($key, $this->cache)) {
  92. return $this->read_record($key);
  93. }
  94. return $this->cache[$key];
  95. }
  96. /**
  97. * Sets (add/update) value in cache.
  98. *
  99. * @param string $key Cache key name
  100. * @param mixed $data Cache data
  101. */
  102. function set($key, $data)
  103. {
  104. $this->cache[$key] = $data;
  105. $this->cache_changed = true;
  106. $this->cache_changes[$key] = true;
  107. }
  108. /**
  109. * Returns cached value without storing it in internal memory.
  110. *
  111. * @param string $key Cache key name
  112. *
  113. * @return mixed Cached value
  114. */
  115. function read($key)
  116. {
  117. if (array_key_exists($key, $this->cache)) {
  118. return $this->cache[$key];
  119. }
  120. return $this->read_record($key, true);
  121. }
  122. /**
  123. * Sets (add/update) value in cache and immediately saves
  124. * it in the backend, no internal memory will be used.
  125. *
  126. * @param string $key Cache key name
  127. * @param mixed $data Cache data
  128. *
  129. * @param boolean True on success, False on failure
  130. */
  131. function write($key, $data)
  132. {
  133. return $this->write_record($key, $this->packed ? serialize($data) : $data);
  134. }
  135. /**
  136. * Clears the cache.
  137. *
  138. * @param string $key Cache key name or pattern
  139. * @param boolean $prefix_mode Enable it to clear all keys starting
  140. * with prefix specified in $key
  141. */
  142. function remove($key=null, $prefix_mode=false)
  143. {
  144. // Remove all keys
  145. if ($key === null) {
  146. $this->cache = array();
  147. $this->cache_changed = false;
  148. $this->cache_changes = array();
  149. $this->cache_sums = array();
  150. }
  151. // Remove keys by name prefix
  152. else if ($prefix_mode) {
  153. foreach (array_keys($this->cache) as $k) {
  154. if (strpos($k, $key) === 0) {
  155. $this->cache[$k] = null;
  156. $this->cache_changes[$k] = false;
  157. unset($this->cache_sums[$k]);
  158. }
  159. }
  160. }
  161. // Remove one key by name
  162. else {
  163. $this->cache[$key] = null;
  164. $this->cache_changes[$key] = false;
  165. unset($this->cache_sums[$key]);
  166. }
  167. // Remove record(s) from the backend
  168. $this->remove_record($key, $prefix_mode);
  169. }
  170. /**
  171. * Remove cache records older than ttl
  172. */
  173. function expunge()
  174. {
  175. if ($this->type == 'db' && $this->db) {
  176. $this->db->query(
  177. "DELETE FROM ".$this->db->table_name('cache').
  178. " WHERE user_id = ?".
  179. " AND cache_key LIKE ?".
  180. " AND " . $this->db->unixtimestamp('created')." < ?",
  181. $this->userid,
  182. $this->prefix.'.%',
  183. time() - $this->ttl);
  184. }
  185. }
  186. /**
  187. * Writes the cache back to the DB.
  188. */
  189. function close()
  190. {
  191. if (!$this->cache_changed) {
  192. return;
  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 = $this->packed ? serialize($data) : $data;
  200. if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) {
  201. $this->write_record($key, $data);
  202. }
  203. }
  204. }
  205. $this->write_index();
  206. }
  207. /**
  208. * Reads cache entry.
  209. *
  210. * @param string $key Cache key name
  211. * @param boolean $nostore Enable to skip in-memory store
  212. *
  213. * @return mixed Cached value
  214. */
  215. private function read_record($key, $nostore=false)
  216. {
  217. if (!$this->db) {
  218. return null;
  219. }
  220. if ($this->type != 'db') {
  221. if ($this->type == 'memcache') {
  222. $data = $this->db->get($this->ckey($key));
  223. }
  224. else if ($this->type == 'apc') {
  225. $data = apc_fetch($this->ckey($key));
  226. }
  227. if ($data) {
  228. $md5sum = md5($data);
  229. $data = $this->packed ? unserialize($data) : $data;
  230. if ($nostore) {
  231. return $data;
  232. }
  233. $this->cache_sums[$key] = $md5sum;
  234. $this->cache[$key] = $data;
  235. }
  236. else {
  237. $this->cache[$key] = null;
  238. }
  239. }
  240. else {
  241. $sql_result = $this->db->limitquery(
  242. "SELECT data, cache_key".
  243. " FROM ".$this->db->table_name('cache').
  244. " WHERE user_id = ?".
  245. " AND cache_key = ?".
  246. // for better performance we allow more records for one key
  247. // get the newer one
  248. " ORDER BY created DESC",
  249. 0, 1, $this->userid, $this->prefix.'.'.$key);
  250. if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
  251. $key = substr($sql_arr['cache_key'], strlen($this->prefix)+1);
  252. $md5sum = $sql_arr['data'] ? md5($sql_arr['data']) : null;
  253. if ($sql_arr['data']) {
  254. $data = $this->packed ? unserialize($sql_arr['data']) : $sql_arr['data'];
  255. }
  256. if ($nostore) {
  257. return $data;
  258. }
  259. $this->cache[$key] = $data;
  260. $this->cache_sums[$key] = $md5sum;
  261. }
  262. else {
  263. $this->cache[$key] = null;
  264. }
  265. }
  266. return $this->cache[$key];
  267. }
  268. /**
  269. * Writes single cache record into DB.
  270. *
  271. * @param string $key Cache key name
  272. * @param mxied $data Serialized cache data
  273. *
  274. * @param boolean True on success, False on failure
  275. */
  276. private function write_record($key, $data)
  277. {
  278. if (!$this->db) {
  279. return false;
  280. }
  281. if ($this->type == 'memcache' || $this->type == 'apc') {
  282. return $this->add_record($this->ckey($key), $data);
  283. }
  284. $key_exists = array_key_exists($key, $this->cache_sums);
  285. $key = $this->prefix . '.' . $key;
  286. // Remove NULL rows (here we don't need to check if the record exist)
  287. if ($data == 'N;') {
  288. $this->db->query(
  289. "DELETE FROM ".$this->db->table_name('cache').
  290. " WHERE user_id = ?".
  291. " AND cache_key = ?",
  292. $this->userid, $key);
  293. return true;
  294. }
  295. // update existing cache record
  296. if ($key_exists) {
  297. $result = $this->db->query(
  298. "UPDATE ".$this->db->table_name('cache').
  299. " SET created = ". $this->db->now().", data = ?".
  300. " WHERE user_id = ?".
  301. " AND cache_key = ?",
  302. $data, $this->userid, $key);
  303. }
  304. // add new cache record
  305. else {
  306. // for better performance we allow more records for one key
  307. // so, no need to check if record exist (see rcube_cache::read_record())
  308. $result = $this->db->query(
  309. "INSERT INTO ".$this->db->table_name('cache').
  310. " (created, user_id, cache_key, data)".
  311. " VALUES (".$this->db->now().", ?, ?, ?)",
  312. $this->userid, $key, $data);
  313. }
  314. return $this->db->affected_rows($result);
  315. }
  316. /**
  317. * Deletes the cache record(s).
  318. *
  319. * @param string $key Cache key name or pattern
  320. * @param boolean $prefix_mode Enable it to clear all keys starting
  321. * with prefix specified in $key
  322. *
  323. */
  324. private function remove_record($key=null, $prefix_mode=false)
  325. {
  326. if (!$this->db) {
  327. return;
  328. }
  329. if ($this->type != 'db') {
  330. $this->load_index();
  331. // Remove all keys
  332. if ($key === null) {
  333. foreach ($this->index as $key) {
  334. $this->delete_record($key, false);
  335. }
  336. $this->index = array();
  337. }
  338. // Remove keys by name prefix
  339. else if ($prefix_mode) {
  340. foreach ($this->index as $k) {
  341. if (strpos($k, $key) === 0) {
  342. $this->delete_record($k);
  343. }
  344. }
  345. }
  346. // Remove one key by name
  347. else {
  348. $this->delete_record($key);
  349. }
  350. return;
  351. }
  352. // Remove all keys (in specified cache)
  353. if ($key === null) {
  354. $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
  355. }
  356. // Remove keys by name prefix
  357. else if ($prefix_mode) {
  358. $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
  359. }
  360. // Remove one key by name
  361. else {
  362. $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
  363. }
  364. $this->db->query(
  365. "DELETE FROM ".$this->db->table_name('cache').
  366. " WHERE user_id = ?" . $where,
  367. $this->userid);
  368. }
  369. /**
  370. * Adds entry into memcache/apc DB.
  371. *
  372. * @param string $key Cache key name
  373. * @param mxied $data Serialized cache data
  374. * @param bollean $index Enables immediate index update
  375. *
  376. * @param boolean True on success, False on failure
  377. */
  378. private function add_record($key, $data, $index=false)
  379. {
  380. if ($this->type == 'memcache') {
  381. $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
  382. if (!$result)
  383. $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl);
  384. }
  385. else if ($this->type == 'apc') {
  386. if (apc_exists($key))
  387. apc_delete($key);
  388. $result = apc_store($key, $data, $this->ttl);
  389. }
  390. // Update index
  391. if ($index && $result) {
  392. $this->load_index();
  393. if (array_search($key, $this->index) === false) {
  394. $this->index[] = $key;
  395. $data = serialize($this->index);
  396. $this->add_record($this->ikey(), $data);
  397. }
  398. }
  399. return $result;
  400. }
  401. /**
  402. * Deletes entry from memcache/apc DB.
  403. */
  404. private function delete_record($key, $index=true)
  405. {
  406. if ($this->type == 'memcache') {
  407. // #1488592: use 2nd argument
  408. $this->db->delete($this->ckey($key), 0);
  409. }
  410. else {
  411. apc_delete($this->ckey($key));
  412. }
  413. if ($index) {
  414. if (($idx = array_search($key, $this->index)) !== false) {
  415. unset($this->index[$idx]);
  416. }
  417. }
  418. }
  419. /**
  420. * Writes the index entry into memcache/apc DB.
  421. */
  422. private function write_index()
  423. {
  424. if (!$this->db) {
  425. return;
  426. }
  427. if ($this->type == 'db') {
  428. return;
  429. }
  430. $this->load_index();
  431. // Make sure index contains new keys
  432. foreach ($this->cache as $key => $value) {
  433. if ($value !== null) {
  434. if (array_search($key, $this->index) === false) {
  435. $this->index[] = $key;
  436. }
  437. }
  438. }
  439. $data = serialize($this->index);
  440. $this->add_record($this->ikey(), $data);
  441. }
  442. /**
  443. * Gets the index entry from memcache/apc DB.
  444. */
  445. private function load_index()
  446. {
  447. if (!$this->db) {
  448. return;
  449. }
  450. if ($this->index !== null) {
  451. return;
  452. }
  453. $index_key = $this->ikey();
  454. if ($this->type == 'memcache') {
  455. $data = $this->db->get($index_key);
  456. }
  457. else if ($this->type == 'apc') {
  458. $data = apc_fetch($index_key);
  459. }
  460. $this->index = $data ? unserialize($data) : array();
  461. }
  462. /**
  463. * Creates per-user cache key name (for memcache and apc)
  464. *
  465. * @param string $key Cache key name
  466. *
  467. * @return string Cache key
  468. */
  469. private function ckey($key)
  470. {
  471. return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key);
  472. }
  473. /**
  474. * Creates per-user index cache key name (for memcache and apc)
  475. *
  476. * @return string Cache key
  477. */
  478. private function ikey()
  479. {
  480. // This way each cache will have its own index
  481. return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX');
  482. }
  483. }