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