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 | dd5ed6ef69c9 |
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 } |
