. namespace core\session; use core\clock; use core\di; use stdClass; /** * Session handler base. * * @package core * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class handler { /** @var boolean $requireswritelock does the session need and/or have a lock? */ protected $requireswritelock = false; /** * Start the session. * @return bool success */ public function start() { return session_start(); } /** * Write the session and release lock. If the session was not intentionally opened * with a write lock, then we will abort the session instead if able. */ public function write_close() { if ($this->requires_write_lock()) { session_write_close(); $this->requireswritelock = false; } else { $this->abort(); } } /** * Release lock on the session without writing it. * May not be possible in older versions of PHP. If so, session may be written anyway * so that any locks are released. */ public function abort() { session_abort(); $this->requireswritelock = false; } /** * This is called after init() and before start() to indicate whether the session * opened should be writable or not. This is intentionally captured even if your * handler doesn't support non-locking sessions, so that behavior (upon session close) * matches closely between handlers. * @param bool $requireswritelock true if needs to be open for writing */ public function set_requires_write_lock($requireswritelock) { $this->requireswritelock = $requireswritelock; } /** * Returns all session records. * * @return \Iterator */ public function get_all_sessions(): \Iterator { global $DB; $rs = $DB->get_recordset('sessions'); foreach ($rs as $row) { yield $row; } $rs->close(); } /** * Returns a single session record for this session id. * * @param string $sid * @return stdClass */ public function get_session_by_sid(string $sid): stdClass { global $DB; return $DB->get_record('sessions', ['sid' => $sid]) ?: new stdClass(); } /** * Returns all the session records for this user id. * * @param int $userid * @return array */ public function get_sessions_by_userid(int $userid): array { global $DB; return $DB->get_records('sessions', ['userid' => $userid]); } /** * Insert new empty session record. * * @param int $userid * @return stdClass the new record */ public function add_session(int $userid): stdClass { global $DB; $record = new stdClass(); $record->state = 0; $record->sid = session_id(); $record->sessdata = null; $record->userid = $userid; $record->timecreated = $record->timemodified = di::get(clock::class)->time(); $record->firstip = $record->lastip = getremoteaddr(); $record->id = $DB->insert_record('sessions', $record); return $record; } /** * Update a session record. * * @param stdClass $record * @return bool */ public function update_session(stdClass $record): bool { global $DB; if (!isset($record->id) && isset($record->sid)) { $record->id = $DB->get_field('sessions', 'id', ['sid' => $record->sid]); } return $DB->update_record('sessions', $record); } /** * Destroy a specific session and delete this session record for this session id. * * @param string $id session id * @return bool */ public function destroy(string $id): bool { global $DB; return $DB->delete_records('sessions', ['sid' => $id]); } /** * Destroy all sessions, and delete all the session data. * * @return bool */ public function destroy_all(): bool { global $DB; return $DB->delete_records('sessions'); } /** * Clean up expired sessions. * * @param int $purgebefore Sessions that have not updated for the last purgebefore timestamp will be removed. * @param int $userid */ protected function destroy_expired_user_sessions(int $purgebefore, int $userid): void { $sessions = $this->get_sessions_by_userid($userid); foreach ($sessions as $session) { if ($session->timemodified < $purgebefore) { $this->destroy($session->sid); } } } /** * Clean up all expired sessions. * * @param int $purgebefore */ protected function destroy_all_expired_sessions(int $purgebefore): void { global $DB, $CFG; $authsequence = get_enabled_auth_plugins(); $authsequence = array_flip($authsequence); unset($authsequence['nologin']); // No login means user cannot login. $authsequence = array_flip($authsequence); $authplugins = []; foreach ($authsequence as $authname) { $authplugins[$authname] = get_auth_plugin($authname); } $sql = "SELECT u.*, s.sid, s.timecreated AS s_timecreated, s.timemodified AS s_timemodified FROM {user} u JOIN {sessions} s ON s.userid = u.id WHERE s.timemodified < :purgebefore AND u.id <> :guestid"; $params = ['purgebefore' => $purgebefore, 'guestid' => $CFG->siteguest]; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $user) { foreach ($authplugins as $authplugin) { if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) { continue 2; } } $this->destroy($user->sid); } $rs->close(); } /** * Destroy all sessions for a given plugin. * Typically used when a plugin is disabled or uninstalled, so all sessions (users) for that plugin are logged out. * * @param string $pluginname Auth plugin name. */ public function destroy_by_auth_plugin(string $pluginname): void { global $DB; $rs = $DB->get_recordset('user', ['auth' => $pluginname], 'id ASC', 'id'); foreach ($rs as $user) { $sessions = $this->get_sessions_by_userid($user->id); foreach ($sessions as $session) { $this->destroy($session->sid); } } $rs->close(); } // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameUnderscore /** * Periodic timed-out session cleanup. * * @param int $max_lifetime Sessions that have not updated for the last max_lifetime seconds will be removed. * @return int|false Number of deleted sessions or false if an error occurred. */ public function gc(int $max_lifetime = 0): int|false { global $CFG; // This may take a long time. \core_php_time_limit::raise(); if ($max_lifetime === 0) { $max_lifetime = (int) $CFG->sessiontimeout; } try { // Calculate the timestamp before which sessions are considered expired. $purgebefore = di::get(clock::class)->time() - $max_lifetime; // Delete expired sessions for guest user account. $this->destroy_expired_user_sessions($purgebefore, $CFG->siteguest); // Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory. $this->destroy_expired_user_sessions($purgebefore, 0); // Clean up expired sessions for real users only. $this->destroy_all_expired_sessions($purgebefore); // Cleanup leftovers from the first browser access because it may set multiple cookies and then use only one. $purgebefore = di::get(clock::class)->time() - (60 * 3); $sessions = $this->get_sessions_by_userid(0); foreach ($sessions as $session) { if ($session->timemodified == $session->timecreated && $session->timemodified < $purgebefore) { $this->destroy($session->sid); } } } catch (\Exception $ex) { debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace()); } return 0; } // phpcs:enable /** * Has this session been opened with a writelock? Your handler should call this during * start() if you support read-only sessions. * @return bool true if session is intended to have a write lock. */ public function requires_write_lock() { return $this->requireswritelock; } /** * Init session handler. */ abstract public function init(); /** * Check the backend contains data for this session id. * * Note: this is intended to be called from manager::session_exists() only. * * @param string $sid * @return bool true if session found. */ abstract public function session_exists($sid); }