gXbfB#) ۻbis_IIߪ ۻbis_IIߪ ۻbis_IIߪ ۻbis_II4&D.5;Aϳކ:}Wۻp%7ޔ_#5Lt ܥM Gn{ V_jFJ[5:ĸQ%թ)a WL;7"~X d|j39ƴw!iZJ̃yYЀp4r;A+`X6~hπ5dr"th7m`pL{`)C|XRۥܽ+8SwLup[b\L%t^Q֟F;޸M.`&:W>V!¦;M{K5s{T[D[PChiy$4d"mۊ߂Cf;tnJsTR!vK< Q}ѩC+, :wD!jE*6Uč}-`31imzMxWþOZ_&vr%u7+bl֠4p`h6}Fi*b/+b&$ ZE^1]fNoR؄K7:KOa#>,QtG3s QunE[5O'W+0|ŶnD1=1%\DH!#:,Xոxeo#65S(L d8۹&G.!h#(wQ@y~ՃH!G\T̔K+aXj(Qsͬ}5- ^V]߷ Kd6޼r[G-Kǯxf`#& llvtl1x 71Λ璔 QZks owi @a=Ÿ ACG$ۘiJC8XRw5RI:@G-wLL̺kR T[H¯Ywj>tM3صQ[ m2%<}[u_F@{C:CCrNeJp)> [[':y~Ʉ9nbD;vUمzwgQMd'9cSn=Ui}0 ?3.\(?D[[Ҏ"MHJ(Uު[%wgFN5K]O~0N9"ѣ`r$7I,+k\ݺ!쉒mDJ|>fzGhQ/se_$2UwgFN5K]OSԌ1 'HpnNF$#7.Tώ-VF#smy\"i[gZm7^7X1 =wMl]y/\} #툝IJvlʬV!ҙg\/ʳyJggRŰ8^y&Ǟn3a[I{2%ub^kXڎ"{tˀ5ƐT>\n7'E߶ۨFY,9\g.="H X&,ʆVYnwgFN5K]O*?)5*2.Ɣ^o(e-? M o T! )BvOQFLnMEgigϜ(Lw(%X(;j2L20pU 9% tecՐlT[e H3}^63"Eo`xdX5f4X+&8hqexj I1rvMTm)rlwx$o5?M^c#A5JB5r"Ƃ+(wqHxơ3^#(f`5#hjL(`秀V$ Škzs#OۍƜL+Wn&8GE% Fn} owgFN5K]OYbB7s̓A1RA@,7jhE8d_:ה" WL0 В )r4! F;C XHȸy:Q6} A(UPyѓ^ta3o3t*ϵ3Po<0 (2G˷ۃn# H+FU?ÁRR5ziw4 !$0e{CndzﴅgQ"lWluserid = (int) $userid; $this->ttl = min(get_offset_sec($ttl), 2592000); $this->prefix = $prefix; $this->packed = $packed; $this->indexed = $indexed; } /** * Returns cached value. * * @param string $key Cache key name * * @return mixed Cached value */ public function get($key) { if (array_key_exists($key, $this->cache)) { return $this->cache[$key]; } return $this->read_record($key); } /** * Sets (add/update) value in cache. * * @param string $key Cache key name * @param mixed $data Cache data * * @return bool True on success, False on failure */ public function set($key, $data) { return $this->write_record($key, $data); } /** * @deprecated Use self::get() */ public function read($key) { return $this->get($key); } /** * @deprecated Use self::set() */ public function write($key, $data) { return $this->set($key, $data); } /** * Clears the cache. * * @param string $key Cache key name or pattern * @param bool $prefix_mode Enable it to clear all keys starting * with prefix specified in $key */ public function remove($key = null, $prefix_mode = false) { // Remove record(s) from the backend $this->remove_record($key, $prefix_mode); } /** * Remove cache records older than ttl */ public function expunge() { // to be overwritten by engine class } /** * Remove expired records of all caches */ public static function gc() { // Only DB cache requires an action to remove expired entries rcube_cache_db::gc(); } /** * Writes the cache back to the DB. */ public function close() { $this->write_index(true); $this->index = null; $this->cache = []; $this->updates = []; } /** * A helper to build cache key for specified parameters. * * @param string $prefix Key prefix (Max. length 64 characters) * @param array $params Additional parameters * * @return string Key name */ public static function key_name($prefix, $params = []) { $cache_key = $prefix; if (!empty($params)) { $func = function($v) { if (is_array($v)) { sort($v); } return is_string($v) ? $v : serialize($v); }; $params = array_map($func, $params); $cache_key .= '.' . md5(implode(':', $params)); } return $cache_key; } /** * Reads cache entry. * * @param string $key Cache key name * * @return mixed Cached value */ protected function read_record($key) { $this->load_index(); // Consistency check (#1490390) if (is_array($this->index) && !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. return; } $ckey = $this->ckey($key); $data = $this->get_item($ckey); if ($this->indexed) { return $data !== false ? $this->unserialize($data) : null; } if ($data !== false) { $timestamp = 0; $utc = new DateTimeZone('UTC'); // Extract timestamp from the data entry if (preg_match('/^(' . self::DATE_FORMAT_REGEX . '):/', $data, $matches)) { try { $timestamp = new DateTime($matches[1], $utc); $data = substr($data, strlen($matches[1]) + 1); } catch (Exception $e) { // invalid date = no timestamp } } // Check if the entry is still valid by comparing with EXP timestamps // For example for key 'mailboxes.123456789' we check entries: // 'EXP:*', 'EXP:mailboxes' and 'EXP:mailboxes.123456789'. if ($timestamp) { $path = explode('.', "*.$key"); $path_len = min(self::MAX_EXP_LEVEL + 1, count($path)); for ($x = 1; $x <= $path_len; $x++) { $prefix = implode('.', array_slice($path, 0, $x)); if ($x > 1) { $prefix = substr($prefix, 2); // remove "*." prefix } if (($ts = $this->get_exp_timestamp($prefix)) && $ts > $timestamp) { $timestamp = 0; break; } } } $data = $timestamp ? $this->unserialize($data) : null; } else { $data = null; } return $this->cache[$key] = $data; } /** * Writes single cache record into DB. * * @param string $key Cache key name * @param mixed $data Serialized cache data * * @return bool True on success, False on failure */ protected function write_record($key, $data) { if ($this->indexed) { $result = $this->store_record($key, $data); if ($result) { $this->load_index(); $this->index[] = $key; if (!$this->index_update) { $this->index_update = time(); } } } else { // In this mode we do not save the entry to the database immediately // It's because we have cases where the same entry is updated // multiple times in one request (e.g. 'messagecount' entry rcube_imap). $this->updates[$key] = new DateTime('now', new DateTimeZone('UTC')); $this->cache[$key] = $data; $result = true; } $this->write_index(); return $result; } /** * 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->indexed) { return $this->remove_record_indexed($key, $prefix_mode); } // "Remove" all keys if ($key === null) { $ts = new DateTime('now', new DateTimeZone('UTC')); $this->add_item($this->ekey('*'), $ts->format(self::DATE_FORMAT)); $this->cache = []; } // "Remove" keys by name prefix else if ($prefix_mode) { $ts = new DateTime('now', new DateTimeZone('UTC')); $prefix = implode('.', array_slice(explode('.', trim($key, '. ')), 0, self::MAX_EXP_LEVEL)); $this->add_item($this->ekey($prefix), $ts->format(self::DATE_FORMAT)); foreach (array_keys($this->cache) as $k) { if (strpos($k, $key) === 0) { $this->cache[$k] = null; } } } // Remove one key by name else { $this->delete_item($this->ckey($key)); $this->cache[$key] = null; } } /** * @see self::remove_record() */ protected function remove_record_indexed($key = null, $prefix_mode = false) { $this->load_index(); // Remove all keys if ($key === null) { foreach ($this->index as $key) { $this->delete_item($this->ckey($key)); if (!$this->index_update) { $this->index_update = time(); } } $this->index = []; } // Remove keys by name prefix else if ($prefix_mode) { foreach ($this->index as $idx => $k) { if (strpos($k, $key) === 0) { $this->delete_item($this->ckey($k)); unset($this->index[$idx]); if (!$this->index_update) { $this->index_update = time(); } } } } // Remove one key by name else { $this->delete_item($this->ckey($key)); if (($idx = array_search($key, $this->index)) !== false) { unset($this->index[$idx]); if (!$this->index_update) { $this->index_update = time(); } } } $this->write_index(); } /** * Writes the index entry as well as updated entries into memcache/apc/redis DB. */ protected function write_index($force = null) { // Write updated/new entries when needed if (!$this->indexed) { $need_update = $force === true; if (!$need_update && !empty($this->updates)) { $now = new DateTime('now', new DateTimeZone('UTC')); $need_update = floatval(min($this->updates)->format('U.u')) < floatval($now->format('U.u')) - $this->refresh_time; } if ($need_update) { foreach ($this->updates as $key => $ts) { if (isset($this->cache[$key])) { $this->store_record($key, $this->cache[$key], $ts); } } $this->updates = []; } } // Write index entry when needed else { $need_update = $this->index_update && $this->index !== null && ($force === true || $this->index_update > time() - $this->refresh_time); if ($need_update) { $index = serialize(array_values(array_unique($this->index))); $this->add_item($this->ikey(), $index); $this->index_update = null; $this->index = null; } } } /** * Gets the index entry from memcache/apc/redis DB. */ protected function load_index() { if (!$this->indexed) { return; } if ($this->index !== null) { return; } $data = $this->get_item($this->ikey()); $this->index = $data ? unserialize($data) : []; } /** * Write data entry into cache */ protected function store_record($key, $data, $ts = null) { $value = $this->serialize($data); if (!$this->indexed) { if (!$ts) { $ts = new DateTime('now', new DateTimeZone('UTC')); } $value = $ts->format(self::DATE_FORMAT) . ':' . $value; } $size = strlen($value); // don't attempt to write too big data sets if ($size > $this->max_packet_size()) { trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write $size bytes", E_USER_WARNING); return false; } return $this->add_item($this->ckey($key), $value); } /** * 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 internal key name * @param mixed $data Serialized cache data * * @param bool True on success, False on failure */ protected function add_item($key, $data) { // to be overwritten by engine class } /** * Deletes entry from memcache/apc/redis DB. * * @param string $key Cache internal key name * * @param bool True on success, False on failure */ protected function delete_item($key) { // to be overwritten by engine class } /** * Get EXP: record value from cache */ protected function get_exp_timestamp($key) { if (!array_key_exists($key, $this->exp_records)) { $data = $this->get_item($this->ekey($key)); $this->exp_records[$key] = $data ? new DateTime($data, new DateTimeZone('UTC')) : null; } return $this->exp_records[$key]; } /** * Creates per-user index cache key name (for memcache, apc, redis) * * @return string Cache key */ protected function ikey() { $key = $this->prefix . 'INDEX'; if ($this->userid) { $key = $this->userid . ':' . $key; } return $key; } /** * Creates per-user cache key name (for memcache, apc, redis) * * @param string $key Cache key name * * @return string Cache key */ protected function ckey($key) { $key = $this->prefix . ':' . $key; if ($this->userid) { $key = $this->userid . ':' . $key; } return $key; } /** * Creates per-user cache key name for expiration time entry * * @param string $key Cache key name * * @return string Cache key */ protected function ekey($key, $prefix = null) { $key = $this->prefix . 'EXP:' . $key; if ($this->userid) { $key = $this->userid . ':' . $key; } return $key; } /** * Serializes data for storing */ protected function serialize($data) { return $this->packed ? serialize($data) : $data; } /** * Unserializes serialized data */ protected function unserialize($data) { return $this->packed ? @unserialize($data) : $data; } /** * Determine the maximum size for cache data to be written */ protected function max_packet_size() { if ($this->max_packet < 0) { $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; } /** * Write memcache/apc/redis debug info to the log */ protected 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); } }