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