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