thomascube
2011-01-29 6039aae3878aa5880415290cbc41af4bac4fdcb5
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;
A 46
47   /**
48    * Default constructor
49    */
50   public function __construct($db, $lifetime=60)
51   {
52     $this->db = $db;
53     $this->lifetime = $lifetime;
54     $this->start = microtime(true);
cf2da2 55     $this->ip = $_SERVER['REMOTE_ADDR'];
T 56
57     // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
58     $now = time();
59     $this->now = $now - ($now % ($this->lifetime / 2));
60     $this->prev = $this->now - ($this->lifetime / 2);
929a50 61
A 62     // set custom functions for PHP session management
63     session_set_save_handler(
64       array($this, 'open'),
65       array($this, 'close'),
66       array($this, 'read'),
67       array($this, 'write'),
68       array($this, 'destroy'),
69       array($this, 'gc'));
70   }
71
72
73   public function open($save_path, $session_name)
74   {
75     return true;
76   }
77
78
79   public function close()
80   {
81     return true;
82   }
83
84
85   // read session data
86   public function read($key)
87   {
88     $sql_result = $this->db->query(
89       sprintf("SELECT vars, ip, %s AS changed FROM %s WHERE sess_id = ?",
90         $this->db->unixtimestamp('changed'), get_table_name('session')),
91       $key);
92
93     if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
94       $this->changed = $sql_arr['changed'];
eee694 95       $this->ip      = $sql_arr['ip'];
T 96       $this->vars    = base64_decode($sql_arr['vars']);
97       $this->key     = $key;
929a50 98
eee694 99       if (!empty($this->vars))
T 100         return $this->vars;
929a50 101     }
A 102
103     return false;
104   }
ca1f56 105
929a50 106
cf2da2 107   /**
T 108    * Save session data.
109    * handler for session_read()
110    *
111    * @param string Session ID
112    * @param string Serialized session vars
113    * @return boolean True on success
114    */
929a50 115   public function write($key, $vars)
A 116   {
117     $ts = microtime(true);
118     $now = $this->db->fromunixtime((int)$ts);
119
120     // use internal data from read() for fast requests (up to 0.5 sec.)
121     if ($key == $this->key && $ts - $this->start < 0.5) {
122       $oldvars = $this->vars;
123     } else { // else read data again from DB
124       $oldvars = $this->read($key);
125     }
ca1f56 126
929a50 127     if ($oldvars !== false) {
eee694 128       $a_oldvars = $this->unserialize($oldvars);
T 129       if (is_array($a_oldvars)) {
130         foreach ((array)$this->unsets as $k)
131           unset($a_oldvars[$k]);
929a50 132
eee694 133         $newvars = $this->serialize(array_merge(
T 134           (array)$a_oldvars, (array)$this->unserialize($vars)));
135       }
136       else
137         $newvars = $vars;
929a50 138
cf2da2 139       if ($newvars !== $oldvars) {
929a50 140         $this->db->query(
cf2da2 141           sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
ca1f56 142             get_table_name('session'), $now),
eee694 143           base64_encode($newvars), $key);
cf2da2 144       }
T 145       else if ($ts - $this->changed > $this->lifetime / 2) {
146         $this->db->query("UPDATE ".get_table_name('session')." SET changed=$now WHERE sess_id=?", $key);
929a50 147       }
A 148     }
149     else {
150       $this->db->query(
151         sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
152           "VALUES (?, ?, ?, %s, %s)",
ca1f56 153           get_table_name('session'), $now, $now),
cf2da2 154         $key, base64_encode($vars), (string)$this->ip);
929a50 155     }
A 156
157     $this->unsets = array();
158     return true;
159   }
160
161
cf2da2 162   /**
T 163    * Handler for session_destroy()
164    *
165    * @param string Session ID
166    * @return boolean True on success
167    */
929a50 168   public function destroy($key)
A 169   {
170     $this->db->query(
171       sprintf("DELETE FROM %s WHERE sess_id = ?", get_table_name('session')),
172       $key);
173
174     return true;
175   }
176
177
cf2da2 178   /**
T 179    * Garbage collecting function
180    *
181    * @param string Session lifetime in seconds
182    * @return boolean True on success
183    */
929a50 184   public function gc($maxlifetime)
A 185   {
186     // just delete all expired sessions
187     $this->db->query(
188       sprintf("DELETE FROM %s WHERE changed < %s",
189         get_table_name('session'), $this->db->fromunixtime(time() - $maxlifetime)));
190
191     foreach ($this->gc_handlers as $fct)
192       $fct();
193
194     return true;
195   }
196
197
cf2da2 198   /**
T 199    * Register additional garbage collector functions
200    *
201    * @param mixed Callback function
202    */
929a50 203   public function register_gc_handler($func_name)
A 204   {
205     if ($func_name && !in_array($func_name, $this->gc_handlers))
206       $this->gc_handlers[] = $func_name;
207   }
208
209
cf2da2 210   /**
T 211    * Generate and set new session id
212    */
929a50 213   public function regenerate_id()
A 214   {
cf2da2 215     // delete old session record
T 216     $this->destroy(session_id());
217     $this->vars = false;
218
929a50 219     $randval = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
A 220
221     for ($random = '', $i=1; $i <= 32; $i++) {
222       $random .= substr($randval, mt_rand(0,(strlen($randval) - 1)), 1);
223     }
224
cf2da2 225     // use md5 value for id
T 226     $this->key = md5($random);
227     session_id($this->key);
929a50 228
A 229     $cookie   = session_get_cookie_params();
230     $lifetime = $cookie['lifetime'] ? time() + $cookie['lifetime'] : 0;
231
cf2da2 232     rcmail::setcookie(session_name(), $this->key, $lifetime);
929a50 233
A 234     return true;
235   }
236
237
cf2da2 238   /**
T 239    * Unset a session variable
240    *
241    * @param string Varibale name
242    * @return boolean True on success
243    */
244   public function remove($var=null)
929a50 245   {
A 246     if (empty($var))
247       return $this->destroy(session_id());
248
249     $this->unsets[] = $var;
250     unset($_SESSION[$var]);
251
252     return true;
253   }
cf2da2 254   
T 255   /**
256    * Kill this session
257    */
258   public function kill()
259   {
260     $this->destroy(session_id());
261     rcmail::setcookie($this->cookiename, '-del-', time() - 60);
262   }
929a50 263
A 264
cf2da2 265   /**
T 266    * Serialize session data
267    */
929a50 268   private function serialize($vars)
A 269   {
270     $data = '';
271     if (is_array($vars))
272       foreach ($vars as $var=>$value)
273         $data .= $var.'|'.serialize($value);
274     else
275       $data = 'b:0;';
276     return $data;
277   }
278
279
cf2da2 280   /**
T 281    * Unserialize session data
282    * http://www.php.net/manual/en/function.session-decode.php#56106
283    */
929a50 284   private function unserialize($str)
A 285   {
286     $str = (string)$str;
287     $endptr = strlen($str);
288     $p = 0;
289
290     $serialized = '';
291     $items = 0;
292     $level = 0;
293
294     while ($p < $endptr) {
295       $q = $p;
296       while ($str[$q] != '|')
297         if (++$q >= $endptr) break 2;
298
299       if ($str[$p] == '!') {
300         $p++;
301         $has_value = false;
302       } else {
303         $has_value = true;
304       }
305
306       $name = substr($str, $p, $q - $p);
307       $q++;
308
309       $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
310
311       if ($has_value) {
312         for (;;) {
313           $p = $q;
314           switch (strtolower($str[$q])) {
315             case 'n': /* null */
316             case 'b': /* boolean */
317             case 'i': /* integer */
318             case 'd': /* decimal */
319               do $q++;
320               while ( ($q < $endptr) && ($str[$q] != ';') );
321               $q++;
322               $serialized .= substr($str, $p, $q - $p);
323               if ($level == 0) break 2;
324               break;
325             case 'r': /* reference  */
326               $q+= 2;
327               for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
328               $q++;
329               $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
330               if ($level == 0) break 2;
331               break;
332             case 's': /* string */
333               $q+=2;
334               for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
335               $q+=2;
336               $q+= (int)$length + 2;
337               $serialized .= substr($str, $p, $q - $p);
338               if ($level == 0) break 2;
339               break;
340             case 'a': /* array */
341             case 'o': /* object */
342               do $q++;
343               while ( ($q < $endptr) && ($str[$q] != '{') );
344               $q++;
345               $level++;
346               $serialized .= substr($str, $p, $q - $p);
347               break;
348             case '}': /* end of array|object */
349               $q++;
350               $serialized .= substr($str, $p, $q - $p);
351               if (--$level == 0) break 2;
352               break;
353             default:
354               return false;
355           }
356         }
357       } else {
358         $serialized .= 'N;';
359         $q += 2;
360       }
361       $items++;
362       $p = $q;
363     }
364
365     return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
366   }
367
cf2da2 368   /**
T 369    * Setter for keep_alive interval
370    */
929a50 371   public function set_keep_alive($keep_alive)
A 372   {
373     $this->keep_alive = $keep_alive;
374   }
375
cf2da2 376   /**
T 377    * Getter for keep_alive interval
378    */
929a50 379   public function get_keep_alive()
A 380   {
381     return $this->keep_alive;
382   }
383
cf2da2 384   /**
T 385    * Getter for remote IP saved with this session
386    */
929a50 387   public function get_ip()
A 388   {
389     return $this->ip;
390   }
cf2da2 391   
T 392   /**
393    * Setter for cookie encryption secret
394    */
395   function set_secret($secret)
396   {
397     $this->secret = $secret;
398   }
399
400
401   /**
402    * Enable/disable IP check
403    */
404   function set_ip_check($check)
405   {
406     $this->ip_check = $check;
407   }
408   
409   /**
410    * Setter for the cookie name used for session cookie
411    */
412   function set_cookiename($cookiename)
413   {
414     if ($cookiename)
415       $this->cookiename = $cookiename;
416   }
417
418
419   /**
420    * Check session authentication cookie
421    *
422    * @return boolean True if valid, False if not
423    */
424   function check_auth()
425   {
426     $this->cookie = $_COOKIE[$this->cookiename];
427     $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
428
429     if ($result && $this->_mkcookie($this->now) != $this->cookie) {
430       // Check if using id from previous time slot
431       if ($this->_mkcookie($this->prev) == $this->cookie)
432         $this->set_auth_cookie();
433       else
434         $result = false;
435     }
436
437     return $result;
438   }
439
440
441   /**
442    * Set session authentication cookie
443    */
444   function set_auth_cookie()
445   {
446     $this->cookie = $this->_mkcookie($this->now);
447     rcmail::setcookie($this->cookiename, $this->cookie, 0);
448     $_COOKIE[$this->cookiename] = $this->cookie;
449   }
450
451
452   /**
453    * Create session cookie from session data
454    *
455    * @param int Time slot to use
456    */
457   function _mkcookie($timeslot)
458   {
459     $auth_string = "$this->key,$this->secret,$timeslot";
460     return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
461   }
929a50 462
A 463 }