diff --git a/CHANGELOG b/CHANGELOG index 2666b8548..84172e8e7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ CHANGELOG Roundcube Webmail - Renamed 'log_session' option to 'session_debug' - Don't log full session identifiers in userlogins log (#6625) - Support $HasAttachment/$HasNoAttachment keywords (#6201) +- Support PECL memcached extension as a session and cache storage driver (experimental) - installto.sh: Add possibility to run the update even on the up-to-date installation (#6533) - Redis: Support connection to unix socket - Elastic: Add Prev/Next buttons on message page toolbar (#6648) diff --git a/config/defaults.inc.php b/config/defaults.inc.php index 70e62dc10..7ae6b117b 100644 --- a/config/defaults.inc.php +++ b/config/defaults.inc.php @@ -228,7 +228,7 @@ $config['imap_disabled_caps'] = array(); // This is used to relate IMAP session with Roundcube user sessions $config['imap_log_session'] = false; -// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache'. +// Type of IMAP indexes cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'. $config['imap_cache'] = null; // Enables messages cache. Only 'db' cache is supported. @@ -315,7 +315,7 @@ $config['smtp_conn_options'] = null; // LDAP // ---------------------------------- -// Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache'. +// Type of LDAP cache. Supported values: 'db', 'apc' and 'memcache' or 'memcached'. $config['ldap_cache'] = 'db'; // Lifetime of LDAP cache. Possible units: s, m, h, d, w @@ -478,11 +478,12 @@ $config['session_path'] = null; // Backend to use for session storage. Can either be 'db' (default), 'redis', 'memcache', or 'php' // -// If set to 'memcache', a list of servers need to be specified in 'memcache_hosts' -// Make sure the Memcache extension (http://pecl.php.net/package/memcache) version >= 2.0.0 is installed +// If set to 'memcache' or 'memcached', a list of servers need to be specified in 'memcache_hosts' +// Make sure the Memcache extension (https://pecl.php.net/package/memcache) version >= 2.0.0 +// or the Memcached extension (https://pecl.php.net/package/memcached) version >= 2.0.0 is installed. // // If set to 'redis', a server needs to be specified in 'redis_hosts' -// Make sure the Redis extension (http://pecl.php.net/package/redis) version >= 2.0.0 is installed +// Make sure the Redis extension (https://pecl.php.net/package/redis) version >= 2.0.0 is installed. // // Setting this value to 'php' will use the default session save handler configured in PHP $config['session_storage'] = 'db'; diff --git a/plugins/database_attachments/config.inc.php.dist b/plugins/database_attachments/config.inc.php.dist index b4464343c..2686bd091 100644 --- a/plugins/database_attachments/config.inc.php.dist +++ b/plugins/database_attachments/config.inc.php.dist @@ -2,7 +2,7 @@ // By default this plugin stores attachments in filesystem // and copies them into sql database. -// You can change it to use 'memcache', 'redis' or 'apc'. +// You can change it to use 'memcache', 'memcached', 'redis' or 'apc'. // ----------------------------------------------------------- // WARNING: Remember to set max_allowed_packet in database or // config to match with expected max attachment size. @@ -12,5 +12,3 @@ $config['database_attachments_cache'] = 'db'; // Attachment data expires after specied TTL time in seconds (max.2592000). // Default is 12 hours. $config['database_attachments_cache_ttl'] = 12 * 60 * 60; - -?> diff --git a/plugins/redundant_attachments/config.inc.php.dist b/plugins/redundant_attachments/config.inc.php.dist index b66d304c4..a8d0426ee 100644 --- a/plugins/redundant_attachments/config.inc.php.dist +++ b/plugins/redundant_attachments/config.inc.php.dist @@ -7,12 +7,10 @@ // WARNING: Remember to also set memcache_max_allowed_packet or redis_max_allowed_packet // in config to match with expected maximum attachment size. // ------------------------------------------------------------------------------------- -// This option can be set to 'memcache' or 'redis'. +// This option can be set to 'memcache', 'memcached' or 'redis'. // Don't forget to set redis_*/memcache_* options in Roundcube config file. $config['redundant_attachments_fallback'] = false; // Attachment data expires after specified TTL time in seconds (max.2592000). // Default is 12 hours. $config['redundant_attachments_cache_ttl'] = 12 * 60 * 60; - -?> diff --git a/program/lib/Roundcube/cache/memcached.php b/program/lib/Roundcube/cache/memcached.php new file mode 100644 index 000000000..cd9475a04 --- /dev/null +++ b/program/lib/Roundcube/cache/memcached.php @@ -0,0 +1,215 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Interface class for accessing Memcached cache + * + * @package Framework + * @subpackage Cache + * @author Thomas Bruederli + * @author Aleksander Machniak + */ +class rcube_cache_memcached extends rcube_cache +{ + /** + * Instance of memcached handler + * + * @var Memcached + */ + 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'); + + // Maximum TTL is 30 days, bigger values are treated by Memcached + // as unix timestamp which is not what we want + if ($this->ttl > 60*60*24*30) { + $this->ttl = 60*60*24*30; + } + + 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('Memcached')) { + self::$memcache = false; + + rcube::raise_error(array( + 'code' => 604, 'type' => 'memcache', 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Failed to find Memcached. Make sure php-memcached is installed" + ), + 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); + + self::$memcache = new Memcached($pconnect ? 'roundcube' : null); + + self::$memcache->setOptions(array( + Memcached::OPT_CONNECT_TIMEOUT => $timeout * 1000, + Memcached::OPT_RETRY_TIMEOUT => $timeout, + Memcached::OPT_DISTRIBUTION => Memcached::DISTRIBUTION_CONSISTENT, + Memcached::OPT_COMPRESSION => true, + )); + + 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 { + $host = substr($host, 8); + $port = 0; + } + + self::$memcache->addServer($host, $port); + } + + // test connection + $result = self::$memcache->increment('__CONNECTIONTEST__'); + + if ($result === false && ($res_code = self::$memcache->getResultCode()) !== Memcached::RES_NOTFOUND) { + self::$memcache = false; + + rcube::raise_error(array( + 'code' => 604, 'type' => 'memcache', 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Memcache connection failure (code: $res_code)." + ), + true, 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->set($key, $data, $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.php b/program/lib/Roundcube/rcube.php index 322fd6947..9ec7948a7 100644 --- a/program/lib/Roundcube/rcube.php +++ b/program/lib/Roundcube/rcube.php @@ -67,6 +67,13 @@ class rcube */ public $memcache; + /** + * Instance of Memcached class. + * + * @var Memcached + */ + public $memcached; + /** * Instance of Redis class. * @@ -216,6 +223,20 @@ class rcube return $this->memcache; } + /** + * Get global handle for memcached access + * + * @return object Memcached + */ + public function get_memcached() + { + if (!isset($this->memcached)) { + $this->memcached = rcube_cache_memcached::engine(); + } + + return $this->memcached; + } + /** * Get global handle for redis access * @@ -1089,8 +1110,10 @@ class rcube if ($this->memcache) { $this->memcache->close(); - // after close() need to re-init memcache - $this->memcache_init(); + } + + if ($this->memcached) { + $this->memcached->quit(); } if ($this->smtp) { diff --git a/program/lib/Roundcube/session/memcache.php b/program/lib/Roundcube/session/memcache.php index 2e917cabc..7b8b31f87 100644 --- a/program/lib/Roundcube/session/memcache.php +++ b/program/lib/Roundcube/session/memcache.php @@ -12,7 +12,7 @@ | See the README file for a full license statement. | | | | PURPOSE: | - | Provide database supported session management | + | Provide memcache supported session management | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli | | Author: Aleksander Machniak | diff --git a/program/lib/Roundcube/session/memcached.php b/program/lib/Roundcube/session/memcached.php new file mode 100644 index 000000000..5e06e3141 --- /dev/null +++ b/program/lib/Roundcube/session/memcached.php @@ -0,0 +1,197 @@ + | + | Author: Aleksander Machniak | + | Author: Cor Bosman | + +-----------------------------------------------------------------------+ +*/ + +/** + * Class to provide memcached session storage + * + * @package Framework + * @subpackage Core + * @author Thomas Bruederli + * @author Aleksander Machniak + * @author Cor Bosman + */ +class rcube_session_memcached extends rcube_session +{ + private $memcache; + private $debug; + + /** + * Object constructor + * + * @param rcube_config $config Configuration + */ + public function __construct($config) + { + parent::__construct($config); + + $this->memcache = rcube::get_instance()->get_memcached(); + $this->debug = $config->get('memcache_debug'); + + if (!$this->memcache) { + rcube::raise_error(array( + 'code' => 604, 'type' => 'db', + 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Failed to connect to memcached. Please check configuration"), + true, true); + } + + // register sessions handler + $this->register_session_handler(); + } + + /** + * Opens the session + * + * @param string $save_path Session save path + * @param string $session_name Session name + * + * @return bool True on success, False on failure + */ + public function open($save_path, $session_name) + { + return true; + } + + /** + * Close the session + * + * @return bool True on success, False on failure + */ + public function close() + { + return true; + } + + /** + * Destroy the session + * + * @param string $key Session identifier + * + * @return bool True on success, False on failure + */ + public function destroy($key) + { + if ($key) { + // #1488592: use 2nd argument + $result = $this->memcache->delete($key, 0); + + if ($this->debug) { + $this->debug('delete', $key, null, $result); + } + } + + return true; + } + + /** + * Read session data from memcache + * + * @param string $key Session identifier + * + * @return string Serialized data string + */ + public function read($key) + { + if ($arr = $this->memcache->get($key)) { + $this->changed = $arr['changed']; + $this->ip = $arr['ip']; + $this->vars = $arr['vars']; + $this->key = $key; + } + + if ($this->debug) { + $this->debug('get', $key, $arr ? serialize($arr) : ''); + } + + return $this->vars ?: ''; + } + + /** + * Write data to memcache storage + * + * @param string $key Session identifier + * @param string $vars Session data string + * + * @return bool True on success, False on failure + */ + public function write($key, $vars) + { + if ($this->ignore_write) { + return true; + } + + $data = array('changed' => time(), 'ip' => $this->ip, 'vars' => $vars); + $result = $this->memcache->set($key, $data, $this->lifetime + 60); + + if ($this->debug) { + $this->debug('set', $key, serialize($data), $result); + } + + return $result; + } + + /** + * Update memcache session data + * + * @param string $key Session identifier + * @param string $newvars New session data string + * @param string $oldvars Old session data string + * + * @return bool True on success, False on failure + */ + public function update($key, $newvars, $oldvars) + { + $ts = microtime(true); + + if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) { + $data = array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars); + $result = $this->memcache->set($key, $data, $this->lifetime + 60); + + if ($this->debug) { + $this->debug('set', $key, serialize($data), $result); + } + + return $result; + } + + return true; + } + + /** + * Write memcache debug info to the log + * + * @param string $type Operation type + * @param string $key Session identifier + * @param string $data Data to log + * @param bool $result Opearation result + */ + protected function debug($type, $key, $data = null, $result = null) + { + $line = strtoupper($type) . ' ' . $key; + + if ($data !== null) { + $line .= ' ' . $data; + } + + rcube::debug('memcache', $line, $result); + } +}