]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_session.php
01b93670cfe93a9943a574ef18191f94d015e276
[roundcube.git] / program / include / rcube_session.php
1 <?php
2
3 /*
4  +-----------------------------------------------------------------------+
5  | program/include/rcube_session.php                                     |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
9  | Licensed under the GNU GPL                                            |
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
23 /**
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  */
30 class rcube_session
31 {
32   private $db;
33   private $ip;
34   private $start;
35   private $changed;
36   private $unsets = array();
37   private $gc_handlers = array();
38   private $cookiename = 'roundcube_sessauth';
39   private $vars = false;
40   private $key;
41   private $now;
42   private $prev;
43   private $secret = '';
44   private $ip_check = false;
45   private $logging = false;
46   private $keep_alive = 0;
47   private $memcache;
48
49   /**
50    * Default constructor
51    */
52   public function __construct($db, $config)
53   {
54     $this->db      = $db;
55     $this->start   = microtime(true);
56     $this->ip      = $_SERVER['REMOTE_ADDR'];
57     $this->logging = $config->get('log_session', false);
58     $this->mc_debug = $config->get('memcache_debug', false);
59
60     $lifetime = $config->get('session_lifetime', 1) * 60;
61     $this->set_lifetime($lifetime);
62
63     // use memcache backend
64     if ($config->get('session_storage', 'db') == 'memcache') {
65       $this->memcache = rcmail::get_instance()->get_memcache();
66
67       // set custom functions for PHP session management if memcache is available
68       if ($this->memcache) {
69         session_set_save_handler(
70           array($this, 'open'),
71           array($this, 'close'),
72           array($this, 'mc_read'),
73           array($this, 'mc_write'),
74           array($this, 'mc_destroy'),
75           array($this, 'gc'));
76       }
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       }
94   }
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
109   /**
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)
127   {
128     $sql_result = $this->db->query(
129       "SELECT vars, ip, changed FROM ".get_table_name('session')
130       ." WHERE sess_id = ?", $key);
131
132     if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
133       $this->changed = strtotime($sql_arr['changed']);
134       $this->ip      = $sql_arr['ip'];
135       $this->vars    = base64_decode($sql_arr['vars']);
136       $this->key     = $key;
137
138       if (!empty($this->vars))
139         return $this->vars;
140     }
141
142     return false;
143   }
144
145
146   /**
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    */
154   public function db_write($key, $vars)
155   {
156     $ts = microtime(true);
157     $now = $this->db->fromunixtime((int)$ts);
158
159     // no session row in DB (db_read() returns false)
160     if (!$this->key) {
161       $oldvars = false;
162     }
163     // use internal data from read() for fast requests (up to 0.5 sec.)
164     else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) {
165       $oldvars = $this->vars;
166     }
167     else { // else read data again from DB
168       $oldvars = $this->db_read($key);
169     }
170
171     if ($oldvars !== false) {
172       $newvars = $this->_fixvars($vars, $oldvars);
173
174       if ($newvars !== $oldvars) {
175         $this->db->query(
176           sprintf("UPDATE %s SET vars=?, changed=%s WHERE sess_id=?",
177             get_table_name('session'), $now),
178           base64_encode($newvars), $key);
179       }
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);
182       }
183     }
184     else {
185       $this->db->query(
186         sprintf("INSERT INTO %s (sess_id, vars, ip, created, changed) ".
187           "VALUES (?, ?, ?, %s, %s)",
188           get_table_name('session'), $now, $now),
189         $key, base64_encode($vars), (string)$this->ip);
190     }
191
192     return true;
193   }
194
195
196   /**
197    * Merge vars with old vars and apply unsets
198    */
199   private function _fixvars($vars, $oldvars)
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     }
213
214     $this->unsets = array();
215     return $newvars;
216   }
217
218
219   /**
220    * Handler for session_destroy()
221    *
222    * @param string Session ID
223    * @return boolean True on success
224    */
225   public function db_destroy($key)
226   {
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
235   /**
236    * Garbage collecting function
237    *
238    * @param string Session lifetime in seconds
239    * @return boolean True on success
240    */
241   public function db_gc($maxlifetime)
242   {
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
248     $this->gc();
249
250     return true;
251   }
252
253
254   /**
255    * Read session data from memcache
256    *
257    * @param string Session ID
258    * @return string Session vars
259    */
260   public function mc_read($key)
261   {
262     $value = $this->memcache->get($key);
263     if ($this->mc_debug) write_log('memcache', "get($key): " . strlen($value));
264     if ($value && ($arr = unserialize($value))) {
265       $this->changed = $arr['changed'];
266       $this->ip      = $arr['ip'];
267       $this->vars    = $arr['vars'];
268       $this->key     = $key;
269
270       if (!empty($this->vars))
271         return $this->vars;
272     }
273
274     return false;
275   }
276
277   /**
278    * Save session data.
279    * handler for session_read()
280    *
281    * @param string Session ID
282    * @param string Serialized session vars
283    * @return boolean True on success
284    */
285   public function mc_write($key, $vars)
286   {
287     $ts = microtime(true);
288
289     // no session data in cache (mc_read() returns false)
290     if (!$this->key)
291       $oldvars = false;
292     // use internal data for fast requests (up to 0.5 sec.)
293     else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5))
294       $oldvars = $this->vars;
295     else // else read data again
296       $oldvars = $this->mc_read($key);
297
298     $newvars = $oldvars !== false ? $this->_fixvars($vars, $oldvars) : $vars;
299     
300     if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 2) {
301       $value = serialize(array('changed' => time(), 'ip' => $this->ip, 'vars' => $newvars));
302       $ret = $this->memcache->set($key, $value, MEMCACHE_COMPRESSED, $this->lifetime);
303       if ($this->mc_debug) {
304         write_log('memcache', "set($key): " . strlen($value) . ": " . ($ret ? 'OK' : 'ERR'));
305         write_log('memcache', "... get($key): " . strlen($this->memcache->get($key)));
306       }
307       return $ret;
308     }
309     
310     return true;
311   }
312
313   /**
314    * Handler for session_destroy() with memcache backend
315    *
316    * @param string Session ID
317    * @return boolean True on success
318    */
319   public function mc_destroy($key)
320   {
321     $ret = $this->memcache->delete($key);
322     if ($this->mc_debug) write_log('memcache', "delete($key): " . ($ret ? 'OK' : 'ERR'));
323     return $ret;
324   }
325
326
327   /**
328    * Execute registered garbage collector routines
329    */
330   public function gc()
331   {
332     foreach ($this->gc_handlers as $fct)
333       $fct();
334   }
335
336
337   /**
338    * Cleanup session data before saving
339    */
340   public function cleanup()
341   {
342     // current compose information is stored in $_SESSION['compose'], move it to $_SESSION['compose_data_<ID>']
343     if ($compose_id = $_SESSION['compose']['id']) {
344       $_SESSION['compose_data_'.$compose_id] = $_SESSION['compose'];
345       $this->remove('compose');
346     }
347   }
348
349
350   /**
351    * Register additional garbage collector functions
352    *
353    * @param mixed Callback function
354    */
355   public function register_gc_handler($func_name)
356   {
357     if ($func_name && !in_array($func_name, $this->gc_handlers))
358       $this->gc_handlers[] = $func_name;
359   }
360
361
362   /**
363    * Generate and set new session id
364    *
365    * @param boolean $destroy If enabled the current session will be destroyed
366    */
367   public function regenerate_id($destroy=true)
368   {
369     session_regenerate_id($destroy);
370
371     $this->vars = false;
372     $this->key  = session_id();
373
374     return true;
375   }
376
377
378   /**
379    * Unset a session variable
380    *
381    * @param string Varibale name
382    * @return boolean True on success
383    */
384   public function remove($var=null)
385   {
386     if (empty($var))
387       return $this->destroy(session_id());
388
389     $this->unsets[] = $var;
390     unset($_SESSION[$var]);
391
392     return true;
393   }
394   
395   /**
396    * Kill this session
397    */
398   public function kill()
399   {
400     $this->vars = false;
401     $this->destroy(session_id());
402     rcmail::setcookie($this->cookiename, '-del-', time() - 60);
403   }
404
405
406   /**
407    * Serialize session data
408    */
409   private function serialize($vars)
410   {
411     $data = '';
412     if (is_array($vars))
413       foreach ($vars as $var=>$value)
414         $data .= $var.'|'.serialize($value);
415     else
416       $data = 'b:0;';
417     return $data;
418   }
419
420
421   /**
422    * Unserialize session data
423    * http://www.php.net/manual/en/function.session-decode.php#56106
424    */
425   private function unserialize($str)
426   {
427     $str = (string)$str;
428     $endptr = strlen($str);
429     $p = 0;
430
431     $serialized = '';
432     $items = 0;
433     $level = 0;
434
435     while ($p < $endptr) {
436       $q = $p;
437       while ($str[$q] != '|')
438         if (++$q >= $endptr) break 2;
439
440       if ($str[$p] == '!') {
441         $p++;
442         $has_value = false;
443       } else {
444         $has_value = true;
445       }
446
447       $name = substr($str, $p, $q - $p);
448       $q++;
449
450       $serialized .= 's:' . strlen($name) . ':"' . $name . '";';
451
452       if ($has_value) {
453         for (;;) {
454           $p = $q;
455           switch (strtolower($str[$q])) {
456             case 'n': /* null */
457             case 'b': /* boolean */
458             case 'i': /* integer */
459             case 'd': /* decimal */
460               do $q++;
461               while ( ($q < $endptr) && ($str[$q] != ';') );
462               $q++;
463               $serialized .= substr($str, $p, $q - $p);
464               if ($level == 0) break 2;
465               break;
466             case 'r': /* reference  */
467               $q+= 2;
468               for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) $id .= $str[$q];
469               $q++;
470               $serialized .= 'R:' . ($id + 1) . ';'; /* increment pointer because of outer array */
471               if ($level == 0) break 2;
472               break;
473             case 's': /* string */
474               $q+=2;
475               for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) $length .= $str[$q];
476               $q+=2;
477               $q+= (int)$length + 2;
478               $serialized .= substr($str, $p, $q - $p);
479               if ($level == 0) break 2;
480               break;
481             case 'a': /* array */
482             case 'o': /* object */
483               do $q++;
484               while ( ($q < $endptr) && ($str[$q] != '{') );
485               $q++;
486               $level++;
487               $serialized .= substr($str, $p, $q - $p);
488               break;
489             case '}': /* end of array|object */
490               $q++;
491               $serialized .= substr($str, $p, $q - $p);
492               if (--$level == 0) break 2;
493               break;
494             default:
495               return false;
496           }
497         }
498       } else {
499         $serialized .= 'N;';
500         $q += 2;
501       }
502       $items++;
503       $p = $q;
504     }
505
506     return unserialize( 'a:' . $items . ':{' . $serialized . '}' );
507   }
508
509
510   /**
511    * Setter for session lifetime
512    */
513   public function set_lifetime($lifetime)
514   {
515       $this->lifetime = max(120, $lifetime);
516
517       // valid time range is now - 1/2 lifetime to now + 1/2 lifetime
518       $now = time();
519       $this->now = $now - ($now % ($this->lifetime / 2));
520       $this->prev = $this->now - ($this->lifetime / 2);
521   }
522
523   /**
524    * Setter for keep_alive interval
525    */
526   public function set_keep_alive($keep_alive)
527   {
528     $this->keep_alive = $keep_alive;
529     
530     if ($this->lifetime < $keep_alive)
531         $this->set_lifetime($keep_alive + 30);
532   }
533
534   /**
535    * Getter for keep_alive interval
536    */
537   public function get_keep_alive()
538   {
539     return $this->keep_alive;
540   }
541
542   /**
543    * Getter for remote IP saved with this session
544    */
545   public function get_ip()
546   {
547     return $this->ip;
548   }
549   
550   /**
551    * Setter for cookie encryption secret
552    */
553   function set_secret($secret)
554   {
555     $this->secret = $secret;
556   }
557
558
559   /**
560    * Enable/disable IP check
561    */
562   function set_ip_check($check)
563   {
564     $this->ip_check = $check;
565   }
566   
567   /**
568    * Setter for the cookie name used for session cookie
569    */
570   function set_cookiename($cookiename)
571   {
572     if ($cookiename)
573       $this->cookiename = $cookiename;
574   }
575
576
577   /**
578    * Check session authentication cookie
579    *
580    * @return boolean True if valid, False if not
581    */
582   function check_auth()
583   {
584     $this->cookie = $_COOKIE[$this->cookiename];
585     $result = $this->ip_check ? $_SERVER['REMOTE_ADDR'] == $this->ip : true;
586
587     if (!$result)
588       $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . $_SERVER['REMOTE_ADDR']);
589
590     if ($result && $this->_mkcookie($this->now) != $this->cookie) {
591       // Check if using id from previous time slot
592       if ($this->_mkcookie($this->prev) == $this->cookie) {
593         $this->set_auth_cookie();
594       }
595       else {
596         $result = false;
597         $this->log("Session authentication failed for " . $this->key . "; invalid auth cookie sent");
598       }
599     }
600
601     return $result;
602   }
603
604
605   /**
606    * Set session authentication cookie
607    */
608   function set_auth_cookie()
609   {
610     $this->cookie = $this->_mkcookie($this->now);
611     rcmail::setcookie($this->cookiename, $this->cookie, 0);
612     $_COOKIE[$this->cookiename] = $this->cookie;
613   }
614
615
616   /**
617    * Create session cookie from session data
618    *
619    * @param int Time slot to use
620    */
621   function _mkcookie($timeslot)
622   {
623     $auth_string = "$this->key,$this->secret,$timeslot";
624     return "S" . (function_exists('sha1') ? sha1($auth_string) : md5($auth_string));
625   }
626   
627   /**
628    * 
629    */
630   function log($line)
631   {
632     if ($this->logging)
633       write_log('session', $line);
634   }
635
636 }