MDL-19774 fixed incorrect session init in cron - credit goes to Jay Knight
[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  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
27 /**
28   * Factory method returning moodle_session object.
29   * @return moodle_session
30   */
31 function session_get_instance() {
32     global $CFG, $DB;
34     static $session = null;
36     if (is_null($session)) {
37         if (empty($CFG->sessiontimeout)) {
38             $CFG->sessiontimeout = 7200;
39         }
41         if (defined('SESSION_CUSTOM_CLASS')) {
42             // this is a hook for webservices, key based login, etc.
43             if (defined('SESSION_CUSTOM_FILE')) {
44                 require_once($CFG->dirroot.SESSION_CUSTOM_FILE);
45             }
46             $session_class = SESSION_CUSTOM_CLASS;
47             $session = new $session_class();
49         } else if ((!isset($CFG->dbsessions) or $CFG->dbsessions) and $DB->session_lock_supported()) {
50             // default recommended session type
51             $session = new database_session();
53         } else {
54             // legacy limited file based storage - some features and auth plugins will not work, sorry
55             $session = new legacy_file_session();
56         }
57     }
59     return $session;
60 }
62 /**
63  * @package   moodlecore
64  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
65  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
66  */
67 interface moodle_session {
68     /**
69      * Terminate current session
70      * @return void
71      */
72     public function terminate_current();
74     /**
75      * No more changes in session expected.
76      * Unblocks the sessions, other scripts may start executing in parallel.
77      * @return void
78      */
79     public function write_close();
81     /**
82      * Check for existing session with id $sid
83      * @param unknown_type $sid
84      * @return boolean true if session found.
85      */
86     public function session_exists($sid);
87 }
89 /**
90  * Class handling all session and cookies related stuff.
91  *
92  * @package   moodlecore
93  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
94  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
95  */
96 abstract class session_stub implements moodle_session {
97     protected $justloggedout;
99     public function __construct() {
100         global $CFG;
102         if (!defined('NO_MOODLE_COOKIES')) {
103             if (empty($CFG->version) or $CFG->version < 2009011900) {
104                 // no session before sessions table gets created
105                 define('NO_MOODLE_COOKIES', true);
106             } else if (CLI_SCRIPT) {
107                 // CLI scripts can not have session
108                 define('NO_MOODLE_COOKIES', true);
109             } else {
110                 define('NO_MOODLE_COOKIES', false);
111             }
112         }
114         if (NO_MOODLE_COOKIES) {
115             // session not used at all
116             $CFG->usesid = 0;
118             $_SESSION = array();
119             $_SESSION['SESSION'] = new object();
120             $_SESSION['USER']    = new object();
122         } else {
123             $this->prepare_cookies();
124             $this->init_session_storage();
126             $newsession = empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
128             if (!empty($CFG->usesid) && $newsession) {
129                 sid_start_ob();
130             } else {
131                 $CFG->usesid = 0;
132                 ini_set('session.use_trans_sid', '0');
133             }
135             session_name('MoodleSession'.$CFG->sessioncookie);
136             session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
137             session_start();
138             if (!isset($_SESSION['SESSION'])) {
139                 $_SESSION['SESSION'] = new object();
140                 if (!$newsession and !$this->justloggedout) {
141                     $_SESSION['SESSION']->has_timed_out = true;
142                 }
143             }
144             if (!isset($_SESSION['USER'])) {
145                 $_SESSION['USER'] = new object();
146             }
147         }
149         $this->check_user_initialised();
151         $this->check_security();
152     }
154     /**
155      * Terminates active moodle session
156      */
157     public function terminate_current() {
158         global $CFG, $SESSION, $USER, $DB;
160         try {
161             $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
162         } catch (Exception $ignored) {
163             // probably install/upgrade - ignore this problem
164         }
166         if (NO_MOODLE_COOKIES) {
167             return;
168         }
170         // Initialize variable to pass-by-reference to headers_sent(&$file, &$line)
171         $_SESSION = array();
172         $_SESSION['SESSION'] = new object();
173         $_SESSION['USER']    = new object();
174         $_SESSION['USER']->id = 0;
175         if (isset($CFG->mnet_localhost_id)) {
176             $_SESSION['USER']->mnethostid = $CFG->mnet_localhost_id;
177         }
178         $SESSION = $_SESSION['SESSION']; // this may not work properly
179         $USER    = $_SESSION['USER'];    // this may not work properly
181         $file = null;
182         $line = null;
183         if (headers_sent($file, $line)) {
184             error_log('Can not terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
185         }
187         // now let's try to get a new session id and delete the old one
188         $this->justloggedout = true;
189         session_regenerate_id(true);
190         $this->justloggedout = false;
192         // write the new session
193         session_write_close();
194     }
196     /**
197      * No more changes in session expected.
198      * Unblocks the sessions, other scripts may start executing in parallel.
199      * @return void
200      */
201     public function write_close() {
202         if (NO_MOODLE_COOKIES) {
203             return;
204         }
206         session_write_close();
207     }
209     /**
210      * Initialise $USER object, handles google access
211      * and sets up not logged in user properly.
212      *
213      * @return void
214      */
215     protected function check_user_initialised() {
216         if (isset($_SESSION['USER']->id)) {
217             // already set up $USER
218             return;
219         }
221         $user = null;
223         if (!empty($CFG->opentogoogle) and !NO_MOODLE_COOKIES) {
224             if (!empty($_SERVER['HTTP_USER_AGENT'])) {
225                 // allow web spiders in as guest users
226                 if (strpos($_SERVER['HTTP_USER_AGENT'], 'Googlebot') !== false ) {
227                     $user = guest_user();
228                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'google.com') !== false ) { // Google
229                     $user = guest_user();
230                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'Yahoo! Slurp') !== false ) {  // Yahoo
231                     $user = guest_user();
232                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], '[ZSEBOT]') !== false ) {  // Zoomspider
233                     $user = guest_user();
234                 } else if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSNBOT') !== false ) {  // MSN Search
235                     $user = guest_user();
236                 }
237             }
238             if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
239                 // automaticaly log in users coming from search engine results
240                 if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
241                     $user = guest_user();
242                 } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
243                     $user = guest_user();
244                 }
245             }
246         }
248         if (!$user) {
249             $user = new object();
250             $user->id = 0; // to enable proper function of $CFG->notloggedinroleid hack
251             if (isset($CFG->mnet_localhost_id)) {
252                 $user->mnethostid = $CFG->mnet_localhost_id;
253             } else {
254                 $user->mnethostid = 1;
255             }
256         }
257         session_set_user($user);
258     }
260     /**
261      * Does various session security checks
262      * @global void
263      */
264     protected function check_security() {
265         global $CFG;
267         if (NO_MOODLE_COOKIES) {
268             return;
269         }
271         if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
272             /// Make sure current IP matches the one for this session
273             $remoteaddr = getremoteaddr();
275             if (empty($_SESSION['USER']->sessionip)) {
276                 $_SESSION['USER']->sessionip = $remoteaddr;
277             }
279             if ($_SESSION['USER']->sessionip != $remoteaddr) {
280                 // this is a security feature - terminate the session in case of any doubt
281                 $this->terminate_current();
282                 print_error('sessionipnomatch2', 'error');
283             }
284         }
285     }
287     /**
288      * Prepare cookies and various system settings
289      */
290     protected function prepare_cookies() {
291         global $CFG;
293         if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
294             $CFG->cookiesecure = 0;
295         }
297         if (!isset($CFG->cookiehttponly)) {
298             $CFG->cookiehttponly = 0;
299         }
301     /// Set sessioncookie and sessioncookiepath variable if it isn't already
302         if (!isset($CFG->sessioncookie)) {
303             $CFG->sessioncookie = '';
304         }
305         if (!isset($CFG->sessioncookiedomain)) {
306             $CFG->sessioncookiedomain = '';
307         }
308         if (!isset($CFG->sessioncookiepath)) {
309             $CFG->sessioncookiepath = '/';
310         }
312         //discard session ID from POST, GET and globals to tighten security,
313         //this session fixation prevention can not be used in cookieless mode
314         if (empty($CFG->usesid)) {
315             unset(${'MoodleSession'.$CFG->sessioncookie});
316             unset($_GET['MoodleSession'.$CFG->sessioncookie]);
317             unset($_POST['MoodleSession'.$CFG->sessioncookie]);
318             unset($_REQUEST['MoodleSession'.$CFG->sessioncookie]);
319         }
320         //compatibility hack for Moodle Cron, cookies not deleted, but set to "deleted" - should not be needed with NO_MOODLE_COOKIES in cron.php now
321         if (!empty($_COOKIE['MoodleSession'.$CFG->sessioncookie]) && $_COOKIE['MoodleSession'.$CFG->sessioncookie] == "deleted") {
322             unset($_COOKIE['MoodleSession'.$CFG->sessioncookie]);
323         }
324     }
326     /**
327      * Inits session storage.
328      */
329     protected abstract function init_session_storage();
332 /**
333  * Legacy moodle sessions stored in files, not recommended any more.
334  *
335  * @package   moodlecore
336  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
337  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
338  */
339 class legacy_file_session extends session_stub {
340     protected function init_session_storage() {
341         global $CFG;
343         ini_set('session.save_handler', 'files');
345         // Some distros disable GC by setting probability to 0
346         // overriding the PHP default of 1
347         // (gc_probability is divided by gc_divisor, which defaults to 1000)
348         if (ini_get('session.gc_probability') == 0) {
349             ini_set('session.gc_probability', 1);
350         }
352         ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
354         if (!file_exists($CFG->dataroot .'/sessions')) {
355             make_upload_directory('sessions');
356         }
357         if (!is_writable($CFG->dataroot .'/sessions/')) {
358             print_error('sessionnotwritable', 'error');
359         }
360         // Need to disable debugging since disk_free_space()
361         // will fail on very large partitions (see MDL-19222)
362         $freespace = @disk_free_space($CFG->dataroot.'/sessions');
363         if (!($freespace > 2048) and $freespace !== false) {
364             print_error('sessiondiskfull', 'error');
365         }
366         ini_set('session.save_path', $CFG->dataroot .'/sessions');
367     }
368     /**
369      * Check for existing session with id $sid
370      * @param unknown_type $sid
371      * @return boolean true if session found.
372      */
373     public function session_exists($sid){
374         $sid = clean_param($sid, PARAM_FILE);
375         $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
376         return file_exists($sessionfile);
377     }
382 /**
383  * Recommended moodle session storage.
384  *
385  * @package   moodlecore
386  * @copyright 1999 onwards Martin Dougiamas  {@link http://moodle.com}
387  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
388  */
389 class database_session extends session_stub {
390     protected $record   = null;
391     protected $database = null;
393     public function __construct() {
394         global $DB;
395         $this->database = $DB;
396         parent::__construct();
398         if (!empty($this->record->state)) {
399             // something is very wrong
400             session_kill($this->record->sid);
402             if ($this->record->state == 9) {
403                 print_error('dbsessionmysqlpacketsize', 'error');
404             }
405         }
406     }
408     public function session_exists($sid){
409         global $CFG;
410         try {
411             $sql = "SELECT * FROM {sessions} WHERE timemodified < ? AND sid=? AND state=?";
412             $params = array(time() + $CFG->sessiontimeout, $sid, 0);
414             return $this->database->record_exists_sql($sql, $params);
415         } catch (dml_exception $ex) {
416             error_log('Error checking existance of database session');
417             return false;
418         }
419     }
421     protected function init_session_storage() {
422         global $CFG;
424         // gc only from CRON - individual user timeouts now checked during each access
425         ini_set('session.gc_probability', 0);
427         ini_set('session.gc_maxlifetime', $CFG->sessiontimeout);
429         $result = session_set_save_handler(array($this, 'handler_open'),
430                                            array($this, 'handler_close'),
431                                            array($this, 'handler_read'),
432                                            array($this, 'handler_write'),
433                                            array($this, 'handler_destroy'),
434                                            array($this, 'handler_gc'));
435         if (!$result) {
436             print_error('dbsessionhandlerproblem', 'error');
437         }
438     }
440     public function handler_open($save_path, $session_name) {
441         return true;
442     }
444     public function handler_close() {
445         if (isset($this->record->id)) {
446             $this->database->release_session_lock($this->record->id);
447         }
448         $this->record = null;
449         return true;
450     }
452     public function handler_read($sid) {
453         global $CFG;
455         if ($this->record and $this->record->sid != $sid) {
456             error_log('Weird error reading database session - mismatched sid');
457             return '';
458         }
460         try {
461             if ($record = $this->database->get_record('sessions', array('sid'=>$sid))) {
462                 $this->database->get_session_lock($record->id);
464             } else {
465                 $record = new object();
466                 $record->state        = 0;
467                 $record->sid          = $sid;
468                 $record->sessdata     = null;
469                 $record->userid       = 0;
470                 $record->timecreated  = $record->timemodified = time();
471                 $record->firstip      = $record->lastip = getremoteaddr();
472                 $record->id           = $this->database->insert_record_raw('sessions', $record);
474                 $this->database->get_session_lock($record->id);
475             }
476         } catch (dml_exception $ex) {
477             error_log('Can not read or insert database sessions');
478             return '';
479         }
481         // verify timeout
482         if ($record->timemodified + $CFG->sessiontimeout < time()) {
483             $ignoretimeout = false;
484             if (!empty($record->userid)) { // skips not logged in
485                 if ($user = $this->database->get_record('user', array('id'=>$record->userid))) {
486                     if ($user->username !== 'guest') {
487                         $authsequence = get_enabled_auth_plugins(); // auths, in sequence
488                         foreach($authsequence as $authname) {
489                             $authplugin = get_auth_plugin($authname);
490                             if ($authplugin->ignore_timeout_hook($user, $record->sid, $record->timecreated, $record->timemodified)) {
491                                 $ignoretimeout = true;
492                                 break;
493                             }
494                         }
495                     }
496                 }
497             }
498             if ($ignoretimeout) {
499                 //refresh session
500                 $record->timemodified = time();
501                 try {
502                     $this->database->update_record('sessions', $record);
503                 } catch (dml_exception $ex) {
504                     error_log('Can not refresh database session');
505                     return '';
506                 }
507             } else {
508                 //time out session
509                 $record->state        = 0;
510                 $record->sessdata     = null;
511                 $record->userid       = 0;
512                 $record->timecreated  = $record->timemodified = time();
513                 $record->firstip      = $record->lastip = getremoteaddr();
514                 try {
515                     $this->database->update_record('sessions', $record);
516                 } catch (dml_exception $ex) {
517                     error_log('Can not time out database session');
518                     return '';
519                 }
520             }
521         }
523         $data = is_null($record->sessdata) ? '' : base64_decode($record->sessdata);
525         unset($record->sessdata); // conserve memory
526         $this->record = $record;
528         return $data;
529     }
531     public function handler_write($sid, $session_data) {
532         global $USER;
534         // TODO: MDL-20625 we need to rollback all active transactions and log error if any open needed
536         $userid = 0;
537         if (!empty($USER->realuser)) {
538             $userid = $USER->realuser;
539         } else if (!empty($USER->id)) {
540             $userid = $USER->id;
541         }
543         if (isset($this->record->id)) {
544             $record->state              = 0;
545             $record->sid                = $sid;                         // might be regenerating sid
546             $this->record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
547             $this->record->userid       = $userid;
548             $this->record->timemodified = time();
549             $this->record->lastip       = getremoteaddr();
551             // TODO: verify session changed before doing update,
552             //       also make sure the timemodified field is changed only every 10s if nothing else changes  MDL-20462
554             try {
555                 $this->database->update_record_raw('sessions', $this->record);
556             } catch (dml_exception $ex) {
557                 if ($this->database->get_dbfamily() === 'mysql') {
558                     try {
559                         $this->database->set_field('sessions', 'state', 9, array('id'=>$this->record->id));
560                     } catch (Exception $ignored) {
562                     }
563                     error_log('Can not write database session - please verify max_allowed_packet is at least 4M!');
564                 } else {
565                     error_log('Can not write database session');
566                 }
567             }
569         } else {
570             // session already destroyed
571             $record = new object();
572             $record->state        = 0;
573             $record->sid          = $sid;
574             $record->sessdata     = base64_encode($session_data); // there might be some binary mess :-(
575             $record->userid       = $userid;
576             $record->timecreated  = $record->timemodified = time();
577             $record->firstip      = $record->lastip = getremoteaddr();
578             $record->id           = $this->database->insert_record_raw('sessions', $record);
579             $this->record = $record;
581             try {
582                 $this->database->get_session_lock($this->record->id);
583             } catch (dml_exception $ex) {
584                 error_log('Can not write new database session');
585             }
586         }
588         return true;
589     }
591     public function handler_destroy($sid) {
592         session_kill($sid);
594         if (isset($this->record->id) and $this->record->sid === $sid) {
595             $this->database->release_session_lock($this->record->id);
596             $this->record = null;
597         }
599         return true;
600     }
602     public function handler_gc($ignored_maxlifetime) {
603         session_gc();
604         return true;
605     }
608 /**
609  * returns true if legacy session used.
610  * @return bool true if legacy(==file) based session used
611  */
612 function session_is_legacy() {
613     global $CFG, $DB;
614     return ((isset($CFG->dbsessions) and !$CFG->dbsessions) or !$DB->session_lock_supported());
617 /**
618  * Terminates all sessions, auth hooks are not executed.
619  * Useful in upgrade scripts.
620  */
621 function session_kill_all() {
622     global $CFG, $DB;
624     // always check db table - custom session classes use sessions table
625     try {
626         $DB->delete_records('sessions');
627     } catch (dml_exception $ignored) {
628         // do not show any warnings - might be during upgrade/installation
629     }
631     if (session_is_legacy()) {
632         $sessiondir = "$CFG->dataroot/sessions";
633         if (is_dir($sessiondir)) {
634             foreach (glob("$sessiondir/sess_*") as $filename) {
635                 @unlink($filename);
636             }
637         }
638     }
641 /**
642  * Mark session as accessed, prevents timeouts.
643  * @param string $sid
644  */
645 function session_touch($sid) {
646     global $CFG, $DB;
648     // always check db table - custom session classes use sessions table
649     try {
650         $sql = "UPDATE {sessions} SET timemodified=? WHERE sid=?";
651         $params = array(time(), $sid);
652         $DB->execute($sql, $params);
653     } catch (dml_exception $ignored) {
654         // do not show any warnings - might be during upgrade/installation
655     }
657     if (session_is_legacy()) {
658         $sid = clean_param($sid, PARAM_FILE);
659         $sessionfile = clean_param("$CFG->dataroot/sessions/sess_$sid", PARAM_FILE);
660         if (file_exists($sessionfile)) {
661             // if the file is locked it means that it will be updated anyway
662             @touch($sessionfile);
663         }
664     }
667 /**
668  * Terminates one sessions, auth hooks are not executed.
669  *
670  * @param string $sid session id
671  */
672 function session_kill($sid) {
673     global $CFG, $DB;
675     // always check db table - custom session classes use sessions table
676     try {
677         $DB->delete_records('sessions', array('sid'=>$sid));
678     } catch (dml_exception $ignored) {
679         // do not show any warnings - might be during upgrade/installation
680     }
682     if (session_is_legacy()) {
683         $sid = clean_param($sid, PARAM_FILE);
684         $sessionfile = "$CFG->dataroot/sessions/sess_$sid";
685         if (file_exists($sessionfile)) {
686             @unlink($sessionfile);
687         }
688     }
691 /**
692  * Terminates all sessions of one user, auth hooks are not executed.
693  * NOTE: This can not work for file based sessions!
694  *
695  * @param int $userid user id
696  */
697 function session_kill_user($userid) {
698     global $CFG, $DB;
700     // always check db table - custom session classes use sessions table
701     try {
702         $DB->delete_records('sessions', array('userid'=>$userid));
703     } catch (dml_exception $ignored) {
704         // do not show any warnings - might be during upgrade/installation
705     }
707     if (session_is_legacy()) {
708         // log error?
709     }
712 /**
713  * Session garbage collection
714  * - verify timeout for all users
715  * - kill sessions of all deleted users
716  * - kill sessions of users with disabled plugins or 'nologin' plugin
717  *
718  * NOTE: this can not work when legacy file sessions used!
719  */
720 function session_gc() {
721     global $CFG, $DB;
723     $maxlifetime = $CFG->sessiontimeout;
725     try {
726         /// kill all sessions of deleted users
727         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0)");
729         /// kill sessions of users with disabled plugins
730         $auth_sequence = get_enabled_auth_plugins(true);
731         $auth_sequence = array_flip($auth_sequence);
732         unset($auth_sequence['nologin']); // no login allowed
733         $auth_sequence = array_flip($auth_sequence);
734         $notplugins = null;
735         list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
736         $DB->delete_records_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params);
738         /// now get a list of time-out candidates
739         $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
740                   FROM {user} u
741                   JOIN {sessions} s ON s.userid = u.id
742                  WHERE s.timemodified + ? < ? AND u.username <> 'guest'";
743         $params = array($maxlifetime, time());
745         $authplugins = array();
746         foreach($auth_sequence as $authname) {
747             $authplugins[$authname] = get_auth_plugin($authname);
748         }
749         $rs = $DB->get_recordset_sql($sql, $params);
750         foreach ($rs as $user) {
751             foreach ($authplugins as $authplugin) {
752                 if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
753                     continue;
754                 }
755             }
756             $DB->delete_records('sessions', array('sid'=>$user->sid));
757         }
758         $rs->close();
759     } catch (dml_exception $ex) {
760         error_log('Error gc-ing sessions');
761     }
764 /**
765  * Makes sure that $USER->sesskey exists, if $USER itself exists. It sets a new sesskey
766  * if one does not already exist, but does not overwrite existing sesskeys. Returns the
767  * sesskey string if $USER exists, or boolean false if not.
768  *
769  * @uses $USER
770  * @return string
771  */
772 function sesskey() {
773     global $USER;
775     if (empty($USER->sesskey)) {
776         $USER->sesskey = random_string(10);
777     }
779     return $USER->sesskey;
783 /**
784  * Check the sesskey and return true of false for whether it is valid.
785  * (You might like to imagine this function is called sesskey_is_valid().)
786  *
787  * Every script that lets the user perform a significant action (that is,
788  * changes data in the database) should check the sesskey before doing the action.
789  * Depending on your code flow, you may want to use the {@link require_sesskey()}
790  * helper function.
791  *
792  * @param string $sesskey The sesskey value to check (optional). Normally leave this blank
793  *      and this function will do required_param('sesskey', ...).
794  * @return bool whether the sesskey sent in the request matches the one stored in the session.
795  */
796 function confirm_sesskey($sesskey=NULL) {
797     global $USER;
799     if (!empty($USER->ignoresesskey)) {
800         return true;
801     }
803     if (empty($sesskey)) {
804         $sesskey = required_param('sesskey', PARAM_RAW);  // Check script parameters
805     }
807     return (sesskey() === $sesskey);
810 /**
811  * Check the session key using {@link confirm_sesskey()},
812  * and cause a fatal error if it does not match.
813  */
814 function require_sesskey() {
815     if (!confirm_sesskey()) {
816         print_error('invalidsesskey');
817     }
820 /**
821  * Sets a moodle cookie with a weakly encrypted string
822  *
823  * @uses $CFG
824  * @uses DAYSECS
825  * @uses HOURSECS
826  * @param string $thing The string to encrypt and place in a cookie
827  */
828 function set_moodle_cookie($thing) {
829     global $CFG;
831     if (NO_MOODLE_COOKIES) {
832         return;
833     }
835     if ($thing == 'guest') {  // Ignore guest account
836         return;
837     }
839     $cookiename = 'MOODLEID_'.$CFG->sessioncookie;
841     $days = 60;
842     $seconds = DAYSECS*$days;
844     setcookie($cookiename, '', time() - HOURSECS, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
845     setcookie($cookiename, rc4encrypt($thing), time()+$seconds, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
848 /**
849  * Gets a moodle cookie with a weakly encrypted string
850  *
851  * @uses $CFG
852  * @return string
853  */
854 function get_moodle_cookie() {
855     global $CFG;
857     if (NO_MOODLE_COOKIES) {
858         return '';
859     }
861     $cookiename = 'MOODLEID_'.$CFG->sessioncookie;
863     if (empty($_COOKIE[$cookiename])) {
864         return '';
865     } else {
866         $thing = rc4decrypt($_COOKIE[$cookiename]);
867         return ($thing == 'guest') ? '': $thing;  // Ignore guest account
868     }
872 /**
873  * Setup $USER object - called during login, loginas, etc.
874  * Preloads capabilities and checks enrolment plugins
875  *
876  * @param object $user full user record object
877  * @return void
878  */
879 function session_set_user($user) {
880     $_SESSION['USER'] = $user;
881     unset($_SESSION['USER']->description); // conserve memory
882     if (!isset($_SESSION['USER']->access)) {
883         // check enrolments and load caps only once
884         enrol_check_plugins($_SESSION['USER']);
885         load_all_capabilities();
886     }
887     sesskey(); // init session key
890 /**
891  * Is current $USER logged-in-as somebody else?
892  * @return bool
893  */
894 function session_is_loggedinas() {
895     return !empty($_SESSION['USER']->realuser);
898 /**
899  * Returns the $USER object ignoring current login-as session
900  * @return object user object
901  */
902 function session_get_realuser() {
903     if (session_is_loggedinas()) {
904         return $_SESSION['REALUSER'];
905     } else {
906         return $_SESSION['USER'];
907     }
910 /**
911  * Login as another user - no security checks here.
912  * @param int $userid
913  * @param object $context
914  * @return void
915  */
916 function session_loginas($userid, $context) {
917     if (session_is_loggedinas()) {
918         return;
919     }
921     // switch to fresh new $SESSION
922     $_SESSION['REALSESSION'] = $_SESSION['SESSION'];
923     $_SESSION['SESSION']     = new object();
925     /// Create the new $USER object with all details and reload needed capabilities
926     $_SESSION['REALUSER'] = $_SESSION['USER'];
927     $user = get_complete_user_data('id', $userid);
928     $user->realuser       = $_SESSION['REALUSER']->id;
929     $user->loginascontext = $context;
930     session_set_user($user);
933 /**
934  * Terminate login-as session
935  * @return void
936  */
937 function session_unloginas() {
938     if (!session_is_loggedinas()) {
939         return;
940     }
942     $_SESSION['SESSION'] = $_SESSION['REALSESSION'];
943     unset($_SESSION['REALSESSION']);
945     $_SESSION['USER'] = $_SESSION['REALUSER'];
946     unset($_SESSION['REALUSER']);
949 /**
950  * Sets up current user and course environment (lang, etc.) in cron.
951  * Do not use outside of cron script!
952  *
953  * @param object $user full user object, null means default cron user (admin)
954  * @param $course full course record, null means $SITE
955  * @return void
956  */
957 function cron_setup_user($user = NULL, $course = NULL) {
958     global $CFG, $SITE, $PAGE;
960     static $cronuser    = NULL;
961     static $cronsession = NULL;
963     if (empty($cronuser)) {
964         /// ignore admins timezone, language and locale - use site default instead!
965         $cronuser = get_admin();
966         $cronuser->timezone = $CFG->timezone;
967         $cronuser->lang     = '';
968         $cronuser->theme    = '';
969         unset($cronuser->description);
971         $cronsession = new object();
972     }
974     if (!$user) {
975         // cached default cron user (==modified admin for now)
976         session_set_user($cronuser);
977         $_SESSION['SESSION'] = $cronsession;
979     } else {
980         // emulate real user session - needed for caps in cron
981         if ($_SESSION['USER']->id != $user->id) {
982             session_set_user($user);
983             $_SESSION['SESSION'] = new object();
984         }
985     }
987     // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
988     // Temporary hack so that cron does not give fatal errors.
989     $PAGE = new moodle_page();
990     if ($course) {
991         $PAGE->set_course($course);
992     } else {
993         $PAGE->set_course($SITE);
994     }
996     // TODO: it should be possible to improve perf by caching some limited number of users here ;-)
1000 /**
1001 * Enable cookieless sessions by including $CFG->usesid=true;
1002 * in config.php.
1003 * Based on code from php manual by Richard at postamble.co.uk
1004 * 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.
1005 * 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.
1006 * This doesn't require trans_sid to be turned on but this is recommended for better performance
1007 * you should put :
1008 * session.use_trans_sid = 1
1009 * in your php.ini file and make sure that you don't have a line like this in your php.ini
1010 * session.use_only_cookies = 1
1011 * @author Richard at postamble.co.uk and Jamie Pratt
1012 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
1013 */
1014 /**
1015 * You won't call this function directly. This function is used to process
1016 * text buffered by php in an output buffer. All output is run through this function
1017 * before it is ouput.
1018 * @param string $buffer is the output sent from php
1019 * @return string the output sent to the browser
1020 */
1021 function sid_ob_rewrite($buffer){
1022     $replacements = array(
1023         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*")([^"]*)(")/i',
1024         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*\')([^\']*)(\')/i');
1026     $buffer = preg_replace_callback($replacements, 'sid_rewrite_link_tag', $buffer);
1027     $buffer = preg_replace('/<form\s[^>]*>/i',
1028         '\0<input type="hidden" name="' . session_name() . '" value="' . session_id() . '"/>', $buffer);
1030       return $buffer;
1032 /**
1033 * You won't call this function directly. This function is used to process
1034 * text buffered by php in an output buffer. All output is run through this function
1035 * before it is ouput.
1036 * This function only processes absolute urls, it is used when we decide that
1037 * php is processing other urls itself but needs some help with internal absolute urls still.
1038 * @param string $buffer is the output sent from php
1039 * @return string the output sent to the browser
1040 */
1041 function sid_ob_rewrite_absolute($buffer){
1042     $replacements = array(
1043         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*")((?:http|https)[^"]*)(")/i',
1044         '/(<\s*(a|link|script|frame|area)\s[^>]*(href|src)\s*=\s*\')((?:http|https)[^\']*)(\')/i');
1046     $buffer = preg_replace_callback($replacements, 'sid_rewrite_link_tag', $buffer);
1047     $buffer = preg_replace('/<form\s[^>]*>/i',
1048         '\0<input type="hidden" name="' . session_name() . '" value="' . session_id() . '"/>', $buffer);
1049     return $buffer;
1052 /**
1053 * A function to process link, a and script tags found
1054 * by preg_replace_callback in {@link sid_ob_rewrite($buffer)}.
1055 */
1056 function sid_rewrite_link_tag($matches){
1057     $url = $matches[4];
1058     $url = sid_process_url($url);
1059     return $matches[1].$url.$matches[5];
1062 /**
1063 * You can call this function directly. This function is used to process
1064 * urls to add a moodle session id to the url for internal links.
1065 * @param string $url is a url
1066 * @return string the processed url
1067 */
1068 function sid_process_url($url) {
1069     global $CFG;
1071     if ((preg_match('/^(http|https):/i', $url)) // absolute url
1072         &&  ((stripos($url, $CFG->wwwroot)!==0) && stripos($url, $CFG->httpswwwroot)!==0)) { // and not local one
1073         return $url; //don't attach sessid to non local urls
1074     }
1075     if ($url[0]=='#' || (stripos($url, 'javascript:')===0)) {
1076         return $url; //don't attach sessid to anchors
1077     }
1078     if (strpos($url, session_name())!==FALSE) {
1079         return $url; //don't attach sessid to url that already has one sessid
1080     }
1081     if (strpos($url, "?")===FALSE) {
1082         $append = "?".strip_tags(session_name() . '=' . session_id());
1083     }    else {
1084         $append = "&amp;".strip_tags(session_name() . '=' . session_id());
1085     }
1086     //put sessid before any anchor
1087     $p = strpos($url, "#");
1088     if ($p!==FALSE){
1089         $anch = substr($url, $p);
1090         $url = substr($url, 0, $p).$append.$anch ;
1091     } else  {
1092         $url .= $append ;
1093     }
1094     return $url;
1097 /**
1098 * Call this function before there has been any output to the browser to
1099 * buffer output and add session ids to all internal links.
1100 */
1101 function sid_start_ob(){
1102     global $CFG;
1103     //don't attach sess id for bots
1105     if (!empty($_SERVER['HTTP_USER_AGENT'])) {
1106         if (!empty($CFG->opentogoogle)) {
1107             if (strpos($_SERVER['HTTP_USER_AGENT'], 'Googlebot') !== false) {
1108                 @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1109                 $CFG->usesid=false;
1110                 return;
1111             }
1112             if (strpos($_SERVER['HTTP_USER_AGENT'], 'google.com') !== false) {
1113                 @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1114                 $CFG->usesid=false;
1115                 return;
1116             }
1117         }
1118         if (strpos($_SERVER['HTTP_USER_AGENT'], 'W3C_Validator') !== false) {
1119             @ini_set('session.use_trans_sid', '0'); // try and turn off trans_sid
1120             $CFG->usesid=false;
1121             return;
1122         }
1123     }
1125     @ini_set('session.use_trans_sid', '1'); // try and turn on trans_sid
1127     if (ini_get('session.use_trans_sid') != 0) {
1128         // use trans sid as its available
1129         ini_set('url_rewriter.tags', 'a=href,area=href,script=src,link=href,frame=src,form=fakeentry');
1130         ob_start('sid_ob_rewrite_absolute');
1131     } else {
1132         //rewrite all links ourselves
1133         ob_start('sid_ob_rewrite');
1134     }