alecpl
2011-12-29 08ffd939a7530c44cd68b455f75175f79698073c
commit | author | age
929a50 1 <?php
A 2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_session.php                                     |
6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
cf2da2 8  | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
1a716d 9  | Copyright (C) 2011, Kolab Systems AG                                  |
929a50 10  | Licensed under the GNU GPL                                            |
A 11  |                                                                       |
12  | PURPOSE:                                                              |
13  |   Provide database supported session management                       |
14  |                                                                       |
15  +-----------------------------------------------------------------------+
16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17  | Author: Aleksander Machniak <alec@alec.pl>                            |
18  +-----------------------------------------------------------------------+
19
20  $Id: session.inc 2932 2009-09-07 12:51:21Z alec $
21
22 */
23
d062db 24 /**
T 25  * Class to provide database supported session storage
26  *
27  * @package    Core
28  * @author     Thomas Bruederli <roundcube@gmail.com>
29  * @author     Aleksander Machniak <alec@alec.pl>
30  */
929a50 31 class rcube_session
A 32 {
33   private $db;
34   private $ip;
cf2da2 35   private $start;
929a50 36   private $changed;
A 37   private $unsets = array();
38   private $gc_handlers = array();
cf2da2 39   private $cookiename = 'roundcube_sessauth';
929a50 40   private $vars = false;
A 41   private $key;
cf2da2 42   private $now;
T 43   private $prev;
44   private $secret = '';
45   private $ip_check = false;
fcc7f8 46   private $logging = false;
929a50 47   private $keep_alive = 0;
63e992 48   private $memcache;
929a50 49
A 50   /**
51    * Default constructor
52    */
63e992 53   public function __construct($db, $config)
929a50 54   {
d96a15 55     $this->db      = $db;
A 56     $this->start   = microtime(true);
57     $this->ip      = $_SERVER['REMOTE_ADDR'];
fcc7f8 58     $this->logging = $config->get('log_session', false);
cf2da2 59
63e992 60     $lifetime = $config->get('session_lifetime', 1) * 60;
88ca38 61     $this->set_lifetime($lifetime);
929a50 62
63e992 63     // use memcache backend
T 64     if ($config->get('session_storage', 'db') == 'memcache') {
76d401 65       $this->memcache = rcmail::get_instance()->get_memcache();
63e992 66
T 67       // set custom functions for PHP session management if memcache is available
76d401 68       if ($this->memcache) {
63e992 69         session_set_save_handler(
T 70           array($this, 'open'),
71           array($this, 'close'),
72           array($this, 'mc_read'),
73           array($this, 'mc_write'),
74           array($this, 'mc_destroy'),
60a277 75           array($this, 'gc'));
63e992 76       }
T 77       else {
78         raise_error(array('code' => 604, 'type' => 'db',
79           'line' => __LINE__, 'file' => __FILE__,
80           'message' => "Failed to connect to memcached. Please check configuration"),
81           true, true);
82       }
83     }
84     else {
85       // set custom functions for PHP session management
86       session_set_save_handler(
87         array($this, 'open'),
88         array($this, 'close'),
89         array($this, 'db_read'),
90         array($this, 'db_write'),
91         array($this, 'db_destroy'),
92         array($this, 'db_gc'));
93       }
929a50 94   }
A 95
96
97   public function open($save_path, $session_name)
98   {
99     return true;
100   }
101
102
103   public function close()
104   {
105     return true;
106   }
107
108
63e992 109   /**
T 110    * Delete session data for the given key
111    *
112    * @param string Session ID
113    */
114   public function destroy($key)
115   {
116     return $this->memcache ? $this->mc_destroy($key) : $this->db_destroy($key);
117   }
118
119
120   /**
121    * Read session data from database
122    *
123    * @param string Session ID
124    * @return string Session vars
125    */
126   public function db_read($key)
929a50 127   {
A 128     $sql_result = $this->db->query(
d96a15 129       "SELECT vars, ip, changed FROM ".get_table_name('session')
A 130       ." WHERE sess_id = ?", $key);
929a50 131
d96a15 132     if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
ff4ffc 133       $this->changed = strtotime($sql_arr['changed']);
eee694 134       $this->ip      = $sql_arr['ip'];
T 135       $this->vars    = base64_decode($sql_arr['vars']);
136       $this->key     = $key;
929a50 137
eee694 138       if (!empty($this->vars))
T 139         return $this->vars;
929a50 140     }
A 141
142     return false;
143   }
ca1f56 144
929a50 145
cf2da2 146   /**
T 147    * Save session data.
148    * handler for session_read()
149    *
150    * @param string Session ID
151    * @param string Serialized session vars
152    * @return boolean True on success
153    */
63e992 154   public function db_write($key, $vars)
929a50 155   {
A 156     $ts = microtime(true);
157     $now = $this->db->fromunixtime((int)$ts);
158
d96a15 159     // no session row in DB (db_read() returns false)
A 160     if (!$this->key) {
161       $oldvars = false;
162     }
929a50 163     // use internal data from read() for fast requests (up to 0.5 sec.)
d96a15 164     else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
929a50 165       $oldvars = $this->vars;
d96a15 166     }
A 167     else { // else read data again from DB
68f39e 168       $oldvars = $this->db_read($key);
929a50 169     }
ca1f56 170
929a50 171     if ($oldvars !== false) {
63e992 172       $newvars = $this->_fixvars($vars, $oldvars);
68f39e 173
cf2da2 174       if ($newvars !== $oldvars) {
929a50 175         $this->db->query(
cf2da2 176           sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
ca1f56 177             get_table_name('session'), $now),
eee694 178           base64_encode($newvars), $key);
cf2da2 179       }
T 180       else if ($ts - $this->changed > $this->lifetime / 2) {
181         $this->db->query("UPDATE ".get_table_name('session')." SET changed=$now WHERE sess_id=?", $key);
929a50 182       }
A 183     }
184     else {
185       $this->db->query(
186         sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
187           "VALUES (?, ?, ?, %s, %s)",
ca1f56 188           get_table_name('session'), $now, $now),
cf2da2 189         $key, base64_encode($vars), (string)$this->ip);
929a50 190     }
A 191
192     return true;
193   }
68f39e 194
A 195
60a277 196   /**
T 197    * Merge vars with old vars and apply unsets
198    */
63e992 199   private function _fixvars($vars, $oldvars)
T 200   {
201     if ($oldvars !== false) {
202       $a_oldvars = $this->unserialize($oldvars);
203       if (is_array($a_oldvars)) {
204         foreach ((array)$this->unsets as $k)
205           unset($a_oldvars[$k]);
206
207         $newvars = $this->serialize(array_merge(
208           (array)$a_oldvars, (array)$this->unserialize($vars)));
209       }
210       else
211         $newvars = $vars;
212     }
68f39e 213
63e992 214     $this->unsets = array();
T 215     return $newvars;
216   }
929a50 217
A 218
cf2da2 219   /**
T 220    * Handler for session_destroy()
221    *
222    * @param string Session ID
223    * @return boolean True on success
224    */
63e992 225   public function db_destroy($key)
929a50 226   {
A 227     $this->db->query(
228       sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')),
229       $key);
230
231     return true;
232   }
233
234
cf2da2 235   /**
T 236    * Garbage collecting function
237    *
238    * @param string Session lifetime in seconds
239    * @return boolean True on success
240    */
63e992 241   public function db_gc($maxlifetime)
929a50 242   {
A 243     // just delete all expired sessions
244     $this->db->query(
245       sprintf("DELETE FROM %s WHERE changed < %s",
246         get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
247
68f39e 248     $this->gc();
929a50 249
A 250     return true;
251   }
252
253
cf2da2 254   /**
63e992 255    * Read session data from memcache
T 256    *
257    * @param string Session ID
258    * @return string Session vars
259    */
260   public function mc_read($key)
261   {
262     if ($value = $this->memcache->get($key)) {
263       $arr = unserialize($value);
264       $this->changed = $arr['changed'];
265       $this->ip      = $arr['ip'];
266       $this->vars    = $arr['vars'];
267       $this->key     = $key;
268
269       if (!empty($this->vars))
270         return $this->vars;
271     }
272
273     return false;
274   }
275
276   /**
277    * Save session data.
278    * handler for session_read()
279    *
280    * @param string Session ID
281    * @param string Serialized session vars
282    * @return boolean True on success
283    */
284   public function mc_write($key, $vars)
285   {
286     $ts = microtime(true);
287
d96a15 288     // no session data in cache (mc_read() returns false)
A 289     if (!$this->key)
290       $oldvars = false;
63e992 291     // use internal data for fast requests (up to 0.5 sec.)
d96a15 292     else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
63e992 293       $oldvars = $this->vars;
T 294     else // else read data again
295       $oldvars = $this->mc_read($key);
296
297     $newvars = $oldvars !== false ? $this->_fixvars($vars, $oldvars) : $vars;
298     
299     if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2)
300       return $this->memcache->set($key, serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars)), MEMCACHE_COMPRESSED, $this->lifetime);
301     
302     return true;
303   }
304
305   /**
306    * Handler for session_destroy() with memcache backend
307    *
308    * @param string Session ID
309    * @return boolean True on success
310    */
311   public function mc_destroy($key)
312   {
313     return $this->memcache->delete($key);
314   }
315
316
317   /**
318    * Execute registered garbage collector routines
319    */
68f39e 320   public function gc()
63e992 321   {
T 322     foreach ($this->gc_handlers as $fct)
210303 323       call_user_func($fct);
63e992 324   }
T 325
326
327   /**
cf2da2 328    * Register additional garbage collector functions
T 329    *
330    * @param mixed Callback function
331    */
929a50 332   public function register_gc_handler($func_name)
A 333   {
334     if ($func_name && !in_array($func_name, $this->gc_handlers))
335       $this->gc_handlers[] = $func_name;
336   }
337
338
cf2da2 339   /**
T 340    * Generate and set new session id
c294ea 341    *
A 342    * @param boolean $destroy If enabled the current session will be destroyed
cf2da2 343    */
c294ea 344   public function regenerate_id($destroy=true)
929a50 345   {
c294ea 346     session_regenerate_id($destroy);
cf2da2 347
c294ea 348     $this->vars = false;
A 349     $this->key  = session_id();
929a50 350
A 351     return true;
352   }
353
354
cf2da2 355   /**
T 356    * Unset a session variable
357    *
358    * @param string Varibale name
359    * @return boolean True on success
360    */
361   public function remove($var=null)
929a50 362   {
A 363     if (empty($var))
364       return $this->destroy(session_id());
365
366     $this->unsets[] = $var;
367     unset($_SESSION[$var]);
368
369     return true;
370   }
cf2da2 371   
T 372   /**
373    * Kill this session
374    */
375   public function kill()
376   {
784a42 377     $this->vars = false;
cf2da2 378     $this->destroy(session_id());
T 379     rcmail::setcookie($this->cookiename, '-del-', time() - 60);
380   }
929a50 381
A 382
cf2da2 383   /**
81f5dd 384    * Re-read session data from storage backend
T 385    */
386   public function reload()
387   {
388     if ($this->key && $this->memcache)
8c2b88 389       $data = $this->mc_read($this->key);
81f5dd 390     else if ($this->key)
8c2b88 391       $data = $this->db_read($this->key);
T 392
393     if ($data)
394      session_decode($data);
81f5dd 395   }
T 396
397
398   /**
cf2da2 399    * Serialize session data
T 400    */
929a50 401   private function serialize($vars)
A 402   {
403     $data = '';
404     if (is_array($vars))
405       foreach ($vars as $var=>$value)
406         $data .= $var.'|'.serialize($value);
407     else
408       $data = 'b:0;';
409     return $data;
410   }
411
412
cf2da2 413   /**
T 414    * Unserialize session data
415    * http://www.php.net/manual/en/function.session-decode.php#56106
416    */
929a50 417   private function unserialize($str)
A 418   {
419     $str = (string)$str;
420     $endptr = strlen($str);
421     $p = 0;
422
423     $serialized = '';
424     $items = 0;
425     $level = 0;
426
427     while ($p < $endptr) {
428       $q = $p;
429       while ($str[$q] != '|')
430         if (++$q >= $endptr) break 2;
431
432       if ($str[$p] == '!') {
433         $p++;
434         $has_value = false;
435       } else {
436         $has_value = true;
437       }
438
439       $name = substr($str, $p, $q - $p);
440       $q++;
441
442       $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
443
444       if ($has_value) {
445         for (;;) {
446           $p = $q;
447           switch (strtolower($str[$q])) {
448             case 'n': /* null */
449             case 'b': /* boolean */
450             case 'i': /* integer */
451             case 'd': /* decimal */
452               do $q++;
453               while ( ($q < $endptr) && ($str[$q] != ';') );
454               $q++;
455               $serialized .= substr($str, $p, $q - $p);
456               if ($level == 0) break 2;
457               break;
458             case 'r': /* reference  */
459               $q+= 2;
460               for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
461               $q++;
462               $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
463               if ($level == 0) break 2;
464               break;
465             case 's': /* string */
466               $q+=2;
467               for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
468               $q+=2;
469               $q+= (int)$length + 2;
470               $serialized .= substr($str, $p, $q - $p);
471               if ($level == 0) break 2;
472               break;
473             case 'a': /* array */
474             case 'o': /* object */
475               do $q++;
476               while ( ($q < $endptr) && ($str[$q] != '{') );
477               $q++;
478               $level++;
479               $serialized .= substr($str, $p, $q - $p);
480               break;
481             case '}': /* end of array|object */
482               $q++;
483               $serialized .= substr($str, $p, $q - $p);
484               if (--$level == 0) break 2;
485               break;
486             default:
487               return false;
488           }
489         }
490       } else {
491         $serialized .= 'N;';
492         $q += 2;
493       }
494       $items++;
495       $p = $q;
496     }
497
498     return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
499   }
500
88ca38 501
T 502   /**
503    * Setter for session lifetime
504    */
505   public function set_lifetime($lifetime)
506   {
507       $this->lifetime = max(120, $lifetime);
508
509       // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
510       $now = time();
511       $this->now = $now - ($now % ($this->lifetime / 2));
512       $this->prev = $this->now - ($this->lifetime / 2);
513   }
514
cf2da2 515   /**
T 516    * Setter for keep_alive interval
517    */
929a50 518   public function set_keep_alive($keep_alive)
A 519   {
520     $this->keep_alive = $keep_alive;
88ca38 521     
T 522     if ($this->lifetime < $keep_alive)
523         $this->set_lifetime($keep_alive + 30);
929a50 524   }
A 525
cf2da2 526   /**
T 527    * Getter for keep_alive interval
528    */
929a50 529   public function get_keep_alive()
A 530   {
531     return $this->keep_alive;
532   }
533
cf2da2 534   /**
T 535    * Getter for remote IP saved with this session
536    */
929a50 537   public function get_ip()
A 538   {
539     return $this->ip;
540   }
cf2da2 541   
T 542   /**
543    * Setter for cookie encryption secret
544    */
545   function set_secret($secret)
546   {
547     $this->secret = $secret;
548   }
549
550
551   /**
552    * Enable/disable IP check
553    */
554   function set_ip_check($check)
555   {
556     $this->ip_check = $check;
557   }
558   
559   /**
560    * Setter for the cookie name used for session cookie
561    */
562   function set_cookiename($cookiename)
563   {
564     if ($cookiename)
565       $this->cookiename = $cookiename;
566   }
567
568
569   /**
570    * Check session authentication cookie
571    *
572    * @return boolean True if valid, False if not
573    */
574   function check_auth()
575   {
576     $this->cookie = $_COOKIE[$this->cookiename];
577     $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
578
fcc7f8 579     if (!$result)
T 580       $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
581
cf2da2 582     if ($result && $this->_mkcookie($this->now) != $this->cookie) {
T 583       // Check if using id from previous time slot
fcc7f8 584       if ($this->_mkcookie($this->prev) == $this->cookie) {
cf2da2 585         $this->set_auth_cookie();
fcc7f8 586       }
T 587       else {
cf2da2 588         $result = false;
fcc7f8 589         $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent");
T 590       }
cf2da2 591     }
T 592
593     return $result;
594   }
595
596
597   /**
598    * Set session authentication cookie
599    */
600   function set_auth_cookie()
601   {
602     $this->cookie = $this->_mkcookie($this->now);
603     rcmail::setcookie($this->cookiename, $this->cookie, 0);
604     $_COOKIE[$this->cookiename] = $this->cookie;
605   }
606
607
608   /**
609    * Create session cookie from session data
610    *
611    * @param int Time slot to use
612    */
613   function _mkcookie($timeslot)
614   {
615     $auth_string = "$this->key,$this->secret,$timeslot";
616     return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
617   }
fcc7f8 618   
T 619   /**
620    * 
621    */
622   function log($line)
623   {
624     if ($this->logging)
625       write_log('session', $line);
626   }
929a50 627
A 628 }