MDL-35029 core_session Refresh guest session if autologin as guest is enabled
[moodle.git] / lib / sessionlib.php
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.
13 //
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
23  */
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.
31      */
32     define('SESSION_ACQUIRE_LOCK_TIMEOUT', 60*2);
33 }
35 /**
36   * Factory method returning moodle_session object.
37   * @return moodle_session
38   */
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;
47         }
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);
54                 }
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();
65             }
66         } catch (Exception $ex) {
67             // prevent repeated inits
68             $session = new emergency_session();
69             throw $ex;
70         }
71     }
73     return $session;
74 }
77 /**
78  * Moodle session abstraction
79  *
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
84  */
85 interface moodle_session {
86     /**
87      * Terminate current session
88      * @return void
89      */
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
96      */
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.
103      */
104     public function session_exists($sid);
108 /**
109  * Fallback session handler when standard session init fails.
110  * This prevents repeated attempts to init faulty handler.
111  *
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
116  */
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();
124     }
126     /**
127      * Terminate current session
128      * @return void
129      */
130     public function terminate_current() {
131         return;
132     }
134     /**
135      * No more changes in session expected.
136      * Unblocks the sessions, other scripts may start executing in parallel.
137      * @return void
138      */
139     public function write_close() {
140         return;
141     }
143     /**
144      * Check for existing session with id $sid
145      * @param unknown_type $sid
146      * @return boolean true if session found.
147      */
148     public function session_exists($sid) {
149         return false;
150     }
154 /**
155  * Class handling all session and cookies related stuff.
156  *
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
161  */
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;
189                 }
190             }
191             if (!isset($_SESSION['USER'])) {
192                 $_SESSION['USER'] = new stdClass();
193             }
194         }
196         $this->check_user_initialised();
198         $this->check_security();
199     }
201     /**
202      * Terminate current session
203      * @return void
204      */
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
212         }
214         if (NO_MOODLE_COOKIES) {
215             return;
216         }
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;
225         }
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);
233         }
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();
242     }
244     /**
245      * No more changes in session expected.
246      * Unblocks the sessions, other scripts may start executing in parallel.
247      * @return void
248      */
249     public function write_close() {
250         if (NO_MOODLE_COOKIES) {
251             return;
252         }
254         session_write_close();
255     }
257     /**
258      * Initialise $USER object, handles google access
259      * and sets up not logged in user properly.
260      *
261      * @return void
262      */
263     protected function check_user_initialised() {
264         global $CFG;
266         if (isset($_SESSION['USER']->id)) {
267             // already set up $USER
268             return;
269         }
271         $user = null;
273         if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
274             if (is_web_crawler()) {
275                 $user = guest_user();
276             }
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();
283                 }
284             }
285         }
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;
294             }
295         }
296         session_set_user($user);
297     }
299     /**
300      * Does various session security checks
301      * @global void
302      */
303     protected function check_security() {
304         global $CFG;
306         if (NO_MOODLE_COOKIES) {
307             return;
308         }
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;
316             }
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');
322             }
323         }
324     }
326     /**
327      * Prepare cookies and various system settings
328      */
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;
334         }
336         if (!isset($CFG->cookiehttponly)) {
337             $CFG->cookiehttponly = 0;
338         }
340     /// Set sessioncookie and sessioncookiepath variable if it isn't already
341         if (!isset($CFG->sessioncookie)) {
342             $CFG->sessioncookie = '';
343         }
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 = '';
355                     }
356                 } else {
357                     if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
358                         // invalid domain - it must be end part of host
359                         $CFG->sessioncookiedomain = '';
360                     }
361                 }
362             }
363         }
365         // make sure the cookiepath is valid for this wwwroot or autodetect if not specified
366         if (!isset($CFG->sessioncookiepath)) {
367             $CFG->sessioncookiepath = '';
368         }
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;
376                 }
377             }
378         }
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]);
390         }
391     }
393     /**
394      * Init session storage.
395      */
396     protected abstract function init_session_storage();
400 /**
401  * Legacy moodle sessions stored in files, not recommended any more.
402  *
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
407  */
408 class legacy_file_session extends session_stub {
409     /**
410      * Init session storage.
411      */
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);
422         }
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');
434         }
435         ini_set('session.save_path', $CFG->dataroot .'/sessions');
436     }
437     /**
438      * Check for existing session with id $sid
439      * @param unknown_type $sid
440      * @return boolean true if session found.
441      */
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);
448     }
452 /**
453  * Recommended moodle session storage.
454  *
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
459  */
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');
481             }
482         }
483     }
485     /**
486      * Check for existing session with id $sid
487      * @param unknown_type $sid
488      * @return boolean true if session found.
489      */
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;
500         }
501     }
503     /**
504      * Init session storage.
505      */
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');
522         }
523     }
525     /**
526      * Open session handler
527      *
528      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
529      *
530      * @param string $save_path
531      * @param string $session_name
532      * @return bool success
533      */
534     public function handler_open($save_path, $session_name) {
535         return true;
536     }
538     /**
539      * Close session handler
540      *
541      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
542      *
543      * @return bool success
544      */
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
551             }
552         }
553         $this->record = null;
554         return true;
555     }
557     /**
558      * Read session handler
559      *
560      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
561      *
562      * @param string $sid
563      * @return string
564      */
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 '';
572         }
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);
584             }
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 '';
590         }
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;
601         }
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))) {
609                     // Refresh session if logged as a guest
610                     if (isguestuser($user)) {
611                         $ignoretimeout = true;
612                     } else {
613                         $authsequence = get_enabled_auth_plugins(); // auths, in sequence
614                         foreach($authsequence as $authname) {
615                             $authplugin = get_auth_plugin($authname);
616                             if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
617                                 $ignoretimeout = true;
618                                 break;
619                             }
620                         }
621                     }
622                 }
623             }
624             if ($ignoretimeout) {
625                 //refresh session
626                 $record->timemodified = time();
627                 try {
628                     $this->database->update_record('sessions', $record);
629                 } catch (Exception $ex) {
630                     // very unlikely error
631                     error_log('Can not refresh database session');
632                     $this->failed = true;
633                     throw $ex;
634                 }
635             } else {
636                 //time out session
637                 $record->state        = 0;
638                 $record->sessdata     = null;
639                 $record->userid       = 0;
640                 $record->timecreated  = $record->timemodified = time();
641                 $record->firstip      = $record->lastip = getremoteaddr();
642                 try {
643                     $this->database->update_record('sessions', $record);
644                 } catch (Exception $ex) {
645                     // very unlikely error
646                     error_log('Can not time out database session');
647                     $this->failed = true;
648                     throw $ex;
649                 }
650             }
651         }
653         $data = is_null($record->sessdata) ? '' : base64_decode($record->sessdata);
655         unset($record->sessdata); // conserve memory
656         $this->record = $record;
658         return $data;
659     }
661     /**
662      * Write session handler.
663      *
664      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
665      *
666      * NOTE: Do not write to output or throw any exceptions!
667      *       Hopefully the next page is going to display nice error or it recovers...
668      *
669      * @param string $sid
670      * @param string $session_data
671      * @return bool success
672      */
673     public function handler_write($sid, $session_data) {
674         global $USER;
676         // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
678         if ($this->failed) {
679             // do not write anything back - we failed to start the session properly
680             return false;
681         }
683         $userid = 0;
684         if (!empty($USER->realuser)) {
685             $userid = $USER->realuser;
686         } else if (!empty($USER->id)) {
687             $userid = $USER->id;
688         }
690         if (isset($this->record->id)) {
691             $this->record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
692             $this->record->userid       = $userid;
693             $this->record->timemodified = time();
694             $this->record->lastip       = getremoteaddr();
696             // TODO: verify session changed before doing update,
697             //       also make sure the timemodified field is changed only every 10s if nothing else changes  MDL-20462
699             try {
700                 $this->database->update_record_raw('sessions', $this->record);
701             } catch (dml_exception $ex) {
702                 if ($this->database->get_dbfamily() === 'mysql') {
703                     try {
704                         $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
705                     } catch (Exception $ignored) {
706                     }
707                     error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
708                 } else {
709                     error_log('Can not write database session');
710                 }
711                 return false;
712             } catch (Exception $ex) {
713                 error_log('Can not write database session');
714                 return false;
715             }
717         } else {
718             // fresh new session
719             try {
720                 $record = new stdClass();
721                 $record->state        = 0;
722                 $record->sid          = $sid;
723                 $record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
724                 $record->userid       = $userid;
725                 $record->timecreated  = $record->timemodified = time();
726                 $record->firstip      = $record->lastip = getremoteaddr();
727                 $record->id           = $this->database->insert_record_raw('sessions', $record);
728                 $this->record = $record;
730                 $this->database->get_session_lock($this->record->id, SESSION_ACQUIRE_LOCK_TIMEOUT);
731             } catch (Exception $ex) {
732                 // this should not happen
733                 error_log('Can not write new database session or acquire session lock');
734                 $this->failed = true;
735                 return false;
736             }
737         }
739         return true;
740     }
742     /**
743      * Destroy session handler
744      *
745      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
746      *
747      * @param string $sid
748      * @return bool success
749      */
750     public function handler_destroy($sid) {
751         session_kill($sid);
753         if (isset($this->record->id) and $this->record->sid === $sid) {
754             try {
755                 $this->database->release_session_lock($this->record->id);
756             } catch (Exception $ex) {
757                 // ignore problems
758             }
759             $this->record = null;
760         }
762         return true;
763     }
765     /**
766      * GC session handler
767      *
768      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
769      *
770      * @param int $ignored_maxlifetime moodle uses special timeout rules
771      * @return bool success
772      */
773     public function handler_gc($ignored_maxlifetime) {
774         session_gc();
775         return true;
776     }
780 /**
781  * returns true if legacy session used.
782  * @return bool true if legacy(==file) based session used
783  */
784 function session_is_legacy() {
785     global $CFG, $DB;
786     return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
789 /**
790  * Terminates all sessions, auth hooks are not executed.
791  * Useful in upgrade scripts.
792  */
793 function session_kill_all() {
794     global $CFG, $DB;
796     // always check db table - custom session classes use sessions table
797     try {
798         $DB->delete_records('sessions');
799     } catch (dml_exception $ignored) {
800         // do not show any warnings - might be during upgrade/installation
801     }
803     if (session_is_legacy()) {
804         $sessiondir = "$CFG->dataroot/sessions";
805         if (is_dir($sessiondir)) {
806             foreach (glob("$sessiondir/sess_*") as $filename) {
807                 @unlink($filename);
808             }
809         }
810     }
813 /**
814  * Mark session as accessed, prevents timeouts.
815  * @param string $sid
816  */
817 function session_touch($sid) {
818     global $CFG, $DB;
820     // always check db table - custom session classes use sessions table
821     try {
822         $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
823         $params = array(time(), $sid);
824         $DB->execute($sql, $params);
825     } catch (dml_exception $ignored) {
826         // do not show any warnings - might be during upgrade/installation
827     }
829     if (session_is_legacy()) {
830         $sid = clean_param($sid, PARAM_FILE);
831         $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
832         if (file_exists($sessionfile)) {
833             // if the file is locked it means that it will be updated anyway
834             @touch($sessionfile);
835         }
836     }
839 /**
840  * Terminates one sessions, auth hooks are not executed.
841  *
842  * @param string $sid session id
843  */
844 function session_kill($sid) {
845     global $CFG, $DB;
847     // always check db table - custom session classes use sessions table
848     try {
849         $DB->delete_records('sessions', array('sid'=>$sid));
850     } catch (dml_exception $ignored) {
851         // do not show any warnings - might be during upgrade/installation
852     }
854     if (session_is_legacy()) {
855         $sid = clean_param($sid, PARAM_FILE);
856         $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
857         if (file_exists($sessionfile)) {
858             @unlink($sessionfile);
859         }
860     }
863 /**
864  * Terminates all sessions of one user, auth hooks are not executed.
865  * NOTE: This can not work for file based sessions!
866  *
867  * @param int $userid user id
868  */
869 function session_kill_user($userid) {
870     global $CFG, $DB;
872     // always check db table - custom session classes use sessions table
873     try {
874         $DB->delete_records('sessions', array('userid'=>$userid));
875     } catch (dml_exception $ignored) {
876         // do not show any warnings - might be during upgrade/installation
877     }
879     if (session_is_legacy()) {
880         // log error?
881     }
884 /**
885  * Session garbage collection
886  * - verify timeout for all users
887  * - kill sessions of all deleted users
888  * - kill sessions of users with disabled plugins or 'nologin' plugin
889  *
890  * NOTE: this can not work when legacy file sessions used!
891  */
892 function session_gc() {
893     global $CFG, $DB;
895     $maxlifetime = $CFG->sessiontimeout;
897     try {
898         /// kill all sessions of deleted users
899         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
901         /// kill sessions of users with disabled plugins
902         $auth_sequence = get_enabled_auth_plugins(true);
903         $auth_sequence = array_flip($auth_sequence);
904         unset($auth_sequence['nologin']); // no login allowed
905         $auth_sequence = array_flip($auth_sequence);
906         $notplugins = null;
907         list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
908         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
910         /// now get a list of time-out candidates
911         $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
912                   FROM {user} u
913                   JOIN {sessions} s ON s.userid = u.id
914                  WHERE s.timemodified + ? < ? AND u.id <> ?";
915         $params = array($maxlifetime, time(), $CFG->siteguest);
917         $authplugins = array();
918         foreach($auth_sequence as $authname) {
919             $authplugins[$authname] = get_auth_plugin($authname);
920         }
921         $rs = $DB->get_recordset_sql($sql, $params);
922         foreach ($rs as $user) {
923             foreach ($authplugins as $authplugin) {
924                 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
925                     continue;
926                 }
927             }
928             $DB->delete_records('sessions', array('sid'=>$user->sid));
929         }
930         $rs->close();
932         // Extending the timeout period for guest sessions as they are renewed.
933         $purgebefore = time() - $maxlifetime;
934         $purgebeforeguests = time() - ($maxlifetime * 5);
936         // delete expired sessions for guest user account
937         $DB->delete_records_select('sessions', 'userid = ? AND timemodified < ?', array($CFG->siteguest, $purgebeforeguests));
938         // delete expired sessions for userid = 0 (not logged in)
939         $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array($purgebefore));
940     } catch (dml_exception $ex) {
941         error_log('Error gc-ing sessions');
942     }
945 /**
946  * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
947  * if one does not already exist, but does not overwrite existing sesskeys. Returns the
948  * sesskey string if $USER exists, or boolean false if not.
949  *
950  * @uses $USER
951  * @return string
952  */
953 function sesskey() {
954     // note: do not use $USER because it may not be initialised yet
955     if (empty($_SESSION['USER']->sesskey)) {
956         $_SESSION['USER']->sesskey = random_string(10);
957     }
959     return $_SESSION['USER']->sesskey;
963 /**
964  * Check the sesskey and return true of false for whether it is valid.
965  * (You might like to imagine this function is called sesskey_is_valid().)
966  *
967  * Every script that lets the user perform a significant action (that is,
968  * changes data in the database) should check the sesskey before doing the action.
969  * Depending on your code flow, you may want to use the {@link require_sesskey()}
970  * helper function.
971  *
972  * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
973  *      and this function will do required_param('sesskey', ...).
974  * @return bool whether the sesskey sent in the request matches the one stored in the session.
975  */
976 function confirm_sesskey($sesskey=NULL) {
977     global $USER;
979     if (!empty($USER->ignoresesskey)) {
980         return true;
981     }
983     if (empty($sesskey)) {
984         $sesskey = required_param('sesskey', PARAM_RAW);  // Check script parameters
985     }
987     return (sesskey() === $sesskey);
990 /**
991  * Check the session key using {@link confirm_sesskey()},
992  * and cause a fatal error if it does not match.
993  */
994 function require_sesskey() {
995     if (!confirm_sesskey()) {
996         print_error('invalidsesskey');
997     }
1000 /**
1001  * Sets a moodle cookie with a weakly encrypted username
1002  *
1003  * @param string $username to encrypt and place in a cookie, '' means delete current cookie
1004  * @return void
1005  */
1006 function set_moodle_cookie($username) {
1007     global $CFG;
1009     if (NO_MOODLE_COOKIES) {
1010         return;
1011     }
1013     if (empty($CFG->rememberusername)) {
1014         // erase current and do not store permanent cookies
1015         $username = '';
1016     }
1018     if ($username === 'guest') {
1019         // keep previous cookie in case of guest account login
1020         return;
1021     }
1023     $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1025     // delete old cookie
1026     setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1028     if ($username !== '') {
1029         // set username cookie for 60 days
1030         setcookie($cookiename, rc4encrypt($username), time()+(DAYSECS*60), $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
1031     }
1034 /**
1035  * Gets a moodle cookie with a weakly encrypted username
1036  *
1037  * @return string username
1038  */
1039 function get_moodle_cookie() {
1040     global $CFG;
1042     if (NO_MOODLE_COOKIES) {
1043         return '';
1044     }
1046     if (empty($CFG->rememberusername)) {
1047         return '';
1048     }
1050     $cookiename = 'MOODLEID1_'.$CFG->sessioncookie;
1052     if (empty($_COOKIE[$cookiename])) {
1053         return '';
1054     } else {
1055         $username = rc4decrypt($_COOKIE[$cookiename]);
1056         if ($username === 'guest' or $username === 'nobody') {
1057             // backwards compatibility - we do not set these cookies any more
1058             $username = '';
1059         }
1060         return $username;
1061     }
1065 /**
1066  * Setup $USER object - called during login, loginas, etc.
1067  *
1068  * Call sync_user_enrolments() manually after log-in, or log-in-as.
1069  *
1070  * @param stdClass $user full user record object
1071  * @return void
1072  */
1073 function session_set_user($user) {
1074     $_SESSION['USER'] = $user;
1075     unset($_SESSION['USER']->description); // conserve memory
1076     sesskey(); // init session key
1078     if (PHPUNIT_TEST) {
1079         // phpunit tests use reversed reference
1080         global $USER;
1081         $USER = $_SESSION['USER'];
1082         $_SESSION['USER'] =& $USER;
1083     }
1086 /**
1087  * Is current $USER logged-in-as somebody else?
1088  * @return bool
1089  */
1090 function session_is_loggedinas() {
1091     return !empty($_SESSION['USER']->realuser);
1094 /**
1095  * Returns the $USER object ignoring current login-as session
1096  * @return stdClass user object
1097  */
1098 function session_get_realuser() {
1099     if (session_is_loggedinas()) {
1100         return $_SESSION['REALUSER'];
1101     } else {
1102         return $_SESSION['USER'];
1103     }
1106 /**
1107  * Login as another user - no security checks here.
1108  * @param int $userid
1109  * @param stdClass $context
1110  * @return void
1111  */
1112 function session_loginas($userid, $context) {
1113     if (session_is_loggedinas()) {
1114         return;
1115     }
1117     // switch to fresh new $SESSION
1118     $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
1119     $_SESSION['SESSION']     = new stdClass();
1121     /// Create the new $USER object with all details and reload needed capabilities
1122     $_SESSION['REALUSER'] = $_SESSION['USER'];
1123     $user = get_complete_user_data('id', $userid);
1124     $user->realuser       = $_SESSION['REALUSER']->id;
1125     $user->loginascontext = $context;
1127     // let enrol plugins deal with new enrolments if necessary
1128     enrol_check_plugins($user);
1129     // set up global $USER
1130     session_set_user($user);
1133 /**
1134  * Sets up current user and course environment (lang, etc.) in cron.
1135  * Do not use outside of cron script!
1136  *
1137  * @param stdClass $user full user object, null means default cron user (admin)
1138  * @param $course full course record, null means $SITE
1139  * @return void
1140  */
1141 function cron_setup_user($user = NULL, $course = NULL) {
1142     global $CFG, $SITE, $PAGE;
1144     static $cronuser    = NULL;
1145     static $cronsession = NULL;
1147     if (empty($cronuser)) {
1148         /// ignore admins timezone, language and locale - use site default instead!
1149         $cronuser = get_admin();
1150         $cronuser->timezone = $CFG->timezone;
1151         $cronuser->lang     = '';
1152         $cronuser->theme    = '';
1153         unset($cronuser->description);
1155         $cronsession = new stdClass();
1156     }
1158     if (!$user) {
1159         // cached default cron user (==modified admin for now)
1160         session_set_user($cronuser);
1161         $_SESSION['SESSION'] = $cronsession;
1163     } else {
1164         // emulate real user session - needed for caps in cron
1165         if ($_SESSION['USER']->id != $user->id) {
1166             session_set_user($user);
1167             $_SESSION['SESSION'] = new stdClass();
1168         }
1169     }
1171     // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
1172     // Temporary hack so that cron does not give fatal errors.
1173     $PAGE = new moodle_page();
1174     if ($course) {
1175         $PAGE->set_course($course);
1176     } else {
1177         $PAGE->set_course($SITE);
1178     }
1180     // TODO: it should be possible to improve perf by caching some limited number of users here ;-)