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