MDL-35029 core_session Refresh guest session if autologin as guest is enabled
[moodle.git] / lib / sessionlib.php
CommitLineData
b37eac91 1<?php
b37eac91 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
57f7b7ce 16
542797b4 17/**
78bfb562
PS
18 * @package core
19 * @subpackage session
20 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
f0d531ad 21 * @copyright 2008, 2009 Petr Skoda {@link http://skodak.org}
78bfb562 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
542797b4 23 */
b37eac91 24
78bfb562
PS
25defined('MOODLE_INTERNAL') || die();
26
2b0e3941
PS
27if (!defined('SESSION_ACQUIRE_LOCK_TIMEOUT')) {
28 /**
29 * How much time to wait for session lock before displaying error (in seconds),
12dfd6df 30 * 2 minutes by default should be a reasonable time before telling users to wait and refresh browser.
2b0e3941
PS
31 */
32 define('SESSION_ACQUIRE_LOCK_TIMEOUT', 60*2);
33}
34
b37eac91 35/**
36 * Factory method returning moodle_session object.
37 * @return moodle_session
38 */
b7b64ff2 39function session_get_instance() {
5e9dd017 40 global $CFG, $DB;
0a2092a3 41
0ad6b20c 42 static $session = null;
43
44 if (is_null($session)) {
e8656bef 45 if (empty($CFG->sessiontimeout)) {
46 $CFG->sessiontimeout = 7200;
47 }
48
2b0e3941
PS
49 try {
50 if (defined('SESSION_CUSTOM_CLASS')) {
51 // this is a hook for webservices, key based login, etc.
52 if (defined('SESSION_CUSTOM_FILE')) {
53 require_once($CFG->dirroot.SESSION_CUSTOM_FILE);
54 }
55 $session_class = SESSION_CUSTOM_CLASS;
56 $session = new $session_class();
0a2092a3 57
2b0e3941
PS
58 } else if ((!isset($CFG->dbsessions) or $CFG->dbsessions) and $DB->session_lock_supported()) {
59 // default recommended session type
60 $session = new database_session();
0a2092a3 61
2b0e3941
PS
62 } else {
63 // legacy limited file based storage - some features and auth plugins will not work, sorry
64 $session = new legacy_file_session();
65 }
66 } catch (Exception $ex) {
67 // prevent repeated inits
68 $session = new emergency_session();
69 throw $ex;
0a2092a3 70 }
0ad6b20c 71 }
72
73 return $session;
74}
75
2b0e3941 76
b37eac91 77/**
f0d531ad
PS
78 * Moodle session abstraction
79 *
80 * @package core
81 * @subpackage session
82 * @copyright 2008 Petr Skoda {@link http://skodak.org}
83 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
b37eac91 84 */
0a2092a3 85interface moodle_session {
56949c17 86 /**
87 * Terminate current session
88 * @return void
89 */
90 public function terminate_current();
91
92 /**
93 * No more changes in session expected.
ed149942 94 * Unblocks the sessions, other scripts may start executing in parallel.
56949c17 95 * @return void
96 */
97 public function write_close();
df997f84 98
2d0acbd5
JP
99 /**
100 * Check for existing session with id $sid
101 * @param unknown_type $sid
102 * @return boolean true if session found.
103 */
104 public function session_exists($sid);
0a2092a3 105}
106
2b0e3941
PS
107
108/**
109 * Fallback session handler when standard session init fails.
110 * This prevents repeated attempts to init faulty handler.
111 *
112 * @package core
113 * @subpackage session
114 * @copyright 2011 Petr Skoda {@link http://skodak.org}
115 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
116 */
117class emergency_session implements moodle_session {
118
119 public function __construct() {
120 // session not used at all
121 $_SESSION = array();
122 $_SESSION['SESSION'] = new stdClass();
123 $_SESSION['USER'] = new stdClass();
124 }
125
126 /**
127 * Terminate current session
128 * @return void
129 */
130 public function terminate_current() {
131 return;
132 }
133
134 /**
135 * No more changes in session expected.
136 * Unblocks the sessions, other scripts may start executing in parallel.
137 * @return void
138 */
139 public function write_close() {
140 return;
141 }
142
143 /**
144 * Check for existing session with id $sid
145 * @param unknown_type $sid
146 * @return boolean true if session found.
147 */
148 public function session_exists($sid) {
149 return false;
150 }
151}
152
153
57f7b7ce 154/**
155 * Class handling all session and cookies related stuff.
b37eac91 156 *
f0d531ad
PS
157 * @package core
158 * @subpackage session
159 * @copyright 2009 Petr Skoda {@link http://skodak.org}
160 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
57f7b7ce 161 */
0a2092a3 162abstract class session_stub implements moodle_session {
3b50631d 163 protected $justloggedout;
164
b7b64ff2 165 public function __construct() {
57f7b7ce 166 global $CFG;
57f7b7ce 167
0ad6b20c 168 if (NO_MOODLE_COOKIES) {
0a2092a3 169 // session not used at all
0ad6b20c 170 $_SESSION = array();
927b2e7b
PS
171 $_SESSION['SESSION'] = new stdClass();
172 $_SESSION['USER'] = new stdClass();
0ad6b20c 173
174 } else {
0a2092a3 175 $this->prepare_cookies();
176 $this->init_session_storage();
177
35d6a2a4 178 $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
179
98eaf27e 180 ini_set('session.use_trans_sid', '0');
0a2092a3 181
57f7b7ce 182 session_name('MoodleSession'.$CFG->sessioncookie);
e6e13284 183 session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
64d69e96 184 session_start();
57f7b7ce 185 if (!isset($_SESSION['SESSION'])) {
927b2e7b 186 $_SESSION['SESSION'] = new stdClass();
3b50631d 187 if (!$newsession and !$this->justloggedout) {
35d6a2a4 188 $_SESSION['SESSION']->has_timed_out = true;
189 }
57f7b7ce 190 }
191 if (!isset($_SESSION['USER'])) {
927b2e7b 192 $_SESSION['USER'] = new stdClass();
57f7b7ce 193 }
57f7b7ce 194 }
57f7b7ce 195
b7b64ff2 196 $this->check_user_initialised();
9bda43e6 197
198 $this->check_security();
b7b64ff2 199 }
200
56949c17 201 /**
2b0e3941
PS
202 * Terminate current session
203 * @return void
56949c17 204 */
205 public function terminate_current() {
2d0acbd5 206 global $CFG, $SESSION, $USER, $DB;
56949c17 207
38549d63 208 try {
9c764f9f 209 $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
38549d63
PS
210 } catch (Exception $ignored) {
211 // probably install/upgrade - ignore this problem
9c764f9f 212 }
df997f84 213
56949c17 214 if (NO_MOODLE_COOKIES) {
215 return;
216 }
217
64d69e96 218 // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
56949c17 219 $_SESSION = array();
927b2e7b
PS
220 $_SESSION['SESSION'] = new stdClass();
221 $_SESSION['USER'] = new stdClass();
35d6a2a4 222 $_SESSION['USER']->id = 0;
56949c17 223 if (isset($CFG->mnet_localhost_id)) {
35d6a2a4 224 $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
56949c17 225 }
35d6a2a4 226 $SESSION = $_SESSION['SESSION']; // this may not work properly
3b50631d 227 $USER = $_SESSION['USER']; // this may not work properly
35d6a2a4 228
56949c17 229 $file = null;
230 $line = null;
231 if (headers_sent($file, $line)) {
232 error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
233 }
234
64d69e96 235 // now let's try to get a new session id and delete the old one
3b50631d 236 $this->justloggedout = true;
237 session_regenerate_id(true);
238 $this->justloggedout = false;
239
64d69e96 240 // write the new session
35d6a2a4 241 session_write_close();
56949c17 242 }
243
244 /**
245 * No more changes in session expected.
ed149942 246 * Unblocks the sessions, other scripts may start executing in parallel.
56949c17 247 * @return void
248 */
249 public function write_close() {
250 if (NO_MOODLE_COOKIES) {
251 return;
252 }
253
254 session_write_close();
255 }
256
b7b64ff2 257 /**
dd9e22f8 258 * Initialise $USER object, handles google access
259 * and sets up not logged in user properly.
b7b64ff2 260 *
261 * @return void
262 */
263 protected function check_user_initialised() {
7c25ee0f
PS
264 global $CFG;
265
b7b64ff2 266 if (isset($_SESSION['USER']->id)) {
267 // already set up $USER
268 return;
269 }
270
271 $user = null;
272
273 if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
81b58cc2
PS
274 if (is_web_crawler()) {
275 $user = guest_user();
b7b64ff2 276 }
1b813f5c 277 if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
b7b64ff2 278 // automaticaly log in users coming from search engine results
279 if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
280 $user = guest_user();
281 } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
282 $user = guest_user();
283 }
284 }
285 }
286
287 if (!$user) {
927b2e7b 288 $user = new stdClass();
b7b64ff2 289 $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
0ad6b20c 290 if (isset($CFG->mnet_localhost_id)) {
b7b64ff2 291 $user->mnethostid = $CFG->mnet_localhost_id;
0a2092a3 292 } else {
293 $user->mnethostid = 1;
57f7b7ce 294 }
295 }
b7b64ff2 296 session_set_user($user);
57f7b7ce 297 }
298
9bda43e6 299 /**
300 * Does various session security checks
301 * @global void
302 */
93f66983 303 protected function check_security() {
304 global $CFG;
305
0a2092a3 306 if (NO_MOODLE_COOKIES) {
307 return;
308 }
309
9bda43e6 310 if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
311 /// Make sure current IP matches the one for this session
93f66983 312 $remoteaddr = getremoteaddr();
313
314 if (empty($_SESSION['USER']->sessionip)) {
315 $_SESSION['USER']->sessionip = $remoteaddr;
316 }
317
318 if ($_SESSION['USER']->sessionip != $remoteaddr) {
9bda43e6 319 // this is a security feature - terminate the session in case of any doubt
56949c17 320 $this->terminate_current();
9bda43e6 321 print_error('sessionipnomatch2', 'error');
93f66983 322 }
323 }
93f66983 324 }
325
57f7b7ce 326 /**
ed149942 327 * Prepare cookies and various system settings
57f7b7ce 328 */
b7b64ff2 329 protected function prepare_cookies() {
0a2092a3 330 global $CFG;
57f7b7ce 331
11e7b506 332 if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
57f7b7ce 333 $CFG->cookiesecure = 0;
334 }
335
336 if (!isset($CFG->cookiehttponly)) {
337 $CFG->cookiehttponly = 0;
338 }
339
340 /// Set sessioncookie and sessioncookiepath variable if it isn't already
341 if (!isset($CFG->sessioncookie)) {
342 $CFG->sessioncookie = '';
343 }
988fc20e
PS
344
345 // make sure cookie domain makes sense for this wwwroot
e6e13284 346 if (!isset($CFG->sessioncookiedomain)) {
347 $CFG->sessioncookiedomain = '';
988fc20e
PS
348 } else if ($CFG->sessioncookiedomain !== '') {
349 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
350 if ($CFG->sessioncookiedomain !== $host) {
351 if (substr($CFG->sessioncookiedomain, 0, 1) === '.') {
352 if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
353 // invalid domain - it must be end part of host
354 $CFG->sessioncookiedomain = '';
355 }
356 } else {
357 if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
358 // invalid domain - it must be end part of host
359 $CFG->sessioncookiedomain = '';
360 }
361 }
362 }
e6e13284 363 }
988fc20e
PS
364
365 // make sure the cookiepath is valid for this wwwroot or autodetect if not specified
57f7b7ce 366 if (!isset($CFG->sessioncookiepath)) {
988fc20e
PS
367 $CFG->sessioncookiepath = '';
368 }
369 if ($CFG->sessioncookiepath !== '/') {
370 $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/';
371 if ($CFG->sessioncookiepath === '') {
372 $CFG->sessioncookiepath = $path;
373 } else {
374 if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') {
375 $CFG->sessioncookiepath = $path;
376 }
377 }
57f7b7ce 378 }
379
380 //discard session ID from POST, GET and globals to tighten security,
98eaf27e
PS
381 //this is session fixation prevention
382 unset(${'MoodleSession'.$CFG->sessioncookie});
383 unset($_GET['MoodleSession'.$CFG->sessioncookie]);
384 unset($_POST['MoodleSession'.$CFG->sessioncookie]);
385 unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
386
57f7b7ce 387 //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
388 if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
389 unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
390 }
57f7b7ce 391 }
392
393 /**
2b0e3941 394 * Init session storage.
57f7b7ce 395 */
f61a032a 396 protected abstract function init_session_storage();
f61a032a 397}
398
2b0e3941 399
f61a032a 400/**
401 * Legacy moodle sessions stored in files, not recommended any more.
b37eac91 402 *
f0d531ad
PS
403 * @package core
404 * @subpackage session
405 * @copyright 2009 Petr Skoda {@link http://skodak.org}
406 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f61a032a 407 */
0a2092a3 408class legacy_file_session extends session_stub {
2b0e3941
PS
409 /**
410 * Init session storage.
411 */
b7b64ff2 412 protected function init_session_storage() {
57f7b7ce 413 global $CFG;
414
0a2092a3 415 ini_set('session.save_handler', 'files');
416
f61a032a 417 // Some distros disable GC by setting probability to 0
418 // overriding the PHP default of 1
419 // (gc_probability is divided by gc_divisor, which defaults to 1000)
420 if (ini_get('session.gc_probability') == 0) {
421 ini_set('session.gc_probability', 1);
422 }
57f7b7ce 423
3b1a9849 424 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
57f7b7ce 425
4031f6a2
PS
426 // make sure sessions dir exists and is writable, throws exception if not
427 make_upload_directory('sessions');
428
2d801928 429 // Need to disable debugging since disk_free_space()
430 // will fail on very large partitions (see MDL-19222)
431 $freespace = @disk_free_space($CFG->dataroot.'/sessions');
432 if (!($freespace > 2048) and $freespace !== false) {
55059253 433 print_error('sessiondiskfull', 'error');
434 }
f61a032a 435 ini_set('session.save_path', $CFG->dataroot .'/sessions');
436 }
2d0acbd5
JP
437 /**
438 * Check for existing session with id $sid
439 * @param unknown_type $sid
440 * @return boolean true if session found.
441 */
442 public function session_exists($sid){
7c25ee0f
PS
443 global $CFG;
444
2d0acbd5 445 $sid = clean_param($sid, PARAM_FILE);
4031f6a2 446 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
2d0acbd5
JP
447 return file_exists($sessionfile);
448 }
f61a032a 449}
450
2b0e3941 451
f61a032a 452/**
453 * Recommended moodle session storage.
b37eac91 454 *
f0d531ad
PS
455 * @package core
456 * @subpackage session
457 * @copyright 2009 Petr Skoda {@link http://skodak.org}
458 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
f61a032a 459 */
0a2092a3 460class database_session extends session_stub {
2b0e3941 461 /** @var stdClass $record session record */
0a2092a3 462 protected $record = null;
2b0e3941
PS
463
464 /** @var moodle_database $database session database */
0a2092a3 465 protected $database = null;
466
2b0e3941
PS
467 /** @var bool $failed session read/init failed, do not write back to DB */
468 protected $failed = false;
469
dd9e22f8 470 public function __construct() {
471 global $DB;
472 $this->database = $DB;
473 parent::__construct();
b9fb7103 474
475 if (!empty($this->record->state)) {
476 // something is very wrong
477 session_kill($this->record->sid);
478
479 if ($this->record->state == 9) {
480 print_error('dbsessionmysqlpacketsize', 'error');
481 }
482 }
dd9e22f8 483 }
df997f84 484
2b0e3941
PS
485 /**
486 * Check for existing session with id $sid
487 * @param unknown_type $sid
488 * @return boolean true if session found.
489 */
2d0acbd5
JP
490 public function session_exists($sid){
491 global $CFG;
492 try {
493 $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
494 $params = array(time() + $CFG->sessiontimeout, $sid, 0);
df997f84 495
2d0acbd5
JP
496 return $this->database->record_exists_sql($sql, $params);
497 } catch (dml_exception $ex) {
498 error_log('Error checking existance of database session');
499 return false;
500 }
501 }
df997f84 502
2b0e3941
PS
503 /**
504 * Init session storage.
505 */
f61a032a 506 protected function init_session_storage() {
507 global $CFG;
508
dd9e22f8 509 // gc only from CRON - individual user timeouts now checked during each access
510 ini_set('session.gc_probability', 0);
0a2092a3 511
ef159e5f 512 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
0a2092a3 513
514 $result = session_set_save_handler(array($this, 'handler_open'),
515 array($this, 'handler_close'),
516 array($this, 'handler_read'),
517 array($this, 'handler_write'),
518 array($this, 'handler_destroy'),
519 array($this, 'handler_gc'));
520 if (!$result) {
eee3bd3f 521 print_error('dbsessionhandlerproblem', 'error');
0a2092a3 522 }
523 }
524
2b0e3941
PS
525 /**
526 * Open session handler
527 *
528 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
529 *
530 * @param string $save_path
531 * @param string $session_name
532 * @return bool success
533 */
dd9e22f8 534 public function handler_open($save_path, $session_name) {
0a2092a3 535 return true;
536 }
537
2b0e3941
PS
538 /**
539 * Close session handler
540 *
541 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
542 *
543 * @return bool success
544 */
0a2092a3 545 public function handler_close() {
3b50631d 546 if (isset($this->record->id)) {
2b0e3941
PS
547 try {
548 $this->database->release_session_lock($this->record->id);
549 } catch (Exception $ex) {
550 // ignore any problems
551 }
3b50631d 552 }
0a2092a3 553 $this->record = null;
554 return true;
555 }
556
2b0e3941
PS
557 /**
558 * Read session handler
559 *
560 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
561 *
562 * @param string $sid
563 * @return string
564 */
0a2092a3 565 public function handler_read($sid) {
566 global $CFG;
567
0a2092a3 568 if ($this->record and $this->record->sid != $sid) {
1c13ff23 569 error_log('Weird error reading database session - mismatched sid');
2b0e3941 570 $this->failed = true;
0a2092a3 571 return '';
572 }
573
574 try {
2b0e3941 575 if (!$record = $this->database->get_record('sessions', array('sid'=>$sid))) {
927b2e7b 576 $record = new stdClass();
0a2092a3 577 $record->state = 0;
578 $record->sid = $sid;
579 $record->sessdata = null;
0a2092a3 580 $record->userid = 0;
581 $record->timecreated = $record->timemodified = time();
582 $record->firstip = $record->lastip = getremoteaddr();
3b50631d 583 $record->id = $this->database->insert_record_raw('sessions', $record);
0a2092a3 584 }
2b0e3941
PS
585 } catch (Exception $ex) {
586 // do not rethrow exceptions here, we need this to work somehow before 1.9.x upgrade and during install
64d69e96 587 error_log('Can not read or insert database sessions');
2b0e3941 588 $this->failed = true;
0a2092a3 589 return '';
590 }
591
2b0e3941
PS
592 try {
593 $this->database->get_session_lock($record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
594 } catch (Exception $ex) {
595 // This is a fatal error, better inform users.
596 // It should not happen very often - all pages that need long time to execute
597 // should close session soon after access control checks
598 error_log('Can not obtain session lock');
599 $this->failed = true;
600 throw $ex;
601 }
602
3b1a9849 603 // verify timeout
604 if ($record->timemodified + $CFG->sessiontimeout < time()) {
dd9e22f8 605 $ignoretimeout = false;
88fdd846 606 if (!empty($record->userid)) { // skips not logged in
607 if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
65cd3f9c
DM
608
609 // Refresh session if logged as a guest
610 if (isguestuser($user)) {
611 $ignoretimeout = true;
612 } else {
88fdd846 613 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
614 foreach($authsequence as $authname) {
615 $authplugin = get_auth_plugin($authname);
e8656bef 616 if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
88fdd846 617 $ignoretimeout = true;
618 break;
619 }
620 }
621 }
dd9e22f8 622 }
623 }
624 if ($ignoretimeout) {
625 //refresh session
626 $record->timemodified = time();
627 try {
628 $this->database->update_record('sessions', $record);
2b0e3941
PS
629 } catch (Exception $ex) {
630 // very unlikely error
dd9e22f8 631 error_log('Can not refresh database session');
2b0e3941
PS
632 $this->failed = true;
633 throw $ex;
dd9e22f8 634 }
635 } else {
636 //time out session
637 $record->state = 0;
638 $record->sessdata = null;
dd9e22f8 639 $record->userid = 0;
640 $record->timecreated = $record->timemodified = time();
641 $record->firstip = $record->lastip = getremoteaddr();
642 try {
643 $this->database->update_record('sessions', $record);
2b0e3941
PS
644 } catch (Exception $ex) {
645 // very unlikely error
dd9e22f8 646 error_log('Can not time out database session');
2b0e3941
PS
647 $this->failed = true;
648 throw $ex;
dd9e22f8 649 }
eee3bd3f 650 }
eee3bd3f 651 }
652
b9fb7103 653 $data = is_null($record->sessdata) ? '' : base64_decode($record->sessdata);
3b1a9849 654
0a2092a3 655 unset($record->sessdata); // conserve memory
656 $this->record = $record;
657
658 return $data;
659 }
660
2b0e3941
PS
661 /**
662 * Write session handler.
663 *
664 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
665 *
666 * NOTE: Do not write to output or throw any exceptions!
667 * Hopefully the next page is going to display nice error or it recovers...
668 *
669 * @param string $sid
670 * @param string $session_data
671 * @return bool success
672 */
0a2092a3 673 public function handler_write($sid, $session_data) {
674 global $USER;
675
ed149942 676 // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
59e0ce0a 677
2b0e3941
PS
678 if ($this->failed) {
679 // do not write anything back - we failed to start the session properly
680 return false;
681 }
682
b9fb7103 683 $userid = 0;
684 if (!empty($USER->realuser)) {
685 $userid = $USER->realuser;
686 } else if (!empty($USER->id)) {
687 $userid = $USER->id;
688 }
689
690 if (isset($this->record->id)) {
b9fb7103 691 $this->record->sessdata = base64_encode($session_data); // there might be some binary mess :-(
692 $this->record->userid = $userid;
693 $this->record->timemodified = time();
694 $this->record->lastip = getremoteaddr();
0a2092a3 695
3a465d1d 696 // TODO: verify session changed before doing update,
ed149942 697 // also make sure the timemodified field is changed only every 10s if nothing else changes MDL-20462
7f79aaea 698
b9fb7103 699 try {
3b50631d 700 $this->database->update_record_raw('sessions', $this->record);
b9fb7103 701 } catch (dml_exception $ex) {
702 if ($this->database->get_dbfamily() === 'mysql') {
703 try {
704 $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
705 } catch (Exception $ignored) {
b9fb7103 706 }
1c13ff23 707 error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
b9fb7103 708 } else {
1c13ff23 709 error_log('Can not write database session');
b9fb7103 710 }
2b0e3941
PS
711 return false;
712 } catch (Exception $ex) {
713 error_log('Can not write database session');
714 return false;
b9fb7103 715 }
eee3bd3f 716
b9fb7103 717 } else {
2b0e3941 718 // fresh new session
b9fb7103 719 try {
2b0e3941
PS
720 $record = new stdClass();
721 $record->state = 0;
722 $record->sid = $sid;
723 $record->sessdata = base64_encode($session_data); // there might be some binary mess :-(
724 $record->userid = $userid;
725 $record->timecreated = $record->timemodified = time();
726 $record->firstip = $record->lastip = getremoteaddr();
727 $record->id = $this->database->insert_record_raw('sessions', $record);
728 $this->record = $record;
729
730 $this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
731 } catch (Exception $ex) {
732 // this should not happen
733 error_log('Can not write new database session or acquire session lock');
734 $this->failed = true;
735 return false;
3b50631d 736 }
0a2092a3 737 }
17d93489 738
0a2092a3 739 return true;
740 }
741
2b0e3941
PS
742 /**
743 * Destroy session handler
744 *
745 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
746 *
747 * @param string $sid
748 * @return bool success
749 */
0a2092a3 750 public function handler_destroy($sid) {
3b50631d 751 session_kill($sid);
7f79aaea 752
3b50631d 753 if (isset($this->record->id) and $this->record->sid === $sid) {
2b0e3941
PS
754 try {
755 $this->database->release_session_lock($this->record->id);
756 } catch (Exception $ex) {
757 // ignore problems
758 }
3b50631d 759 $this->record = null;
0a2092a3 760 }
761
762 return true;
763 }
764
2b0e3941
PS
765 /**
766 * GC session handler
767 *
768 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
769 *
770 * @param int $ignored_maxlifetime moodle uses special timeout rules
771 * @return bool success
772 */
3b1a9849 773 public function handler_gc($ignored_maxlifetime) {
64d69e96 774 session_gc();
0a2092a3 775 return true;
57f7b7ce 776 }
e8656bef 777}
778
2b0e3941 779
64d69e96 780/**
781 * returns true if legacy session used.
782 * @return bool true if legacy(==file) based session used
783 */
784function session_is_legacy() {
785 global $CFG, $DB;
786 return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
787}
788
e8656bef 789/**
790 * Terminates all sessions, auth hooks are not executed.
ed149942 791 * Useful in upgrade scripts.
e8656bef 792 */
793function session_kill_all() {
794 global $CFG, $DB;
795
64d69e96 796 // always check db table - custom session classes use sessions table
e8656bef 797 try {
e8656bef 798 $DB->delete_records('sessions');
799 } catch (dml_exception $ignored) {
64d69e96 800 // do not show any warnings - might be during upgrade/installation
801 }
802
803 if (session_is_legacy()) {
804 $sessiondir = "$CFG->dataroot/sessions";
805 if (is_dir($sessiondir)) {
806 foreach (glob("$sessiondir/sess_*") as $filename) {
807 @unlink($filename);
808 }
809 }
810 }
811}
812
813/**
814 * Mark session as accessed, prevents timeouts.
815 * @param string $sid
816 */
817function session_touch($sid) {
818 global $CFG, $DB;
819
820 // always check db table - custom session classes use sessions table
821 try {
822 $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
823 $params = array(time(), $sid);
824 $DB->execute($sql, $params);
825 } catch (dml_exception $ignored) {
826 // do not show any warnings - might be during upgrade/installation
e8656bef 827 }
0a2092a3 828
64d69e96 829 if (session_is_legacy()) {
830 $sid = clean_param($sid, PARAM_FILE);
831 $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
832 if (file_exists($sessionfile)) {
833 // if the file is locked it means that it will be updated anyway
834 @touch($sessionfile);
835 }
e8656bef 836 }
837}
838
839/**
840 * Terminates one sessions, auth hooks are not executed.
841 *
842 * @param string $sid session id
843 */
844function session_kill($sid) {
845 global $CFG, $DB;
846
64d69e96 847 // always check db table - custom session classes use sessions table
e8656bef 848 try {
64d69e96 849 $DB->delete_records('sessions', array('sid'=>$sid));
e8656bef 850 } catch (dml_exception $ignored) {
64d69e96 851 // do not show any warnings - might be during upgrade/installation
e8656bef 852 }
853
64d69e96 854 if (session_is_legacy()) {
855 $sid = clean_param($sid, PARAM_FILE);
856 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
857 if (file_exists($sessionfile)) {
858 @unlink($sessionfile);
859 }
e8656bef 860 }
861}
862
863/**
864 * Terminates all sessions of one user, auth hooks are not executed.
865 * NOTE: This can not work for file based sessions!
866 *
867 * @param int $userid user id
868 */
869function session_kill_user($userid) {
870 global $CFG, $DB;
871
64d69e96 872 // always check db table - custom session classes use sessions table
e8656bef 873 try {
64d69e96 874 $DB->delete_records('sessions', array('userid'=>$userid));
e8656bef 875 } catch (dml_exception $ignored) {
64d69e96 876 // do not show any warnings - might be during upgrade/installation
877 }
878
879 if (session_is_legacy()) {
880 // log error?
e8656bef 881 }
882}
883
884/**
885 * Session garbage collection
886 * - verify timeout for all users
887 * - kill sessions of all deleted users
888 * - kill sessions of users with disabled plugins or 'nologin' plugin
889 *
890 * NOTE: this can not work when legacy file sessions used!
891 */
892function session_gc() {
893 global $CFG, $DB;
894
895 $maxlifetime = $CFG->sessiontimeout;
896
e8656bef 897 try {
898 /// kill all sessions of deleted users
899 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
900
901 /// kill sessions of users with disabled plugins
902 $auth_sequence = get_enabled_auth_plugins(true);
903 $auth_sequence = array_flip($auth_sequence);
904 unset($auth_sequence['nologin']); // no login allowed
905 $auth_sequence = array_flip($auth_sequence);
906 $notplugins = null;
907 list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
908 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
909
910 /// now get a list of time-out candidates
911 $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
912 FROM {user} u
913 JOIN {sessions} s ON s.userid = u.id
b3df1764
PS
914 WHERE s.timemodified + ? < ? AND u.id <> ?";
915 $params = array($maxlifetime, time(), $CFG->siteguest);
e8656bef 916
917 $authplugins = array();
918 foreach($auth_sequence as $authname) {
919 $authplugins[$authname] = get_auth_plugin($authname);
920 }
921 $rs = $DB->get_recordset_sql($sql, $params);
922 foreach ($rs as $user) {
923 foreach ($authplugins as $authplugin) {
924 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
925 continue;
926 }
927 }
928 $DB->delete_records('sessions', array('sid'=>$user->sid));
929 }
930 $rs->close();
d0c3f547 931
65cd3f9c 932 // Extending the timeout period for guest sessions as they are renewed.
673a8f77 933 $purgebefore = time() - $maxlifetime;
65cd3f9c
DM
934 $purgebeforeguests = time() - ($maxlifetime * 5);
935
673a8f77 936 // delete expired sessions for guest user account
65cd3f9c 937 $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebeforeguests));
d0c3f547 938 // delete expired sessions for userid = 0 (not logged in)
673a8f77 939 $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore));
e8656bef 940 } catch (dml_exception $ex) {
941 error_log('Error gc-ing sessions');
942 }
0ad6b20c 943}
57f7b7ce 944
0ad6b20c 945/**
946 * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
947 * if one does not already exist, but does not overwrite existing sesskeys. Returns the
948 * sesskey string if $USER exists, or boolean false if not.
949 *
950 * @uses $USER
951 * @return string
952 */
953function sesskey() {
492a55e7
PS
954 // note: do not use $USER because it may not be initialised yet
955 if (empty($_SESSION['USER']->sesskey)) {
956 $_SESSION['USER']->sesskey = random_string(10);
0ad6b20c 957 }
57f7b7ce 958
492a55e7 959 return $_SESSION['USER']->sesskey;
0ad6b20c 960}
57f7b7ce 961
57f7b7ce 962
0ad6b20c 963/**
a79ef03f
TH
964 * Check the sesskey and return true of false for whether it is valid.
965 * (You might like to imagine this function is called sesskey_is_valid().)
0ad6b20c 966 *
a79ef03f
TH
967 * Every script that lets the user perform a significant action (that is,
968 * changes data in the database) should check the sesskey before doing the action.
969 * Depending on your code flow, you may want to use the {@link require_sesskey()}
970 * helper function.
971 *
972 * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
973 * and this function will do required_param('sesskey', ...).
974 * @return bool whether the sesskey sent in the request matches the one stored in the session.
0ad6b20c 975 */
976function confirm_sesskey($sesskey=NULL) {
977 global $USER;
57f7b7ce 978
eb85959b 979 if (!empty($USER->ignoresesskey)) {
0ad6b20c 980 return true;
981 }
57f7b7ce 982
0ad6b20c 983 if (empty($sesskey)) {
984 $sesskey = required_param('sesskey', PARAM_RAW); // Check script parameters
57f7b7ce 985 }
986
eb85959b 987 return (sesskey() === $sesskey);
0ad6b20c 988}
989
a79ef03f
TH
990/**
991 * Check the session key using {@link confirm_sesskey()},
992 * and cause a fatal error if it does not match.
993 */
994function require_sesskey() {
995 if (!confirm_sesskey()) {
996 print_error('invalidsesskey');
997 }
998}
999
0ad6b20c 1000/**
8a8f1c7c 1001 * Sets a moodle cookie with a weakly encrypted username
0ad6b20c 1002 *
8a8f1c7c
PS
1003 * @param string $username to encrypt and place in a cookie, '' means delete current cookie
1004 * @return void
0ad6b20c 1005 */
8a8f1c7c 1006function set_moodle_cookie($username) {
0ad6b20c 1007 global $CFG;
1008
0a2092a3 1009 if (NO_MOODLE_COOKIES) {
1010 return;
1011 }
1012
0342fc36
PS
1013 if (empty($CFG->rememberusername)) {
1014 // erase current and do not store permanent cookies
1015 $username = '';
1016 }
1017
8a8f1c7c
PS
1018 if ($username === 'guest') {
1019 // keep previous cookie in case of guest account login
0ad6b20c 1020 return;
57f7b7ce 1021 }
1022
40fe3647 1023 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
0ad6b20c 1024
8a8f1c7c 1025 // delete old cookie
a91557ae 1026 setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
8a8f1c7c
PS
1027
1028 if ($username !== '') {
1029 // set username cookie for 60 days
1030 setcookie($cookiename, rc4encrypt($username), time()+(DAYSECS*60), $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1031 }
0ad6b20c 1032}
1033
1034/**
8a8f1c7c 1035 * Gets a moodle cookie with a weakly encrypted username
0ad6b20c 1036 *
8a8f1c7c 1037 * @return string username
0ad6b20c 1038 */
1039function get_moodle_cookie() {
1040 global $CFG;
1041
0a2092a3 1042 if (NO_MOODLE_COOKIES) {
1043 return '';
1044 }
1045
0342fc36
PS
1046 if (empty($CFG->rememberusername)) {
1047 return '';
1048 }
1049
40fe3647 1050 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
0ad6b20c 1051
1052 if (empty($_COOKIE[$cookiename])) {
1053 return '';
1054 } else {
8a8f1c7c
PS
1055 $username = rc4decrypt($_COOKIE[$cookiename]);
1056 if ($username === 'guest' or $username === 'nobody') {
1057 // backwards compatibility - we do not set these cookies any more
40fe3647 1058 $username = '';
8a8f1c7c
PS
1059 }
1060 return $username;
57f7b7ce 1061 }
0ad6b20c 1062}
57f7b7ce 1063
e8656bef 1064
b7b64ff2 1065/**
1066 * Setup $USER object - called during login, loginas, etc.
e922fe23
PS
1067 *
1068 * Call sync_user_enrolments() manually after log-in, or log-in-as.
b7b64ff2 1069 *
927b2e7b 1070 * @param stdClass $user full user record object
b7b64ff2 1071 * @return void
1072 */
1073function session_set_user($user) {
5c46aee8 1074 $_SESSION['USER'] = $user;
dd9e22f8 1075 unset($_SESSION['USER']->description); // conserve memory
eb85959b 1076 sesskey(); // init session key
458b3386
PS
1077
1078 if (PHPUNIT_TEST) {
1079 // phpunit tests use reversed reference
1080 global $USER;
1081 $USER = $_SESSION['USER'];
1082 $_SESSION['USER'] =& $USER;
1083 }
b7b64ff2 1084}
1085
542797b4 1086/**
1087 * Is current $USER logged-in-as somebody else?
1088 * @return bool
1089 */
b7b64ff2 1090function session_is_loggedinas() {
85f6b737 1091 return !empty($_SESSION['USER']->realuser);
542797b4 1092}
1093
6132768e 1094/**
1095 * Returns the $USER object ignoring current login-as session
927b2e7b 1096 * @return stdClass user object
6132768e 1097 */
b7b64ff2 1098function session_get_realuser() {
1099 if (session_is_loggedinas()) {
6132768e 1100 return $_SESSION['REALUSER'];
1101 } else {
1102 return $_SESSION['USER'];
1103 }
1104}
1105
542797b4 1106/**
1107 * Login as another user - no security checks here.
1108 * @param int $userid
927b2e7b 1109 * @param stdClass $context
542797b4 1110 * @return void
1111 */
8d1964c4 1112function session_loginas($userid, $context) {
b7b64ff2 1113 if (session_is_loggedinas()) {
8d1964c4 1114 return;
1115 }
1116
85f6b737 1117 // switch to fresh new $SESSION
1118 $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
927b2e7b 1119 $_SESSION['SESSION'] = new stdClass();
8d1964c4 1120
ed149942 1121 /// Create the new $USER object with all details and reload needed capabilities
85f6b737 1122 $_SESSION['REALUSER'] = $_SESSION['USER'];
b7b64ff2 1123 $user = get_complete_user_data('id', $userid);
1124 $user->realuser = $_SESSION['REALUSER']->id;
1125 $user->loginascontext = $context;
e922fe23
PS
1126
1127 // let enrol plugins deal with new enrolments if necessary
1128 enrol_check_plugins($user);
1129 // set up global $USER
b7b64ff2 1130 session_set_user($user);
8d1964c4 1131}
1132
e8b7114d 1133/**
ed149942 1134 * Sets up current user and course environment (lang, etc.) in cron.
e8b7114d 1135 * Do not use outside of cron script!
1136 *
927b2e7b 1137 * @param stdClass $user full user object, null means default cron user (admin)
e8b7114d 1138 * @param $course full course record, null means $SITE
1139 * @return void
1140 */
428540d1 1141function cron_setup_user($user = NULL, $course = NULL) {
c13a5e71 1142 global $CFG, $SITE, $PAGE;
e8b7114d 1143
428540d1
PS
1144 static $cronuser = NULL;
1145 static $cronsession = NULL;
e8b7114d 1146
1147 if (empty($cronuser)) {
ed149942 1148 /// ignore admins timezone, language and locale - use site default instead!
e8b7114d 1149 $cronuser = get_admin();
1150 $cronuser->timezone = $CFG->timezone;
dd9e22f8 1151 $cronuser->lang = '';
1152 $cronuser->theme = '';
1153 unset($cronuser->description);
e8b7114d 1154
927b2e7b 1155 $cronsession = new stdClass();
e8b7114d 1156 }
1157
1158 if (!$user) {
1159 // cached default cron user (==modified admin for now)
1160 session_set_user($cronuser);
1161 $_SESSION['SESSION'] = $cronsession;
1162
1163 } else {
1164 // emulate real user session - needed for caps in cron
1165 if ($_SESSION['USER']->id != $user->id) {
1166 session_set_user($user);
927b2e7b 1167 $_SESSION['SESSION'] = new stdClass();
e8b7114d 1168 }
1169 }
1170
43b152f6 1171 // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
1172 // Temporary hack so that cron does not give fatal errors.
1173 $PAGE = new moodle_page();
e8b7114d 1174 if ($course) {
c13a5e71 1175 $PAGE->set_course($course);
e8b7114d 1176 } else {
c13a5e71 1177 $PAGE->set_course($SITE);
e8b7114d 1178 }
1179
1180 // TODO: it should be possible to improve perf by caching some limited number of users here ;-)
1181
1182}