| | 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; } }