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