PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/classes/session/manager.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 768 lines | 460 code | 116 blank | 192 comment | 83 complexity | 4296e71ee42745826853a06e6c4f9538 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-3.0
  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. * Session manager class.
  18. *
  19. * @package core
  20. * @copyright 2013 Petr Skoda {@link http://skodak.org}
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. namespace core\session;
  24. defined('MOODLE_INTERNAL') || die();
  25. /**
  26. * Session manager, this is the public Moodle API for sessions.
  27. *
  28. * Following PHP functions MUST NOT be used directly:
  29. * - session_start() - not necessary, lib/setup.php starts session automatically,
  30. * use define('NO_MOODLE_COOKIE', true) if session not necessary.
  31. * - session_write_close() - use \core\session\manager::write_close() instead.
  32. * - session_destroy() - use require_logout() instead.
  33. *
  34. * @package core
  35. * @copyright 2013 Petr Skoda {@link http://skodak.org}
  36. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37. */
  38. class manager {
  39. /** @var handler $handler active session handler instance */
  40. protected static $handler;
  41. /** @var bool $sessionactive Is the session active? */
  42. protected static $sessionactive = null;
  43. /**
  44. * Start user session.
  45. *
  46. * Note: This is intended to be called only from lib/setup.php!
  47. */
  48. public static function start() {
  49. global $CFG, $DB;
  50. if (isset(self::$sessionactive)) {
  51. debugging('Session was already started!', DEBUG_DEVELOPER);
  52. return;
  53. }
  54. self::load_handler();
  55. // Init the session handler only if everything initialised properly in lib/setup.php file
  56. // and the session is actually required.
  57. if (empty($DB) or empty($CFG->version) or !defined('NO_MOODLE_COOKIES') or NO_MOODLE_COOKIES or CLI_SCRIPT) {
  58. self::$sessionactive = false;
  59. self::init_empty_session();
  60. return;
  61. }
  62. try {
  63. self::$handler->init();
  64. self::prepare_cookies();
  65. $newsid = empty($_COOKIE[session_name()]);
  66. self::$handler->start();
  67. self::initialise_user_session($newsid);
  68. self::check_security();
  69. // Link global $USER and $SESSION,
  70. // this is tricky because PHP does not allow references to references
  71. // and global keyword uses internally once reference to the $GLOBALS array.
  72. // The solution is to use the $GLOBALS['USER'] and $GLOBALS['$SESSION']
  73. // as the main storage of data and put references to $_SESSION.
  74. $GLOBALS['USER'] = $_SESSION['USER'];
  75. $_SESSION['USER'] =& $GLOBALS['USER'];
  76. $GLOBALS['SESSION'] = $_SESSION['SESSION'];
  77. $_SESSION['SESSION'] =& $GLOBALS['SESSION'];
  78. } catch (\Exception $ex) {
  79. @session_write_close();
  80. self::init_empty_session();
  81. self::$sessionactive = false;
  82. throw $ex;
  83. }
  84. self::$sessionactive = true;
  85. }
  86. /**
  87. * Returns current page performance info.
  88. *
  89. * @return array perf info
  90. */
  91. public static function get_performance_info() {
  92. if (!session_id()) {
  93. return array();
  94. }
  95. self::load_handler();
  96. $size = display_size(strlen(session_encode()));
  97. $handler = get_class(self::$handler);
  98. $info = array();
  99. $info['size'] = $size;
  100. $info['html'] = "<span class=\"sessionsize\">Session ($handler): $size</span> ";
  101. $info['txt'] = "Session ($handler): $size ";
  102. return $info;
  103. }
  104. /**
  105. * Create handler instance.
  106. */
  107. protected static function load_handler() {
  108. global $CFG, $DB;
  109. if (self::$handler) {
  110. return;
  111. }
  112. // Find out which handler to use.
  113. if (PHPUNIT_TEST) {
  114. $class = '\core\session\file';
  115. } else if (!empty($CFG->session_handler_class)) {
  116. $class = $CFG->session_handler_class;
  117. } else if (!empty($CFG->dbsessions) and $DB->session_lock_supported()) {
  118. $class = '\core\session\database';
  119. } else {
  120. $class = '\core\session\file';
  121. }
  122. self::$handler = new $class();
  123. }
  124. /**
  125. * Empty current session, fill it with not-logged-in user info.
  126. *
  127. * This is intended for installation scripts, unit tests and other
  128. * special areas. Do NOT use for logout and session termination
  129. * in normal requests!
  130. */
  131. public static function init_empty_session() {
  132. global $CFG;
  133. $GLOBALS['SESSION'] = new \stdClass();
  134. $GLOBALS['USER'] = new \stdClass();
  135. $GLOBALS['USER']->id = 0;
  136. if (isset($CFG->mnet_localhost_id)) {
  137. $GLOBALS['USER']->mnethostid = $CFG->mnet_localhost_id;
  138. } else {
  139. // Not installed yet, the future host id will be most probably 1.
  140. $GLOBALS['USER']->mnethostid = 1;
  141. }
  142. // Link global $USER and $SESSION.
  143. $_SESSION = array();
  144. $_SESSION['USER'] =& $GLOBALS['USER'];
  145. $_SESSION['SESSION'] =& $GLOBALS['SESSION'];
  146. }
  147. /**
  148. * Make sure all cookie and session related stuff is configured properly before session start.
  149. */
  150. protected static function prepare_cookies() {
  151. global $CFG;
  152. if (!isset($CFG->cookiesecure) or (strpos($CFG->wwwroot, 'https://') !== 0 and empty($CFG->sslproxy))) {
  153. $CFG->cookiesecure = 0;
  154. }
  155. if (!isset($CFG->cookiehttponly)) {
  156. $CFG->cookiehttponly = 0;
  157. }
  158. // Set sessioncookie variable if it isn't already.
  159. if (!isset($CFG->sessioncookie)) {
  160. $CFG->sessioncookie = '';
  161. }
  162. $sessionname = 'MoodleSession'.$CFG->sessioncookie;
  163. // Make sure cookie domain makes sense for this wwwroot.
  164. if (!isset($CFG->sessioncookiedomain)) {
  165. $CFG->sessioncookiedomain = '';
  166. } else if ($CFG->sessioncookiedomain !== '') {
  167. $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
  168. if ($CFG->sessioncookiedomain !== $host) {
  169. if (substr($CFG->sessioncookiedomain, 0, 1) === '.') {
  170. if (!preg_match('|^.*'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
  171. // Invalid domain - it must be end part of host.
  172. $CFG->sessioncookiedomain = '';
  173. }
  174. } else {
  175. if (!preg_match('|^.*\.'.preg_quote($CFG->sessioncookiedomain, '|').'$|', $host)) {
  176. // Invalid domain - it must be end part of host.
  177. $CFG->sessioncookiedomain = '';
  178. }
  179. }
  180. }
  181. }
  182. // Make sure the cookiepath is valid for this wwwroot or autodetect if not specified.
  183. if (!isset($CFG->sessioncookiepath)) {
  184. $CFG->sessioncookiepath = '';
  185. }
  186. if ($CFG->sessioncookiepath !== '/') {
  187. $path = parse_url($CFG->wwwroot, PHP_URL_PATH).'/';
  188. if ($CFG->sessioncookiepath === '') {
  189. $CFG->sessioncookiepath = $path;
  190. } else {
  191. if (strpos($path, $CFG->sessioncookiepath) !== 0 or substr($CFG->sessioncookiepath, -1) !== '/') {
  192. $CFG->sessioncookiepath = $path;
  193. }
  194. }
  195. }
  196. // Discard session ID from POST, GET and globals to tighten security,
  197. // this is session fixation prevention.
  198. unset($GLOBALS[$sessionname]);
  199. unset($_GET[$sessionname]);
  200. unset($_POST[$sessionname]);
  201. unset($_REQUEST[$sessionname]);
  202. // Compatibility hack for non-browser access to our web interface.
  203. if (!empty($_COOKIE[$sessionname]) && $_COOKIE[$sessionname] == "deleted") {
  204. unset($_COOKIE[$sessionname]);
  205. }
  206. // Set configuration.
  207. session_name($sessionname);
  208. session_set_cookie_params(0, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, $CFG->cookiesecure, $CFG->cookiehttponly);
  209. ini_set('session.use_trans_sid', '0');
  210. ini_set('session.use_only_cookies', '1');
  211. ini_set('session.hash_function', '0'); // For now MD5 - we do not have room for sha-1 in sessions table.
  212. ini_set('session.use_strict_mode', '0'); // We have custom protection in session init.
  213. ini_set('session.serialize_handler', 'php'); // We can move to 'php_serialize' after we require PHP 5.5.4 form Moodle.
  214. // Moodle does normal session timeouts, this is for leftovers only.
  215. ini_set('session.gc_probability', 1);
  216. ini_set('session.gc_divisor', 1000);
  217. ini_set('session.gc_maxlifetime', 60*60*24*4);
  218. }
  219. /**
  220. * Initialise $_SESSION, handles google access
  221. * and sets up not-logged-in user properly.
  222. *
  223. * WARNING: $USER and $SESSION are set up later, do not use them yet!
  224. *
  225. * @param bool $newsid is this a new session in first http request?
  226. */
  227. protected static function initialise_user_session($newsid) {
  228. global $CFG, $DB;
  229. $sid = session_id();
  230. if (!$sid) {
  231. // No session, very weird.
  232. error_log('Missing session ID, session not started!');
  233. self::init_empty_session();
  234. return;
  235. }
  236. if (!$record = $DB->get_record('sessions', array('sid'=>$sid), 'id, sid, state, userid, lastip, timecreated, timemodified')) {
  237. if (!$newsid) {
  238. if (!empty($_SESSION['USER']->id)) {
  239. // This should not happen, just log it, we MUST not produce any output here!
  240. error_log("Cannot find session record $sid for user ".$_SESSION['USER']->id.", creating new session.");
  241. }
  242. // Prevent session fixation attacks.
  243. session_regenerate_id(true);
  244. }
  245. $_SESSION = array();
  246. }
  247. unset($sid);
  248. if (isset($_SESSION['USER']->id)) {
  249. if (!empty($_SESSION['USER']->realuser)) {
  250. $userid = $_SESSION['USER']->realuser;
  251. } else {
  252. $userid = $_SESSION['USER']->id;
  253. }
  254. // Verify timeout first.
  255. $maxlifetime = $CFG->sessiontimeout;
  256. $timeout = false;
  257. if (isguestuser($userid) or empty($userid)) {
  258. // Ignore guest and not-logged in timeouts, there is very little risk here.
  259. $timeout = false;
  260. } else if ($record->timemodified < time() - $maxlifetime) {
  261. $timeout = true;
  262. $authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
  263. foreach ($authsequence as $authname) {
  264. $authplugin = get_auth_plugin($authname);
  265. if ($authplugin->ignore_timeout_hook($_SESSION['USER'], $record->sid, $record->timecreated, $record->timemodified)) {
  266. $timeout = false;
  267. break;
  268. }
  269. }
  270. }
  271. if ($timeout) {
  272. session_regenerate_id(true);
  273. $_SESSION = array();
  274. $DB->delete_records('sessions', array('id'=>$record->id));
  275. } else {
  276. // Update session tracking record.
  277. $update = new \stdClass();
  278. $updated = false;
  279. if ($record->userid != $userid) {
  280. $update->userid = $record->userid = $userid;
  281. $updated = true;
  282. }
  283. $ip = getremoteaddr();
  284. if ($record->lastip != $ip) {
  285. $update->lastip = $record->lastip = $ip;
  286. $updated = true;
  287. }
  288. $updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency;
  289. if ($record->timemodified == $record->timecreated) {
  290. // Always do first update of existing record.
  291. $update->timemodified = $record->timemodified = time();
  292. $updated = true;
  293. } else if ($record->timemodified < time() - $updatefreq) {
  294. // Update the session modified flag only once every 20 seconds.
  295. $update->timemodified = $record->timemodified = time();
  296. $updated = true;
  297. }
  298. if ($updated) {
  299. $update->id = $record->id;
  300. $DB->update_record('sessions', $update);
  301. }
  302. return;
  303. }
  304. } else {
  305. if ($record) {
  306. // This happens when people switch session handlers...
  307. session_regenerate_id(true);
  308. $_SESSION = array();
  309. $DB->delete_records('sessions', array('id'=>$record->id));
  310. }
  311. }
  312. unset($record);
  313. $timedout = false;
  314. if (!isset($_SESSION['SESSION'])) {
  315. $_SESSION['SESSION'] = new \stdClass();
  316. if (!$newsid) {
  317. $timedout = true;
  318. }
  319. }
  320. $user = null;
  321. if (!empty($CFG->opentogoogle)) {
  322. if (is_web_crawler()) {
  323. $user = guest_user();
  324. }
  325. if (!empty($CFG->guestloginbutton) and !$user and !empty($_SERVER['HTTP_REFERER'])) {
  326. // Automatically log in users coming from search engine results.
  327. if (strpos($_SERVER['HTTP_REFERER'], 'google') !== false ) {
  328. $user = guest_user();
  329. } else if (strpos($_SERVER['HTTP_REFERER'], 'altavista') !== false ) {
  330. $user = guest_user();
  331. }
  332. }
  333. }
  334. // Setup $USER and insert the session tracking record.
  335. if ($user) {
  336. self::set_user($user);
  337. self::add_session_record($user->id);
  338. } else {
  339. self::init_empty_session();
  340. self::add_session_record(0);
  341. }
  342. if ($timedout) {
  343. $_SESSION['SESSION']->has_timed_out = true;
  344. }
  345. }
  346. /**
  347. * Insert new empty session record.
  348. * @param int $userid
  349. * @return \stdClass the new record
  350. */
  351. protected static function add_session_record($userid) {
  352. global $DB;
  353. $record = new \stdClass();
  354. $record->state = 0;
  355. $record->sid = session_id();
  356. $record->sessdata = null;
  357. $record->userid = $userid;
  358. $record->timecreated = $record->timemodified = time();
  359. $record->firstip = $record->lastip = getremoteaddr();
  360. $record->id = $DB->insert_record('sessions', $record);
  361. return $record;
  362. }
  363. /**
  364. * Do various session security checks.
  365. *
  366. * WARNING: $USER and $SESSION are set up later, do not use them yet!
  367. */
  368. protected static function check_security() {
  369. global $CFG;
  370. if (!empty($_SESSION['USER']->id) and !empty($CFG->tracksessionip)) {
  371. // Make sure current IP matches the one for this session.
  372. $remoteaddr = getremoteaddr();
  373. if (empty($_SESSION['USER']->sessionip)) {
  374. $_SESSION['USER']->sessionip = $remoteaddr;
  375. }
  376. if ($_SESSION['USER']->sessionip != $remoteaddr) {
  377. // This is a security feature - terminate the session in case of any doubt.
  378. self::terminate_current();
  379. throw new exception('sessionipnomatch2', 'error');
  380. }
  381. }
  382. }
  383. /**
  384. * Login user, to be called from complete_user_login() only.
  385. * @param \stdClass $user
  386. */
  387. public static function login_user(\stdClass $user) {
  388. global $DB;
  389. // Regenerate session id and delete old session,
  390. // this helps prevent session fixation attacks from the same domain.
  391. $sid = session_id();
  392. session_regenerate_id(true);
  393. $DB->delete_records('sessions', array('sid'=>$sid));
  394. self::add_session_record($user->id);
  395. // Let enrol plugins deal with new enrolments if necessary.
  396. enrol_check_plugins($user);
  397. // Setup $USER object.
  398. self::set_user($user);
  399. }
  400. /**
  401. * Terminate current user session.
  402. * @return void
  403. */
  404. public static function terminate_current() {
  405. global $DB;
  406. if (!self::$sessionactive) {
  407. self::init_empty_session();
  408. self::$sessionactive = false;
  409. return;
  410. }
  411. try {
  412. $DB->delete_records('external_tokens', array('sid'=>session_id(), 'tokentype'=>EXTERNAL_TOKEN_EMBEDDED));
  413. } catch (\Exception $ignored) {
  414. // Probably install/upgrade - ignore this problem.
  415. }
  416. // Initialize variable to pass-by-reference to headers_sent(&$file, &$line).
  417. $file = null;
  418. $line = null;
  419. if (headers_sent($file, $line)) {
  420. error_log('Cannot terminate session properly - headers were already sent in file: '.$file.' on line '.$line);
  421. }
  422. // Write new empty session and make sure the old one is deleted.
  423. $sid = session_id();
  424. session_regenerate_id(true);
  425. $DB->delete_records('sessions', array('sid'=>$sid));
  426. self::init_empty_session();
  427. self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
  428. session_write_close();
  429. self::$sessionactive = false;
  430. }
  431. /**
  432. * No more changes in session expected.
  433. * Unblocks the sessions, other scripts may start executing in parallel.
  434. */
  435. public static function write_close() {
  436. if (self::$sessionactive) {
  437. session_write_close();
  438. } else {
  439. if (session_id()) {
  440. @session_write_close();
  441. }
  442. }
  443. self::$sessionactive = false;
  444. }
  445. /**
  446. * Does the PHP session with given id exist?
  447. *
  448. * Note: this does not actually verify the presence of sessions record.
  449. *
  450. * @param string $sid
  451. * @return bool
  452. */
  453. public static function session_exists($sid) {
  454. self::load_handler();
  455. return self::$handler->session_exists($sid);
  456. }
  457. /**
  458. * Fake last access for given session, this prevents session timeout.
  459. * @param string $sid
  460. */
  461. public static function touch_session($sid) {
  462. global $DB;
  463. // Timeouts depend on core sessions table only, no need to update anything in external stores.
  464. $sql = "UPDATE {sessions} SET timemodified = :now WHERE sid = :sid";
  465. $DB->execute($sql, array('now'=>time(), 'sid'=>$sid));
  466. }
  467. /**
  468. * Terminate all sessions unconditionally.
  469. */
  470. public static function kill_all_sessions() {
  471. global $DB;
  472. self::terminate_current();
  473. self::load_handler();
  474. self::$handler->kill_all_sessions();
  475. try {
  476. $DB->delete_records('sessions');
  477. } catch (\dml_exception $ignored) {
  478. // Do not show any warnings - might be during upgrade/installation.
  479. }
  480. }
  481. /**
  482. * Terminate give session unconditionally.
  483. * @param string $sid
  484. */
  485. public static function kill_session($sid) {
  486. global $DB;
  487. self::load_handler();
  488. if ($sid === session_id()) {
  489. self::write_close();
  490. }
  491. self::$handler->kill_session($sid);
  492. $DB->delete_records('sessions', array('sid'=>$sid));
  493. }
  494. /**
  495. * Terminate all sessions of given user unconditionally.
  496. * @param int $userid
  497. */
  498. public static function kill_user_sessions($userid) {
  499. global $DB;
  500. $sessions = $DB->get_records('sessions', array('userid'=>$userid), 'id DESC', 'id, sid');
  501. foreach ($sessions as $session) {
  502. self::kill_session($session->sid);
  503. }
  504. }
  505. /**
  506. * Set current user.
  507. *
  508. * @param \stdClass $user record
  509. */
  510. public static function set_user(\stdClass $user) {
  511. $GLOBALS['USER'] = $user;
  512. unset($GLOBALS['USER']->description); // Conserve memory.
  513. unset($GLOBALS['USER']->password); // Improve security.
  514. if (isset($GLOBALS['USER']->lang)) {
  515. // Make sure it is a valid lang pack name.
  516. $GLOBALS['USER']->lang = clean_param($GLOBALS['USER']->lang, PARAM_LANG);
  517. }
  518. // Relink session with global $USER just in case it got unlinked somehow.
  519. $_SESSION['USER'] =& $GLOBALS['USER'];
  520. // Init session key.
  521. sesskey();
  522. }
  523. /**
  524. * Periodic timed-out session cleanup.
  525. */
  526. public static function gc() {
  527. global $CFG, $DB;
  528. // This may take a long time...
  529. set_time_limit(0);
  530. $maxlifetime = $CFG->sessiontimeout;
  531. try {
  532. // Kill all sessions of deleted and suspended users without any hesitation.
  533. $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0 OR suspended <> 0)", array(), 'id DESC', 'id, sid');
  534. foreach ($rs as $session) {
  535. self::kill_session($session->sid);
  536. }
  537. $rs->close();
  538. // Kill sessions of users with disabled plugins.
  539. $auth_sequence = get_enabled_auth_plugins(true);
  540. $auth_sequence = array_flip($auth_sequence);
  541. unset($auth_sequence['nologin']); // No login means user cannot login.
  542. $auth_sequence = array_flip($auth_sequence);
  543. list($notplugins, $params) = $DB->get_in_or_equal($auth_sequence, SQL_PARAMS_QM, '', false);
  544. $rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params, 'id DESC', 'id, sid');
  545. foreach ($rs as $session) {
  546. self::kill_session($session->sid);
  547. }
  548. $rs->close();
  549. // Now get a list of time-out candidates - real users only.
  550. $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified
  551. FROM {user} u
  552. JOIN {sessions} s ON s.userid = u.id
  553. WHERE s.timemodified < :purgebefore AND u.id <> :guestid";
  554. $params = array('purgebefore' => (time() - $maxlifetime), 'guestid'=>$CFG->siteguest);
  555. $authplugins = array();
  556. foreach ($auth_sequence as $authname) {
  557. $authplugins[$authname] = get_auth_plugin($authname);
  558. }
  559. $rs = $DB->get_recordset_sql($sql, $params);
  560. foreach ($rs as $user) {
  561. foreach ($authplugins as $authplugin) {
  562. /** @var \auth_plugin_base $authplugin*/
  563. if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
  564. continue;
  565. }
  566. }
  567. self::kill_session($user->sid);
  568. }
  569. $rs->close();
  570. // Delete expired sessions for guest user account, give them larger timeout, there is no security risk here.
  571. $params = array('purgebefore' => (time() - ($maxlifetime * 5)), 'guestid'=>$CFG->siteguest);
  572. $rs = $DB->get_recordset_select('sessions', 'userid = :guestid AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
  573. foreach ($rs as $session) {
  574. self::kill_session($session->sid);
  575. }
  576. $rs->close();
  577. // Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory.
  578. $params = array('purgebefore' => (time() - $maxlifetime));
  579. $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
  580. foreach ($rs as $session) {
  581. self::kill_session($session->sid);
  582. }
  583. $rs->close();
  584. // Cleanup letfovers from the first browser access because it may set multiple cookies and then use only one.
  585. $params = array('purgebefore' => (time() - 60*3));
  586. $rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified = timecreated AND timemodified < :purgebefore', $params, 'id ASC', 'id, sid');
  587. foreach ($rs as $session) {
  588. self::kill_session($session->sid);
  589. }
  590. $rs->close();
  591. } catch (\Exception $ex) {
  592. debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace());
  593. }
  594. }
  595. /**
  596. * Is current $USER logged-in-as somebody else?
  597. * @return bool
  598. */
  599. public static function is_loggedinas() {
  600. return !empty($GLOBALS['USER']->realuser);
  601. }
  602. /**
  603. * Returns the $USER object ignoring current login-as session
  604. * @return \stdClass user object
  605. */
  606. public static function get_realuser() {
  607. if (self::is_loggedinas()) {
  608. return $_SESSION['REALUSER'];
  609. } else {
  610. return $GLOBALS['USER'];
  611. }
  612. }
  613. /**
  614. * Login as another user - no security checks here.
  615. * @param int $userid
  616. * @param \context $context
  617. * @return void
  618. */
  619. public static function loginas($userid, \context $context) {
  620. global $USER;
  621. if (self::is_loggedinas()) {
  622. return;
  623. }
  624. // Switch to fresh new $_SESSION.
  625. $_SESSION = array();
  626. $_SESSION['REALSESSION'] = clone($GLOBALS['SESSION']);
  627. $GLOBALS['SESSION'] = new \stdClass();
  628. $_SESSION['SESSION'] =& $GLOBALS['SESSION'];
  629. // Create the new $USER object with all details and reload needed capabilities.
  630. $_SESSION['REALUSER'] = clone($GLOBALS['USER']);
  631. $user = get_complete_user_data('id', $userid);
  632. $user->realuser = $_SESSION['REALUSER']->id;
  633. $user->loginascontext = $context;
  634. // Let enrol plugins deal with new enrolments if necessary.
  635. enrol_check_plugins($user);
  636. // Create event before $USER is updated.
  637. $event = \core\event\user_loggedinas::create(
  638. array(
  639. 'objectid' => $USER->id,
  640. 'context' => $context,
  641. 'relateduserid' => $userid,
  642. 'other' => array(
  643. 'originalusername' => fullname($USER, true),
  644. 'loggedinasusername' => fullname($user, true)
  645. )
  646. )
  647. );
  648. // Set up global $USER.
  649. \core\session\manager::set_user($user);
  650. $event->trigger();
  651. }
  652. }