From d07b032bcd90108e3df5b9fcddc4d97ff08cbbd5 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sat, 3 Mar 2018 17:28:40 +0100 Subject: [PATCH] Refactor cache code with separate engine-specific classes --- CHANGELOG | 1 + config/defaults.inc.php | 6 + program/lib/Roundcube/rcube.php | 202 +----- program/lib/Roundcube/rcube_cache.php | 470 ++++-------- program/lib/Roundcube/rcube_cache_apc.php | 146 ++++ program/lib/Roundcube/rcube_cache_db.php | 271 +++++++ .../lib/Roundcube/rcube_cache_memcache.php | 217 ++++++ program/lib/Roundcube/rcube_cache_redis.php | 254 +++++++ program/lib/Roundcube/rcube_cache_shared.php | 674 ------------------ tests/Framework/CacheShared.php | 20 - 10 files changed, 1048 insertions(+), 1213 deletions(-) create mode 100644 program/lib/Roundcube/rcube_cache_apc.php create mode 100644 program/lib/Roundcube/rcube_cache_db.php create mode 100644 program/lib/Roundcube/rcube_cache_memcache.php create mode 100644 program/lib/Roundcube/rcube_cache_redis.php delete mode 100644 program/lib/Roundcube/rcube_cache_shared.php delete mode 100644 tests/Framework/CacheShared.php diff --git a/CHANGELOG b/CHANGELOG index 27170124b..752f52938 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- Support Redis cache - Improved Mailvelope integration - Added private key listing and generating to identity settings - Enable encrypt & sign option if Mailvelope supports it diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 205befdbf..988a35f41 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -118,6 +118,9 @@ $config['memcache_debug'] = false; // Log APC conversation to /apc or to syslog $config['apc_debug'] = false; +// Log Redis conversation to /redis or to syslog +$config['redis_debug'] = false; + // ---------------------------------- // IMAP @@ -350,6 +353,9 @@ $config['memcache_max_allowed_packet'] = '2M'; // Maximum size of an object in APC cache (in bytes). Default: 2MB $config['apc_max_allowed_packet'] = '2M'; +// Maximum size of an object in Redis cache (in bytes). Default: 2MB +$config['redis_max_allowed_packet'] = '2M'; + // ---------------------------------- // SYSTEM diff --git a/program/lib/Roundcube/rcube.php b/program/lib/Roundcube/rcube.php index 9e2a6f019..680f80408 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -209,72 +209,12 @@ class rcube public function get_memcache() { if (!isset($this->memcache)) { - // no memcache support in PHP - if (!class_exists('Memcache')) { - $this->memcache = false; - return false; - } - - $this->memcache = new Memcache; - $this->memcache_init(); - - // test connection and failover (will result in $this->mc_available == 0 on complete failure) - $this->memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist - - if (!$this->mc_available) { - $this->memcache = false; - } + $this->memcache = rcube_cache_memcache::engine(); } return $this->memcache; } - /** - * Get global handle for memcache access - * - * @return object Memcache - */ - protected function memcache_init() - { - $this->mc_available = 0; - - // add all configured hosts to pool - $pconnect = $this->config->get('memcache_pconnect', true); - $timeout = $this->config->get('memcache_timeout', 1); - $retry_interval = $this->config->get('memcache_retry_interval', 15); - - foreach ($this->config->get('memcache_hosts', array()) as $host) { - if (substr($host, 0, 7) != 'unix://') { - list($host, $port) = explode(':', $host); - if (!$port) $port = 11211; - } - else { - $port = 0; - } - - $this->mc_available += intval($this->memcache->addServer( - $host, $port, $pconnect, 1, $timeout, $retry_interval, false, array($this, 'memcache_failure'))); - } - } - - /** - * Callback for memcache failure - */ - public function memcache_failure($host, $port) - { - static $seen = array(); - - // only report once - if (!$seen["$host:$port"]++) { - $this->mc_available--; - self::raise_error(array( - 'code' => 604, 'type' => 'db', - 'line' => __LINE__, 'file' => __FILE__, - 'message' => "Memcache failure on host $host:$port"), - true, false); - } - } - /** * Get global handle for redis access * @@ -283,150 +223,26 @@ class rcube public function get_redis() { if (!isset($this->redis)) { - if (!class_exists('Redis')) { - $this->redis = false; - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'redis', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Failed to find Redis. Make sure php-redis is included" - ), - true, - true - ); - } - - $this->redis = new Redis; - $this->redis_init(); - - if (!$this->redis_available) { - $this->redis = false; - } + $this->redis = rcube_cache_redis::engine(); } return $this->redis; } /** - * Get global handle for redis access - */ - protected function redis_init() - { - $this->redis_available = false; - - $hosts = $this->config->get('redis_hosts'); - - // host config is wrong - if (!is_array($hosts) || empty($hosts)) { - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'redis', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Redis host not configured" - ), - true, - true - ); - } - - // only allow 1 host for now until we support clustering - if (count($hosts) > 1) { - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'redis', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Redis cluster not yet supported" - ), - true, - true - ); - } - - foreach ($hosts as $redis_host) { - // explode individual fields - list($host, $port, $database, $password) = array_pad(explode(':', $redis_host, 4), 4, null); - - $params = parse_url($redis_host); - if ($params['scheme'] == 'redis') { - $host = (isset($params['host'])) ? $params['host'] : null; - $port = (isset($params['port'])) ? $params['port'] : null; - $database = (isset($params['database'])) ? $params['database'] : null; - $password = (isset($params['password'])) ? $params['password'] : null; - } - - // set default values if not set - $host = ($host !== null) ? $host : '127.0.0.1'; - $port = ($port !== null) ? $port : 6379; - $database = ($database !== null) ? $database : 0; - - if ($this->redis->connect($host, $port) === false) { - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'session', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Could not connect to Redis server. Please check host and port" - ), - true, - true - ); - } - - if ($password != null && $this->redis->auth($password) === false) { - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'session', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Could not authenticate with Redis server. Please check password." - ), - true, - true - ); - } - - if ($database != 0 && $this->redis->select($database) === false) { - rcube::raise_error( - array( - 'code' => 604, - 'type' => 'session', - 'line' => __LINE__, - 'file' => __FILE__, - 'message' => "Could not select Redis database. Please check database setting." - ), - true, - true - ); - } - } - - if ($this->redis->ping() == "+PONG") { - $this->redis_available = true; - } - } - - /** - * Initialize and get cache object + * Initialize and get user cache object * * @param string $name Cache identifier * @param string $type Cache type ('db', 'apc', 'memcache', 'redis') * @param string $ttl Expiration time for cache items * @param bool $packed Enables/disables data serialization * - * @return rcube_cache Cache object + * @return rcube_cache User cache object */ public function get_cache($name, $type='db', $ttl=0, $packed=true) { if (!isset($this->caches[$name]) && ($userid = $this->get_user_id())) { - $this->caches[$name] = new rcube_cache($type, $userid, $name, $ttl, $packed); + $this->caches[$name] = rcube_cache::factory($type, $userid, $name, $ttl, $packed); } return $this->caches[$name]; @@ -438,9 +254,9 @@ class rcube * @param string $name Cache identifier * @param bool $packed Enables/disables data serialization * - * @return rcube_cache_shared Cache object + * @return rcube_cache Shared cache object */ - public function get_cache_shared($name, $packed=true) + public function get_cache_shared($name, $packed = true) { $shared_name = "shared_$name"; @@ -458,7 +274,7 @@ class rcube $ttl = $this->config->get('shared_cache_ttl', '10d'); } - $this->caches[$shared_name] = new rcube_cache_shared($type, $name, $ttl, $packed); + $this->caches[$shared_name] = rcube_cache::factory($type, null, $name, $ttl, $packed); } return $this->caches[$shared_name]; @@ -684,9 +500,7 @@ class rcube public function gc() { rcube_cache::gc(); - rcube_cache_shared::gc(); $this->get_storage()->cache_gc(); - $this->gc_temp(); } diff --git a/program/lib/Roundcube/rcube_cache.php b/program/lib/Roundcube/rcube_cache.php index 19306513d..a02493b8c 100644 --- a/program/lib/Roundcube/rcube_cache.php +++ b/program/lib/Roundcube/rcube_cache.php @@ -28,29 +28,22 @@ */ class rcube_cache { - /** - * Instance of database handler - * - * @var rcube_db|Memcache|Redis|bool - */ - private $db; - private $type; - private $userid; - private $prefix; - private $table; - private $ttl; - private $packed; - private $index; - private $debug; - private $index_changed = false; - private $cache = array(); - private $cache_changes = array(); - private $cache_sums = array(); - private $max_packet = -1; + protected $type; + protected $userid; + protected $prefix; + protected $ttl; + protected $packed; + protected $index; + protected $index_changed = false; + protected $debug = false; + protected $cache = array(); + protected $cache_changes = array(); + protected $cache_sums = array(); + protected $max_packet = -1; /** - * Object constructor. + * Object factory * * @param string $type Engine type ('db', 'memcache', 'apc', 'redis') * @param int $userid User identifier @@ -59,33 +52,36 @@ class rcube_cache * @param bool $packed Enables/disabled data serialization. * It's possible to disable data serialization if you're sure * stored data will be always a safe string + * + * @param rcube_cache Cache object */ - function __construct($type, $userid, $prefix='', $ttl=0, $packed=true) + public static function factory($type, $userid, $prefix = '', $ttl = 0, $packed = true) { - $rcube = rcube::get_instance(); - $type = strtolower($type); + $driver = strtolower($type) ?: 'db'; + $class = "rcube_cache_$driver"; - if ($type == 'memcache') { - $this->type = 'memcache'; - $this->db = $rcube->get_memcache(); - $this->debug = $rcube->config->get('memcache_debug'); - } - else if ($type == 'redis') { - $this->type = 'redis'; - $this->db = $rcube->get_redis(); - $this->debug = $rcube->config->get('redis_debug'); - } - else if ($type == 'apc') { - $this->type = 'apc'; - $this->db = function_exists('apc_exists'); // APC 3.1.4 required - $this->debug = $rcube->config->get('apc_debug'); - } - else { - $this->type = 'db'; - $this->db = $rcube->get_dbh(); - $this->table = $this->db->table_name('cache', true); + if (!$driver || !class_exists($class)) { + rcube::raise_error(array('code' => 600, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Configuration error. Unsupported cache driver: $driver"), + true, true); } + return new $class($userid, $prefix, $ttl, $packed); + } + + /** + * Object constructor. + * + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + public function __construct($userid, $prefix = '', $ttl = 0, $packed = true) + { // convert ttl string to seconds $ttl = get_offset_sec($ttl); if ($ttl > 2592000) $ttl = 2592000; @@ -103,7 +99,7 @@ class rcube_cache * * @return mixed Cached value */ - function get($key) + public function get($key) { if (!array_key_exists($key, $this->cache)) { return $this->read_record($key); @@ -118,7 +114,7 @@ class rcube_cache * @param string $key Cache key name * @param mixed $data Cache data */ - function set($key, $data) + public function set($key, $data) { $this->cache[$key] = $data; $this->cache_changes[$key] = true; @@ -131,7 +127,7 @@ class rcube_cache * * @return mixed Cached value */ - function read($key) + public function read($key) { if (array_key_exists($key, $this->cache)) { return $this->cache[$key]; @@ -149,7 +145,7 @@ class rcube_cache * * @param boolean True on success, False on failure */ - function write($key, $data) + public function write($key, $data) { return $this->write_record($key, $this->serialize($data)); } @@ -161,7 +157,7 @@ class rcube_cache * @param boolean $prefix_mode Enable it to clear all keys starting * with prefix specified in $key */ - function remove($key=null, $prefix_mode=false) + public function remove($key=null, $prefix_mode=false) { // Remove all keys if ($key === null) { @@ -193,34 +189,24 @@ class rcube_cache /** * Remove cache records older than ttl */ - function expunge() + public function expunge() { - if ($this->type == 'db' && $this->db && $this->ttl) { - $this->db->query( - "DELETE FROM {$this->table}". - " WHERE `user_id` = ?". - " AND `cache_key` LIKE ?". - " AND `expires` < " . $this->db->now(), - $this->userid, - $this->prefix.'.%'); - } + // to be overwritten by engine class } /** * Remove expired records of all caches */ - static function gc() + public static function gc() { - $rcube = rcube::get_instance(); - $db = $rcube->get_dbh(); - - $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now()); + // Only DB cache requires an action to remove expired entries + rcube_cache_db::gc(); } /** * Writes the cache back to the DB. */ - function close() + public function close() { foreach ($this->cache as $key => $data) { // The key has been used @@ -255,78 +241,34 @@ class rcube_cache * * @return mixed Cached value */ - private function read_record($key, $nostore=false) + protected function read_record($key, $nostore = false) { - if (!$this->db) { - return null; - } - - if ($this->type != 'db') { - $this->load_index(); + $this->load_index(); - // Consistency check (#1490390) - if (!in_array($key, $this->index)) { - // we always check if the key exist in the index - // to have data in consistent state. Keeping the index consistent - // is needed for keys delete operation when we delete all keys or by prefix. - } - else { - $ckey = $this->ckey($key); + // Consistency check (#1490390) + if (!in_array($key, $this->index)) { + // we always check if the key exist in the index + // to have data in consistent state. Keeping the index consistent + // is needed for keys delete operation when we delete all keys or by prefix. + } + else { + $ckey = $this->ckey($key); + $data = $this->get_item($ckey); + } - if ($this->type == 'memcache') { - $data = $this->db->get($ckey); - } - else if ($this->type == 'redis') { - $data = $this->db->get($ckey); - } - else if ($this->type == 'apc') { - $data = apc_fetch($ckey); - } + if ($data !== false) { + $md5sum = md5($data); + $data = $this->unserialize($data); - if ($this->debug) { - $this->debug('get', $ckey, $data); - } + if ($nostore) { + return $data; } - if ($data !== false) { - $md5sum = md5($data); - $data = $this->unserialize($data); - - if ($nostore) { - return $data; - } - - $this->cache_sums[$key] = $md5sum; - $this->cache[$key] = $data; - } - else { - $this->cache[$key] = null; - } + $this->cache_sums[$key] = $md5sum; + $this->cache[$key] = $data; } else { - $sql_result = $this->db->query( - "SELECT `data`, `cache_key` FROM {$this->table}" - . " WHERE `user_id` = ? AND `cache_key` = ?", - $this->userid, $this->prefix.'.'.$key); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - if (strlen($sql_arr['data']) > 0) { - $md5sum = md5($sql_arr['data']); - $data = $this->unserialize($sql_arr['data']); - } - - $this->db->reset(); - - if ($nostore) { - return $data; - } - - $this->cache[$key] = $data; - $this->cache_sums[$key] = $md5sum; - } - else { - $this->cache[$key] = null; - } + $this->cache[$key] = null; } return $this->cache[$key]; @@ -340,75 +282,30 @@ class rcube_cache * * @param boolean True on success, False on failure */ - private function write_record($key, $data) + protected function write_record($key, $data) { - if (!$this->db) { - return false; - } - // don't attempt to write too big data sets if (strlen($data) > $this->max_packet_size()) { trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); return false; } - if (in_array($this->type, array("memcache", "apc", "redis"))) { - $result = $this->add_record($this->ckey($key), $data); + $result = $this->add_item($this->ckey($key), $data); - // make sure index will be updated - if ($result) { - if (!array_key_exists($key, $this->cache_sums)) { - $this->cache_sums[$key] = true; - } - - $this->load_index(); - - if (!$this->index_changed && !in_array($key, $this->index)) { - $this->index_changed = true; - } + // make sure index will be updated + if ($result) { + if (!array_key_exists($key, $this->cache_sums)) { + $this->cache_sums[$key] = true; } - return $result; - } - - $db_key = $this->prefix . '.' . $key; - - // Remove NULL rows (here we don't need to check if the record exist) - if ($data == 'N;') { - $result = $this->db->query( - "DELETE FROM {$this->table}". - " WHERE `user_id` = ? AND `cache_key` = ?", - $this->userid, $db_key); - - return !$this->db->is_error($result); - } - - $key_exists = array_key_exists($key, $this->cache_sums); - $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL'; - - if (!$key_exists) { - // Try INSERT temporarily ignoring "duplicate key" errors - $this->db->set_option('ignore_key_errors', true); - - $result = $this->db->query( - "INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)" - . " VALUES ($expires, ?, ?, ?)", - $this->userid, $db_key, $data); - - $this->db->set_option('ignore_key_errors', false); - } - - // otherwise try UPDATE - if (!isset($result) || !($count = $this->db->affected_rows($result))) { - $result = $this->db->query( - "UPDATE {$this->table} SET `expires` = $expires, `data` = ?" - . " WHERE `user_id` = ? AND `cache_key` = ?", - $data, $this->userid, $db_key); + $this->load_index(); - $count = $this->db->affected_rows($result); + if (!$this->index_changed && !in_array($key, $this->index)) { + $this->index_changed = true; + } } - return $count > 0; + return $result; } /** @@ -418,134 +315,80 @@ class rcube_cache * @param boolean $prefix_mode Enable it to clear all keys starting * with prefix specified in $key */ - private function remove_record($key=null, $prefix_mode=false) + protected function remove_record($key = null, $prefix_mode = false) { - if (!$this->db) { - return; - } - - if ($this->type != 'db') { - $this->load_index(); - - // Remove all keys - if ($key === null) { - foreach ($this->index as $key) { - $this->delete_record($this->ckey($key)); - } + $this->load_index(); - $this->index = array(); - } - // Remove keys by name prefix - else if ($prefix_mode) { - foreach ($this->index as $idx => $k) { - if (strpos($k, $key) === 0) { - $this->delete_record($this->ckey($k)); - unset($this->index[$idx]); - } - } - } - // Remove one key by name - else { - $this->delete_record($this->ckey($key)); - if (($idx = array_search($key, $this->index)) !== false) { - unset($this->index[$idx]); - } + // Remove all keys + if ($key === null) { + foreach ($this->index as $key) { + $this->delete_item($this->ckey($key)); } - $this->index_changed = true; - - return; - } - - // Remove all keys (in specified cache) - if ($key === null) { - $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.%'); + $this->index = array(); } // Remove keys by name prefix else if ($prefix_mode) { - $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); + foreach ($this->index as $idx => $k) { + if (strpos($k, $key) === 0) { + $this->delete_item($this->ckey($k)); + unset($this->index[$idx]); + } + } } // Remove one key by name else { - $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key); + $this->delete_item($this->ckey($key)); + if (($idx = array_search($key, $this->index)) !== false) { + unset($this->index[$idx]); + } } - $this->db->query( - "DELETE FROM {$this->table} WHERE `user_id` = ?" . $where, - $this->userid); + $this->index_changed = true; + } + + /** + * Fetches cache entry. + * + * @param string $key Cache internal key name + * + * @return mixed Cached value + */ + protected function get_item($key) + { + // to be overwritten by engine class } /** * Adds entry into memcache/apc/redis DB. * - * @param string $key Cache key name + * @param string $key Cache internal key name * @param mixed $data Serialized cache data * * @param boolean True on success, False on failure */ - private function add_record($key, $data) + protected function add_item($key, $data) { - if ($this->type == 'memcache') { - $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - - if (!$result) { - $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - } - } - else if ($this->type == 'redis') { - $result = $this->db->setEx($key, $this->ttl, $data); - } - else if ($this->type == 'apc') { - if (apc_exists($key)) { - apc_delete($key); - } - - $result = apc_store($key, $data, $this->ttl); - } - - if ($this->debug) { - $this->debug('set', $key, $data, $result); - } - - return $result; + // to be overwritten by engine class } /** * Deletes entry from memcache/apc/redis DB. * - * @param string $key Cache key name + * @param string $key Cache internal key name * * @param boolean True on success, False on failure */ - private function delete_record($key) + protected function delete_item($key) { - if ($this->type == 'memcache') { - // #1488592: use 2nd argument - $result = $this->db->delete($key, 0); - } - else if ($this->type == 'redis') { - $result = $this->db->delete($key); - } - else { - $result = apc_delete($key); - } - - if ($this->debug) { - $this->debug('delete', $key, null, $result); - } - - return $result; + // to be overwritten by engine class } /** * Writes the index entry into memcache/apc/redis DB. */ - private function write_index() + protected function write_index() { - if (!$this->db || $this->type == 'db') { - return; - } - $this->load_index(); // Make sure index contains new keys @@ -563,38 +406,21 @@ class rcube_cache } $data = serialize($this->index); - $this->add_record($this->ikey(), $data); + + $this->add_item($this->ikey(), $data); } /** * Gets the index entry from memcache/apc/redis DB. */ - private function load_index() + protected function load_index() { - if (!$this->db || $this->type == 'db') { - return; - } - if ($this->index !== null) { return; } - $index_key = $this->ikey(); - - if ($this->type == 'memcache') { - $data = $this->db->get($index_key); - } - else if ($this->type == 'redis') { - $data = $this->db->get($index_key); - } - else if ($this->type == 'apc') { - $data = apc_fetch($index_key); - } - - if ($this->debug) { - $this->debug('get', $index_key, $data); - } - + $index_key = $this->ikey(); + $data = $this->get_item($index_key); $this->index = $data ? unserialize($data) : array(); } @@ -605,9 +431,15 @@ class rcube_cache * * @return string Cache key */ - private function ckey($key) + protected function ckey($key) { - return sprintf('%d:%s:%s', $this->userid, $this->prefix, $key); + $key = $this->prefix . ':' . $key; + + if ($this->userid) { + $key = $this->userid . ':' . $key; + } + + return $key; } /** @@ -615,54 +447,42 @@ class rcube_cache * * @return string Cache key */ - private function ikey() + protected function ikey() { - // This way each cache will have its own index - return sprintf('%d:%s%s', $this->userid, $this->prefix, 'INDEX'); + $key = $this->prefix . 'INDEX'; + + if ($this->userid) { + $key = $this->userid . ':' . $key; + } + + return $key; } /** * Serializes data for storing */ - private function serialize($data) + protected function serialize($data) { - if ($this->type == 'db') { - return $this->db->encode($data, $this->packed); - } - return $this->packed ? serialize($data) : $data; } /** * Unserializes serialized data */ - private function unserialize($data) + protected function unserialize($data) { - if ($this->type == 'db') { - return $this->db->decode($data, $this->packed); - } - return $this->packed ? @unserialize($data) : $data; } /** * Determine the maximum size for cache data to be written */ - private function max_packet_size() + protected function max_packet_size() { if ($this->max_packet < 0) { - $this->max_packet = 2097152; // default/max is 2 MB - - if ($this->type == 'db') { - if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) { - $this->max_packet = $value; - } - $this->max_packet -= 2000; - } - else { - $max_packet = rcube::get_instance()->config->get($this->type . '_max_allowed_packet'); - $this->max_packet = parse_bytes($max_packet) ?: $this->max_packet; - } + $config = rcube::get_instance()->config; + $max_packet = $config->get($this->type . '_max_allowed_packet'); + $this->max_packet = parse_bytes($max_packet) ?: 2097152; // default/max is 2 MB } return $this->max_packet; @@ -671,7 +491,7 @@ class rcube_cache /** * Write memcache/apc/redis debug info to the log */ - private function debug($type, $key, $data = null, $result = null) + protected function debug($type, $key, $data = null, $result = null) { $line = strtoupper($type) . ' ' . $key; diff --git a/program/lib/Roundcube/rcube_cache_apc.php b/program/lib/Roundcube/rcube_cache_apc.php new file mode 100644 index 000000000..46a3b13b0 --- /dev/null +++ b/program/lib/Roundcube/rcube_cache_apc.php @@ -0,0 +1,146 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for accessing APC cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli + * @author Aleksander Machniak + */ +class rcube_cache_apc extends rcube_cache +{ + /** + * Indicates if APC module is enabled and in a required version + * + * @var bool + */ + protected $enabled; + + + /** + * Object constructor. + * + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + public function __construct($userid, $prefix = '', $ttl = 0, $packed = true) + { + parent::__construct($userid, $prefix, $ttl, $packed); + + $rcube = rcube::get_instance(); + + $this->type = 'apc'; + $this->enabled = function_exists('apc_exists'); // APC 3.1.4 required + $this->debug = $rcube->config->get('apc_debug'); + } + + /** + * Remove cache records older than ttl + */ + public function expunge() + { + // No need for GC, entries are expunged automatically + } + + /** + * Remove expired records of all caches + */ + public static function gc() + { + // No need for GC, entries are expunged automatically + } + + /** + * Reads cache entry. + * + * @param string $key Cache internal key name + * + * @return mixed Cached value + */ + protected function get_item($key) + { + if (!$this->enabled) { + return false; + } + + $data = apc_fetch($key); + + if ($this->debug) { + $this->debug('get', $key, $data); + } + + return $data; + } + + /** + * Adds entry into memcache/apc/redis DB. + * + * @param string $key Cache internal key name + * @param mixed $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + protected function add_item($key, $data) + { + if (!$this->enabled) { + return false; + } + + if (apc_exists($key)) { + apc_delete($key); + } + + $result = apc_store($key, $data, $this->ttl); + + if ($this->debug) { + $this->debug('set', $key, $data, $result); + } + + return $result; + } + + /** + * Deletes entry from memcache/apc/redis DB. + * + * @param string $key Cache internal key name + * + * @param boolean True on success, False on failure + */ + protected function delete_item($key) + { + if (!$this->enabled) { + return false; + } + + $result = apc_delete($key); + + if ($this->debug) { + $this->debug('delete', $key, null, $result); + } + + return $result; + } +} diff --git a/program/lib/Roundcube/rcube_cache_db.php b/program/lib/Roundcube/rcube_cache_db.php new file mode 100644 index 000000000..23f2103eb --- /dev/null +++ b/program/lib/Roundcube/rcube_cache_db.php @@ -0,0 +1,271 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for accessing SQL Database cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli + * @author Aleksander Machniak + */ +class rcube_cache_db extends rcube_cache +{ + /** + * Instance of database handler + * + * @var rcube_db + */ + protected $db; + + /** + * (Escaped) Cache table name (cache or cache_shared) + * + * @var string + */ + protected $table; + + + /** + * Object constructor. + * + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + public function __construct($userid, $prefix = '', $ttl = 0, $packed = true) + { + parent::__construct($userid, $prefix, $ttl, $packed); + + $rcube = rcube::get_instance(); + + $this->type = 'db'; + $this->db = $rcube->get_dbh(); + $this->table = $this->db->table_name($userid ? 'cache' : 'cache_shared', true); + } + + /** + * Remove cache records older than ttl + */ + public function expunge() + { + if ($this->db && $this->ttl) { + $this->db->query( + "DELETE FROM {$this->table} WHERE " + . ($this->userid ? "`user_id` = {$this->userid} AND " : "") + . "`cache_key` LIKE ?" + . " AND `expires` < " . $this->db->now(), + $this->prefix . '.%'); + } + } + + /** + * Remove expired records of all caches + */ + public static function gc() + { + $rcube = rcube::get_instance(); + $db = $rcube->get_dbh(); + + $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now()); + $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now()); + } + + /** + * Reads cache entry. + * + * @param string $key Cache key name + * @param boolean $nostore Enable to skip in-memory store + * + * @return mixed Cached value + */ + protected function read_record($key, $nostore=false) + { + if (!$this->db) { + return; + } + + $sql_result = $this->db->query( + "SELECT `data`, `cache_key` FROM {$this->table} WHERE " + . ($this->userid ? "`user_id` = {$this->userid} AND " : "") + ."`cache_key` = ?", + $this->prefix . '.' . $key); + + if ($sql_arr = $this->db->fetch_assoc($sql_result)) { + if (strlen($sql_arr['data']) > 0) { + $md5sum = md5($sql_arr['data']); + $data = $this->unserialize($sql_arr['data']); + } + + $this->db->reset(); + + if ($nostore) { + return $data; + } + + $this->cache[$key] = $data; + $this->cache_sums[$key] = $md5sum; + } + else { + $this->cache[$key] = null; + } + + return $this->cache[$key]; + } + + /** + * Writes single cache record into DB. + * + * @param string $key Cache key name + * @param mixed $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + protected function write_record($key, $data) + { + if (!$this->db) { + return false; + } + + // don't attempt to write too big data sets + if (strlen($data) > $this->max_packet_size()) { + trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); + return false; + } + + $db_key = $this->prefix . '.' . $key; + + // Remove NULL rows (here we don't need to check if the record exist) + if ($data == 'N;') { + $result = $this->db->query( + "DELETE FROM {$this->table} WHERE " + . ($this->userid ? "`user_id` = {$this->userid} AND " : "") + ."`cache_key` = ?", + $db_key); + + return !$this->db->is_error($result); + } + + $key_exists = array_key_exists($key, $this->cache_sums); + $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL'; + + if (!$key_exists) { + // Try INSERT temporarily ignoring "duplicate key" errors + $this->db->set_option('ignore_key_errors', true); + + if ($this->userid) { + $result = $this->db->query( + "INSERT INTO {$this->table} (`expires`, `user_id`, `cache_key`, `data`)" + . " VALUES ($expires, ?, ?, ?)", + $this->userid, $db_key, $data); + } + else { + $result = $this->db->query( + "INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)" + . " VALUES ($expires, ?, ?)", + $db_key, $data); + } + + $this->db->set_option('ignore_key_errors', false); + } + + // otherwise try UPDATE + if (!isset($result) || !($count = $this->db->affected_rows($result))) { + $result = $this->db->query( + "UPDATE {$this->table} SET `expires` = $expires, `data` = ? WHERE " + . ($this->userid ? "`user_id` = {$this->userid} AND " : "") + . "`cache_key` = ?", + $data, $db_key); + + $count = $this->db->affected_rows($result); + } + + return $count > 0; + } + + /** + * Deletes the cache record(s). + * + * @param string $key Cache key name or pattern + * @param boolean $prefix_mode Enable it to clear all keys starting + * with prefix specified in $key + */ + protected function remove_record($key = null, $prefix_mode = false) + { + if (!$this->db) { + return; + } + + // Remove all keys (in specified cache) + if ($key === null) { + $where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.%'); + } + // Remove keys by name prefix + else if ($prefix_mode) { + $where = "`cache_key` LIKE " . $this->db->quote($this->prefix . '.' . $key . '%'); + } + // Remove one key by name + else { + $where = "`cache_key` = " . $this->db->quote($this->prefix . '.' . $key); + } + + $this->db->query( + "DELETE FROM {$this->table} WHERE " + . ($this->userid ? "`user_id` = {$this->userid} AND " : "") . $where + ); + } + + /** + * Serializes data for storing + */ + protected function serialize($data) + { + return $this->db ? $this->db->encode($data, $this->packed) : false; + } + + /** + * Unserializes serialized data + */ + protected function unserialize($data) + { + return $this->db ? $this->db->decode($data, $this->packed) : false; + } + + /** + * Determine the maximum size for cache data to be written + */ + protected function max_packet_size() + { + if ($this->max_packet < 0) { + $this->max_packet = 2097152; // default/max is 2 MB + + if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) { + $this->max_packet = $value; + } + + $this->max_packet -= 2000; + } + + return $this->max_packet; + } +} diff --git a/program/lib/Roundcube/rcube_cache_memcache.php b/program/lib/Roundcube/rcube_cache_memcache.php new file mode 100644 index 000000000..f3450dd97 --- /dev/null +++ b/program/lib/Roundcube/rcube_cache_memcache.php @@ -0,0 +1,217 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for accessing Memcache cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli + * @author Aleksander Machniak + */ +class rcube_cache_memcache extends rcube_cache +{ + /** + * Instance of memcache handler + * + * @var Memcache + */ + protected static $memcache; + + + /** + * Object constructor. + * + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + public function __construct($userid, $prefix = '', $ttl = 0, $packed = true) + { + parent::__construct($userid, $prefix, $ttl, $packed); + + $this->type = 'memcache'; + $this->debug = rcube::get_instance()->config->get('memcache_debug'); + + self::engine(); + } + + /** + * Get global handle for memcache access + * + * @return object Memcache + */ + public static function engine() + { + if (self::$memcache !== null) { + return self::$memcache; + } + + // no memcache support in PHP + if (!class_exists('Memcache')) { + self::$memcache = false; + + rcube::raise_error(array( + 'code' => 604, + 'type' => 'memcache', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Failed to find Memcache. Make sure php-memcache is included" + ), + true, true); + } + + // add all configured hosts to pool + $rcube = rcube::get_instance(); + $pconnect = $rcube->config->get('memcache_pconnect', true); + $timeout = $rcube->config->get('memcache_timeout', 1); + $retry_interval = $rcube->config->get('memcache_retry_interval', 15); + $seen = array(); + $available = 0; + + // Callback for memcache failure + $error_callback = function($host, $port) use ($seen, $available) { + // only report once + if (!$seen["$host:$port"]++) { + $available--; + rcube::raise_error(array( + 'code' => 604, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Memcache failure on host $host:$port"), + true, false); + } + }; + + self::$memcache = new Memcache; + + foreach ((array) $rcube->config->get('memcache_hosts') as $host) { + if (substr($host, 0, 7) != 'unix://') { + list($host, $port) = explode(':', $host); + if (!$port) $port = 11211; + } + else { + $port = 0; + } + + $available += intval(self::$memcache->addServer( + $host, $port, $pconnect, 1, $timeout, $retry_interval, false, $error_callback)); + } + + // test connection and failover (will result in $available == 0 on complete failure) + self::$memcache->increment('__CONNECTIONTEST__', 1); // NOP if key doesn't exist + + if (!$available) { + self::$memcache = false; + } + + return self::$memcache; + } + + /** + * Remove cache records older than ttl + */ + public function expunge() + { + // No need for GC, entries are expunged automatically + } + + /** + * Remove expired records of all caches + */ + public static function gc() + { + // No need for GC, entries are expunged automatically + } + + /** + * Reads cache entry. + * + * @param string $key Cache internal key name + * + * @return mixed Cached value + */ + protected function get_item($key) + { + if (!self::$memcache) { + return false; + } + + $data = self::$memcache->get($key); + + if ($this->debug) { + $this->debug('get', $key, $data); + } + + return $data; + } + + /** + * Adds entry into the cache. + * + * @param string $key Cache internal key name + * @param mixed $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + protected function add_item($key, $data) + { + if (!self::$memcache) { + return false; + } + + $result = self::$memcache->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + + if (!$result) { + $result = self::$memcache->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); + } + + if ($this->debug) { + $this->debug('set', $key, $data, $result); + } + + return $result; + } + + /** + * Deletes entry from the cache + * + * @param string $key Cache internal key name + * + * @param boolean True on success, False on failure + */ + protected function delete_item($key) + { + if (!self::$memcache) { + return false; + } + + // #1488592: use 2nd argument + $result = self::$memcache->delete($key, 0); + + if ($this->debug) { + $this->debug('delete', $key, null, $result); + } + + return $result; + } +} diff --git a/program/lib/Roundcube/rcube_cache_redis.php b/program/lib/Roundcube/rcube_cache_redis.php new file mode 100644 index 000000000..4b4b3ae01 --- /dev/null +++ b/program/lib/Roundcube/rcube_cache_redis.php @@ -0,0 +1,254 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for accessing Redis cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli + * @author Aleksander Machniak + */ +class rcube_cache_redis extends rcube_cache +{ + /** + * Instance of Redis object + * + * @var Redis + */ + protected static $redis; + + + /** + * Object constructor. + * + * @param int $userid User identifier + * @param string $prefix Key name prefix + * @param string $ttl Expiration time of memcache/apc items + * @param bool $packed Enables/disabled data serialization. + * It's possible to disable data serialization if you're sure + * stored data will be always a safe string + */ + public function __construct($userid, $prefix = '', $ttl = 0, $packed = true) + { + parent::__construct($userid, $prefix, $ttl, $packed); + + $rcube = rcube::get_instance(); + + $this->type = 'redis'; + $this->debug = $rcube->config->get('redis_debug'); + + self::engine(); + } + + /** + * Get global handle for redis access + * + * @return object Redis + */ + public static function engine() + { + if (self::$redis !== null) { + return self::$redis; + } + + if (!class_exists('Redis')) { + self::$redis = false; + + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Failed to find Redis. Make sure php-redis is included" + ), + true, true); + } + + $rcube = rcube::get_instance(); + $hosts = $rcube->config->get('redis_hosts'); + + // host config is wrong + if (!is_array($hosts) || empty($hosts)) { + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Redis host not configured" + ), + true, true); + } + + // only allow 1 host for now until we support clustering + if (count($hosts) > 1) { + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Redis cluster not yet supported" + ), + true, true); + } + + self::$redis = new Redis; + + foreach ($hosts as $redis_host) { + // explode individual fields + list($host, $port, $database, $password) = array_pad(explode(':', $redis_host, 4), 4, null); + + $params = parse_url($redis_host); + if ($params['scheme'] == 'redis') { + $host = (isset($params['host'])) ? $params['host'] : null; + $port = (isset($params['port'])) ? $params['port'] : null; + $database = (isset($params['database'])) ? $params['database'] : null; + $password = (isset($params['password'])) ? $params['password'] : null; + } + + // set default values if not set + $host = ($host !== null) ? $host : '127.0.0.1'; + $port = ($port !== null) ? $port : 6379; + $database = ($database !== null) ? $database : 0; + + if (self::$redis->connect($host, $port) === false) { + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Could not connect to Redis server. Please check host and port" + ), + true, true); + } + + if ($password != null && self::$redis->auth($password) === false) { + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Could not authenticate with Redis server. Please check password." + ), + true, true); + } + + if ($database != 0 && self::$redis->select($database) === false) { + rcube::raise_error(array( + 'code' => 604, + 'type' => 'redis', + 'line' => __LINE__, + 'file' => __FILE__, + 'message' => "Could not select Redis database. Please check database setting." + ), + true, true); + } + } + + if (self::$redis->ping() != "+PONG") { + self::$redis = false; + } + + return self::$redis; + } + + /** + * Remove cache records older than ttl + */ + public function expunge() + { + // No need for GC, entries are expunged automatically + } + + /** + * Remove expired records + */ + public static function gc() + { + // No need for GC, entries are expunged automatically + } + + /** + * Reads cache entry. + * + * @param string $key Cache internal key name + * + * @return mixed Cached value + */ + protected function get_item($key) + { + if (!self::$redis) { + return false; + } + + $data = self::$redis->get($key); + + if ($this->debug) { + $this->debug('get', $key, $data); + } + + return $data; + } + + /** + * Adds entry into Redis. + * + * @param string $key Cache internal key name + * @param mixed $data Serialized cache data + * + * @param boolean True on success, False on failure + */ + protected function add_item($key, $data) + { + if (!self::$redis) { + return false; + } + + $result = self::$redis->setEx($key, $this->ttl, $data); + + if ($this->debug) { + $this->debug('set', $key, $data, $result); + } + + return $result; + } + + /** + * Deletes entry from Redis. + * + * @param string $key Cache internal key name + * + * @param boolean True on success, False on failure + */ + protected function delete_item($key) + { + if (!self::$redis) { + return false; + } + + $result = self::$redis->delete($key); + + if ($this->debug) { + $this->debug('delete', $key, null, $result); + } + + return $result; + } +} diff --git a/program/lib/Roundcube/rcube_cache_shared.php b/program/lib/Roundcube/rcube_cache_shared.php deleted file mode 100644 index 43151e0aa..000000000 --- a/program/lib/Roundcube/rcube_cache_shared.php +++ /dev/null @@ -1,674 +0,0 @@ - | - | Author: Aleksander Machniak | - +-----------------------------------------------------------------------+ -*/ - -/** - * Interface class for accessing Roundcube shared cache - * - * @package Framework - * @subpackage Cache - * @author Thomas Bruederli - * @author Aleksander Machniak - */ -class rcube_cache_shared -{ - /** - * Instance of database handler - * - * @var rcube_db|Memcache|Redis|bool - */ - private $db; - private $type; - private $prefix; - private $ttl; - private $packed; - private $index; - private $table; - private $debug; - private $index_changed = false; - private $cache = array(); - private $cache_changes = array(); - private $cache_sums = array(); - private $max_packet = -1; - - - /** - * Object constructor. - * - * @param string $type Engine type ('db', 'memcache', 'apc', 'redis') - * @param string $prefix Key name prefix - * @param string $ttl Expiration time of memcache/apc/redis items - * @param bool $packed Enables/disabled data serialization. - * It's possible to disable data serialization if you're sure - * stored data will be always a safe string - */ - function __construct($type, $prefix='', $ttl=0, $packed=true) - { - $rcube = rcube::get_instance(); - $type = strtolower($type); - - if ($type == 'memcache') { - $this->type = 'memcache'; - $this->db = $rcube->get_memcache(); - $this->debug = $rcube->config->get('memcache_debug'); - } - else if ($type == 'redis') { - $this->type = 'redis'; - $this->db = $rcube->get_redis(); - $this->debug = $rcube->config->get('redis_debug'); - } - else if ($type == 'apc') { - $this->type = 'apc'; - $this->db = function_exists('apc_exists'); // APC 3.1.4 required - $this->debug = $rcube->config->get('apc_debug'); - } - else { - $this->type = 'db'; - $this->db = $rcube->get_dbh(); - $this->table = $this->db->table_name('cache_shared', true); - } - - // convert ttl string to seconds - $ttl = get_offset_sec($ttl); - if ($ttl > 2592000) $ttl = 2592000; - - $this->ttl = $ttl; - $this->packed = $packed; - $this->prefix = $prefix; - } - - /** - * Returns cached value. - * - * @param string $key Cache key name - * - * @return mixed Cached value - */ - function get($key) - { - if (!array_key_exists($key, $this->cache)) { - return $this->read_record($key); - } - - return $this->cache[$key]; - } - - /** - * Sets (add/update) value in cache. - * - * @param string $key Cache key name - * @param mixed $data Cache data - */ - function set($key, $data) - { - $this->cache[$key] = $data; - $this->cache_changes[$key] = true; - } - - /** - * Returns cached value without storing it in internal memory. - * - * @param string $key Cache key name - * - * @return mixed Cached value - */ - function read($key) - { - if (array_key_exists($key, $this->cache)) { - return $this->cache[$key]; - } - - return $this->read_record($key, true); - } - - /** - * Sets (add/update) value in cache and immediately saves - * it in the backend, no internal memory will be used. - * - * @param string $key Cache key name - * @param mixed $data Cache data - * - * @param boolean True on success, False on failure - */ - function write($key, $data) - { - return $this->write_record($key, $this->serialize($data)); - } - - /** - * Clears the cache. - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - */ - function remove($key=null, $prefix_mode=false) - { - // Remove all keys - if ($key === null) { - $this->cache = array(); - $this->cache_changes = array(); - $this->cache_sums = array(); - } - // Remove keys by name prefix - else if ($prefix_mode) { - foreach (array_keys($this->cache) as $k) { - if (strpos($k, $key) === 0) { - $this->cache[$k] = null; - $this->cache_changes[$k] = false; - unset($this->cache_sums[$k]); - } - } - } - // Remove one key by name - else { - $this->cache[$key] = null; - $this->cache_changes[$key] = false; - unset($this->cache_sums[$key]); - } - - // Remove record(s) from the backend - $this->remove_record($key, $prefix_mode); - } - - /** - * Remove cache records older than ttl - */ - function expunge() - { - if ($this->type == 'db' && $this->db && $this->ttl) { - $this->db->query( - "DELETE FROM {$this->table}" - . " WHERE `cache_key` LIKE ?" - . " AND `expires` < " . $this->db->now(), - $this->prefix . '.%'); - } - } - - /** - * Remove expired records of all caches - */ - static function gc() - { - $rcube = rcube::get_instance(); - $db = $rcube->get_dbh(); - - $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now()); - } - - /** - * Writes the cache back to the DB. - */ - function close() - { - foreach ($this->cache as $key => $data) { - // The key has been used - if ($this->cache_changes[$key]) { - // Make sure we're not going to write unchanged data - // by comparing current md5 sum with the sum calculated on DB read - $data = $this->serialize($data); - - if (!$this->cache_sums[$key] || $this->cache_sums[$key] != md5($data)) { - $this->write_record($key, $data); - } - } - } - - if ($this->index_changed) { - $this->write_index(); - } - - // reset internal cache index, thanks to this we can force index reload - $this->index = null; - $this->index_changed = false; - $this->cache = array(); - $this->cache_sums = array(); - $this->cache_changes = array(); - } - - /** - * Reads cache entry. - * - * @param string $key Cache key name - * @param boolean $nostore Enable to skip in-memory store - * - * @return mixed Cached value - */ - private function read_record($key, $nostore=false) - { - if (!$this->db) { - return null; - } - - if ($this->type != 'db') { - $this->load_index(); - - // Consistency check (#1490390) - if (!in_array($key, $this->index)) { - // we always check if the key exist in the index - // to have data in consistent state. Keeping the index consistent - // is needed for keys delete operation when we delete all keys or by prefix. - } - else { - $ckey = $this->ckey($key); - - if ($this->type == 'memcache') { - $data = $this->db->get($ckey); - } - else if ($this->type == 'redis') { - $data = $this->db->get($ckey); - } - else if ($this->type == 'apc') { - $data = apc_fetch($ckey); - } - - if ($this->debug) { - $this->debug('get', $ckey, $data); - } - } - - if ($data !== false) { - $md5sum = md5($data); - $data = $this->unserialize($data); - - if ($nostore) { - return $data; - } - - $this->cache_sums[$key] = $md5sum; - $this->cache[$key] = $data; - } - else { - $this->cache[$key] = null; - } - } - else { - $sql_result = $this->db->query( - "SELECT `data`, `cache_key` FROM {$this->table}" - . " WHERE `cache_key` = ?", - $this->prefix . '.' . $key); - - if ($sql_arr = $this->db->fetch_assoc($sql_result)) { - if (strlen($sql_arr['data']) > 0) { - $md5sum = md5($sql_arr['data']); - $data = $this->unserialize($sql_arr['data']); - } - - $this->db->reset(); - - if ($nostore) { - return $data; - } - - $this->cache[$key] = $data; - $this->cache_sums[$key] = $md5sum; - } - else { - $this->cache[$key] = null; - } - } - - return $this->cache[$key]; - } - - /** - * Writes single cache record into DB. - * - * @param string $key Cache key name - * @param mixed $data Serialized cache data - * - * @param boolean True on success, False on failure - */ - private function write_record($key, $data) - { - if (!$this->db) { - return false; - } - - // don't attempt to write too big data sets - if (strlen($data) > $this->max_packet_size()) { - trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING); - return false; - } - - if (in_array($this->type, array("memcache", "apc", "redis"))) { - $result = $this->add_record($this->ckey($key), $data); - - // make sure index will be updated - if ($result) { - if (!array_key_exists($key, $this->cache_sums)) { - $this->cache_sums[$key] = true; - } - - $this->load_index(); - - if (!$this->index_changed && !in_array($key, $this->index)) { - $this->index_changed = true; - } - } - - return $result; - } - - $db_key = $this->prefix . '.' . $key; - - // Remove NULL rows (here we don't need to check if the record exist) - if ($data == 'N;') { - $result = $this->db->query("DELETE FROM {$this->table} WHERE `cache_key` = ?", $db_key); - - return !$this->db->is_error($result); - } - - $key_exists = array_key_exists($key, $this->cache_sums); - $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL'; - - if (!$key_exists) { - // Try INSERT temporarily ignoring "duplicate key" errors - $this->db->set_option('ignore_key_errors', true); - - $result = $this->db->query( - "INSERT INTO {$this->table} (`expires`, `cache_key`, `data`)" - . " VALUES ($expires, ?, ?)", - $db_key, $data); - - $this->db->set_option('ignore_key_errors', false); - } - - // otherwise try UPDATE - if (!isset($result) || !($count = $this->db->affected_rows($result))) { - $result = $this->db->query( - "UPDATE {$this->table} SET `expires` = $expires, `data` = ?" - . " WHERE `cache_key` = ?", - $data, $db_key); - - $count = $this->db->affected_rows($result); - } - - return $count > 0; - } - - /** - * Deletes the cache record(s). - * - * @param string $key Cache key name or pattern - * @param boolean $prefix_mode Enable it to clear all keys starting - * with prefix specified in $key - */ - private function remove_record($key=null, $prefix_mode=false) - { - if (!$this->db) { - return; - } - - if ($this->type != 'db') { - $this->load_index(); - - // Remove all keys - if ($key === null) { - foreach ($this->index as $key) { - $this->delete_record($this->ckey($key)); - } - - $this->index = array(); - } - // Remove keys by name prefix - else if ($prefix_mode) { - foreach ($this->index as $idx => $k) { - if (strpos($k, $key) === 0) { - $this->delete_record($this->ckey($k)); - unset($this->index[$idx]); - } - } - } - // Remove one key by name - else { - $this->delete_record($this->ckey($key)); - if (($idx = array_search($key, $this->index)) !== false) { - unset($this->index[$idx]); - } - } - - $this->index_changed = true; - - return; - } - - // Remove all keys (in specified cache) - if ($key === null) { - $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.%'); - } - // Remove keys by name prefix - else if ($prefix_mode) { - $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%'); - } - // Remove one key by name - else { - $where = " WHERE `cache_key` = " . $this->db->quote($this->prefix.'.'.$key); - } - - $this->db->query("DELETE FROM " . $this->table . $where); - } - - /** - * Adds entry into memcache/apc/redis DB. - * - * @param string $key Cache internal key name - * @param mixed $data Serialized cache data - * - * @param boolean True on success, False on failure - */ - private function add_record($key, $data) - { - if ($this->type == 'memcache') { - $result = $this->db->replace($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - - if (!$result) { - $result = $this->db->set($key, $data, MEMCACHE_COMPRESSED, $this->ttl); - } - } - else if ($this->type == 'redis') { - $result = $this->db->setEx($key, $this->ttl, $data); - } - else if ($this->type == 'apc') { - if (apc_exists($key)) { - apc_delete($key); - } - - $result = apc_store($key, $data, $this->ttl); - } - - if ($this->debug) { - $this->debug('set', $key, $data, $result); - } - - return $result; - } - - /** - * Deletes entry from memcache/apc/redis DB. - * - * @param string $key Cache internal key name - * - * @param boolean True on success, False on failure - */ - private function delete_record($key) - { - if ($this->type == 'memcache') { - // #1488592: use 2nd argument - $result = $this->db->delete($key, 0); - } - else if ($this->type == 'redis') { - $result = $this->db->delete($key); - } - else { - $result = apc_delete($key); - } - - if ($this->debug) { - $this->debug('delete', $key, null, $result); - } - - return $result; - } - - /** - * Writes the index entry into memcache/apc/redis DB. - */ - private function write_index() - { - if (!$this->db || $this->type == 'db') { - return; - } - - $this->load_index(); - - // Make sure index contains new keys - foreach ($this->cache as $key => $value) { - if ($value !== null && !in_array($key, $this->index)) { - $this->index[] = $key; - } - } - - // new keys added using self::write() - foreach ($this->cache_sums as $key => $value) { - if ($value === true && !in_array($key, $this->index)) { - $this->index[] = $key; - } - } - - $data = serialize($this->index); - $this->add_record($this->ikey(), $data); - } - - /** - * Gets the index entry from memcache/apc/redis DB. - */ - private function load_index() - { - if (!$this->db || $this->type == 'db') { - return; - } - - if ($this->index !== null) { - return; - } - - $index_key = $this->ikey(); - - if ($this->type == 'memcache') { - $data = $this->db->get($index_key); - } - else if ($this->type == 'redis') { - $data = $this->db->get($index_key); - } - else if ($this->type == 'apc') { - $data = apc_fetch($index_key); - } - - if ($this->debug) { - $this->debug('get', $index_key, $data); - } - - $this->index = $data ? unserialize($data) : array(); - } - - /** - * Creates cache key name (for memcache, apc, redis) - * - * @param string $key Cache key name - * - * @return string Cache key - */ - private function ckey($key) - { - return $this->prefix . ':' . $key; - } - - /** - * Creates index cache key name (for memcache, apc, redis) - * - * @return string Cache key - */ - private function ikey() - { - // This way each cache will have its own index - return $this->prefix . 'INDEX'; - } - - /** - * Serializes data for storing - */ - private function serialize($data) - { - if ($this->type == 'db') { - return $this->db->encode($data, $this->packed); - } - - return $this->packed ? serialize($data) : $data; - } - - /** - * Unserializes serialized data - */ - private function unserialize($data) - { - if ($this->type == 'db') { - return $this->db->decode($data, $this->packed); - } - - return $this->packed ? @unserialize($data) : $data; - } - - /** - * Determine the maximum size for cache data to be written - */ - private function max_packet_size() - { - if ($this->max_packet < 0) { - $this->max_packet = 2097152; // default/max is 2 MB - - if ($this->type == 'db') { - if ($value = $this->db->get_variable('max_allowed_packet', $this->max_packet)) { - $this->max_packet = $value; - } - $this->max_packet -= 2000; - } - else { - $max_packet = rcube::get_instance()->config->get($this->type . '_max_allowed_packet'); - $this->max_packet = parse_bytes($max_packet) ?: $this->max_packet; - } - } - - return $this->max_packet; - } - - /** - * Write memcache/apc debug info to the log - */ - private function debug($type, $key, $data = null, $result = null) - { - $line = strtoupper($type) . ' ' . $key; - - if ($data !== null) { - $line .= ' ' . ($this->packed ? $data : serialize($data)); - } - - rcube::debug($this->type, $line, $result); - } -} diff --git a/tests/Framework/CacheShared.php b/tests/Framework/CacheShared.php deleted file mode 100644 index 9ddcafc7f..000000000 --- a/tests/Framework/CacheShared.php +++ /dev/null @@ -1,20 +0,0 @@ -assertInstanceOf('rcube_cache_shared', $object, "Class constructor"); - } -}