Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_session.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
5 | This file is part of the Roundcube Webmail client | | |
6 | Copyright (C) 2005-2014, The Roundcube Dev Team | | |
7 | Copyright (C) 2011, Kolab Systems AG | | |
8 | | | |
9 | Licensed under the GNU General Public License version 3 or | | |
10 | any later version with exceptions for skins & plugins. | | |
11 | See the README file for a full license statement. | | |
12 | | | |
13 | PURPOSE: | | |
14 | Provide database supported session management | | |
15 +-----------------------------------------------------------------------+ | |
16 | Author: Thomas Bruederli <roundcube@gmail.com> | | |
17 | Author: Aleksander Machniak <alec@alec.pl> | | |
18 | Author: Cor Bosman <cor@roundcu.be> | | |
19 +-----------------------------------------------------------------------+ | |
20 */ | |
21 | |
22 /** | |
23 * Abstract class to provide database supported session storage | |
24 * | |
25 * @package Framework | |
26 * @subpackage Core | |
27 * @author Thomas Bruederli <roundcube@gmail.com> | |
28 * @author Aleksander Machniak <alec@alec.pl> | |
29 */ | |
30 abstract class rcube_session | |
31 { | |
32 protected $config; | |
33 protected $key; | |
34 protected $ip; | |
35 protected $changed; | |
36 protected $start; | |
37 protected $vars; | |
38 protected $now; | |
39 protected $time_diff = 0; | |
40 protected $reloaded = false; | |
41 protected $appends = array(); | |
42 protected $unsets = array(); | |
43 protected $gc_enabled = 0; | |
44 protected $gc_handlers = array(); | |
45 protected $cookiename = 'roundcube_sessauth'; | |
46 protected $ip_check = false; | |
47 protected $logging = false; | |
48 | |
49 | |
50 /** | |
51 * Blocks session data from being written to database. | |
52 * Can be used if write-race conditions are to be expected | |
53 * @var boolean | |
54 */ | |
55 public $nowrite = false; | |
56 | |
57 /** | |
58 * Factory, returns driver-specific instance of the class | |
59 * | |
60 * @param object $config | |
61 * @return Object rcube_session | |
62 */ | |
63 public static function factory($config) | |
64 { | |
65 // get session storage driver | |
66 $storage = $config->get('session_storage', 'db'); | |
67 | |
68 // class name for this storage | |
69 $class = "rcube_session_" . $storage; | |
70 | |
71 // try to instantiate class | |
72 if (class_exists($class)) { | |
73 return new $class($config); | |
74 } | |
75 | |
76 // no storage found, raise error | |
77 rcube::raise_error(array('code' => 604, 'type' => 'session', | |
78 'line' => __LINE__, 'file' => __FILE__, | |
79 'message' => "Failed to find session driver. Check session_storage config option"), | |
80 true, true); | |
81 } | |
82 | |
83 /** | |
84 * @param Object $config | |
85 */ | |
86 public function __construct($config) | |
87 { | |
88 $this->config = $config; | |
89 | |
90 // set ip check | |
91 $this->set_ip_check($this->config->get('ip_check')); | |
92 | |
93 // set cookie name | |
94 if ($this->config->get('session_auth_name')) { | |
95 $this->set_cookiename($this->config->get('session_auth_name')); | |
96 } | |
97 } | |
98 | |
99 /** | |
100 * register session handler | |
101 */ | |
102 public function register_session_handler() | |
103 { | |
104 ini_set('session.serialize_handler', 'php'); | |
105 | |
106 // set custom functions for PHP session management | |
107 session_set_save_handler( | |
108 array($this, 'open'), | |
109 array($this, 'close'), | |
110 array($this, 'read'), | |
111 array($this, 'sess_write'), | |
112 array($this, 'destroy'), | |
113 array($this, 'gc') | |
114 ); | |
115 } | |
116 | |
117 /** | |
118 * Wrapper for session_start() | |
119 */ | |
120 public function start() | |
121 { | |
122 $this->start = microtime(true); | |
123 $this->ip = rcube_utils::remote_addr(); | |
124 $this->logging = $this->config->get('log_session', false); | |
125 | |
126 $lifetime = $this->config->get('session_lifetime', 1) * 60; | |
127 $this->set_lifetime($lifetime); | |
128 | |
129 session_start(); | |
130 } | |
131 | |
132 /** | |
133 * Abstract methods should be implemented by driver classes | |
134 */ | |
135 abstract function open($save_path, $session_name); | |
136 abstract function close(); | |
137 abstract function destroy($key); | |
138 abstract function read($key); | |
139 abstract function write($key, $vars); | |
140 abstract function update($key, $newvars, $oldvars); | |
141 | |
142 /** | |
143 * session write handler. This calls the implementation methods for write/update after some initial checks. | |
144 * | |
145 * @param $key | |
146 * @param $vars | |
147 * | |
148 * @return bool | |
149 */ | |
150 public function sess_write($key, $vars) | |
151 { | |
152 if ($this->nowrite) { | |
153 return true; | |
154 } | |
155 | |
156 // check cache | |
157 $oldvars = $this->get_cache($key); | |
158 | |
159 // if there are cached vars, update store, else insert new data | |
160 if ($oldvars) { | |
161 $newvars = $this->_fixvars($vars, $oldvars); | |
162 return $this->update($key, $newvars, $oldvars); | |
163 } | |
164 else { | |
165 return $this->write($key, $vars); | |
166 } | |
167 } | |
168 | |
169 /** | |
170 * Wrapper for session_write_close() | |
171 */ | |
172 public function write_close() | |
173 { | |
174 session_write_close(); | |
175 | |
176 // write_close() is called on script shutdown, see rcube::shutdown() | |
177 // execute cleanup functionality if enabled by session gc handler | |
178 // we do this after closing the session for better performance | |
179 $this->gc_shutdown(); | |
180 } | |
181 | |
182 /** | |
183 * Creates a new (separate) session | |
184 * | |
185 * @param array Session data | |
186 * | |
187 * @return string Session identifier (on success) | |
188 */ | |
189 public function create($data) | |
190 { | |
191 $length = strlen(session_id()); | |
192 $key = rcube_utils::random_bytes($length); | |
193 | |
194 // create new session | |
195 if ($this->write($key, $this->serialize($data))) { | |
196 return $key; | |
197 } | |
198 } | |
199 | |
200 /** | |
201 * Merge vars with old vars and apply unsets | |
202 */ | |
203 protected function _fixvars($vars, $oldvars) | |
204 { | |
205 if ($oldvars !== null) { | |
206 $a_oldvars = $this->unserialize($oldvars); | |
207 if (is_array($a_oldvars)) { | |
208 // remove unset keys on oldvars | |
209 foreach ((array)$this->unsets as $var) { | |
210 if (isset($a_oldvars[$var])) { | |
211 unset($a_oldvars[$var]); | |
212 } | |
213 else { | |
214 $path = explode('.', $var); | |
215 $k = array_pop($path); | |
216 $node = &$this->get_node($path, $a_oldvars); | |
217 unset($node[$k]); | |
218 } | |
219 } | |
220 | |
221 $newvars = $this->serialize(array_merge( | |
222 (array)$a_oldvars, (array)$this->unserialize($vars))); | |
223 } | |
224 else { | |
225 $newvars = $vars; | |
226 } | |
227 } | |
228 | |
229 $this->unsets = array(); | |
230 return $newvars; | |
231 } | |
232 | |
233 /** | |
234 * Execute registered garbage collector routines | |
235 */ | |
236 public function gc($maxlifetime) | |
237 { | |
238 // move gc execution to the script shutdown function | |
239 // see rcube::shutdown() and rcube_session::write_close() | |
240 $this->gc_enabled = $maxlifetime; | |
241 | |
242 return true; | |
243 } | |
244 | |
245 /** | |
246 * Register additional garbage collector functions | |
247 * | |
248 * @param mixed Callback function | |
249 */ | |
250 public function register_gc_handler($func) | |
251 { | |
252 foreach ($this->gc_handlers as $handler) { | |
253 if ($handler == $func) { | |
254 return; | |
255 } | |
256 } | |
257 | |
258 $this->gc_handlers[] = $func; | |
259 } | |
260 | |
261 /** | |
262 * Garbage collector handler to run on script shutdown | |
263 */ | |
264 protected function gc_shutdown() | |
265 { | |
266 if ($this->gc_enabled) { | |
267 foreach ($this->gc_handlers as $fct) { | |
268 call_user_func($fct); | |
269 } | |
270 } | |
271 } | |
272 | |
273 /** | |
274 * Generate and set new session id | |
275 * | |
276 * @param boolean $destroy If enabled the current session will be destroyed | |
277 * @return bool | |
278 */ | |
279 public function regenerate_id($destroy=true) | |
280 { | |
281 session_regenerate_id($destroy); | |
282 | |
283 $this->vars = null; | |
284 $this->key = session_id(); | |
285 | |
286 return true; | |
287 } | |
288 | |
289 /** | |
290 * See if we have vars of this key already cached, and if so, return them. | |
291 * | |
292 * @param string $key Session ID | |
293 * | |
294 * @return string | |
295 */ | |
296 protected function get_cache($key) | |
297 { | |
298 // no session data in cache (read() returns false) | |
299 if (!$this->key) { | |
300 $cache = null; | |
301 } | |
302 // use internal data for fast requests (up to 0.5 sec.) | |
303 else if ($key == $this->key && (!$this->vars || $ts - $this->start < 0.5)) { | |
304 $cache = $this->vars; | |
305 } | |
306 else { // else read data again | |
307 $cache = $this->read($key); | |
308 } | |
309 | |
310 return $cache; | |
311 } | |
312 | |
313 /** | |
314 * Append the given value to the certain node in the session data array | |
315 * | |
316 * Warning: Do not use if you already modified $_SESSION in the same request (#1490608) | |
317 * | |
318 * @param string Path denoting the session variable where to append the value | |
319 * @param string Key name under which to append the new value (use null for appending to an indexed list) | |
320 * @param mixed Value to append to the session data array | |
321 */ | |
322 public function append($path, $key, $value) | |
323 { | |
324 // re-read session data from DB because it might be outdated | |
325 if (!$this->reloaded && microtime(true) - $this->start > 0.5) { | |
326 $this->reload(); | |
327 $this->reloaded = true; | |
328 $this->start = microtime(true); | |
329 } | |
330 | |
331 $node = &$this->get_node(explode('.', $path), $_SESSION); | |
332 | |
333 if ($key !== null) { | |
334 $node[$key] = $value; | |
335 $path .= '.' . $key; | |
336 } | |
337 else { | |
338 $node[] = $value; | |
339 } | |
340 | |
341 $this->appends[] = $path; | |
342 | |
343 // when overwriting a previously unset variable | |
344 if ($this->unsets[$path]) { | |
345 unset($this->unsets[$path]); | |
346 } | |
347 } | |
348 | |
349 /** | |
350 * Unset a session variable | |
351 * | |
352 * @param string Variable name (can be a path denoting a certain node in the session array, e.g. compose.attachments.5) | |
353 * @return boolean True on success | |
354 */ | |
355 public function remove($var=null) | |
356 { | |
357 if (empty($var)) { | |
358 return $this->destroy(session_id()); | |
359 } | |
360 | |
361 $this->unsets[] = $var; | |
362 | |
363 if (isset($_SESSION[$var])) { | |
364 unset($_SESSION[$var]); | |
365 } | |
366 else { | |
367 $path = explode('.', $var); | |
368 $key = array_pop($path); | |
369 $node = &$this->get_node($path, $_SESSION); | |
370 unset($node[$key]); | |
371 } | |
372 | |
373 return true; | |
374 } | |
375 | |
376 /** | |
377 * Kill this session | |
378 */ | |
379 public function kill() | |
380 { | |
381 $this->vars = null; | |
382 $this->ip = rcube_utils::remote_addr(); // update IP (might have changed) | |
383 $this->destroy(session_id()); | |
384 rcube_utils::setcookie($this->cookiename, '-del-', time() - 60); | |
385 } | |
386 | |
387 /** | |
388 * Re-read session data from storage backend | |
389 */ | |
390 public function reload() | |
391 { | |
392 // collect updated data from previous appends | |
393 $merge_data = array(); | |
394 foreach ((array)$this->appends as $var) { | |
395 $path = explode('.', $var); | |
396 $value = $this->get_node($path, $_SESSION); | |
397 $k = array_pop($path); | |
398 $node = &$this->get_node($path, $merge_data); | |
399 $node[$k] = $value; | |
400 } | |
401 | |
402 if ($this->key) { | |
403 $data = $this->read($this->key); | |
404 } | |
405 | |
406 if ($data) { | |
407 session_decode($data); | |
408 | |
409 // apply appends and unsets to reloaded data | |
410 $_SESSION = array_merge_recursive($_SESSION, $merge_data); | |
411 | |
412 foreach ((array)$this->unsets as $var) { | |
413 if (isset($_SESSION[$var])) { | |
414 unset($_SESSION[$var]); | |
415 } | |
416 else { | |
417 $path = explode('.', $var); | |
418 $k = array_pop($path); | |
419 $node = &$this->get_node($path, $_SESSION); | |
420 unset($node[$k]); | |
421 } | |
422 } | |
423 } | |
424 } | |
425 | |
426 /** | |
427 * Returns a reference to the node in data array referenced by the given path. | |
428 * e.g. ['compose','attachments'] will return $_SESSION['compose']['attachments'] | |
429 */ | |
430 protected function &get_node($path, &$data_arr) | |
431 { | |
432 $node = &$data_arr; | |
433 if (!empty($path)) { | |
434 foreach ((array)$path as $key) { | |
435 if (!isset($node[$key])) | |
436 $node[$key] = array(); | |
437 $node = &$node[$key]; | |
438 } | |
439 } | |
440 | |
441 return $node; | |
442 } | |
443 | |
444 /** | |
445 * Serialize session data | |
446 */ | |
447 protected function serialize($vars) | |
448 { | |
449 $data = ''; | |
450 if (is_array($vars)) { | |
451 foreach ($vars as $var=>$value) | |
452 $data .= $var.'|'.serialize($value); | |
453 } | |
454 else { | |
455 $data = 'b:0;'; | |
456 } | |
457 | |
458 return $data; | |
459 } | |
460 | |
461 /** | |
462 * Unserialize session data | |
463 * http://www.php.net/manual/en/function.session-decode.php#56106 | |
464 */ | |
465 protected function unserialize($str) | |
466 { | |
467 $str = (string)$str; | |
468 $endptr = strlen($str); | |
469 $p = 0; | |
470 | |
471 $serialized = ''; | |
472 $items = 0; | |
473 $level = 0; | |
474 | |
475 while ($p < $endptr) { | |
476 $q = $p; | |
477 while ($str[$q] != '|') | |
478 if (++$q >= $endptr) | |
479 break 2; | |
480 | |
481 if ($str[$p] == '!') { | |
482 $p++; | |
483 $has_value = false; | |
484 } | |
485 else { | |
486 $has_value = true; | |
487 } | |
488 | |
489 $name = substr($str, $p, $q - $p); | |
490 $q++; | |
491 | |
492 $serialized .= 's:' . strlen($name) . ':"' . $name . '";'; | |
493 | |
494 if ($has_value) { | |
495 for (;;) { | |
496 $p = $q; | |
497 switch (strtolower($str[$q])) { | |
498 case 'n': // null | |
499 case 'b': // boolean | |
500 case 'i': // integer | |
501 case 'd': // decimal | |
502 do $q++; | |
503 while ( ($q < $endptr) && ($str[$q] != ';') ); | |
504 $q++; | |
505 $serialized .= substr($str, $p, $q - $p); | |
506 if ($level == 0) | |
507 break 2; | |
508 break; | |
509 case 'r': // reference | |
510 $q+= 2; | |
511 for ($id = ''; ($q < $endptr) && ($str[$q] != ';'); $q++) | |
512 $id .= $str[$q]; | |
513 $q++; | |
514 // increment pointer because of outer array | |
515 $serialized .= 'R:' . ($id + 1) . ';'; | |
516 if ($level == 0) | |
517 break 2; | |
518 break; | |
519 case 's': // string | |
520 $q+=2; | |
521 for ($length=''; ($q < $endptr) && ($str[$q] != ':'); $q++) | |
522 $length .= $str[$q]; | |
523 $q+=2; | |
524 $q+= (int)$length + 2; | |
525 $serialized .= substr($str, $p, $q - $p); | |
526 if ($level == 0) | |
527 break 2; | |
528 break; | |
529 case 'a': // array | |
530 case 'o': // object | |
531 do $q++; | |
532 while ($q < $endptr && $str[$q] != '{'); | |
533 $q++; | |
534 $level++; | |
535 $serialized .= substr($str, $p, $q - $p); | |
536 break; | |
537 case '}': // end of array|object | |
538 $q++; | |
539 $serialized .= substr($str, $p, $q - $p); | |
540 if (--$level == 0) | |
541 break 2; | |
542 break; | |
543 default: | |
544 return false; | |
545 } | |
546 } | |
547 } | |
548 else { | |
549 $serialized .= 'N;'; | |
550 $q += 2; | |
551 } | |
552 $items++; | |
553 $p = $q; | |
554 } | |
555 | |
556 return unserialize( 'a:' . $items . ':{' . $serialized . '}' ); | |
557 } | |
558 | |
559 /** | |
560 * Setter for session lifetime | |
561 */ | |
562 public function set_lifetime($lifetime) | |
563 { | |
564 $this->lifetime = max(120, $lifetime); | |
565 | |
566 // valid time range is now - 1/2 lifetime to now + 1/2 lifetime | |
567 $now = time(); | |
568 $this->now = $now - ($now % ($this->lifetime / 2)); | |
569 } | |
570 | |
571 /** | |
572 * Getter for remote IP saved with this session | |
573 */ | |
574 public function get_ip() | |
575 { | |
576 return $this->ip; | |
577 } | |
578 | |
579 /** | |
580 * Setter for cookie encryption secret | |
581 */ | |
582 function set_secret($secret = null) | |
583 { | |
584 // generate random hash and store in session | |
585 if (!$secret) { | |
586 if (!empty($_SESSION['auth_secret'])) { | |
587 $secret = $_SESSION['auth_secret']; | |
588 } | |
589 else { | |
590 $secret = rcube_utils::random_bytes(strlen($this->key)); | |
591 } | |
592 } | |
593 | |
594 $_SESSION['auth_secret'] = $secret; | |
595 } | |
596 | |
597 /** | |
598 * Enable/disable IP check | |
599 */ | |
600 function set_ip_check($check) | |
601 { | |
602 $this->ip_check = $check; | |
603 } | |
604 | |
605 /** | |
606 * Setter for the cookie name used for session cookie | |
607 */ | |
608 function set_cookiename($cookiename) | |
609 { | |
610 if ($cookiename) { | |
611 $this->cookiename = $cookiename; | |
612 } | |
613 } | |
614 | |
615 /** | |
616 * Check session authentication cookie | |
617 * | |
618 * @return boolean True if valid, False if not | |
619 */ | |
620 function check_auth() | |
621 { | |
622 $this->cookie = $_COOKIE[$this->cookiename]; | |
623 $result = $this->ip_check ? rcube_utils::remote_addr() == $this->ip : true; | |
624 | |
625 if (!$result) { | |
626 $this->log("IP check failed for " . $this->key . "; expected " . $this->ip . "; got " . rcube_utils::remote_addr()); | |
627 } | |
628 | |
629 if ($result && $this->_mkcookie($this->now) != $this->cookie) { | |
630 $this->log("Session auth check failed for " . $this->key . "; timeslot = " . date('Y-m-d H:i:s', $this->now)); | |
631 $result = false; | |
632 | |
633 // Check if using id from a previous time slot | |
634 for ($i = 1; $i <= 2; $i++) { | |
635 $prev = $this->now - ($this->lifetime / 2) * $i; | |
636 if ($this->_mkcookie($prev) == $this->cookie) { | |
637 $this->log("Send new auth cookie for " . $this->key . ": " . $this->cookie); | |
638 $this->set_auth_cookie(); | |
639 $result = true; | |
640 } | |
641 } | |
642 } | |
643 | |
644 if (!$result) { | |
645 $this->log("Session authentication failed for " . $this->key | |
646 . "; invalid auth cookie sent; timeslot = " . date('Y-m-d H:i:s', $prev)); | |
647 } | |
648 | |
649 return $result; | |
650 } | |
651 | |
652 /** | |
653 * Set session authentication cookie | |
654 */ | |
655 public function set_auth_cookie() | |
656 { | |
657 $this->cookie = $this->_mkcookie($this->now); | |
658 rcube_utils::setcookie($this->cookiename, $this->cookie, 0); | |
659 $_COOKIE[$this->cookiename] = $this->cookie; | |
660 } | |
661 | |
662 /** | |
663 * Create session cookie for specified time slot. | |
664 * | |
665 * @param int Time slot to use | |
666 * | |
667 * @return string | |
668 */ | |
669 protected function _mkcookie($timeslot) | |
670 { | |
671 // make sure the secret key exists | |
672 $this->set_secret(); | |
673 | |
674 // no need to hash this, it's just a random string | |
675 return $_SESSION['auth_secret'] . '-' . $timeslot; | |
676 } | |
677 | |
678 /** | |
679 * Writes debug information to the log | |
680 */ | |
681 function log($line) | |
682 { | |
683 if ($this->logging) { | |
684 rcube::write_log('session', $line); | |
685 } | |
686 } | |
687 } |