MDL-25907 purge sessions of not-logged-in users
[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             $CFG->usesid = 0;
111             $_SESSION = array();
112             $_SESSION['SESSION'] = new stdClass();
113             $_SESSION['USER']    = new stdClass();
115         } else {
116             $this->prepare_cookies();
117             $this->init_session_storage();
119             $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
121             if (!empty($CFG->usesid) && $newsession) {
122                 sid_start_ob();
123             } else {
124                 $CFG->usesid = 0;
125                 ini_set('session.use_trans_sid', '0');
126             }
128             session_name('MoodleSession'.$CFG->sessioncookie);
129             session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
130             session_start();
131             if (!isset($_SESSION['SESSION'])) {
132                 $_SESSION['SESSION'] = new stdClass();
133                 if (!$newsession and !$this->justloggedout) {
134                     $_SESSION['SESSION']->has_timed_out = true;
135                 }
136             }
137             if (!isset($_SESSION['USER'])) {
138                 $_SESSION['USER'] = new stdClass();
139             }
140         }
142         $this->check_user_initialised();
144         $this->check_security();
145     }
147     /**
148      * Terminates active moodle session
149      */
150     public function terminate_current() {
151         global $CFG, $SESSION, $USER, $DB;
153         try {
154             $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
155         } catch (Exception $ignored) {
156             // probably install/upgrade - ignore this problem
157         }
159         if (NO_MOODLE_COOKIES) {
160             return;
161         }
163         // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
164         $_SESSION = array();
165         $_SESSION['SESSION'] = new stdClass();
166         $_SESSION['USER']    = new stdClass();
167         $_SESSION['USER']->id = 0;
168         if (isset($CFG->mnet_localhost_id)) {
169             $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
170         }
171         $SESSION = $_SESSION['SESSION']; // this may not work properly
172         $USER    = $_SESSION['USER'];    // this may not work properly
174         $file = null;
175         $line = null;
176         if (headers_sent($file, $line)) {
177             error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
178         }
180         // now let's try to get a new session id and delete the old one
181         $this->justloggedout = true;
182         session_regenerate_id(true);
183         $this->justloggedout = false;
185         // write the new session
186         session_write_close();
187     }
189     /**
190      * No more changes in session expected.
191      * Unblocks the sessions, other scripts may start executing in parallel.
192      * @return void
193      */
194     public function write_close() {
195         if (NO_MOODLE_COOKIES) {
196             return;
197         }
199         session_write_close();
200     }
202     /**
203      * Initialise $USER object, handles google access
204      * and sets up not logged in user properly.
205      *
206      * @return void
207      */
208     protected function check_user_initialised() {
209         global $CFG;
211         if (isset($_SESSION['USER']->id)) {
212             // already set up $USER
213             return;
214         }
216         $user = null;
218         if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
219             if (!empty($_SERVER['HTTP_USER_AGENT'])) {
220                 // allow web spiders in as guest users
221                 if (strpos($_SERVER['HTTP_USER_AGENT'], 'Googlebot') !== false ) {
222                     $user = guest_user();
223                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'google.com') !== false ) { // Google
224                     $user = guest_user();
225                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'Yahoo! Slurp') !== false ) {  // Yahoo
226                     $user = guest_user();
227                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], '[ZSEBOT]') !== false ) {  // Zoomspider
228                     $user = guest_user();
229                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSNBOT') !== false ) {  // MSN Search
230                     $user = guest_user();
231                 }
232             }
233             if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
234                 // automaticaly log in users coming from search engine results
235                 if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
236                     $user = guest_user();
237                 } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
238                     $user = guest_user();
239                 }
240             }
241         }
243         if (!$user) {
244             $user = new stdClass();
245             $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
246             if (isset($CFG->mnet_localhost_id)) {
247                 $user->mnethostid = $CFG->mnet_localhost_id;
248             } else {
249                 $user->mnethostid = 1;
250             }
251         }
252         session_set_user($user);
253     }
255     /**
256      * Does various session security checks
257      * @global void
258      */
259     protected function check_security() {
260         global $CFG;
262         if (NO_MOODLE_COOKIES) {
263             return;
264         }
266         if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
267             /// Make sure current IP matches the one for this session
268             $remoteaddr = getremoteaddr();
270             if (empty($_SESSION['USER']->sessionip)) {
271                 $_SESSION['USER']->sessionip = $remoteaddr;
272             }
274             if ($_SESSION['USER']->sessionip != $remoteaddr) {
275                 // this is a security feature - terminate the session in case of any doubt
276                 $this->terminate_current();
277                 print_error('sessionipnomatch2', 'error');
278             }
279         }
280     }
282     /**
283      * Prepare cookies and various system settings
284      */
285     protected function prepare_cookies() {
286         global $CFG;
288         if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
289             $CFG->cookiesecure = 0;
290         }
292         if (!isset($CFG->cookiehttponly)) {
293             $CFG->cookiehttponly = 0;
294         }
296     /// Set sessioncookie and sessioncookiepath variable if it isn't already
297         if (!isset($CFG->sessioncookie)) {
298             $CFG->sessioncookie = '';
299         }
300         if (!isset($CFG->sessioncookiedomain)) {
301             $CFG->sessioncookiedomain = '';
302         }
303         if (!isset($CFG->sessioncookiepath)) {
304             $CFG->sessioncookiepath = '/';
305         }
307         //discard session ID from POST, GET and globals to tighten security,
308         //this session fixation prevention can not be used in cookieless mode
309         if (empty($CFG->usesid)) {
310             unset(${'MoodleSession'.$CFG->sessioncookie});
311             unset($_GET['MoodleSession'.$CFG->sessioncookie]);
312             unset($_POST['MoodleSession'.$CFG->sessioncookie]);
313             unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
314         }
315         //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
316         if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
317             unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
318         }
319     }
321     /**
322      * Inits session storage.
323      */
324     protected abstract function init_session_storage();
327 /**
328  * Legacy moodle sessions stored in files, not recommended any more.
329  *
330  * @package    core
331  * @subpackage session
332  * @copyright  2009 Petr Skoda  {@link http://skodak.org}
333  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
334  */
335 class legacy_file_session extends session_stub {
336     protected function init_session_storage() {
337         global $CFG;
339         ini_set('session.save_handler', 'files');
341         // Some distros disable GC by setting probability to 0
342         // overriding the PHP default of 1
343         // (gc_probability is divided by gc_divisor, which defaults to 1000)
344         if (ini_get('session.gc_probability') == 0) {
345             ini_set('session.gc_probability', 1);
346         }
348         ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
350         // make sure sessions dir exists and is writable, throws exception if not
351         make_upload_directory('sessions');
353         // Need to disable debugging since disk_free_space()
354         // will fail on very large partitions (see MDL-19222)
355         $freespace = @disk_free_space($CFG->dataroot.'/sessions');
356         if (!($freespace > 2048) and $freespace !== false) {
357             print_error('sessiondiskfull', 'error');
358         }
359         ini_set('session.save_path', $CFG->dataroot .'/sessions');
360     }
361     /**
362      * Check for existing session with id $sid
363      * @param unknown_type $sid
364      * @return boolean true if session found.
365      */
366     public function session_exists($sid){
367         global $CFG;
369         $sid = clean_param($sid, PARAM_FILE);
370         $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
371         return file_exists($sessionfile);
372     }
375 /**
376  * Recommended moodle session storage.
377  *
378  * @package    core
379  * @subpackage session
380  * @copyright  2009 Petr Skoda  {@link http://skodak.org}
381  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
382  */
383 class database_session extends session_stub {
384     protected $record   = null;
385     protected $database = null;
387     public function __construct() {
388         global $DB;
389         $this->database = $DB;
390         parent::__construct();
392         if (!empty($this->record->state)) {
393             // something is very wrong
394             session_kill($this->record->sid);
396             if ($this->record->state == 9) {
397                 print_error('dbsessionmysqlpacketsize', 'error');
398             }
399         }
400     }
402     public function session_exists($sid){
403         global $CFG;
404         try {
405             $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
406             $params = array(time() + $CFG->sessiontimeout, $sid, 0);
408             return $this->database->record_exists_sql($sql, $params);
409         } catch (dml_exception $ex) {
410             error_log('Error checking existance of database session');
411             return false;
412         }
413     }
415     protected function init_session_storage() {
416         global $CFG;
418         // gc only from CRON - individual user timeouts now checked during each access
419         ini_set('session.gc_probability', 0);
421         ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
423         $result = session_set_save_handler(array($this, 'handler_open'),
424                                            array($this, 'handler_close'),
425                                            array($this, 'handler_read'),
426                                            array($this, 'handler_write'),
427                                            array($this, 'handler_destroy'),
428                                            array($this, 'handler_gc'));
429         if (!$result) {
430             print_error('dbsessionhandlerproblem', 'error');
431         }
432     }
434     public function handler_open($save_path, $session_name) {
435         return true;
436     }
438     public function handler_close() {
439         if (isset($this->record->id)) {
440             $this->database->release_session_lock($this->record->id);
441         }
442         $this->record = null;
443         return true;
444     }
446     public function handler_read($sid) {
447         global $CFG;
449         if ($this->record and $this->record->sid != $sid) {
450             error_log('Weird error reading database session - mismatched sid');
451             return '';
452         }
454         try {
455             if ($record = $this->database->get_record('sessions', array('sid'=>$sid))) {
456                 $this->database->get_session_lock($record->id);
458             } else {
459                 $record = new stdClass();
460                 $record->state        = 0;
461                 $record->sid          = $sid;
462                 $record->sessdata     = null;
463                 $record->userid       = 0;
464                 $record->timecreated  = $record->timemodified = time();
465                 $record->firstip      = $record->lastip = getremoteaddr();
466                 $record->id           = $this->database->insert_record_raw('sessions', $record);
468                 $this->database->get_session_lock($record->id);
469             }
470         } catch (dml_exception $ex) {
471             error_log('Can not read or insert database sessions');
472             return '';
473         }
475         // verify timeout
476         if ($record->timemodified + $CFG->sessiontimeout < time()) {
477             $ignoretimeout = false;
478             if (!empty($record->userid)) { // skips not logged in
479                 if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
480                     if (!isguestuser($user)) {
481                         $authsequence = get_enabled_auth_plugins(); // auths, in sequence
482                         foreach($authsequence as $authname) {
483                             $authplugin = get_auth_plugin($authname);
484                             if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
485                                 $ignoretimeout = true;
486                                 break;
487                             }
488                         }
489                     }
490                 }
491             }
492             if ($ignoretimeout) {
493                 //refresh session
494                 $record->timemodified = time();
495                 try {
496                     $this->database->update_record('sessions', $record);
497                 } catch (dml_exception $ex) {
498                     error_log('Can not refresh database session');
499                     return '';
500                 }
501             } else {
502                 //time out session
503                 $record->state        = 0;
504                 $record->sessdata     = null;
505                 $record->userid       = 0;
506                 $record->timecreated  = $record->timemodified = time();
507                 $record->firstip      = $record->lastip = getremoteaddr();
508                 try {
509                     $this->database->update_record('sessions', $record);
510                 } catch (dml_exception $ex) {
511                     error_log('Can not time out database session');
512                     return '';
513                 }
514             }
515         }
517         $data = is_null($record->sessdata) ? '' : base64_decode($record->sessdata);
519         unset($record->sessdata); // conserve memory
520         $this->record = $record;
522         return $data;
523     }
525     public function handler_write($sid, $session_data) {
526         global $USER;
528         // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
530         $userid = 0;
531         if (!empty($USER->realuser)) {
532             $userid = $USER->realuser;
533         } else if (!empty($USER->id)) {
534             $userid = $USER->id;
535         }
537         if (isset($this->record->id)) {
538             $record = new stdClass();
539             $record->state              = 0;
540             $record->sid                = $sid;                         // might be regenerating sid
541             $this->record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
542             $this->record->userid       = $userid;
543             $this->record->timemodified = time();
544             $this->record->lastip       = getremoteaddr();
546             // TODO: verify session changed before doing update,
547             //       also make sure the timemodified field is changed only every 10s if nothing else changes  MDL-20462
549             try {
550                 $this->database->update_record_raw('sessions', $this->record);
551             } catch (dml_exception $ex) {
552                 if ($this->database->get_dbfamily() === 'mysql') {
553                     try {
554                         $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
555                     } catch (Exception $ignored) {
557                     }
558                     error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
559                 } else {
560                     error_log('Can not write database session');
561                 }
562             }
564         } else {
565             // session already destroyed
566             $record = new stdClass();
567             $record->state        = 0;
568             $record->sid          = $sid;
569             $record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
570             $record->userid       = $userid;
571             $record->timecreated  = $record->timemodified = time();
572             $record->firstip      = $record->lastip = getremoteaddr();
573             $record->id           = $this->database->insert_record_raw('sessions', $record);
574             $this->record = $record;
576             try {
577                 $this->database->get_session_lock($this->record->id);
578             } catch (dml_exception $ex) {
579                 error_log('Can not write new database session');
580             }
581         }
583         return true;
584     }
586     public function handler_destroy($sid) {
587         session_kill($sid);
589         if (isset($this->record->id) and $this->record->sid === $sid) {
590             $this->database->release_session_lock($this->record->id);
591             $this->record = null;
592         }
594         return true;
595     }
597     public function handler_gc($ignored_maxlifetime) {
598         session_gc();
599         return true;
600     }
603 /**
604  * returns true if legacy session used.
605  * @return bool true if legacy(==file) based session used
606  */
607 function session_is_legacy() {
608     global $CFG, $DB;
609     return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
612 /**
613  * Terminates all sessions, auth hooks are not executed.
614  * Useful in upgrade scripts.
615  */
616 function session_kill_all() {
617     global $CFG, $DB;
619     // always check db table - custom session classes use sessions table
620     try {
621         $DB->delete_records('sessions');
622     } catch (dml_exception $ignored) {
623         // do not show any warnings - might be during upgrade/installation
624     }
626     if (session_is_legacy()) {
627         $sessiondir = "$CFG->dataroot/sessions";
628         if (is_dir($sessiondir)) {
629             foreach (glob("$sessiondir/sess_*") as $filename) {
630                 @unlink($filename);
631             }
632         }
633     }
636 /**
637  * Mark session as accessed, prevents timeouts.
638  * @param string $sid
639  */
640 function session_touch($sid) {
641     global $CFG, $DB;
643     // always check db table - custom session classes use sessions table
644     try {
645         $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
646         $params = array(time(), $sid);
647         $DB->execute($sql, $params);
648     } catch (dml_exception $ignored) {
649         // do not show any warnings - might be during upgrade/installation
650     }
652     if (session_is_legacy()) {
653         $sid = clean_param($sid, PARAM_FILE);
654         $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
655         if (file_exists($sessionfile)) {
656             // if the file is locked it means that it will be updated anyway
657             @touch($sessionfile);
658         }
659     }
662 /**
663  * Terminates one sessions, auth hooks are not executed.
664  *
665  * @param string $sid session id
666  */
667 function session_kill($sid) {
668     global $CFG, $DB;
670     // always check db table - custom session classes use sessions table
671     try {
672         $DB->delete_records('sessions', array('sid'=>$sid));
673     } catch (dml_exception $ignored) {
674         // do not show any warnings - might be during upgrade/installation
675     }
677     if (session_is_legacy()) {
678         $sid = clean_param($sid, PARAM_FILE);
679         $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
680         if (file_exists($sessionfile)) {
681             @unlink($sessionfile);
682         }
683     }
686 /**
687  * Terminates all sessions of one user, auth hooks are not executed.
688  * NOTE: This can not work for file based sessions!
689  *
690  * @param int $userid user id
691  */
692 function session_kill_user($userid) {
693     global $CFG, $DB;
695     // always check db table - custom session classes use sessions table
696     try {
697         $DB->delete_records('sessions', array('userid'=>$userid));
698     } catch (dml_exception $ignored) {
699         // do not show any warnings - might be during upgrade/installation
700     }
702     if (session_is_legacy()) {
703         // log error?
704     }
707 /**
708  * Session garbage collection
709  * - verify timeout for all users
710  * - kill sessions of all deleted users
711  * - kill sessions of users with disabled plugins or 'nologin' plugin
712  *
713  * NOTE: this can not work when legacy file sessions used!
714  */
715 function session_gc() {
716     global $CFG, $DB;
718     $maxlifetime = $CFG->sessiontimeout;
720     try {
721         /// kill all sessions of deleted users
722         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
724         /// kill sessions of users with disabled plugins
725         $auth_sequence = get_enabled_auth_plugins(true);
726         $auth_sequence = array_flip($auth_sequence);
727         unset($auth_sequence['nologin']); // no login allowed
728         $auth_sequence = array_flip($auth_sequence);
729         $notplugins = null;
730         list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
731         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
733         /// now get a list of time-out candidates
734         $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
735                   FROM {user} u
736                   JOIN {sessions} s ON s.userid = u.id
737                  WHERE s.timemodified + ? < ? AND u.id <> ?";
738         $params = array($maxlifetime, time(), $CFG->siteguest);
740         $authplugins = array();
741         foreach($auth_sequence as $authname) {
742             $authplugins[$authname] = get_auth_plugin($authname);
743         }
744         $rs = $DB->get_recordset_sql($sql, $params);
745         foreach ($rs as $user) {
746             foreach ($authplugins as $authplugin) {
747                 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
748                     continue;
749                 }
750             }
751             $DB->delete_records('sessions', array('sid'=>$user->sid));
752         }
753         $rs->close();
755         // delete expired sessions for userid = 0 (not logged in)
756         $DB->delete_records_select('sessions', 'userid = 0 AND timemodified < ?', array(time() - $maxlifeitme));
757     } catch (dml_exception $ex) {
758         error_log('Error gc-ing sessions');
759     }
762 /**
763  * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
764  * if one does not already exist, but does not overwrite existing sesskeys. Returns the
765  * sesskey string if $USER exists, or boolean false if not.
766  *
767  * @uses $USER
768  * @return string
769  */
770 function sesskey() {
771     // note: do not use $USER because it may not be initialised yet
772     if (empty($_SESSION['USER']->sesskey)) {
773         $_SESSION['USER']->sesskey = random_string(10);
774     }
776     return $_SESSION['USER']->sesskey;
780 /**
781  * Check the sesskey and return true of false for whether it is valid.
782  * (You might like to imagine this function is called sesskey_is_valid().)
783  *
784  * Every script that lets the user perform a significant action (that is,
785  * changes data in the database) should check the sesskey before doing the action.
786  * Depending on your code flow, you may want to use the {@link require_sesskey()}
787  * helper function.
788  *
789  * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
790  *      and this function will do required_param('sesskey', ...).
791  * @return bool whether the sesskey sent in the request matches the one stored in the session.
792  */
793 function confirm_sesskey($sesskey=NULL) {
794     global $USER;
796     if (!empty($USER->ignoresesskey)) {
797         return true;
798     }
800     if (empty($sesskey)) {
801         $sesskey = required_param('sesskey', PARAM_RAW);  // Check script parameters
802     }
804     return (sesskey() === $sesskey);
807 /**
808  * Check the session key using {@link confirm_sesskey()},
809  * and cause a fatal error if it does not match.
810  */
811 function require_sesskey() {
812     if (!confirm_sesskey()) {
813         print_error('invalidsesskey');
814     }
817 /**
818  * Sets a moodle cookie with a weakly encrypted username
819  *
820  * @param string $username to encrypt and place in a cookie, '' means delete current cookie
821  * @return void
822  */
823 function set_moodle_cookie($username) {
824     global $CFG;
826     if (NO_MOODLE_COOKIES) {
827         return;
828     }
830     if ($username === 'guest') {
831         // keep previous cookie in case of guest account login
832         return;
833     }
835     $cookiename = 'MOODLEID_'.$CFG->sessioncookie;
837     // delete old cookie
838     setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
840     if ($username !== '') {
841         // set username cookie for 60 days
842         setcookie($cookiename, rc4encrypt($username), time()+(DAYSECS*60), $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
843     }
846 /**
847  * Gets a moodle cookie with a weakly encrypted username
848  *
849  * @return string username
850  */
851 function get_moodle_cookie() {
852     global $CFG;
854     if (NO_MOODLE_COOKIES) {
855         return '';
856     }
858     $cookiename = 'MOODLEID_'.$CFG->sessioncookie;
860     if (empty($_COOKIE[$cookiename])) {
861         return '';
862     } else {
863         $username = rc4decrypt($_COOKIE[$cookiename]);
864         if ($username === 'guest' or $username === 'nobody') {
865             // backwards compatibility - we do not set these cookies any more
866             return '';
867         }
868         return $username;
869     }
873 /**
874  * Setup $USER object - called during login, loginas, etc.
875  * Preloads capabilities and checks enrolment plugins
876  *
877  * @param stdClass $user full user record object
878  * @return void
879  */
880 function session_set_user($user) {
881     $_SESSION['USER'] = $user;
882     unset($_SESSION['USER']->description); // conserve memory
883     if (!isset($_SESSION['USER']->access)) {
884         // check enrolments and load caps only once
885         enrol_check_plugins($_SESSION['USER']);
886         load_all_capabilities();
887     }
888     sesskey(); // init session key
891 /**
892  * Is current $USER logged-in-as somebody else?
893  * @return bool
894  */
895 function session_is_loggedinas() {
896     return !empty($_SESSION['USER']->realuser);
899 /**
900  * Returns the $USER object ignoring current login-as session
901  * @return stdClass user object
902  */
903 function session_get_realuser() {
904     if (session_is_loggedinas()) {
905         return $_SESSION['REALUSER'];
906     } else {
907         return $_SESSION['USER'];
908     }
911 /**
912  * Login as another user - no security checks here.
913  * @param int $userid
914  * @param stdClass $context
915  * @return void
916  */
917 function session_loginas($userid, $context) {
918     if (session_is_loggedinas()) {
919         return;
920     }
922     // switch to fresh new $SESSION
923     $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
924     $_SESSION['SESSION']     = new stdClass();
926     /// Create the new $USER object with all details and reload needed capabilities
927     $_SESSION['REALUSER'] = $_SESSION['USER'];
928     $user = get_complete_user_data('id', $userid);
929     $user->realuser       = $_SESSION['REALUSER']->id;
930     $user->loginascontext = $context;
931     session_set_user($user);
934 /**
935  * Sets up current user and course environment (lang, etc.) in cron.
936  * Do not use outside of cron script!
937  *
938  * @param stdClass $user full user object, null means default cron user (admin)
939  * @param $course full course record, null means $SITE
940  * @return void
941  */
942 function cron_setup_user($user = NULL, $course = NULL) {
943     global $CFG, $SITE, $PAGE;
945     static $cronuser    = NULL;
946     static $cronsession = NULL;
948     if (empty($cronuser)) {
949         /// ignore admins timezone, language and locale - use site default instead!
950         $cronuser = get_admin();
951         $cronuser->timezone = $CFG->timezone;
952         $cronuser->lang     = '';
953         $cronuser->theme    = '';
954         unset($cronuser->description);
956         $cronsession = new stdClass();
957     }
959     if (!$user) {
960         // cached default cron user (==modified admin for now)
961         session_set_user($cronuser);
962         $_SESSION['SESSION'] = $cronsession;
964     } else {
965         // emulate real user session - needed for caps in cron
966         if ($_SESSION['USER']->id != $user->id) {
967             session_set_user($user);
968             $_SESSION['SESSION'] = new stdClass();
969         }
970     }
972     // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
973     // Temporary hack so that cron does not give fatal errors.
974     $PAGE = new moodle_page();
975     if ($course) {
976         $PAGE->set_course($course);
977     } else {
978         $PAGE->set_course($SITE);
979     }
981     // TODO: it should be possible to improve perf by caching some limited number of users here ;-)
985 /**
986 * Enable cookieless sessions by including $CFG->usesid=true;
987 * in config.php.
988 * Based on code from php manual by Richard at postamble.co.uk
989 * Attempts to use cookies if cookies not present then uses session ids attached to all urls and forms to pass session id from page to page.
990 * If site is open to google, google is given guest access as usual and there are no sessions. No session ids will be attached to urls for googlebot.
991 * This doesn't require trans_sid to be turned on but this is recommended for better performance
992 * you should put :
993 * session.use_trans_sid = 1
994 * in your php.ini file and make sure that you don't have a line like this in your php.ini
995 * session.use_only_cookies = 1
996 * @author Richard at postamble.co.uk and Jamie Pratt
997 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
998 */
999 /**
1000 * You won't call this function directly. This function is used to process
1001 * text buffered by php in an output buffer. All output is run through this function
1002 * before it is ouput.
1003 * @param string $buffer is the output sent from php
1004 * @return string the output sent to the browser
1005 */
1006 function sid_ob_rewrite($buffer){
1007     $replacements = array(
1008         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*")([^"]*)(")/i',
1009         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*\')([^\']*)(\')/i');
1011     $buffer = preg_replace_callback($replacements, 'sid_rewrite_link_tag', $buffer);
1012     $buffer = preg_replace('/<form\s[^>]*>/i',
1013         '\0<input type="hidden" name="' . session_name() . '" value="' . session_id() . '"/>', $buffer);
1015       return $buffer;
1017 /**
1018 * You won't call this function directly. This function is used to process
1019 * text buffered by php in an output buffer. All output is run through this function
1020 * before it is ouput.
1021 * This function only processes absolute urls, it is used when we decide that
1022 * php is processing other urls itself but needs some help with internal absolute urls still.
1023 * @param string $buffer is the output sent from php
1024 * @return string the output sent to the browser
1025 */
1026 function sid_ob_rewrite_absolute($buffer){
1027     $replacements = array(
1028         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*")((?:http|https)[^"]*)(")/i',
1029         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*\')((?:http|https)[^\']*)(\')/i');
1031     $buffer = preg_replace_callback($replacements, 'sid_rewrite_link_tag', $buffer);
1032     $buffer = preg_replace('/<form\s[^>]*>/i',
1033         '\0<input type="hidden" name="' . session_name() . '" value="' . session_id() . '"/>', $buffer);
1034     return $buffer;
1037 /**
1038 * A function to process link, a and script tags found
1039 * by preg_replace_callback in {@link sid_ob_rewrite($buffer)}.
1040 */
1041 function sid_rewrite_link_tag($matches){
1042     $url = $matches[4];
1043     $url = sid_process_url($url);
1044     return $matches[1].$url.$matches[5];
1047 /**
1048 * You can call this function directly. This function is used to process
1049 * urls to add a moodle session id to the url for internal links.
1050 * @param string $url is a url
1051 * @return string the processed url
1052 */
1053 function sid_process_url($url) {
1054     global $CFG;
1056     if ((preg_match('/^(http|https):/i', $url)) // absolute url
1057         &&  ((stripos($url, $CFG->wwwroot)!==0) && stripos($url, $CFG->httpswwwroot)!==0)) { // and not local one
1058         return $url; //don't attach sessid to non local urls
1059     }
1060     if ($url[0]=='#' || (stripos($url, 'javascript:')===0)) {
1061         return $url; //don't attach sessid to anchors
1062     }
1063     if (strpos($url, session_name())!==FALSE) {
1064         return $url; //don't attach sessid to url that already has one sessid
1065     }
1066     if (strpos($url, "?")===FALSE) {
1067         $append = "?".strip_tags(session_name() . '=' . session_id());
1068     }    else {
1069         $append = "&amp;".strip_tags(session_name() . '=' . session_id());
1070     }
1071     //put sessid before any anchor
1072     $p = strpos($url, "#");
1073     if ($p!==FALSE){
1074         $anch = substr($url, $p);
1075         $url = substr($url, 0, $p).$append.$anch ;
1076     } else  {
1077         $url .= $append ;
1078     }
1079     return $url;
1082 /**
1083 * Call this function before there has been any output to the browser to
1084 * buffer output and add session ids to all internal links.
1085 */
1086 function sid_start_ob(){
1087     global $CFG;
1088     //don't attach sess id for bots
1090     if (!empty($_SERVER['HTTP_USER_AGENT'])) {
1091         if (!empty($CFG->opentogoogle)) {
1092             if (strpos($_SERVER['HTTP_USER_AGENT'], 'Googlebot') !== false) {
1093                 @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1094                 $CFG->usesid=false;
1095                 return;
1096             }
1097             if (strpos($_SERVER['HTTP_USER_AGENT'], 'google.com') !== false) {
1098                 @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1099                 $CFG->usesid=false;
1100                 return;
1101             }
1102         }
1103         if (strpos($_SERVER['HTTP_USER_AGENT'], 'W3C_Validator') !== false) {
1104             @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1105             $CFG->usesid=false;
1106             return;
1107         }
1108     }
1110     @ini_set('session.use_trans_sid', '1'); // try and turn on trans_sid
1112     if (ini_get('session.use_trans_sid') != 0) {
1113         // use trans sid as its available
1114         ini_set('url_rewriter.tags', 'a=href,area=href,script=src,link=href,frame=src,form=fakeentry');
1115         ob_start('sid_ob_rewrite_absolute');
1116     } else {
1117         //rewrite all links ourselves
1118         ob_start('sid_ob_rewrite');
1119     }