Commit | Line | Data |
---|---|---|
d79d5ac2 PS |
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/>. | |
16 | ||
17 | /** | |
18 | * Session manager class. | |
19 | * | |
20 | * @package core | |
21 | * @copyright 2013 Petr Skoda {@link http://skodak.org} | |
22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
23 | */ | |
24 | ||
25 | namespace core\session; | |
26 | ||
27 | defined('MOODLE_INTERNAL') || die(); | |
28 | ||
29 | /** | |
30 | * Session manager, this is the public Moodle API for sessions. | |
31 | * | |
32 | * Following PHP functions MUST NOT be used directly: | |
33 | * - session_start() - not necessary, lib/setup.php starts session automatically, | |
34 | * use define('NO_MOODLE_COOKIE', true) if session not necessary. | |
35 | * - session_write_close() - use \core\session\manager::write_close() instead. | |
36 | * - session_destroy() - use require_logout() instead. | |
37 | * | |
38 | * @package core | |
39 | * @copyright 2013 Petr Skoda {@link http://skodak.org} | |
40 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
41 | */ | |
42 | class manager { | |
43 | /** @var handler $handler active session handler instance */ | |
44 | protected static $handler; | |
45 | ||
46 | /** @var bool $sessionactive Is the session active? */ | |
47 | protected static $sessionactive = null; | |
48 | ||
49 | /** | |
50 | * Start user session. | |
51 | * | |
52 | * Note: This is intended to be called only from lib/setup.php! | |
53 | */ | |
54 | public static function start() { | |
55 | global $CFG, $DB; | |
56 | ||
57 | if (isset(self::$sessionactive)) { | |
58 | debugging('Session was already started!', DEBUG_DEVELOPER); | |
59 | return; | |
60 | } | |
61 | ||
62 | self::load_handler(); | |
63 | ||
64 | // Init the session handler only if everything initialised properly in lib/setup.php file | |
65 | // and the session is actually required. | |
66 | if (empty($DB) or empty($CFG->version) or !defined('NO_MOODLE_COOKIES') or NO_MOODLE_COOKIES or CLI_SCRIPT) { | |
67 | self::$sessionactive = false; | |
68 | self::init_empty_session(); | |
69 | return; | |
70 | } | |
71 | ||
72 | try { | |
73 | self::$handler->init(); | |
74 | self::prepare_cookies(); | |
1ac585fe | 75 | $isnewsession = empty($_COOKIE[session_name()]); |
d79d5ac2 | 76 | |
1ac585fe FW |
77 | if (!self::$handler->start()) { |
78 | // Could not successfully start/recover session. | |
79 | throw new \core\session\exception(get_string('servererror')); | |
80 | } | |
d79d5ac2 | 81 | |
1ac585fe | 82 | self::initialise_user_session($isnewsession); |
d79d5ac2 PS |
83 | self::check_security(); |
84 | ||
2e00d01d PS |
85 | // Link global $USER and $SESSION, |
86 | // this is tricky because PHP does not allow references to references | |
87 | // and global keyword uses internally once reference to the $GLOBALS array. | |
88 | // The solution is to use the $GLOBALS['USER'] and $GLOBALS['$SESSION'] | |
89 | // as the main storage of data and put references to $_SESSION. | |
90 | $GLOBALS['USER'] = $_SESSION['USER']; | |
91 | $_SESSION['USER'] =& $GLOBALS['USER']; | |
92 | $GLOBALS['SESSION'] = $_SESSION['SESSION']; | |
93 | $_SESSION['SESSION'] =& $GLOBALS['SESSION']; | |
94 | ||
d79d5ac2 | 95 | } catch (\Exception $ex) { |
d79d5ac2 PS |
96 | self::init_empty_session(); |
97 | self::$sessionactive = false; | |
98 | throw $ex; | |
99 | } | |
100 | ||
101 | self::$sessionactive = true; | |
102 | } | |
103 | ||
104 | /** | |
105 | * Returns current page performance info. | |
106 | * | |
107 | * @return array perf info | |
108 | */ | |
109 | public static function get_performance_info() { | |
110 | if (!session_id()) { | |
111 | return array(); | |
112 | } | |
113 | ||
114 | self::load_handler(); | |
115 | $size = display_size(strlen(session_encode())); | |
116 | $handler = get_class(self::$handler); | |
117 | ||
118 | $info = array(); | |
119 | $info['size'] = $size; | |
120 | $info['html'] = "<span class=\"sessionsize\">Session ($handler): $size</span> "; | |
121 | $info['txt'] = "Session ($handler): $size "; | |
122 | ||
123 | return $info; | |
124 | } | |
125 | ||
126 | /** | |
127 | * Create handler instance. | |
128 | */ | |
129 | protected static function load_handler() { | |
130 | global $CFG, $DB; | |
131 | ||
132 | if (self::$handler) { | |
133 | return; | |
134 | } | |
135 | ||
136 | // Find out which handler to use. | |
137 | if (PHPUNIT_TEST) { | |
138 | $class = '\core\session\file'; | |
139 | ||
140 | } else if (!empty($CFG->session_handler_class)) { | |
141 | $class = $CFG->session_handler_class; | |
142 | ||
143 | } else if (!empty($CFG->dbsessions) and $DB->session_lock_supported()) { | |
144 | $class = '\core\session\database'; | |
145 | ||
146 | } else { | |
147 | $class = '\core\session\file'; | |
148 | } | |
149 | self::$handler = new $class(); | |
150 | } | |
151 | ||
152 | /** | |
153 | * Empty current session, fill it with not-logged-in user info. | |
2e00d01d PS |
154 | * |
155 | * This is intended for installation scripts, unit tests and other | |
156 | * special areas. Do NOT use for logout and session termination | |
157 | * in normal requests! | |
d79d5ac2 | 158 | */ |
2e00d01d | 159 | public static function init_empty_session() { |
d79d5ac2 PS |
160 | global $CFG; |
161 | ||
0346323c | 162 | if (isset($GLOBALS['SESSION']->notifications)) { |
2f244f1c | 163 | // Backup notifications. These should be preserved across session changes until the user fetches and clears them. |
0346323c AN |
164 | $notifications = $GLOBALS['SESSION']->notifications; |
165 | } | |
2e00d01d PS |
166 | $GLOBALS['SESSION'] = new \stdClass(); |
167 | ||
168 | $GLOBALS['USER'] = new \stdClass(); | |
169 | $GLOBALS['USER']->id = 0; | |
0346323c | 170 | |
2f244f1c AN |
171 | if (!empty($notifications)) { |
172 | // Restore notifications. | |
173 | $GLOBALS['SESSION']->notifications = $notifications; | |
174 | } | |
d79d5ac2 | 175 | if (isset($CFG->mnet_localhost_id)) { |
2e00d01d | 176 | $GLOBALS['USER']->mnethostid = $CFG->mnet_localhost_id; |
d79d5ac2 PS |
177 | } else { |
178 | // Not installed yet, the future host id will be most probably 1. | |
2e00d01d | 179 | $GLOBALS['USER']->mnethostid = 1; |
d79d5ac2 PS |
180 | } |
181 | ||
2e00d01d PS |
182 | // Link global $USER and $SESSION. |
183 | $_SESSION = array(); | |
184 | $_SESSION['USER'] =& $GLOBALS['USER']; | |
185 | $_SESSION['SESSION'] =& $GLOBALS['SESSION']; | |
d79d5ac2 PS |
186 | } |
187 | ||
188 | /** | |
189 | * Make sure all cookie and session related stuff is configured properly before session start. | |
190 | */ | |
191 | protected static function prepare_cookies() { | |
192 | global $CFG; | |
193 | ||
657ddbf5 | 194 | $cookiesecure = is_moodle_cookie_secure(); |
d79d5ac2 PS |
195 | |
196 | if (!isset($CFG->cookiehttponly)) { | |
197 | $CFG->cookiehttponly = 0; | |
198 | } | |
199 | ||
200 | // Set sessioncookie variable if it isn't already. | |
201 | if (!isset($CFG->sessioncookie)) { | |
202 | $CFG->sessioncookie = ''; | |
203 | } | |
204 | $sessionname = 'MoodleSession'.$CFG->sessioncookie; | |
205 | ||
206 | // Make sure cookie domain makes sense for this wwwroot. | |
207 | if (!isset($CFG->sessioncookiedomain)) { | |
208 | $CFG->sessioncookiedomain = ''; | |
209 | } else if ($CFG->sessioncookiedomain !== '') { | |
210 | $host = parse_url($CFG->wwwroot, PHP_URL_HOST); | |
211 | if ($CFG->sessioncookiedomain !== $host) { | |
212 | if (substr($CFG->sessioncookiedomain, 0, 1) === '.') { | |
213 | if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) { | |
214 | // Invalid domain - it must be end part of host. | |
215 | $CFG->sessioncookiedomain = ''; | |
216 | } | |
217 | } else { | |
218 | if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) { | |
219 | // Invalid domain - it must be end part of host. | |
220 | $CFG->sessioncookiedomain = ''; | |
221 | } | |
222 | } | |
223 | } | |
224 | } | |
225 | ||
226 | // Make sure the cookiepath is valid for this wwwroot or autodetect if not specified. | |
227 | if (!isset($CFG->sessioncookiepath)) { | |
228 | $CFG->sessioncookiepath = ''; | |
229 | } | |
230 | if ($CFG->sessioncookiepath !== '/') { | |
231 | $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/'; | |
232 | if ($CFG->sessioncookiepath === '') { | |
233 | $CFG->sessioncookiepath = $path; | |
234 | } else { | |
235 | if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') { | |
236 | $CFG->sessioncookiepath = $path; | |
237 | } | |
238 | } | |
239 | } | |
240 | ||
241 | // Discard session ID from POST, GET and globals to tighten security, | |
242 | // this is session fixation prevention. | |
243 | unset($GLOBALS[$sessionname]); | |
244 | unset($_GET[$sessionname]); | |
245 | unset($_POST[$sessionname]); | |
246 | unset($_REQUEST[$sessionname]); | |
247 | ||
248 | // Compatibility hack for non-browser access to our web interface. | |
249 | if (!empty($_COOKIE[$sessionname]) && $_COOKIE[$sessionname] == "deleted") { | |
250 | unset($_COOKIE[$sessionname]); | |
251 | } | |
252 | ||
253 | // Set configuration. | |
254 | session_name($sessionname); | |
c823bfee AN |
255 | // The session cookie expiry time cannot be extended so this needs to be set to a reasonable period, longer than |
256 | // the sessiontimeout. | |
257 | // This ensures that the cookie is unlikely to timeout before the session does. | |
258 | $sessionlifetime = $CFG->sessiontimeout + WEEKSECS; | |
259 | session_set_cookie_params($sessionlifetime, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, | |
260 | $cookiesecure, $CFG->cookiehttponly); | |
d79d5ac2 PS |
261 | ini_set('session.use_trans_sid', '0'); |
262 | ini_set('session.use_only_cookies', '1'); | |
263 | ini_set('session.hash_function', '0'); // For now MD5 - we do not have room for sha-1 in sessions table. | |
264 | ini_set('session.use_strict_mode', '0'); // We have custom protection in session init. | |
265 | ini_set('session.serialize_handler', 'php'); // We can move to 'php_serialize' after we require PHP 5.5.4 form Moodle. | |
266 | ||
267 | // Moodle does normal session timeouts, this is for leftovers only. | |
268 | ini_set('session.gc_probability', 1); | |
269 | ini_set('session.gc_divisor', 1000); | |
270 | ini_set('session.gc_maxlifetime', 60*60*24*4); | |
271 | } | |
272 | ||
273 | /** | |
2e00d01d | 274 | * Initialise $_SESSION, handles google access |
d79d5ac2 PS |
275 | * and sets up not-logged-in user properly. |
276 | * | |
2e00d01d PS |
277 | * WARNING: $USER and $SESSION are set up later, do not use them yet! |
278 | * | |
d79d5ac2 PS |
279 | * @param bool $newsid is this a new session in first http request? |
280 | */ | |
281 | protected static function initialise_user_session($newsid) { | |
282 | global $CFG, $DB; | |
283 | ||
284 | $sid = session_id(); | |
285 | if (!$sid) { | |
286 | // No session, very weird. | |
287 | error_log('Missing session ID, session not started!'); | |
288 | self::init_empty_session(); | |
289 | return; | |
290 | } | |
291 | ||
292 | if (!$record = $DB->get_record('sessions', array('sid'=>$sid), 'id, sid, state, userid, lastip, timecreated, timemodified')) { | |
293 | if (!$newsid) { | |
294 | if (!empty($_SESSION['USER']->id)) { | |
295 | // This should not happen, just log it, we MUST not produce any output here! | |
296 | error_log("Cannot find session record $sid for user ".$_SESSION['USER']->id.", creating new session."); | |
297 | } | |
226991e9 PS |
298 | // Prevent session fixation attacks. |
299 | session_regenerate_id(true); | |
d79d5ac2 | 300 | } |
d79d5ac2 PS |
301 | $_SESSION = array(); |
302 | } | |
303 | unset($sid); | |
304 | ||
305 | if (isset($_SESSION['USER']->id)) { | |
306 | if (!empty($_SESSION['USER']->realuser)) { | |
307 | $userid = $_SESSION['USER']->realuser; | |
308 | } else { | |
309 | $userid = $_SESSION['USER']->id; | |
310 | } | |
311 | ||
312 | // Verify timeout first. | |
313 | $maxlifetime = $CFG->sessiontimeout; | |
314 | $timeout = false; | |
315 | if (isguestuser($userid) or empty($userid)) { | |
316 | // Ignore guest and not-logged in timeouts, there is very little risk here. | |
317 | $timeout = false; | |
318 | ||
319 | } else if ($record->timemodified < time() - $maxlifetime) { | |
320 | $timeout = true; | |
321 | $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. | |
322 | foreach ($authsequence as $authname) { | |
323 | $authplugin = get_auth_plugin($authname); | |
324 | if ($authplugin->ignore_timeout_hook($_SESSION['USER'], $record->sid, $record->timecreated, $record->timemodified)) { | |
325 | $timeout = false; | |
326 | break; | |
327 | } | |
328 | } | |
329 | } | |
330 | ||
331 | if ($timeout) { | |
332 | session_regenerate_id(true); | |
333 | $_SESSION = array(); | |
334 | $DB->delete_records('sessions', array('id'=>$record->id)); | |
335 | ||
336 | } else { | |
337 | // Update session tracking record. | |
338 | ||
339 | $update = new \stdClass(); | |
340 | $updated = false; | |
341 | ||
342 | if ($record->userid != $userid) { | |
343 | $update->userid = $record->userid = $userid; | |
344 | $updated = true; | |
345 | } | |
346 | ||
347 | $ip = getremoteaddr(); | |
348 | if ($record->lastip != $ip) { | |
349 | $update->lastip = $record->lastip = $ip; | |
350 | $updated = true; | |
351 | } | |
352 | ||
353 | $updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency; | |
354 | ||
355 | if ($record->timemodified == $record->timecreated) { | |
356 | // Always do first update of existing record. | |
357 | $update->timemodified = $record->timemodified = time(); | |
358 | $updated = true; | |
359 | ||
360 | } else if ($record->timemodified < time() - $updatefreq) { | |
361 | // Update the session modified flag only once every 20 seconds. | |
362 | $update->timemodified = $record->timemodified = time(); | |
363 | $updated = true; | |
364 | } | |
365 | ||
366 | if ($updated) { | |
367 | $update->id = $record->id; | |
368 | $DB->update_record('sessions', $update); | |
369 | } | |
370 | ||
371 | return; | |
372 | } | |
373 | } else { | |
374 | if ($record) { | |
375 | // This happens when people switch session handlers... | |
376 | session_regenerate_id(true); | |
377 | $_SESSION = array(); | |
378 | $DB->delete_records('sessions', array('id'=>$record->id)); | |
379 | } | |
380 | } | |
381 | unset($record); | |
382 | ||
383 | $timedout = false; | |
384 | if (!isset($_SESSION['SESSION'])) { | |
385 | $_SESSION['SESSION'] = new \stdClass(); | |
386 | if (!$newsid) { | |
387 | $timedout = true; | |
388 | } | |
389 | } | |
390 | ||
391 | $user = null; | |
392 | ||
393 | if (!empty($CFG->opentogoogle)) { | |
34c6ec18 | 394 | if (\core_useragent::is_web_crawler()) { |
d79d5ac2 PS |
395 | $user = guest_user(); |
396 | } | |
dcee0b94 SL |
397 | $referer = get_local_referer(false); |
398 | if (!empty($CFG->guestloginbutton) and !$user and !empty($referer)) { | |
d79d5ac2 | 399 | // Automatically log in users coming from search engine results. |
dcee0b94 | 400 | if (strpos($referer, 'google') !== false ) { |
d79d5ac2 | 401 | $user = guest_user(); |
dcee0b94 | 402 | } else if (strpos($referer, 'altavista') !== false ) { |
d79d5ac2 PS |
403 | $user = guest_user(); |
404 | } | |
405 | } | |
406 | } | |
407 | ||
408 | // Setup $USER and insert the session tracking record. | |
409 | if ($user) { | |
410 | self::set_user($user); | |
411 | self::add_session_record($user->id); | |
412 | } else { | |
413 | self::init_empty_session(); | |
414 | self::add_session_record(0); | |
415 | } | |
416 | ||
417 | if ($timedout) { | |
418 | $_SESSION['SESSION']->has_timed_out = true; | |
419 | } | |
420 | } | |
421 | ||
422 | /** | |
423 | * Insert new empty session record. | |
424 | * @param int $userid | |
425 | * @return \stdClass the new record | |
426 | */ | |
427 | protected static function add_session_record($userid) { | |
428 | global $DB; | |
429 | $record = new \stdClass(); | |
430 | $record->state = 0; | |
431 | $record->sid = session_id(); | |
432 | $record->sessdata = null; | |
433 | $record->userid = $userid; | |
434 | $record->timecreated = $record->timemodified = time(); | |
435 | $record->firstip = $record->lastip = getremoteaddr(); | |
436 | ||
437 | $record->id = $DB->insert_record('sessions', $record); | |
438 | ||
439 | return $record; | |
440 | } | |
441 | ||
442 | /** | |
443 | * Do various session security checks. | |
2e00d01d PS |
444 | * |
445 | * WARNING: $USER and $SESSION are set up later, do not use them yet! | |
1ac585fe | 446 | * @throws \core\session\exception |
d79d5ac2 PS |
447 | */ |
448 | protected static function check_security() { | |
449 | global $CFG; | |
450 | ||
451 | if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) { | |
452 | // Make sure current IP matches the one for this session. | |
453 | $remoteaddr = getremoteaddr(); | |
454 | ||
455 | if (empty($_SESSION['USER']->sessionip)) { | |
456 | $_SESSION['USER']->sessionip = $remoteaddr; | |
457 | } | |
458 | ||
459 | if ($_SESSION['USER']->sessionip != $remoteaddr) { | |
460 | // This is a security feature - terminate the session in case of any doubt. | |
461 | self::terminate_current(); | |
462 | throw new exception('sessionipnomatch2', 'error'); | |
463 | } | |
464 | } | |
465 | } | |
466 | ||
467 | /** | |
468 | * Login user, to be called from complete_user_login() only. | |
469 | * @param \stdClass $user | |
470 | */ | |
471 | public static function login_user(\stdClass $user) { | |
472 | global $DB; | |
473 | ||
474 | // Regenerate session id and delete old session, | |
475 | // this helps prevent session fixation attacks from the same domain. | |
476 | ||
477 | $sid = session_id(); | |
478 | session_regenerate_id(true); | |
479 | $DB->delete_records('sessions', array('sid'=>$sid)); | |
480 | self::add_session_record($user->id); | |
481 | ||
482 | // Let enrol plugins deal with new enrolments if necessary. | |
483 | enrol_check_plugins($user); | |
484 | ||
485 | // Setup $USER object. | |
486 | self::set_user($user); | |
487 | } | |
488 | ||
489 | /** | |
490 | * Terminate current user session. | |
491 | * @return void | |
492 | */ | |
493 | public static function terminate_current() { | |
494 | global $DB; | |
495 | ||
496 | if (!self::$sessionactive) { | |
497 | self::init_empty_session(); | |
498 | self::$sessionactive = false; | |
499 | return; | |
500 | } | |
501 | ||
502 | try { | |
503 | $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED)); | |
504 | } catch (\Exception $ignored) { | |
505 | // Probably install/upgrade - ignore this problem. | |
506 | } | |
507 | ||
508 | // Initialize variable to pass-by-reference to headers_sent(&$file, &$line). | |
509 | $file = null; | |
510 | $line = null; | |
511 | if (headers_sent($file, $line)) { | |
512 | error_log('Cannot terminate session properly - headers were already sent in file: '.$file.' on line '.$line); | |
513 | } | |
514 | ||
515 | // Write new empty session and make sure the old one is deleted. | |
516 | $sid = session_id(); | |
517 | session_regenerate_id(true); | |
518 | $DB->delete_records('sessions', array('sid'=>$sid)); | |
519 | self::init_empty_session(); | |
2e00d01d | 520 | self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet. |
d79d5ac2 PS |
521 | session_write_close(); |
522 | self::$sessionactive = false; | |
523 | } | |
524 | ||
525 | /** | |
526 | * No more changes in session expected. | |
527 | * Unblocks the sessions, other scripts may start executing in parallel. | |
528 | */ | |
529 | public static function write_close() { | |
1ac585fe FW |
530 | if (version_compare(PHP_VERSION, '5.6.0', '>=')) { |
531 | // More control over whether session data | |
532 | // is persisted or not. | |
533 | if (self::$sessionactive && session_id()) { | |
534 | // Write session and release lock only if | |
535 | // indication session start was clean. | |
536 | session_write_close(); | |
537 | } else { | |
538 | // Otherwise, if possibile lock exists want | |
539 | // to clear it, but do not write session. | |
540 | @session_abort(); | |
541 | } | |
d79d5ac2 | 542 | } else { |
1ac585fe FW |
543 | // Any indication session was started, attempt |
544 | // to close it. | |
545 | if (self::$sessionactive || session_id()) { | |
546 | session_write_close(); | |
d79d5ac2 PS |
547 | } |
548 | } | |
549 | self::$sessionactive = false; | |
550 | } | |
551 | ||
552 | /** | |
553 | * Does the PHP session with given id exist? | |
554 | * | |
c6b5f18d PS |
555 | * The session must exist both in session table and actual |
556 | * session backend and the session must not be timed out. | |
557 | * | |
558 | * Timeout evaluation is simplified, the auth hooks are not executed. | |
d79d5ac2 PS |
559 | * |
560 | * @param string $sid | |
561 | * @return bool | |
562 | */ | |
563 | public static function session_exists($sid) { | |
c6b5f18d PS |
564 | global $DB, $CFG; |
565 | ||
566 | if (empty($CFG->version)) { | |
567 | // Not installed yet, do not try to access database. | |
568 | return false; | |
569 | } | |
570 | ||
571 | // Note: add sessions->state checking here if it gets implemented. | |
572 | if (!$record = $DB->get_record('sessions', array('sid' => $sid), 'id, userid, timemodified')) { | |
573 | return false; | |
574 | } | |
575 | ||
576 | if (empty($record->userid) or isguestuser($record->userid)) { | |
577 | // Ignore guest and not-logged-in timeouts, there is very little risk here. | |
578 | } else if ($record->timemodified < time() - $CFG->sessiontimeout) { | |
579 | return false; | |
580 | } | |
581 | ||
582 | // There is no need the existence of handler storage in public API. | |
d79d5ac2 PS |
583 | self::load_handler(); |
584 | return self::$handler->session_exists($sid); | |
585 | } | |
586 | ||
587 | /** | |
588 | * Fake last access for given session, this prevents session timeout. | |
589 | * @param string $sid | |
590 | */ | |
591 | public static function touch_session($sid) { | |
592 | global $DB; | |
593 | ||
594 | // Timeouts depend on core sessions table only, no need to update anything in external stores. | |
595 | ||
596 | $sql = "UPDATE {sessions} SET timemodified = :now WHERE sid = :sid"; | |
597 | $DB->execute($sql, array('now'=>time(), 'sid'=>$sid)); | |
598 | } | |
599 | ||
600 | /** | |
601 | * Terminate all sessions unconditionally. | |
602 | */ | |
603 | public static function kill_all_sessions() { | |
604 | global $DB; | |
605 | ||
606 | self::terminate_current(); | |
607 | ||
608 | self::load_handler(); | |
609 | self::$handler->kill_all_sessions(); | |
610 | ||
611 | try { | |
612 | $DB->delete_records('sessions'); | |
613 | } catch (\dml_exception $ignored) { | |
614 | // Do not show any warnings - might be during upgrade/installation. | |
615 | } | |
616 | } | |
617 | ||
618 | /** | |
619 | * Terminate give session unconditionally. | |
620 | * @param string $sid | |
621 | */ | |
622 | public static function kill_session($sid) { | |
623 | global $DB; | |
624 | ||
625 | self::load_handler(); | |
626 | ||
627 | if ($sid === session_id()) { | |
628 | self::write_close(); | |
629 | } | |
630 | ||
631 | self::$handler->kill_session($sid); | |
632 | ||
633 | $DB->delete_records('sessions', array('sid'=>$sid)); | |
634 | } | |
635 | ||
636 | /** | |
637 | * Terminate all sessions of given user unconditionally. | |
638 | * @param int $userid | |
866f03de | 639 | * @param string $keepsid keep this sid if present |
d79d5ac2 | 640 | */ |
866f03de | 641 | public static function kill_user_sessions($userid, $keepsid = null) { |
d79d5ac2 PS |
642 | global $DB; |
643 | ||
644 | $sessions = $DB->get_records('sessions', array('userid'=>$userid), 'id DESC', 'id, sid'); | |
645 | foreach ($sessions as $session) { | |
866f03de PS |
646 | if ($keepsid and $keepsid === $session->sid) { |
647 | continue; | |
648 | } | |
d79d5ac2 PS |
649 | self::kill_session($session->sid); |
650 | } | |
651 | } | |
652 | ||
89e9321f PS |
653 | /** |
654 | * Terminate other sessions of current user depending | |
655 | * on $CFG->limitconcurrentlogins restriction. | |
656 | * | |
657 | * This is expected to be called right after complete_user_login(). | |
658 | * | |
659 | * NOTE: | |
660 | * * Do not use from SSO auth plugins, this would not work. | |
661 | * * Do not use from web services because they do not have sessions. | |
662 | * | |
663 | * @param int $userid | |
664 | * @param string $sid session id to be always keep, usually the current one | |
665 | * @return void | |
666 | */ | |
667 | public static function apply_concurrent_login_limit($userid, $sid = null) { | |
668 | global $CFG, $DB; | |
669 | ||
670 | // NOTE: the $sid parameter is here mainly to allow testing, | |
671 | // in most cases it should be current session id. | |
672 | ||
673 | if (isguestuser($userid) or empty($userid)) { | |
674 | // This applies to real users only! | |
675 | return; | |
676 | } | |
677 | ||
678 | if (empty($CFG->limitconcurrentlogins) or $CFG->limitconcurrentlogins < 0) { | |
679 | return; | |
680 | } | |
681 | ||
682 | $count = $DB->count_records('sessions', array('userid' => $userid)); | |
683 | ||
684 | if ($count <= $CFG->limitconcurrentlogins) { | |
685 | return; | |
686 | } | |
687 | ||
688 | $i = 0; | |
689 | $select = "userid = :userid"; | |
690 | $params = array('userid' => $userid); | |
691 | if ($sid) { | |
692 | if ($DB->record_exists('sessions', array('sid' => $sid, 'userid' => $userid))) { | |
693 | $select .= " AND sid <> :sid"; | |
694 | $params['sid'] = $sid; | |
695 | $i = 1; | |
696 | } | |
697 | } | |
698 | ||
699 | $sessions = $DB->get_records_select('sessions', $select, $params, 'timecreated DESC', 'id, sid'); | |
700 | foreach ($sessions as $session) { | |
701 | $i++; | |
702 | if ($i <= $CFG->limitconcurrentlogins) { | |
703 | continue; | |
704 | } | |
705 | self::kill_session($session->sid); | |
706 | } | |
707 | } | |
708 | ||
d79d5ac2 PS |
709 | /** |
710 | * Set current user. | |
711 | * | |
712 | * @param \stdClass $user record | |
713 | */ | |
714 | public static function set_user(\stdClass $user) { | |
2e00d01d PS |
715 | $GLOBALS['USER'] = $user; |
716 | unset($GLOBALS['USER']->description); // Conserve memory. | |
717 | unset($GLOBALS['USER']->password); // Improve security. | |
718 | if (isset($GLOBALS['USER']->lang)) { | |
d79d5ac2 | 719 | // Make sure it is a valid lang pack name. |
2e00d01d | 720 | $GLOBALS['USER']->lang = clean_param($GLOBALS['USER']->lang, PARAM_LANG); |
d79d5ac2 | 721 | } |
d79d5ac2 | 722 | |
2e00d01d PS |
723 | // Relink session with global $USER just in case it got unlinked somehow. |
724 | $_SESSION['USER'] =& $GLOBALS['USER']; | |
725 | ||
726 | // Init session key. | |
727 | sesskey(); | |
d79d5ac2 PS |
728 | } |
729 | ||
730 | /** | |
731 | * Periodic timed-out session cleanup. | |
732 | */ | |
733 | public static function gc() { | |
734 | global $CFG, $DB; | |
735 | ||
736 | // This may take a long time... | |
3ef7279f | 737 | \core_php_time_limit::raise(); |
d79d5ac2 PS |
738 | |
739 | $maxlifetime = $CFG->sessiontimeout; | |
740 | ||
741 | try { | |
742 | // Kill all sessions of deleted and suspended users without any hesitation. | |
743 | $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0 OR suspended <> 0)", array(), 'id DESC', 'id, sid'); | |
744 | foreach ($rs as $session) { | |
745 | self::kill_session($session->sid); | |
746 | } | |
747 | $rs->close(); | |
748 | ||
749 | // Kill sessions of users with disabled plugins. | |
750 | $auth_sequence = get_enabled_auth_plugins(true); | |
751 | $auth_sequence = array_flip($auth_sequence); | |
752 | unset($auth_sequence['nologin']); // No login means user cannot login. | |
753 | $auth_sequence = array_flip($auth_sequence); | |
754 | ||
755 | list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false); | |
756 | $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params, 'id DESC', 'id, sid'); | |
757 | foreach ($rs as $session) { | |
758 | self::kill_session($session->sid); | |
759 | } | |
760 | $rs->close(); | |
761 | ||
762 | // Now get a list of time-out candidates - real users only. | |
763 | $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified | |
764 | FROM {user} u | |
765 | JOIN {sessions} s ON s.userid = u.id | |
766 | WHERE s.timemodified < :purgebefore AND u.id <> :guestid"; | |
767 | $params = array('purgebefore' => (time() - $maxlifetime), 'guestid'=>$CFG->siteguest); | |
768 | ||
769 | $authplugins = array(); | |
770 | foreach ($auth_sequence as $authname) { | |
771 | $authplugins[$authname] = get_auth_plugin($authname); | |
772 | } | |
773 | $rs = $DB->get_recordset_sql($sql, $params); | |
774 | foreach ($rs as $user) { | |
775 | foreach ($authplugins as $authplugin) { | |
776 | /** @var \auth_plugin_base $authplugin*/ | |
777 | if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) { | |
778 | continue; | |
779 | } | |
780 | } | |
781 | self::kill_session($user->sid); | |
782 | } | |
783 | $rs->close(); | |
784 | ||
785 | // Delete expired sessions for guest user account, give them larger timeout, there is no security risk here. | |
786 | $params = array('purgebefore' => (time() - ($maxlifetime * 5)), 'guestid'=>$CFG->siteguest); | |
787 | $rs = $DB->get_recordset_select('sessions', 'userid = :guestid AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid'); | |
788 | foreach ($rs as $session) { | |
789 | self::kill_session($session->sid); | |
790 | } | |
791 | $rs->close(); | |
792 | ||
793 | // Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory. | |
794 | $params = array('purgebefore' => (time() - $maxlifetime)); | |
795 | $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid'); | |
796 | foreach ($rs as $session) { | |
797 | self::kill_session($session->sid); | |
798 | } | |
799 | $rs->close(); | |
800 | ||
801 | // Cleanup letfovers from the first browser access because it may set multiple cookies and then use only one. | |
802 | $params = array('purgebefore' => (time() - 60*3)); | |
803 | $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified = timecreated AND timemodified < :purgebefore', $params, 'id ASC', 'id, sid'); | |
804 | foreach ($rs as $session) { | |
805 | self::kill_session($session->sid); | |
806 | } | |
807 | $rs->close(); | |
808 | ||
809 | } catch (\Exception $ex) { | |
810 | debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace()); | |
811 | } | |
812 | } | |
813 | ||
814 | /** | |
815 | * Is current $USER logged-in-as somebody else? | |
816 | * @return bool | |
817 | */ | |
818 | public static function is_loggedinas() { | |
2e00d01d | 819 | return !empty($GLOBALS['USER']->realuser); |
d79d5ac2 PS |
820 | } |
821 | ||
822 | /** | |
823 | * Returns the $USER object ignoring current login-as session | |
824 | * @return \stdClass user object | |
825 | */ | |
826 | public static function get_realuser() { | |
827 | if (self::is_loggedinas()) { | |
828 | return $_SESSION['REALUSER']; | |
829 | } else { | |
2e00d01d | 830 | return $GLOBALS['USER']; |
d79d5ac2 PS |
831 | } |
832 | } | |
833 | ||
834 | /** | |
835 | * Login as another user - no security checks here. | |
836 | * @param int $userid | |
837 | * @param \context $context | |
838 | * @return void | |
839 | */ | |
840 | public static function loginas($userid, \context $context) { | |
841 | global $USER; | |
842 | ||
843 | if (self::is_loggedinas()) { | |
844 | return; | |
845 | } | |
846 | ||
2e00d01d PS |
847 | // Switch to fresh new $_SESSION. |
848 | $_SESSION = array(); | |
849 | $_SESSION['REALSESSION'] = clone($GLOBALS['SESSION']); | |
850 | $GLOBALS['SESSION'] = new \stdClass(); | |
851 | $_SESSION['SESSION'] =& $GLOBALS['SESSION']; | |
d79d5ac2 PS |
852 | |
853 | // Create the new $USER object with all details and reload needed capabilities. | |
2e00d01d | 854 | $_SESSION['REALUSER'] = clone($GLOBALS['USER']); |
d79d5ac2 PS |
855 | $user = get_complete_user_data('id', $userid); |
856 | $user->realuser = $_SESSION['REALUSER']->id; | |
857 | $user->loginascontext = $context; | |
858 | ||
859 | // Let enrol plugins deal with new enrolments if necessary. | |
860 | enrol_check_plugins($user); | |
861 | ||
862 | // Create event before $USER is updated. | |
863 | $event = \core\event\user_loggedinas::create( | |
864 | array( | |
865 | 'objectid' => $USER->id, | |
866 | 'context' => $context, | |
867 | 'relateduserid' => $userid, | |
868 | 'other' => array( | |
869 | 'originalusername' => fullname($USER, true), | |
870 | 'loggedinasusername' => fullname($user, true) | |
871 | ) | |
872 | ) | |
873 | ); | |
874 | // Set up global $USER. | |
875 | \core\session\manager::set_user($user); | |
876 | $event->trigger(); | |
877 | } | |
57996fe9 AN |
878 | |
879 | /** | |
880 | * Add a JS session keepalive to the page. | |
881 | * | |
882 | * A JS session keepalive script will be called to update the session modification time every $frequency seconds. | |
883 | * | |
884 | * Upon failure, the specified error message will be shown to the user. | |
885 | * | |
886 | * @param string $identifier The string identifier for the message to show on failure. | |
887 | * @param string $component The string component for the message to show on failure. | |
888 | * @param int $frequency The update frequency in seconds. | |
889 | * @throws coding_exception IF the frequency is longer than the session lifetime. | |
890 | */ | |
891 | public static function keepalive($identifier = 'sessionerroruser', $component = 'error', $frequency = null) { | |
892 | global $CFG, $PAGE; | |
893 | ||
894 | if ($frequency) { | |
895 | if ($frequency > $CFG->sessiontimeout) { | |
896 | // Sanity check the frequency. | |
897 | throw new \coding_exception('Keepalive frequency is longer than the session lifespan.'); | |
898 | } | |
899 | } else { | |
900 | // A frequency of sessiontimeout / 3 allows for one missed request whilst still preserving the session. | |
901 | $frequency = $CFG->sessiontimeout / 3; | |
902 | } | |
903 | ||
904 | // Add the session keepalive script to the list of page output requirements. | |
905 | $sessionkeepaliveurl = new \moodle_url('/lib/sessionkeepalive_ajax.php'); | |
906 | $PAGE->requires->string_for_js($identifier, $component); | |
907 | $PAGE->requires->yui_module('moodle-core-checknet', 'M.core.checknet.init', array(array( | |
908 | // The JS config takes this is milliseconds rather than seconds. | |
909 | 'frequency' => $frequency * 1000, | |
910 | 'message' => array($identifier, $component), | |
911 | 'uri' => $sessionkeepaliveurl->out(), | |
912 | ))); | |
913 | } | |
914 | ||
d79d5ac2 | 915 | } |