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