4 +-----------------------------------------------------------------------+
5 | program/include/rcube_session.php |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team |
9 | Copyright (C) 2011, Kolab Systems AG |
10 | Licensed under the GNU GPL |
13 | Provide database supported session management |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com> |
17 | Author: Aleksander Machniak <alec@alec.pl> |
18 +-----------------------------------------------------------------------+
20 $Id: session.inc 2932 2009-09-07 12:51:21Z alec $
25 * Class to provide database supported session storage
28 * @author Thomas Bruederli <roundcube@gmail.com>
29 * @author Aleksander Machniak <alec@alec.pl>
37 private $unsets = array();
38 private $gc_handlers = array();
39 private $cookiename = 'roundcube_sessauth';
40 private $vars = false;
45 private $ip_check = false;
46 private $logging = false;
47 private $keep_alive = 0;
53 public function __construct($db, $config)
56 $this->start = microtime(true);
57 $this->ip = $_SERVER['REMOTE_ADDR'];
58 $this->logging = $config->get('log_session', false);
59 $this->mc_debug = $config->get('memcache_debug', false);
61 $lifetime = $config->get('session_lifetime', 1) * 60;
62 $this->set_lifetime($lifetime);
64 // use memcache backend
65 if ($config->get('session_storage', 'db') == 'memcache') {
66 $this->memcache = rcmail::get_instance()->get_memcache();
68 // set custom functions for PHP session management if memcache is available
69 if ($this->memcache) {
70 session_set_save_handler(
72 array($this, 'close'),
73 array($this, 'mc_read'),
74 array($this, 'mc_write'),
75 array($this, 'mc_destroy'),
79 raise_error(array('code' => 604, 'type' => 'db',
80 'line' => __LINE__, 'file' => __FILE__,
81 'message' => "Failed to connect to memcached. Please check configuration"),
86 // set custom functions for PHP session management
87 session_set_save_handler(
89 array($this, 'close'),
90 array($this, 'db_read'),
91 array($this, 'db_write'),
92 array($this, 'db_destroy'),
93 array($this, 'db_gc'));
98 public function open($save_path, $session_name)
104 public function close()
111 * Delete session data for the given key
113 * @param string Session ID
115 public function destroy($key)
117 return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
122 * Read session data from database
124 * @param string Session ID
125 * @return string Session vars
127 public function db_read($key)
129 $sql_result = $this->db->query(
130 "SELECT vars, ip, changed FROM ".get_table_name('session')
131 ." WHERE sess_id = ?", $key);
133 if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
134 $this->changed = strtotime($sql_arr['changed']);
135 $this->ip = $sql_arr['ip'];
136 $this->vars = base64_decode($sql_arr['vars']);
139 if (!empty($this->vars))
149 * handler for session_read()
151 * @param string Session ID
152 * @param string Serialized session vars
153 * @return boolean True on success
155 public function db_write($key, $vars)
157 $ts = microtime(true);
158 $now = $this->db->fromunixtime((int)$ts);
160 // no session row in DB (db_read() returns false)
164 // use internal data from read() for fast requests (up to 0.5 sec.)
165 else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
166 $oldvars = $this->vars;
168 else { // else read data again from DB
169 $oldvars = $this->db_read($key);
172 if ($oldvars !== false) {
173 $newvars = $this->_fixvars($vars, $oldvars);
175 if ($newvars !== $oldvars) {
177 sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
178 get_table_name('session'), $now),
179 base64_encode($newvars), $key);
181 else if ($ts - $this->changed > $this->lifetime / 2) {
182 $this->db->query("UPDATE ".get_table_name('session')." SET changed=$now WHERE sess_id=?", $key);
187 sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
188 "VALUES (?, ?, ?, %s, %s)",
189 get_table_name('session'), $now, $now),
190 $key, base64_encode($vars), (string)$this->ip);
198 * Merge vars with old vars and apply unsets
200 private function _fixvars($vars, $oldvars)
202 if ($oldvars !== false) {
203 $a_oldvars = $this->unserialize($oldvars);
204 if (is_array($a_oldvars)) {
205 foreach ((array)$this->unsets as $k)
206 unset($a_oldvars[$k]);
208 $newvars = $this->serialize(array_merge(
209 (array)$a_oldvars, (array)$this->unserialize($vars)));
215 $this->unsets = array();
221 * Handler for session_destroy()
223 * @param string Session ID
224 * @return boolean True on success
226 public function db_destroy($key)
229 sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')),
237 * Garbage collecting function
239 * @param string Session lifetime in seconds
240 * @return boolean True on success
242 public function db_gc($maxlifetime)
244 // just delete all expired sessions
246 sprintf("DELETE FROM %s WHERE changed < %s",
247 get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
256 * Read session data from memcache
258 * @param string Session ID
259 * @return string Session vars
261 public function mc_read($key)
263 $value = $this->memcache->get($key);
264 if ($this->mc_debug) write_log('memcache', "get($key): " . strlen($value));
265 if ($value && ($arr = unserialize($value))) {
266 $this->changed = $arr['changed'];
267 $this->ip = $arr['ip'];
268 $this->vars = $arr['vars'];
271 if (!empty($this->vars))
280 * handler for session_read()
282 * @param string Session ID
283 * @param string Serialized session vars
284 * @return boolean True on success
286 public function mc_write($key, $vars)
288 $ts = microtime(true);
290 // no session data in cache (mc_read() returns false)
293 // use internal data for fast requests (up to 0.5 sec.)
294 else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
295 $oldvars = $this->vars;
296 else // else read data again
297 $oldvars = $this->mc_read($key);
299 $newvars = $oldvars !== false ? $this->_fixvars($vars, $oldvars) : $vars;
301 if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2) {
302 $value = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars));
303 $ret = $this->memcache->set($key, $value, MEMCACHE_COMPRESSED, $this->lifetime);
304 if ($this->mc_debug) {
305 write_log('memcache', "set($key): " . strlen($value) . ": " . ($ret ? 'OK' : 'ERR'));
306 write_log('memcache', "... get($key): " . strlen($this->memcache->get($key)));
315 * Handler for session_destroy() with memcache backend
317 * @param string Session ID
318 * @return boolean True on success
320 public function mc_destroy($key)
322 $ret = $this->memcache->delete($key);
323 if ($this->mc_debug) write_log('memcache', "delete($key): " . ($ret ? 'OK' : 'ERR'));
329 * Execute registered garbage collector routines
333 foreach ($this->gc_handlers as $fct)
334 call_user_func($fct);
339 * Register additional garbage collector functions
341 * @param mixed Callback function
343 public function register_gc_handler($func_name)
345 if ($func_name && !in_array($func_name, $this->gc_handlers))
346 $this->gc_handlers[] = $func_name;
351 * Generate and set new session id
353 * @param boolean $destroy If enabled the current session will be destroyed
355 public function regenerate_id($destroy=true)
357 session_regenerate_id($destroy);
360 $this->key = session_id();
367 * Unset a session variable
369 * @param string Varibale name
370 * @return boolean True on success
372 public function remove($var=null)
375 return $this->destroy(session_id());
377 $this->unsets[] = $var;
378 unset($_SESSION[$var]);
386 public function kill()
389 $this->destroy(session_id());
390 rcmail::setcookie($this->cookiename, '-del-', time() - 60);
395 * Re-read session data from storage backend
397 public function reload()
399 if ($this->key && $this->memcache)
400 $data = $this->mc_read($this->key);
402 $data = $this->db_read($this->key);
405 session_decode($data);
410 * Serialize session data
412 private function serialize($vars)
416 foreach ($vars as $var=>$value)
417 $data .= $var.'|'.serialize($value);
425 * Unserialize session data
426 * http://www.php.net/manual/en/function.session-decode.php#56106
428 private function unserialize($str)
431 $endptr = strlen($str);
438 while ($p < $endptr) {
440 while ($str[$q] != '|')
441 if (++$q >= $endptr) break 2;
443 if ($str[$p] == '!') {
450 $name = substr($str, $p, $q - $p);
453 $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
458 switch (strtolower($str[$q])) {
460 case 'b': /* boolean */
461 case 'i': /* integer */
462 case 'd': /* decimal */
464 while ( ($q < $endptr) && ($str[$q] != ';') );
466 $serialized .= substr($str, $p, $q - $p);
467 if ($level == 0) break 2;
469 case 'r': /* reference */
471 for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
473 $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
474 if ($level == 0) break 2;
476 case 's': /* string */
478 for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
480 $q+= (int)$length + 2;
481 $serialized .= substr($str, $p, $q - $p);
482 if ($level == 0) break 2;
484 case 'a': /* array */
485 case 'o': /* object */
487 while ( ($q < $endptr) && ($str[$q] != '{') );
490 $serialized .= substr($str, $p, $q - $p);
492 case '}': /* end of array|object */
494 $serialized .= substr($str, $p, $q - $p);
495 if (--$level == 0) break 2;
509 return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
514 * Setter for session lifetime
516 public function set_lifetime($lifetime)
518 $this->lifetime = max(120, $lifetime);
520 // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
522 $this->now = $now - ($now % ($this->lifetime / 2));
523 $this->prev = $this->now - ($this->lifetime / 2);
527 * Setter for keep_alive interval
529 public function set_keep_alive($keep_alive)
531 $this->keep_alive = $keep_alive;
533 if ($this->lifetime < $keep_alive)
534 $this->set_lifetime($keep_alive + 30);
538 * Getter for keep_alive interval
540 public function get_keep_alive()
542 return $this->keep_alive;
546 * Getter for remote IP saved with this session
548 public function get_ip()
554 * Setter for cookie encryption secret
556 function set_secret($secret)
558 $this->secret = $secret;
563 * Enable/disable IP check
565 function set_ip_check($check)
567 $this->ip_check = $check;
571 * Setter for the cookie name used for session cookie
573 function set_cookiename($cookiename)
576 $this->cookiename = $cookiename;
581 * Check session authentication cookie
583 * @return boolean True if valid, False if not
585 function check_auth()
587 $this->cookie = $_COOKIE[$this->cookiename];
588 $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
591 $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
593 if ($result && $this->_mkcookie($this->now) != $this->cookie) {
594 // Check if using id from previous time slot
595 if ($this->_mkcookie($this->prev) == $this->cookie) {
596 $this->set_auth_cookie();
600 $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent");
609 * Set session authentication cookie
611 function set_auth_cookie()
613 $this->cookie = $this->_mkcookie($this->now);
614 rcmail::setcookie($this->cookiename, $this->cookie, 0);
615 $_COOKIE[$this->cookiename] = $this->cookie;
620 * Create session cookie from session data
622 * @param int Time slot to use
624 function _mkcookie($timeslot)
626 $auth_string = "$this->key,$this->secret,$timeslot";
627 return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
636 write_log('session', $line);