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