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