MDL-37061 always validate lang when setting session user
[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 (!defined('NO_MOODLE_COOKIES')) {
45         // Moodle session was not initialised yet in lib/setup.php.
46         $session = new emergency_session();
47         return $session;
48     }
50     if (is_null($session)) {
51         if (empty($CFG->sessiontimeout)) {
52             $CFG->sessiontimeout = 7200;
53         }
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);
60                 }
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();
71             }
72         } catch (Exception $ex) {
73             // prevent repeated inits
74             $session = new emergency_session();
75             throw $ex;
76         }
77     }
79     return $session;
80 }
83 /**
84  * Moodle session abstraction
85  *
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
90  */
91 interface moodle_session {
92     /**
93      * Terminate current session
94      * @return void
95      */
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
102      */
103     public function write_close();
105     /**
106      * Check for existing session with id $sid
107      * @param unknown_type $sid
108      * @return boolean true if session found.
109      */
110     public function session_exists($sid);
114 /**
115  * Fallback session handler when standard session init fails.
116  * This prevents repeated attempts to init faulty handler.
117  *
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
122  */
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();
130     }
132     /**
133      * Terminate current session
134      * @return void
135      */
136     public function terminate_current() {
137         return;
138     }
140     /**
141      * No more changes in session expected.
142      * Unblocks the sessions, other scripts may start executing in parallel.
143      * @return void
144      */
145     public function write_close() {
146         return;
147     }
149     /**
150      * Check for existing session with id $sid
151      * @param unknown_type $sid
152      * @return boolean true if session found.
153      */
154     public function session_exists($sid) {
155         return false;
156     }
160 /**
161  * Class handling all session and cookies related stuff.
162  *
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
167  */
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;
195                 }
196             }
197             if (!isset($_SESSION['USER'])) {
198                 $_SESSION['USER'] = new stdClass();
199             }
200         }
202         $this->check_user_initialised();
204         $this->check_security();
205     }
207     /**
208      * Terminate current session
209      * @return void
210      */
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
218         }
220         if (NO_MOODLE_COOKIES) {
221             return;
222         }
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;
231         }
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);
239         }
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();
248     }
250     /**
251      * No more changes in session expected.
252      * Unblocks the sessions, other scripts may start executing in parallel.
253      * @return void
254      */
255     public function write_close() {
256         if (NO_MOODLE_COOKIES) {
257             return;
258         }
260         session_write_close();
261     }
263     /**
264      * Initialise $USER object, handles google access
265      * and sets up not logged in user properly.
266      *
267      * @return void
268      */
269     protected function check_user_initialised() {
270         global $CFG;
272         if (isset($_SESSION['USER']->id)) {
273             // already set up $USER
274             return;
275         }
277         $user = null;
279         if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
280             if (is_web_crawler()) {
281                 $user = guest_user();
282             }
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();
289                 }
290             }
291         }
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;
300             }
301         }
302         session_set_user($user);
303     }
305     /**
306      * Does various session security checks
307      * @global void
308      */
309     protected function check_security() {
310         global $CFG;
312         if (NO_MOODLE_COOKIES) {
313             return;
314         }
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;
322             }
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');
328             }
329         }
330     }
332     /**
333      * Prepare cookies and various system settings
334      */
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;
340         }
342         if (!isset($CFG->cookiehttponly)) {
343             $CFG->cookiehttponly = 0;
344         }
346     /// Set sessioncookie and sessioncookiepath variable if it isn't already
347         if (!isset($CFG->sessioncookie)) {
348             $CFG->sessioncookie = '';
349         }
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 = '';
361                     }
362                 } else {
363                     if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
364                         // invalid domain - it must be end part of host
365                         $CFG->sessioncookiedomain = '';
366                     }
367                 }
368             }
369         }
371         // make sure the cookiepath is valid for this wwwroot or autodetect if not specified
372         if (!isset($CFG->sessioncookiepath)) {
373             $CFG->sessioncookiepath = '';
374         }
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;
382                 }
383             }
384         }
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]);
396         }
397     }
399     /**
400      * Init session storage.
401      */
402     protected abstract function init_session_storage();
406 /**
407  * Legacy moodle sessions stored in files, not recommended any more.
408  *
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
413  */
414 class legacy_file_session extends session_stub {
415     /**
416      * Init session storage.
417      */
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);
428         }
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');
440         }
441         ini_set('session.save_path', $CFG->dataroot .'/sessions');
442     }
443     /**
444      * Check for existing session with id $sid
445      * @param unknown_type $sid
446      * @return boolean true if session found.
447      */
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);
454     }
458 /**
459  * Recommended moodle session storage.
460  *
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
465  */
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');
490             }
491         }
492     }
494     /**
495      * Check for existing session with id $sid
496      * @param string $sid
497      * @return boolean true if session found.
498      */
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;
509         }
510     }
512     /**
513      * Init session storage.
514      */
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');
531         }
532     }
534     /**
535      * Open session handler
536      *
537      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
538      *
539      * @param string $save_path
540      * @param string $session_name
541      * @return bool success
542      */
543     public function handler_open($save_path, $session_name) {
544         return true;
545     }
547     /**
548      * Close session handler
549      *
550      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
551      *
552      * @return bool success
553      */
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
560             }
561         }
562         $this->record = null;
563         return true;
564     }
566     /**
567      * Read session handler
568      *
569      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
570      *
571      * @param string $sid
572      * @return string
573      */
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 '';
581         }
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);
594             }
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 '';
600         }
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);
610             }
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;
618         }
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 '';
625         }
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;
643                             }
644                         }
645                     }
646                 }
647             }
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;
658                 }
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;
673                 }
674             }
675         }
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);
683         }
685         unset($record->sessdata); // conserve memory
686         $this->record = $record;
688         return $data;
689     }
691     /**
692      * Write session handler.
693      *
694      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
695      *
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...
698      *
699      * @param string $sid
700      * @param string $session_data
701      * @return bool success
702      */
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;
711         }
713         $userid = 0;
714         if (!empty($USER->realuser)) {
715             $userid = $USER->realuser;
716         } else if (!empty($USER->id)) {
717             $userid = $USER->id;
718         }
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()
730             ) {
731                 // No need to update anything!
732                 return true;
733             }
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) {
748                     }
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');
752                 }
753                 return false;
754             } catch (Exception $ex) {
755                 error_log('Can not write database session');
756                 return false;
757             }
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;
780             }
781         }
783         return true;
784     }
786     /**
787      * Destroy session handler
788      *
789      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
790      *
791      * @param string $sid
792      * @return bool success
793      */
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
802             }
803             $this->record = null;
804         }
806         $this->lasthash = null;
808         return true;
809     }
811     /**
812      * GC session handler
813      *
814      * {@see http://php.net/manual/en/function.session-set-save-handler.php}
815      *
816      * @param int $ignored_maxlifetime moodle uses special timeout rules
817      * @return bool success
818      */
819     public function handler_gc($ignored_maxlifetime) {
820         session_gc();
821         return true;
822     }
826 /**
827  * returns true if legacy session used.
828  * @return bool true if legacy(==file) based session used
829  */
830 function session_is_legacy() {
831     global $CFG, $DB;
832     return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
835 /**
836  * Terminates all sessions, auth hooks are not executed.
837  * Useful in upgrade scripts.
838  */
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
847     }
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);
854             }
855         }
856     }
859 /**
860  * Mark session as accessed, prevents timeouts.
861  * @param string $sid
862  */
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
873     }
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);
881         }
882     }
885 /**
886  * Terminates one sessions, auth hooks are not executed.
887  *
888  * @param string $sid session id
889  */
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
898     }
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);
905         }
906     }
909 /**
910  * Terminates all sessions of one user, auth hooks are not executed.
911  * NOTE: This can not work for file based sessions!
912  *
913  * @param int $userid user id
914  */
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
923     }
925     if (session_is_legacy()) {
926         // log error?
927     }
930 /**
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
935  *
936  * NOTE: this can not work when legacy file sessions used!
937  */
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);
966         }
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;
972                 }
973             }
974             $DB->delete_records('sessions', array('sid'=>$user->sid));
975         }
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');
988     }
991 /**
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.
995  *
996  * @uses $USER
997  * @return string
998  */
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;
1004         }
1005         $_SESSION['USER']->sesskey = random_string(10);
1006     }
1008     return $_SESSION['USER']->sesskey;
1012 /**
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().)
1015  *
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.
1020  *
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.
1024  */
1025 function confirm_sesskey($sesskey=NULL) {
1026     global $USER;
1028     if (!empty($USER->ignoresesskey)) {
1029         return true;
1030     }
1032     if (empty($sesskey)) {
1033         $sesskey = required_param('sesskey', PARAM_RAW);  // Check script parameters
1034     }
1036     return (sesskey() === $sesskey);
1039 /**
1040  * Check the session key using {@link confirm_sesskey()},
1041  * and cause a fatal error if it does not match.
1042  */
1043 function require_sesskey() {
1044     if (!confirm_sesskey()) {
1045         print_error('invalidsesskey');
1046     }
1049 /**
1050  * Sets a moodle cookie with a weakly encrypted username
1051  *
1052  * @param string $username to encrypt and place in a cookie, '' means delete current cookie
1053  * @return void
1054  */
1055 function set_moodle_cookie($username) {
1056     global $CFG;
1058     if (NO_MOODLE_COOKIES) {
1059         return;
1060     }
1062     if (empty($CFG->rememberusername)) {
1063         // erase current and do not store permanent cookies
1064         $username = '';
1065     }
1067     if ($username === 'guest') {
1068         // keep previous cookie in case of guest account login
1069         return;
1070     }
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);
1080     }
1083 /**
1084  * Gets a moodle cookie with a weakly encrypted username
1085  *
1086  * @return string username
1087  */
1088 function get_moodle_cookie() {
1089     global $CFG;
1091     if (NO_MOODLE_COOKIES) {
1092         return '';
1093     }
1095     if (empty($CFG->rememberusername)) {
1096         return '';
1097     }
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 = '';
1108         }
1109         return $username;
1110     }
1114 /**
1115  * Setup $USER object - called during login, loginas, etc.
1116  *
1117  * Call sync_user_enrolments() manually after log-in, or log-in-as.
1118  *
1119  * @param stdClass $user full user record object
1120  * @return void
1121  */
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);
1128     }
1129     sesskey(); // init session key
1131     if (PHPUNIT_TEST) {
1132         // phpunit tests use reversed reference
1133         global $USER;
1134         $USER = $_SESSION['USER'];
1135         $_SESSION['USER'] =& $USER;
1136     }
1139 /**
1140  * Is current $USER logged-in-as somebody else?
1141  * @return bool
1142  */
1143 function session_is_loggedinas() {
1144     return !empty($_SESSION['USER']->realuser);
1147 /**
1148  * Returns the $USER object ignoring current login-as session
1149  * @return stdClass user object
1150  */
1151 function session_get_realuser() {
1152     if (session_is_loggedinas()) {
1153         return $_SESSION['REALUSER'];
1154     } else {
1155         return $_SESSION['USER'];
1156     }
1159 /**
1160  * Login as another user - no security checks here.
1161  * @param int $userid
1162  * @param stdClass $context
1163  * @return void
1164  */
1165 function session_loginas($userid, $context) {
1166     if (session_is_loggedinas()) {
1167         return;
1168     }
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);
1186 /**
1187  * Sets up current user and course environment (lang, etc.) in cron.
1188  * Do not use outside of cron script!
1189  *
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
1193  */
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();
1209     }
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();
1221         }
1222     }
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);
1231     }
1233     // TODO: it should be possible to improve perf by caching some limited number of users here ;-)