source: github/program/include/rcube_session.php @ 638e345

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 638e345 was 638e345, checked in by thomascube <thomas@…>, 2 years ago

Don't attempt to read session data again if the initial read didn't return a result

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