Merge branch 'MDL-39790_master' of git://github.com/totara/openbadges into MOODLE_25_...
[moodle.git] / lib / sessionlib.php
blobbe19e443eeec61a15c0d10ffc418c9c4c51370c3
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 (!defined('NO_MOODLE_COOKIES') or empty($DB)) {
46 // Moodle was not initialised properly in lib/setup.php.
47 $session = new emergency_session();
48 return $session;
51 if (empty($CFG->sessiontimeout)) {
52 $CFG->sessiontimeout = 7200;
55 try {
56 if (defined('SESSION_CUSTOM_CLASS')) {
57 // this is a hook for webservices, key based login, etc.
58 if (defined('SESSION_CUSTOM_FILE')) {
59 require_once($CFG->dirroot.SESSION_CUSTOM_FILE);
61 $session_class = SESSION_CUSTOM_CLASS;
62 $session = new $session_class();
64 } else if ((!isset($CFG->dbsessions) or $CFG->dbsessions) and $DB->session_lock_supported()) {
65 // default recommended session type
66 $session = new database_session();
68 } else {
69 // legacy limited file based storage - some features and auth plugins will not work, sorry
70 $session = new legacy_file_session();
72 } catch (Exception $ex) {
73 // prevent repeated inits
74 $session = new emergency_session();
75 throw $ex;
79 return $session;
83 /**
84 * Moodle session abstraction
86 * @package core
87 * @subpackage session
88 * @copyright 2008 Petr Skoda {@link http://skodak.org}
89 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
91 interface moodle_session {
92 /**
93 * Terminate current session
94 * @return void
96 public function terminate_current();
98 /**
99 * No more changes in session expected.
100 * Unblocks the sessions, other scripts may start executing in parallel.
101 * @return void
103 public function write_close();
106 * Check for existing session with id $sid
107 * @param unknown_type $sid
108 * @return boolean true if session found.
110 public function session_exists($sid);
115 * Fallback session handler when standard session init fails.
116 * This prevents repeated attempts to init faulty handler.
118 * @package core
119 * @subpackage session
120 * @copyright 2011 Petr Skoda {@link http://skodak.org}
121 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
123 class emergency_session implements moodle_session {
125 public function __construct() {
126 // session not used at all
127 $_SESSION = array();
128 $_SESSION['SESSION'] = new stdClass();
129 $_SESSION['USER'] = new stdClass();
133 * Terminate current session
134 * @return void
136 public function terminate_current() {
137 return;
141 * No more changes in session expected.
142 * Unblocks the sessions, other scripts may start executing in parallel.
143 * @return void
145 public function write_close() {
146 return;
150 * Check for existing session with id $sid
151 * @param unknown_type $sid
152 * @return boolean true if session found.
154 public function session_exists($sid) {
155 return false;
161 * Class handling all session and cookies related stuff.
163 * @package core
164 * @subpackage session
165 * @copyright 2009 Petr Skoda {@link http://skodak.org}
166 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
168 abstract class session_stub implements moodle_session {
169 protected $justloggedout;
171 public function __construct() {
172 global $CFG;
174 if (NO_MOODLE_COOKIES) {
175 // session not used at all
176 $_SESSION = array();
177 $_SESSION['SESSION'] = new stdClass();
178 $_SESSION['USER'] = new stdClass();
180 } else {
181 $this->prepare_cookies();
182 $this->init_session_storage();
184 $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
186 ini_set('session.use_trans_sid', '0');
188 session_name('MoodleSession'.$CFG->sessioncookie);
189 session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
190 session_start();
191 if (!isset($_SESSION['SESSION'])) {
192 $_SESSION['SESSION'] = new stdClass();
193 if (!$newsession and !$this->justloggedout) {
194 $_SESSION['SESSION']->has_timed_out = true;
197 if (!isset($_SESSION['USER'])) {
198 $_SESSION['USER'] = new stdClass();
202 $this->check_user_initialised();
204 $this->check_security();
208 * Terminate current session
209 * @return void
211 public function terminate_current() {
212 global $CFG, $SESSION, $USER, $DB;
214 try {
215 $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
216 } catch (Exception $ignored) {
217 // probably install/upgrade - ignore this problem
220 if (NO_MOODLE_COOKIES) {
221 return;
224 // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
225 $_SESSION = array();
226 $_SESSION['SESSION'] = new stdClass();
227 $_SESSION['USER'] = new stdClass();
228 $_SESSION['USER']->id = 0;
229 if (isset($CFG->mnet_localhost_id)) {
230 $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
232 $SESSION = $_SESSION['SESSION']; // this may not work properly
233 $USER = $_SESSION['USER']; // this may not work properly
235 $file = null;
236 $line = null;
237 if (headers_sent($file, $line)) {
238 error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
241 // now let's try to get a new session id and delete the old one
242 $this->justloggedout = true;
243 session_regenerate_id(true);
244 $this->justloggedout = false;
246 // write the new session
247 session_write_close();
251 * No more changes in session expected.
252 * Unblocks the sessions, other scripts may start executing in parallel.
253 * @return void
255 public function write_close() {
256 if (NO_MOODLE_COOKIES) {
257 return;
260 session_write_close();
264 * Initialise $USER object, handles google access
265 * and sets up not logged in user properly.
267 * @return void
269 protected function check_user_initialised() {
270 global $CFG;
272 if (isset($_SESSION['USER']->id)) {
273 // already set up $USER
274 return;
277 $user = null;
279 if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
280 if (is_web_crawler()) {
281 $user = guest_user();
283 if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
284 // automaticaly log in users coming from search engine results
285 if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
286 $user = guest_user();
287 } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
288 $user = guest_user();
293 if (!$user) {
294 $user = new stdClass();
295 $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
296 if (isset($CFG->mnet_localhost_id)) {
297 $user->mnethostid = $CFG->mnet_localhost_id;
298 } else {
299 $user->mnethostid = 1;
302 session_set_user($user);
306 * Does various session security checks
307 * @global void
309 protected function check_security() {
310 global $CFG;
312 if (NO_MOODLE_COOKIES) {
313 return;
316 if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
317 /// Make sure current IP matches the one for this session
318 $remoteaddr = getremoteaddr();
320 if (empty($_SESSION['USER']->sessionip)) {
321 $_SESSION['USER']->sessionip = $remoteaddr;
324 if ($_SESSION['USER']->sessionip != $remoteaddr) {
325 // this is a security feature - terminate the session in case of any doubt
326 $this->terminate_current();
327 print_error('sessionipnomatch2', 'error');
333 * Prepare cookies and various system settings
335 protected function prepare_cookies() {
336 global $CFG;
338 if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
339 $CFG->cookiesecure = 0;
342 if (!isset($CFG->cookiehttponly)) {
343 $CFG->cookiehttponly = 0;
346 /// Set sessioncookie and sessioncookiepath variable if it isn't already
347 if (!isset($CFG->sessioncookie)) {
348 $CFG->sessioncookie = '';
351 // make sure cookie domain makes sense for this wwwroot
352 if (!isset($CFG->sessioncookiedomain)) {
353 $CFG->sessioncookiedomain = '';
354 } else if ($CFG->sessioncookiedomain !== '') {
355 $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
356 if ($CFG->sessioncookiedomain !== $host) {
357 if (substr($CFG->sessioncookiedomain, 0, 1) === '.') {
358 if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
359 // invalid domain - it must be end part of host
360 $CFG->sessioncookiedomain = '';
362 } else {
363 if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
364 // invalid domain - it must be end part of host
365 $CFG->sessioncookiedomain = '';
371 // make sure the cookiepath is valid for this wwwroot or autodetect if not specified
372 if (!isset($CFG->sessioncookiepath)) {
373 $CFG->sessioncookiepath = '';
375 if ($CFG->sessioncookiepath !== '/') {
376 $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/';
377 if ($CFG->sessioncookiepath === '') {
378 $CFG->sessioncookiepath = $path;
379 } else {
380 if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') {
381 $CFG->sessioncookiepath = $path;
386 //discard session ID from POST, GET and globals to tighten security,
387 //this is session fixation prevention
388 unset(${'MoodleSession'.$CFG->sessioncookie});
389 unset($_GET['MoodleSession'.$CFG->sessioncookie]);
390 unset($_POST['MoodleSession'.$CFG->sessioncookie]);
391 unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
393 //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
394 if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
395 unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
400 * Init session storage.
402 protected abstract function init_session_storage();
407 * Legacy moodle sessions stored in files, not recommended any more.
409 * @package core
410 * @subpackage session
411 * @copyright 2009 Petr Skoda {@link http://skodak.org}
412 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
414 class legacy_file_session extends session_stub {
416 * Init session storage.
418 protected function init_session_storage() {
419 global $CFG;
421 ini_set('session.save_handler', 'files');
423 // Some distros disable GC by setting probability to 0
424 // overriding the PHP default of 1
425 // (gc_probability is divided by gc_divisor, which defaults to 1000)
426 if (ini_get('session.gc_probability') == 0) {
427 ini_set('session.gc_probability', 1);
430 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
432 // make sure sessions dir exists and is writable, throws exception if not
433 make_upload_directory('sessions');
435 // Need to disable debugging since disk_free_space()
436 // will fail on very large partitions (see MDL-19222)
437 $freespace = @disk_free_space($CFG->dataroot.'/sessions');
438 if (!($freespace > 2048) and $freespace !== false) {
439 print_error('sessiondiskfull', 'error');
441 ini_set('session.save_path', $CFG->dataroot .'/sessions');
444 * Check for existing session with id $sid
445 * @param unknown_type $sid
446 * @return boolean true if session found.
448 public function session_exists($sid){
449 global $CFG;
451 $sid = clean_param($sid, PARAM_FILE);
452 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
453 return file_exists($sessionfile);
459 * Recommended moodle session storage.
461 * @package core
462 * @subpackage session
463 * @copyright 2009 Petr Skoda {@link http://skodak.org}
464 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
466 class database_session extends session_stub {
467 /** @var stdClass $record session record */
468 protected $record = null;
470 /** @var moodle_database $database session database */
471 protected $database = null;
473 /** @var bool $failed session read/init failed, do not write back to DB */
474 protected $failed = false;
476 /** @var string hash of the session data content */
477 protected $lasthash = null;
479 public function __construct() {
480 global $DB;
481 $this->database = $DB;
482 parent::__construct();
484 if (!empty($this->record->state)) {
485 // something is very wrong
486 session_kill($this->record->sid);
488 if ($this->record->state == 9) {
489 print_error('dbsessionmysqlpacketsize', 'error');
495 * Check for existing session with id $sid
496 * @param string $sid
497 * @return boolean true if session found.
499 public function session_exists($sid){
500 global $CFG;
501 try {
502 $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
503 $params = array(time() + $CFG->sessiontimeout, $sid, 0);
505 return $this->database->record_exists_sql($sql, $params);
506 } catch (dml_exception $ex) {
507 error_log('Error checking existance of database session');
508 return false;
513 * Init session storage.
515 protected function init_session_storage() {
516 global $CFG;
518 // gc only from CRON - individual user timeouts now checked during each access
519 ini_set('session.gc_probability', 0);
521 ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
523 $result = session_set_save_handler(array($this, 'handler_open'),
524 array($this, 'handler_close'),
525 array($this, 'handler_read'),
526 array($this, 'handler_write'),
527 array($this, 'handler_destroy'),
528 array($this, 'handler_gc'));
529 if (!$result) {
530 print_error('dbsessionhandlerproblem', 'error');
535 * Open session handler
537 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
539 * @param string $save_path
540 * @param string $session_name
541 * @return bool success
543 public function handler_open($save_path, $session_name) {
544 return true;
548 * Close session handler
550 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
552 * @return bool success
554 public function handler_close() {
555 if (isset($this->record->id)) {
556 try {
557 $this->database->release_session_lock($this->record->id);
558 } catch (Exception $ex) {
559 // ignore any problems
562 $this->record = null;
563 return true;
567 * Read session handler
569 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
571 * @param string $sid
572 * @return string
574 public function handler_read($sid) {
575 global $CFG;
577 if ($this->record and $this->record->sid != $sid) {
578 error_log('Weird error reading database session - mismatched sid');
579 $this->failed = true;
580 return '';
583 try {
584 // Do not fetch full record yet, wait until it is locked.
585 if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id, userid')) {
586 $record = new stdClass();
587 $record->state = 0;
588 $record->sid = $sid;
589 $record->sessdata = null;
590 $record->userid = 0;
591 $record->timecreated = $record->timemodified = time();
592 $record->firstip = $record->lastip = getremoteaddr();
593 $record->id = $this->database->insert_record_raw('sessions', $record);
595 } catch (Exception $ex) {
596 // do not rethrow exceptions here, we need this to work somehow before 1.9.x upgrade and during install
597 error_log('Can not read or insert database sessions');
598 $this->failed = true;
599 return '';
602 try {
603 if (!empty($CFG->sessionlockloggedinonly) and (isguestuser($record->userid) or empty($record->userid))) {
604 // No session locking for guests and not-logged-in users,
605 // these users mostly read stuff, there should not be any major
606 // session race conditions. Hopefully they do not access other
607 // pages while being logged-in.
608 } else {
609 $this->database->get_session_lock($record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
611 } catch (Exception $ex) {
612 // This is a fatal error, better inform users.
613 // It should not happen very often - all pages that need long time to execute
614 // should close session soon after access control checks
615 error_log('Can not obtain session lock');
616 $this->failed = true;
617 throw $ex;
620 // Finally read the full session data because we know we have the lock now.
621 if (!$record = $this->database->get_record('sessions', array('id'=>$record->id))) {
622 error_log('Cannot read session record');
623 $this->failed = true;
624 return '';
627 // verify timeout
628 if ($record->timemodified + $CFG->sessiontimeout < time()) {
629 $ignoretimeout = false;
630 if (!empty($record->userid)) { // skips not logged in
631 if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
633 // Refresh session if logged as a guest
634 if (isguestuser($user)) {
635 $ignoretimeout = true;
636 } else {
637 $authsequence = get_enabled_auth_plugins(); // auths, in sequence
638 foreach($authsequence as $authname) {
639 $authplugin = get_auth_plugin($authname);
640 if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
641 $ignoretimeout = true;
642 break;
648 if ($ignoretimeout) {
649 //refresh session
650 $record->timemodified = time();
651 try {
652 $this->database->update_record('sessions', $record);
653 } catch (Exception $ex) {
654 // very unlikely error
655 error_log('Can not refresh database session');
656 $this->failed = true;
657 throw $ex;
659 } else {
660 //time out session
661 $record->state = 0;
662 $record->sessdata = null;
663 $record->userid = 0;
664 $record->timecreated = $record->timemodified = time();
665 $record->firstip = $record->lastip = getremoteaddr();
666 try {
667 $this->database->update_record('sessions', $record);
668 } catch (Exception $ex) {
669 // very unlikely error
670 error_log('Can not time out database session');
671 $this->failed = true;
672 throw $ex;
677 if (is_null($record->sessdata)) {
678 $data = '';
679 $this->lasthash = sha1('');
680 } else {
681 $data = base64_decode($record->sessdata);
682 $this->lasthash = sha1($record->sessdata);
685 unset($record->sessdata); // conserve memory
686 $this->record = $record;
688 return $data;
692 * Write session handler.
694 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
696 * NOTE: Do not write to output or throw any exceptions!
697 * Hopefully the next page is going to display nice error or it recovers...
699 * @param string $sid
700 * @param string $session_data
701 * @return bool success
703 public function handler_write($sid, $session_data) {
704 global $USER;
706 // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
708 if ($this->failed) {
709 // do not write anything back - we failed to start the session properly
710 return false;
713 $userid = 0;
714 if (!empty($USER->realuser)) {
715 $userid = $USER->realuser;
716 } else if (!empty($USER->id)) {
717 $userid = $USER->id;
720 if (isset($this->record->id)) {
721 $data = base64_encode($session_data); // There might be some binary mess :-(
723 // Skip db update if nothing changed,
724 // do not update the timemodified each second.
725 $hash = sha1($data);
726 if ($this->lasthash === $hash
727 and $this->record->userid == $userid
728 and (time() - $this->record->timemodified < 20)
729 and $this->record->lastip == getremoteaddr()
731 // No need to update anything!
732 return true;
735 $this->record->sessdata = $data;
736 $this->record->userid = $userid;
737 $this->record->timemodified = time();
738 $this->record->lastip = getremoteaddr();
740 try {
741 $this->database->update_record_raw('sessions', $this->record);
742 $this->lasthash = $hash;
743 } catch (dml_exception $ex) {
744 if ($this->database->get_dbfamily() === 'mysql') {
745 try {
746 $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
747 } catch (Exception $ignored) {
749 error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
750 } else {
751 error_log('Can not write database session');
753 return false;
754 } catch (Exception $ex) {
755 error_log('Can not write database session');
756 return false;
759 } else {
760 // fresh new session
761 try {
762 $record = new stdClass();
763 $record->state = 0;
764 $record->sid = $sid;
765 $record->sessdata = base64_encode($session_data); // there might be some binary mess :-(
766 $record->userid = $userid;
767 $record->timecreated = $record->timemodified = time();
768 $record->firstip = $record->lastip = getremoteaddr();
769 $record->id = $this->database->insert_record_raw('sessions', $record);
771 $this->record = $this->database->get_record('sessions', array('id'=>$record->id));
772 $this->lasthash = sha1($record->sessdata);
774 $this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
775 } catch (Exception $ex) {
776 // this should not happen
777 error_log('Can not write new database session or acquire session lock');
778 $this->failed = true;
779 return false;
783 return true;
787 * Destroy session handler
789 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
791 * @param string $sid
792 * @return bool success
794 public function handler_destroy($sid) {
795 session_kill($sid);
797 if (isset($this->record->id) and $this->record->sid === $sid) {
798 try {
799 $this->database->release_session_lock($this->record->id);
800 } catch (Exception $ex) {
801 // ignore problems
803 $this->record = null;
806 $this->lasthash = null;
808 return true;
812 * GC session handler
814 * {@see http://php.net/manual/en/function.session-set-save-handler.php}
816 * @param int $ignored_maxlifetime moodle uses special timeout rules
817 * @return bool success
819 public function handler_gc($ignored_maxlifetime) {
820 session_gc();
821 return true;
827 * returns true if legacy session used.
828 * @return bool true if legacy(==file) based session used
830 function session_is_legacy() {
831 global $CFG, $DB;
832 return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
836 * Terminates all sessions, auth hooks are not executed.
837 * Useful in upgrade scripts.
839 function session_kill_all() {
840 global $CFG, $DB;
842 // always check db table - custom session classes use sessions table
843 try {
844 $DB->delete_records('sessions');
845 } catch (dml_exception $ignored) {
846 // do not show any warnings - might be during upgrade/installation
849 if (session_is_legacy()) {
850 $sessiondir = "$CFG->dataroot/sessions";
851 if (is_dir($sessiondir)) {
852 foreach (glob("$sessiondir/sess_*") as $filename) {
853 @unlink($filename);
860 * Mark session as accessed, prevents timeouts.
861 * @param string $sid
863 function session_touch($sid) {
864 global $CFG, $DB;
866 // always check db table - custom session classes use sessions table
867 try {
868 $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
869 $params = array(time(), $sid);
870 $DB->execute($sql, $params);
871 } catch (dml_exception $ignored) {
872 // do not show any warnings - might be during upgrade/installation
875 if (session_is_legacy()) {
876 $sid = clean_param($sid, PARAM_FILE);
877 $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
878 if (file_exists($sessionfile)) {
879 // if the file is locked it means that it will be updated anyway
880 @touch($sessionfile);
886 * Terminates one sessions, auth hooks are not executed.
888 * @param string $sid session id
890 function session_kill($sid) {
891 global $CFG, $DB;
893 // always check db table - custom session classes use sessions table
894 try {
895 $DB->delete_records('sessions', array('sid'=>$sid));
896 } catch (dml_exception $ignored) {
897 // do not show any warnings - might be during upgrade/installation
900 if (session_is_legacy()) {
901 $sid = clean_param($sid, PARAM_FILE);
902 $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
903 if (file_exists($sessionfile)) {
904 @unlink($sessionfile);
910 * Terminates all sessions of one user, auth hooks are not executed.
911 * NOTE: This can not work for file based sessions!
913 * @param int $userid user id
915 function session_kill_user($userid) {
916 global $CFG, $DB;
918 // always check db table - custom session classes use sessions table
919 try {
920 $DB->delete_records('sessions', array('userid'=>$userid));
921 } catch (dml_exception $ignored) {
922 // do not show any warnings - might be during upgrade/installation
925 if (session_is_legacy()) {
926 // log error?
931 * Session garbage collection
932 * - verify timeout for all users
933 * - kill sessions of all deleted users
934 * - kill sessions of users with disabled plugins or 'nologin' plugin
936 * NOTE: this can not work when legacy file sessions used!
938 function session_gc() {
939 global $CFG, $DB;
941 $maxlifetime = $CFG->sessiontimeout;
943 try {
944 /// kill all sessions of deleted users
945 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
947 /// kill sessions of users with disabled plugins
948 $auth_sequence = get_enabled_auth_plugins(true);
949 $auth_sequence = array_flip($auth_sequence);
950 unset($auth_sequence['nologin']); // no login allowed
951 $auth_sequence = array_flip($auth_sequence);
952 $notplugins = null;
953 list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
954 $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
956 /// now get a list of time-out candidates
957 $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
958 FROM {user} u
959 JOIN {sessions} s ON s.userid = u.id
960 WHERE s.timemodified + ? < ? AND u.id <> ?";
961 $params = array($maxlifetime, time(), $CFG->siteguest);
963 $authplugins = array();
964 foreach($auth_sequence as $authname) {
965 $authplugins[$authname] = get_auth_plugin($authname);
967 $rs = $DB->get_recordset_sql($sql, $params);
968 foreach ($rs as $user) {
969 foreach ($authplugins as $authplugin) {
970 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
971 continue;
974 $DB->delete_records('sessions', array('sid'=>$user->sid));
976 $rs->close();
978 // Extending the timeout period for guest sessions as they are renewed.
979 $purgebefore = time() - $maxlifetime;
980 $purgebeforeguests = time() - ($maxlifetime * 5);
982 // delete expired sessions for guest user account
983 $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebeforeguests));
984 // delete expired sessions for userid = 0 (not logged in)
985 $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore));
986 } catch (dml_exception $ex) {
987 error_log('Error gc-ing sessions');
992 * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
993 * if one does not already exist, but does not overwrite existing sesskeys. Returns the
994 * sesskey string if $USER exists, or boolean false if not.
996 * @uses $USER
997 * @return string
999 function sesskey() {
1000 // note: do not use $USER because it may not be initialised yet
1001 if (empty($_SESSION['USER']->sesskey)) {
1002 if (!isset($_SESSION['USER'])) {
1003 $_SESSION['USER'] = new stdClass;
1005 $_SESSION['USER']->sesskey = random_string(10);
1008 return $_SESSION['USER']->sesskey;
1013 * Check the sesskey and return true of false for whether it is valid.
1014 * (You might like to imagine this function is called sesskey_is_valid().)
1016 * Every script that lets the user perform a significant action (that is,
1017 * changes data in the database) should check the sesskey before doing the action.
1018 * Depending on your code flow, you may want to use the {@link require_sesskey()}
1019 * helper function.
1021 * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
1022 * and this function will do required_param('sesskey', ...).
1023 * @return bool whether the sesskey sent in the request matches the one stored in the session.
1025 function confirm_sesskey($sesskey=NULL) {
1026 global $USER;
1028 if (!empty($USER->ignoresesskey)) {
1029 return true;
1032 if (empty($sesskey)) {
1033 $sesskey = required_param('sesskey', PARAM_RAW); // Check script parameters
1036 return (sesskey() === $sesskey);
1040 * Check the session key using {@link confirm_sesskey()},
1041 * and cause a fatal error if it does not match.
1043 function require_sesskey() {
1044 if (!confirm_sesskey()) {
1045 print_error('invalidsesskey');
1050 * Sets a moodle cookie with a weakly encrypted username
1052 * @param string $username to encrypt and place in a cookie, '' means delete current cookie
1053 * @return void
1055 function set_moodle_cookie($username) {
1056 global $CFG;
1058 if (NO_MOODLE_COOKIES) {
1059 return;
1062 if (empty($CFG->rememberusername)) {
1063 // erase current and do not store permanent cookies
1064 $username = '';
1067 if ($username === 'guest') {
1068 // keep previous cookie in case of guest account login
1069 return;
1072 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1074 // delete old cookie
1075 setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1077 if ($username !== '') {
1078 // set username cookie for 60 days
1079 setcookie($cookiename, rc4encrypt($username), time()+(DAYSECS*60), $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1084 * Gets a moodle cookie with a weakly encrypted username
1086 * @return string username
1088 function get_moodle_cookie() {
1089 global $CFG;
1091 if (NO_MOODLE_COOKIES) {
1092 return '';
1095 if (empty($CFG->rememberusername)) {
1096 return '';
1099 $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1101 if (empty($_COOKIE[$cookiename])) {
1102 return '';
1103 } else {
1104 $username = rc4decrypt($_COOKIE[$cookiename]);
1105 if ($username === 'guest' or $username === 'nobody') {
1106 // backwards compatibility - we do not set these cookies any more
1107 $username = '';
1109 return $username;
1115 * Setup $USER object - called during login, loginas, etc.
1117 * Call sync_user_enrolments() manually after log-in, or log-in-as.
1119 * @param stdClass $user full user record object
1120 * @return void
1122 function session_set_user($user) {
1123 $_SESSION['USER'] = $user;
1124 unset($_SESSION['USER']->description); // conserve memory
1125 if (isset($_SESSION['USER']->lang)) {
1126 // Make sure it is a valid lang pack name.
1127 $_SESSION['USER']->lang = clean_param($_SESSION['USER']->lang, PARAM_LANG);
1129 sesskey(); // init session key
1131 if (PHPUNIT_TEST || defined('BEHAT_TEST')) {
1132 // phpunit tests use reversed reference
1133 global $USER;
1134 $USER = $_SESSION['USER'];
1135 $_SESSION['USER'] =& $USER;
1140 * Is current $USER logged-in-as somebody else?
1141 * @return bool
1143 function session_is_loggedinas() {
1144 return !empty($_SESSION['USER']->realuser);
1148 * Returns the $USER object ignoring current login-as session
1149 * @return stdClass user object
1151 function session_get_realuser() {
1152 if (session_is_loggedinas()) {
1153 return $_SESSION['REALUSER'];
1154 } else {
1155 return $_SESSION['USER'];
1160 * Login as another user - no security checks here.
1161 * @param int $userid
1162 * @param stdClass $context
1163 * @return void
1165 function session_loginas($userid, $context) {
1166 if (session_is_loggedinas()) {
1167 return;
1170 // switch to fresh new $SESSION
1171 $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
1172 $_SESSION['SESSION'] = new stdClass();
1174 /// Create the new $USER object with all details and reload needed capabilities
1175 $_SESSION['REALUSER'] = $_SESSION['USER'];
1176 $user = get_complete_user_data('id', $userid);
1177 $user->realuser = $_SESSION['REALUSER']->id;
1178 $user->loginascontext = $context;
1180 // let enrol plugins deal with new enrolments if necessary
1181 enrol_check_plugins($user);
1182 // set up global $USER
1183 session_set_user($user);
1187 * Sets up current user and course environment (lang, etc.) in cron.
1188 * Do not use outside of cron script!
1190 * @param stdClass $user full user object, null means default cron user (admin)
1191 * @param $course full course record, null means $SITE
1192 * @return void
1194 function cron_setup_user($user = NULL, $course = NULL) {
1195 global $CFG, $SITE, $PAGE;
1197 static $cronuser = NULL;
1198 static $cronsession = NULL;
1200 if (empty($cronuser)) {
1201 /// ignore admins timezone, language and locale - use site default instead!
1202 $cronuser = get_admin();
1203 $cronuser->timezone = $CFG->timezone;
1204 $cronuser->lang = '';
1205 $cronuser->theme = '';
1206 unset($cronuser->description);
1208 $cronsession = new stdClass();
1211 if (!$user) {
1212 // cached default cron user (==modified admin for now)
1213 session_set_user($cronuser);
1214 $_SESSION['SESSION'] = $cronsession;
1216 } else {
1217 // emulate real user session - needed for caps in cron
1218 if ($_SESSION['USER']->id != $user->id) {
1219 session_set_user($user);
1220 $_SESSION['SESSION'] = new stdClass();
1224 // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
1225 // Temporary hack so that cron does not give fatal errors.
1226 $PAGE = new moodle_page();
1227 if ($course) {
1228 $PAGE->set_course($course);
1229 } else {
1230 $PAGE->set_course($SITE);
1233 // TODO: it should be possible to improve perf by caching some limited number of users here ;-)