MDL-32003 protect xmldb class internals
[moodle.git] / lib / sessionlib.php
blobbe15e142d4b7f6e324c80ba571f43d9f37007146
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * @package core
19 * @subpackage session
20 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
21 * @copyright 2008, 2009 Petr Skoda {@link http://skodak.org}
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 if (!defined('SESSION_ACQUIRE_LOCK_TIMEOUT')) {
28 /**
29 * How much time to wait for session lock before displaying error (in seconds),
30 * 2 minutes by default should be a reasonable time before telling users to wait and refresh browser.
32 define('SESSION_ACQUIRE_LOCK_TIMEOUT', 60*2);
35 /**
36 * Factory method returning moodle_session object.
37 * @return moodle_session
39 function session_get_instance() {
40 global $CFG, $DB;
42 static $session = null;
44 if (is_null($session)) {
45 if (empty($CFG->sessiontimeout)) {
46 $CFG->sessiontimeout = 7200;
49 try {
50 if (defined('SESSION_CUSTOM_CLASS')) {
51 // this is a hook for webservices, key based login, etc.
52 if (defined('SESSION_CUSTOM_FILE')) {
53 require_once($CFG->dirroot.SESSION_CUSTOM_FILE);
55 $session_class = SESSION_CUSTOM_CLASS;
56 $session = new $session_class();
58 } else if ((!isset($CFG->dbsessions) or $CFG->dbsessions) and $DB->session_lock_supported()) {
59 // default recommended session type
60 $session = new database_session();
62 } else {
63 // legacy limited file based storage - some features and auth plugins will not work, sorry
64 $session = new legacy_file_session();
66 } catch (Exception $ex) {
67 // prevent repeated inits
68 $session = new emergency_session();
69 throw $ex;
73 return $session;
77 /**
78 * Moodle session abstraction
80 * @package core
81 * @subpackage session
82 * @copyright 2008 Petr Skoda {@link http://skodak.org}
83 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
85 interface moodle_session {
86 /**
87 * Terminate current session
88 * @return void
90 public function terminate_current();
92 /**
93 * No more changes in session expected.
94 * Unblocks the sessions, other scripts may start executing in parallel.
95 * @return void
97 public function write_close();
99 /**
100 * Check for existing session with id $sid
101 * @param unknown_type $sid
102 * @return boolean true if session found.
104 public function session_exists($sid);
109 * Fallback session handler when standard session init fails.
110 * This prevents repeated attempts to init faulty handler.
112 * @package core
113 * @subpackage session
114 * @copyright 2011 Petr Skoda {@link http://skodak.org}
115 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
117 class emergency_session implements moodle_session {
119 public function __construct() {
120 // session not used at all
121 $_SESSION = array();
122 $_SESSION['SESSION'] = new stdClass();
123 $_SESSION['USER'] = new stdClass();
127 * Terminate current session
128 * @return void
130 public function terminate_current() {
131 return;
135 * No more changes in session expected.
136 * Unblocks the sessions, other scripts may start executing in parallel.
137 * @return void
139 public function write_close() {
140 return;
144 * Check for existing session with id $sid
145 * @param unknown_type $sid
146 * @return boolean true if session found.
148 public function session_exists($sid) {
149 return false;
155 * Class handling all session and cookies related stuff.
157 * @package core
158 * @subpackage session
159 * @copyright 2009 Petr Skoda {@link http://skodak.org}
160 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
162 abstract class session_stub implements moodle_session {
163 protected $justloggedout;
165 public function __construct() {
166 global $CFG;
168 if (NO_MOODLE_COOKIES) {
169 // session not used at all
170 $_SESSION = array();
171 $_SESSION['SESSION'] = new stdClass();
172 $_SESSION['USER'] = new stdClass();
174 } else {
175 $this->prepare_cookies();
176 $this->init_session_storage();
178 $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
180 ini_set('session.use_trans_sid', '0');
182 session_name('MoodleSession'.$CFG->sessioncookie);
183 session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
184 session_start();
185 if (!isset($_SESSION['SESSION'])) {
186 $_SESSION['SESSION'] = new stdClass();
187 if (!$newsession and !$this->justloggedout) {
188 $_SESSION['SESSION']->has_timed_out = true;
191 if (!isset($_SESSION['USER'])) {
192 $_SESSION['USER'] = new stdClass();
196 $this->check_user_initialised();
198 $this->check_security();
202 * Terminate current session
203 * @return void
205 public function terminate_current() {
206 global $CFG, $SESSION, $USER, $DB;
208 try {
209 $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
210 } catch (Exception $ignored) {
211 // probably install/upgrade - ignore this problem
214 if (NO_MOODLE_COOKIES) {
215 return;
218 // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
219 $_SESSION = array();
220 $_SESSION['SESSION'] = new stdClass();
221 $_SESSION['USER'] = new stdClass();
222 $_SESSION['USER']->id = 0;
223 if (isset($CFG->mnet_localhost_id)) {
224 $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
226 $SESSION = $_SESSION['SESSION']; // this may not work properly
227 $USER = $_SESSION['USER']; // this may not work properly
229 $file = null;
230 $line = null;
231 if (headers_sent($file, $line)) {
232 error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
235 // now let's try to get a new session id and delete the old one
236 $this->justloggedout = true;
237 session_regenerate_id(true);
238 $this->justloggedout = false;
240 // write the new session
241 session_write_close();
245 * No more changes in session expected.
246 * Unblocks the sessions, other scripts may start executing in parallel.
247 * @return void
249 public function write_close() {
250 if (NO_MOODLE_COOKIES) {
251 return;
254 session_write_close();
258 * Initialise $USER object, handles google access
259 * and sets up not logged in user properly.
261 * @return void
263 protected function check_user_initialised() {
264 global $CFG;
266 if (isset($_SESSION['USER']->id)) {
267 // already set up $USER
268 return;
271 $user = null;
273 if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
274 if (is_web_crawler()) {
275 $user = guest_user();
277 if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
278 // automaticaly log in users coming from search engine results
279 if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
280 $user = guest_user();
281 } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
282 $user = guest_user();
287 if (!$user) {
288 $user = new stdClass();
289 $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
290 if (isset($CFG->mnet_localhost_id)) {
291 $user->mnethostid = $CFG->mnet_localhost_id;
292 } else {
293 $user->mnethostid = 1;
296 session_set_user($user);
300 * Does various session security checks
301 * @global void
303 protected function check_security() {
304 global $CFG;
306 if (NO_MOODLE_COOKIES) {
307 return;
310 if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
311 /// Make sure current IP matches the one for this session
312 $remoteaddr = getremoteaddr();
314 if (empty($_SESSION['USER']->sessionip)) {
315 $_SESSION['USER']->sessionip = $remoteaddr;
318 if ($_SESSION['USER']->sessionip != $remoteaddr) {
319 // this is a security feature - terminate the session in case of any doubt
320 $this->terminate_current();
321 print_error('sessionipnomatch2', 'error');
327 * Prepare cookies and various system settings
329 protected function prepare_cookies() {
330 global $CFG;
332 if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
333 $CFG->cookiesecure = 0;
336 if (!isset($CFG->cookiehttponly)) {
337 $CFG->cookiehttponly = 0;
340 /// Set sessioncookie and sessioncookiepath variable if it isn't already
341 if (!isset($CFG->sessioncookie)) {
342 $CFG->sessioncookie = '';
345 // make sure cookie domain makes sense for this wwwroot
346 if (!isset($CFG->sessioncookiedomain)) {
347 $CFG->sessioncookiedomain = '';
348 } else if ($CFG->sessioncookiedomain !== '') {
349 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
350 if ($CFG->sessioncookiedomain !== $host) {
351 if (substr($CFG->sessioncookiedomain, 0, 1) === '.') {
352 if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
353 // invalid domain - it must be end part of host
354 $CFG->sessioncookiedomain = '';
356 } else {
357 if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
358 // invalid domain - it must be end part of host
359 $CFG->sessioncookiedomain = '';
365 // make sure the cookiepath is valid for this wwwroot or autodetect if not specified
366 if (!isset($CFG->sessioncookiepath)) {
367 $CFG->sessioncookiepath = '';
369 if ($CFG->sessioncookiepath !== '/') {
370 $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/';
371 if ($CFG->sessioncookiepath === '') {
372 $CFG->sessioncookiepath = $path;
373 } else {
374 if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') {
375 $CFG->sessioncookiepath = $path;
380 //discard session ID from POST, GET and globals to tighten security,
381 //this is session fixation prevention
382 unset(${'MoodleSession'.$CFG->sessioncookie});
383 unset($_GET['MoodleSession'.$CFG->sessioncookie]);
384 unset($_POST['MoodleSession'.$CFG->sessioncookie]);
385 unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
387 //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
388 if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
389 unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
394 * Init session storage.
396 protected abstract function init_session_storage();
401 * Legacy moodle sessions stored in files, not recommended any more.
403 * @package core
404 * @subpackage session
405 * @copyright 2009 Petr Skoda {@link http://skodak.org}
406 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
408 class legacy_file_session extends session_stub {
410 * Init session storage.
412 protected function init_session_storage() {
413 global $CFG;
415 ini_set('session.save_handler', 'files');
417 // Some distros disable GC by setting probability to 0
418 // overriding the PHP default of 1
419 // (gc_probability is divided by gc_divisor, which defaults to 1000)
420 if (ini_get('session.gc_probability') == 0) {
421 ini_set('session.gc_probability', 1);
424 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
426 // make sure sessions dir exists and is writable, throws exception if not
427 make_upload_directory('sessions');
429 // Need to disable debugging since disk_free_space()
430 // will fail on very large partitions (see MDL-19222)
431 $freespace = @disk_free_space($CFG->dataroot.'/sessions');
432 if (!($freespace > 2048) and $freespace !== false) {
433 print_error('sessiondiskfull', 'error');
435 ini_set('session.save_path', $CFG->dataroot .'/sessions');
438 * Check for existing session with id $sid
439 * @param unknown_type $sid
440 * @return boolean true if session found.
442 public function session_exists($sid){
443 global $CFG;
445 $sid = clean_param($sid, PARAM_FILE);
446 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
447 return file_exists($sessionfile);
453 * Recommended moodle session storage.
455 * @package core
456 * @subpackage session
457 * @copyright 2009 Petr Skoda {@link http://skodak.org}
458 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
460 class database_session extends session_stub {
461 /** @var stdClass $record session record */
462 protected $record = null;
464 /** @var moodle_database $database session database */
465 protected $database = null;
467 /** @var bool $failed session read/init failed, do not write back to DB */
468 protected $failed = false;
470 public function __construct() {
471 global $DB;
472 $this->database = $DB;
473 parent::__construct();
475 if (!empty($this->record->state)) {
476 // something is very wrong
477 session_kill($this->record->sid);
479 if ($this->record->state == 9) {
480 print_error('dbsessionmysqlpacketsize', 'error');
486 * Check for existing session with id $sid
487 * @param unknown_type $sid
488 * @return boolean true if session found.
490 public function session_exists($sid){
491 global $CFG;
492 try {
493 $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
494 $params = array(time() + $CFG->sessiontimeout, $sid, 0);
496 return $this->database->record_exists_sql($sql, $params);
497 } catch (dml_exception $ex) {
498 error_log('Error checking existance of database session');
499 return false;
504 * Init session storage.
506 protected function init_session_storage() {
507 global $CFG;
509 // gc only from CRON - individual user timeouts now checked during each access
510 ini_set('session.gc_probability', 0);
512 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
514 $result = session_set_save_handler(array($this, 'handler_open'),
515 array($this, 'handler_close'),
516 array($this, 'handler_read'),
517 array($this, 'handler_write'),
518 array($this, 'handler_destroy'),
519 array($this, 'handler_gc'));
520 if (!$result) {
521 print_error('dbsessionhandlerproblem', 'error');
526 * Open session handler
528 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
530 * @param string $save_path
531 * @param string $session_name
532 * @return bool success
534 public function handler_open($save_path, $session_name) {
535 return true;
539 * Close session handler
541 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
543 * @return bool success
545 public function handler_close() {
546 if (isset($this->record->id)) {
547 try {
548 $this->database->release_session_lock($this->record->id);
549 } catch (Exception $ex) {
550 // ignore any problems
553 $this->record = null;
554 return true;
558 * Read session handler
560 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
562 * @param string $sid
563 * @return string
565 public function handler_read($sid) {
566 global $CFG;
568 if ($this->record and $this->record->sid != $sid) {
569 error_log('Weird error reading database session - mismatched sid');
570 $this->failed = true;
571 return '';
574 try {
575 if (!$record = $this->database->get_record('sessions', array('sid'=>$sid))) {
576 $record = new stdClass();
577 $record->state = 0;
578 $record->sid = $sid;
579 $record->sessdata = null;
580 $record->userid = 0;
581 $record->timecreated = $record->timemodified = time();
582 $record->firstip = $record->lastip = getremoteaddr();
583 $record->id = $this->database->insert_record_raw('sessions', $record);
585 } catch (Exception $ex) {
586 // do not rethrow exceptions here, we need this to work somehow before 1.9.x upgrade and during install
587 error_log('Can not read or insert database sessions');
588 $this->failed = true;
589 return '';
592 try {
593 $this->database->get_session_lock($record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
594 } catch (Exception $ex) {
595 // This is a fatal error, better inform users.
596 // It should not happen very often - all pages that need long time to execute
597 // should close session soon after access control checks
598 error_log('Can not obtain session lock');
599 $this->failed = true;
600 throw $ex;
603 // verify timeout
604 if ($record->timemodified + $CFG->sessiontimeout < time()) {
605 $ignoretimeout = false;
606 if (!empty($record->userid)) { // skips not logged in
607 if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
608 if (!isguestuser($user)) {
609 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
610 foreach($authsequence as $authname) {
611 $authplugin = get_auth_plugin($authname);
612 if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
613 $ignoretimeout = true;
614 break;
620 if ($ignoretimeout) {
621 //refresh session
622 $record->timemodified = time();
623 try {
624 $this->database->update_record('sessions', $record);
625 } catch (Exception $ex) {
626 // very unlikely error
627 error_log('Can not refresh database session');
628 $this->failed = true;
629 throw $ex;
631 } else {
632 //time out session
633 $record->state = 0;
634 $record->sessdata = null;
635 $record->userid = 0;
636 $record->timecreated = $record->timemodified = time();
637 $record->firstip = $record->lastip = getremoteaddr();
638 try {
639 $this->database->update_record('sessions', $record);
640 } catch (Exception $ex) {
641 // very unlikely error
642 error_log('Can not time out database session');
643 $this->failed = true;
644 throw $ex;
649 $data = is_null($record->sessdata) ? '' : base64_decode($record->sessdata);
651 unset($record->sessdata); // conserve memory
652 $this->record = $record;
654 return $data;
658 * Write session handler.
660 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
662 * NOTE: Do not write to output or throw any exceptions!
663 * Hopefully the next page is going to display nice error or it recovers...
665 * @param string $sid
666 * @param string $session_data
667 * @return bool success
669 public function handler_write($sid, $session_data) {
670 global $USER;
672 // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
674 if ($this->failed) {
675 // do not write anything back - we failed to start the session properly
676 return false;
679 $userid = 0;
680 if (!empty($USER->realuser)) {
681 $userid = $USER->realuser;
682 } else if (!empty($USER->id)) {
683 $userid = $USER->id;
686 if (isset($this->record->id)) {
687 $record = new stdClass();
688 $record->state = 0;
689 $record->sid = $sid; // might be regenerating sid
690 $this->record->sessdata = base64_encode($session_data); // there might be some binary mess :-(
691 $this->record->userid = $userid;
692 $this->record->timemodified = time();
693 $this->record->lastip = getremoteaddr();
695 // TODO: verify session changed before doing update,
696 // also make sure the timemodified field is changed only every 10s if nothing else changes MDL-20462
698 try {
699 $this->database->update_record_raw('sessions', $this->record);
700 } catch (dml_exception $ex) {
701 if ($this->database->get_dbfamily() === 'mysql') {
702 try {
703 $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
704 } catch (Exception $ignored) {
706 error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
707 } else {
708 error_log('Can not write database session');
710 return false;
711 } catch (Exception $ex) {
712 error_log('Can not write database session');
713 return false;
716 } else {
717 // fresh new session
718 try {
719 $record = new stdClass();
720 $record->state = 0;
721 $record->sid = $sid;
722 $record->sessdata = base64_encode($session_data); // there might be some binary mess :-(
723 $record->userid = $userid;
724 $record->timecreated = $record->timemodified = time();
725 $record->firstip = $record->lastip = getremoteaddr();
726 $record->id = $this->database->insert_record_raw('sessions', $record);
727 $this->record = $record;
729 $this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
730 } catch (Exception $ex) {
731 // this should not happen
732 error_log('Can not write new database session or acquire session lock');
733 $this->failed = true;
734 return false;
738 return true;
742 * Destroy session handler
744 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
746 * @param string $sid
747 * @return bool success
749 public function handler_destroy($sid) {
750 session_kill($sid);
752 if (isset($this->record->id) and $this->record->sid === $sid) {
753 try {
754 $this->database->release_session_lock($this->record->id);
755 } catch (Exception $ex) {
756 // ignore problems
758 $this->record = null;
761 return true;
765 * GC session handler
767 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
769 * @param int $ignored_maxlifetime moodle uses special timeout rules
770 * @return bool success
772 public function handler_gc($ignored_maxlifetime) {
773 session_gc();
774 return true;
780 * returns true if legacy session used.
781 * @return bool true if legacy(==file) based session used
783 function session_is_legacy() {
784 global $CFG, $DB;
785 return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
789 * Terminates all sessions, auth hooks are not executed.
790 * Useful in upgrade scripts.
792 function session_kill_all() {
793 global $CFG, $DB;
795 // always check db table - custom session classes use sessions table
796 try {
797 $DB->delete_records('sessions');
798 } catch (dml_exception $ignored) {
799 // do not show any warnings - might be during upgrade/installation
802 if (session_is_legacy()) {
803 $sessiondir = "$CFG->dataroot/sessions";
804 if (is_dir($sessiondir)) {
805 foreach (glob("$sessiondir/sess_*") as $filename) {
806 @unlink($filename);
813 * Mark session as accessed, prevents timeouts.
814 * @param string $sid
816 function session_touch($sid) {
817 global $CFG, $DB;
819 // always check db table - custom session classes use sessions table
820 try {
821 $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
822 $params = array(time(), $sid);
823 $DB->execute($sql, $params);
824 } catch (dml_exception $ignored) {
825 // do not show any warnings - might be during upgrade/installation
828 if (session_is_legacy()) {
829 $sid = clean_param($sid, PARAM_FILE);
830 $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
831 if (file_exists($sessionfile)) {
832 // if the file is locked it means that it will be updated anyway
833 @touch($sessionfile);
839 * Terminates one sessions, auth hooks are not executed.
841 * @param string $sid session id
843 function session_kill($sid) {
844 global $CFG, $DB;
846 // always check db table - custom session classes use sessions table
847 try {
848 $DB->delete_records('sessions', array('sid'=>$sid));
849 } catch (dml_exception $ignored) {
850 // do not show any warnings - might be during upgrade/installation
853 if (session_is_legacy()) {
854 $sid = clean_param($sid, PARAM_FILE);
855 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
856 if (file_exists($sessionfile)) {
857 @unlink($sessionfile);
863 * Terminates all sessions of one user, auth hooks are not executed.
864 * NOTE: This can not work for file based sessions!
866 * @param int $userid user id
868 function session_kill_user($userid) {
869 global $CFG, $DB;
871 // always check db table - custom session classes use sessions table
872 try {
873 $DB->delete_records('sessions', array('userid'=>$userid));
874 } catch (dml_exception $ignored) {
875 // do not show any warnings - might be during upgrade/installation
878 if (session_is_legacy()) {
879 // log error?
884 * Session garbage collection
885 * - verify timeout for all users
886 * - kill sessions of all deleted users
887 * - kill sessions of users with disabled plugins or 'nologin' plugin
889 * NOTE: this can not work when legacy file sessions used!
891 function session_gc() {
892 global $CFG, $DB;
894 $maxlifetime = $CFG->sessiontimeout;
896 try {
897 /// kill all sessions of deleted users
898 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
900 /// kill sessions of users with disabled plugins
901 $auth_sequence = get_enabled_auth_plugins(true);
902 $auth_sequence = array_flip($auth_sequence);
903 unset($auth_sequence['nologin']); // no login allowed
904 $auth_sequence = array_flip($auth_sequence);
905 $notplugins = null;
906 list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
907 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
909 /// now get a list of time-out candidates
910 $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
911 FROM {user} u
912 JOIN {sessions} s ON s.userid = u.id
913 WHERE s.timemodified + ? < ? AND u.id <> ?";
914 $params = array($maxlifetime, time(), $CFG->siteguest);
916 $authplugins = array();
917 foreach($auth_sequence as $authname) {
918 $authplugins[$authname] = get_auth_plugin($authname);
920 $rs = $DB->get_recordset_sql($sql, $params);
921 foreach ($rs as $user) {
922 foreach ($authplugins as $authplugin) {
923 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
924 continue;
927 $DB->delete_records('sessions', array('sid'=>$user->sid));
929 $rs->close();
931 $purgebefore = time() - $maxlifetime;
932 // delete expired sessions for guest user account
933 $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebefore));
934 // delete expired sessions for userid = 0 (not logged in)
935 $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore));
936 } catch (dml_exception $ex) {
937 error_log('Error gc-ing sessions');
942 * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
943 * if one does not already exist, but does not overwrite existing sesskeys. Returns the
944 * sesskey string if $USER exists, or boolean false if not.
946 * @uses $USER
947 * @return string
949 function sesskey() {
950 // note: do not use $USER because it may not be initialised yet
951 if (empty($_SESSION['USER']->sesskey)) {
952 $_SESSION['USER']->sesskey = random_string(10);
955 return $_SESSION['USER']->sesskey;
960 * Check the sesskey and return true of false for whether it is valid.
961 * (You might like to imagine this function is called sesskey_is_valid().)
963 * Every script that lets the user perform a significant action (that is,
964 * changes data in the database) should check the sesskey before doing the action.
965 * Depending on your code flow, you may want to use the {@link require_sesskey()}
966 * helper function.
968 * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
969 * and this function will do required_param('sesskey', ...).
970 * @return bool whether the sesskey sent in the request matches the one stored in the session.
972 function confirm_sesskey($sesskey=NULL) {
973 global $USER;
975 if (!empty($USER->ignoresesskey)) {
976 return true;
979 if (empty($sesskey)) {
980 $sesskey = required_param('sesskey', PARAM_RAW); // Check script parameters
983 return (sesskey() === $sesskey);
987 * Check the session key using {@link confirm_sesskey()},
988 * and cause a fatal error if it does not match.
990 function require_sesskey() {
991 if (!confirm_sesskey()) {
992 print_error('invalidsesskey');
997 * Sets a moodle cookie with a weakly encrypted username
999 * @param string $username to encrypt and place in a cookie, '' means delete current cookie
1000 * @return void
1002 function set_moodle_cookie($username) {
1003 global $CFG;
1005 if (NO_MOODLE_COOKIES) {
1006 return;
1009 if (empty($CFG->rememberusername)) {
1010 // erase current and do not store permanent cookies
1011 $username = '';
1014 if ($username === 'guest') {
1015 // keep previous cookie in case of guest account login
1016 return;
1019 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1021 // delete old cookie
1022 setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1024 if ($username !== '') {
1025 // set username cookie for 60 days
1026 setcookie($cookiename, rc4encrypt($username), time()+(DAYSECS*60), $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1031 * Gets a moodle cookie with a weakly encrypted username
1033 * @return string username
1035 function get_moodle_cookie() {
1036 global $CFG;
1038 if (NO_MOODLE_COOKIES) {
1039 return '';
1042 if (empty($CFG->rememberusername)) {
1043 return '';
1046 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1048 if (empty($_COOKIE[$cookiename])) {
1049 return '';
1050 } else {
1051 $username = rc4decrypt($_COOKIE[$cookiename]);
1052 if ($username === 'guest' or $username === 'nobody') {
1053 // backwards compatibility - we do not set these cookies any more
1054 $username = '';
1056 return $username;
1062 * Setup $USER object - called during login, loginas, etc.
1064 * Call sync_user_enrolments() manually after log-in, or log-in-as.
1066 * @param stdClass $user full user record object
1067 * @return void
1069 function session_set_user($user) {
1070 $_SESSION['USER'] = $user;
1071 unset($_SESSION['USER']->description); // conserve memory
1072 sesskey(); // init session key
1074 if (PHPUNIT_TEST) {
1075 // phpunit tests use reversed reference
1076 global $USER;
1077 $USER = $_SESSION['USER'];
1078 $_SESSION['USER'] =& $USER;
1083 * Is current $USER logged-in-as somebody else?
1084 * @return bool
1086 function session_is_loggedinas() {
1087 return !empty($_SESSION['USER']->realuser);
1091 * Returns the $USER object ignoring current login-as session
1092 * @return stdClass user object
1094 function session_get_realuser() {
1095 if (session_is_loggedinas()) {
1096 return $_SESSION['REALUSER'];
1097 } else {
1098 return $_SESSION['USER'];
1103 * Login as another user - no security checks here.
1104 * @param int $userid
1105 * @param stdClass $context
1106 * @return void
1108 function session_loginas($userid, $context) {
1109 if (session_is_loggedinas()) {
1110 return;
1113 // switch to fresh new $SESSION
1114 $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
1115 $_SESSION['SESSION'] = new stdClass();
1117 /// Create the new $USER object with all details and reload needed capabilities
1118 $_SESSION['REALUSER'] = $_SESSION['USER'];
1119 $user = get_complete_user_data('id', $userid);
1120 $user->realuser = $_SESSION['REALUSER']->id;
1121 $user->loginascontext = $context;
1123 // let enrol plugins deal with new enrolments if necessary
1124 enrol_check_plugins($user);
1125 // set up global $USER
1126 session_set_user($user);
1130 * Sets up current user and course environment (lang, etc.) in cron.
1131 * Do not use outside of cron script!
1133 * @param stdClass $user full user object, null means default cron user (admin)
1134 * @param $course full course record, null means $SITE
1135 * @return void
1137 function cron_setup_user($user = NULL, $course = NULL) {
1138 global $CFG, $SITE, $PAGE;
1140 static $cronuser = NULL;
1141 static $cronsession = NULL;
1143 if (empty($cronuser)) {
1144 /// ignore admins timezone, language and locale - use site default instead!
1145 $cronuser = get_admin();
1146 $cronuser->timezone = $CFG->timezone;
1147 $cronuser->lang = '';
1148 $cronuser->theme = '';
1149 unset($cronuser->description);
1151 $cronsession = new stdClass();
1154 if (!$user) {
1155 // cached default cron user (==modified admin for now)
1156 session_set_user($cronuser);
1157 $_SESSION['SESSION'] = $cronsession;
1159 } else {
1160 // emulate real user session - needed for caps in cron
1161 if ($_SESSION['USER']->id != $user->id) {
1162 session_set_user($user);
1163 $_SESSION['SESSION'] = new stdClass();
1167 // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
1168 // Temporary hack so that cron does not give fatal errors.
1169 $PAGE = new moodle_page();
1170 if ($course) {
1171 $PAGE->set_course($course);
1172 } else {
1173 $PAGE->set_course($SITE);
1176 // TODO: it should be possible to improve perf by caching some limited number of users here ;-)