Merge branch 'main_MDL-66151' of https://github.com/mattporritt/moodle
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
issueNumber: MDL-66151
|
||||
notes:
|
||||
core_role:
|
||||
- message: |
|
||||
Move all session management to the \core\session\manager class.
|
||||
This removes the dependancy to use the "sessions" table.
|
||||
Session management plugins (like redis) now need to inherit
|
||||
the base \core\session\handler class which implements
|
||||
SessionHandlerInterface and override methods as required.
|
||||
The following methods in \core\session\manager have been deprecated:
|
||||
* kill_all_sessions use destroy_all instead
|
||||
* kill_session use destroy instead
|
||||
* kill_sessions_for_auth_plugin use destroy_by_auth_plugin instead
|
||||
* kill_user_sessions use destroy_user_sessions instead
|
||||
type: improved
|
||||
@@ -79,7 +79,7 @@ if ($form->is_cancelled()) {
|
||||
$DB->set_field('tool_mfa', 'revoked', 1, ['userid' => $user->id, 'factor' => 'email']);
|
||||
|
||||
// Remotely logout all sessions for user.
|
||||
$manager = \core\session\manager::kill_user_sessions($instance->userid);
|
||||
\core\session\manager::destroy_user_sessions($instance->userid);
|
||||
|
||||
// Log event.
|
||||
$ip = $instance->createdfromip;
|
||||
|
||||
@@ -931,7 +931,7 @@ class process {
|
||||
}
|
||||
|
||||
if ($dologout) {
|
||||
\core\session\manager::kill_user_sessions($existinguser->id);
|
||||
\core\session\manager::destroy_user_sessions($existinguser->id);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@
|
||||
if (!is_siteadmin($user) and $USER->id != $user->id and $user->suspended != 1) {
|
||||
$user->suspended = 1;
|
||||
// Force logout.
|
||||
\core\session\manager::kill_user_sessions($user->id);
|
||||
\core\session\manager::destroy_user_sessions($user->id);
|
||||
user_update_user($user, false);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -839,7 +839,7 @@ class auth_plugin_ldap extends auth_plugin_base {
|
||||
$updateuser->suspended = 1;
|
||||
user_update_user($updateuser, false);
|
||||
echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n";
|
||||
\core\session\manager::kill_user_sessions($user->id);
|
||||
\core\session\manager::destroy_user_sessions($user->id);
|
||||
}
|
||||
} else {
|
||||
print_string('nouserentriestoremove', 'auth_ldap');
|
||||
|
||||
+3
-3
@@ -868,7 +868,7 @@ class auth_plugin_mnet extends auth_plugin_base {
|
||||
array('useragent'=>$useragent, 'userid'=>$userid));
|
||||
|
||||
if (isset($remoteclient) && isset($remoteclient->id)) {
|
||||
\core\session\manager::kill_user_sessions($userid);
|
||||
\core\session\manager::destroy_user_sessions($userid);
|
||||
}
|
||||
return $returnstring;
|
||||
}
|
||||
@@ -888,7 +888,7 @@ class auth_plugin_mnet extends auth_plugin_base {
|
||||
$session = $DB->get_record('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
|
||||
$DB->delete_records('mnet_session', array('username'=>$username, 'mnethostid'=>$remoteclient->id, 'useragent'=>$useragent));
|
||||
if (false != $session) {
|
||||
\core\session\manager::kill_session($session->session_id);
|
||||
\core\session\manager::destroy($session->session_id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -905,7 +905,7 @@ class auth_plugin_mnet extends auth_plugin_base {
|
||||
global $CFG;
|
||||
if (is_array($sessionArray)) {
|
||||
while($session = array_pop($sessionArray)) {
|
||||
\core\session\manager::kill_session($session->session_id);
|
||||
\core\session\manager::destroy($session->session_id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class helper {
|
||||
// If there is a match, kill the session.
|
||||
if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) {
|
||||
// Delete this user's sessions.
|
||||
\core\session\manager::kill_user_sessions($session->userid);
|
||||
\core\session\manager::destroy_user_sessions($session->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-2
@@ -604,7 +604,8 @@ class cachestore_redis extends store implements
|
||||
* @return bool True if the lock was acquired, false if it was not.
|
||||
*/
|
||||
public function acquire_lock($key, $ownerid) {
|
||||
$timelimit = time() + $this->lockwait;
|
||||
$clock = \core\di::get(\core\clock::class);
|
||||
$timelimit = $clock->time() + $this->lockwait;
|
||||
do {
|
||||
// If the key doesn't already exist, grab it and return true.
|
||||
if ($this->redis->setnx($key, $ownerid)) {
|
||||
@@ -620,7 +621,7 @@ class cachestore_redis extends store implements
|
||||
}
|
||||
// Wait 1 second then retry.
|
||||
sleep(1);
|
||||
} while (time() < $timelimit);
|
||||
} while ($clock->time() < $timelimit);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1163,11 +1163,11 @@ EOF;
|
||||
if (empty($sid)) {
|
||||
throw new coding_exception('failed to get moodle session');
|
||||
}
|
||||
$userid = $DB->get_field('sessions', 'userid', ['sid' => $sid]);
|
||||
if (empty($userid)) {
|
||||
throw new coding_exception('failed to get user from seession id '.$sid);
|
||||
$session = \core\session\manager::get_session_by_sid($sid);
|
||||
if (empty($session->userid)) {
|
||||
throw new coding_exception('failed to get user from session id: '.$sid);
|
||||
}
|
||||
return $DB->get_record('user', ['id' => $userid]);
|
||||
return $DB->get_record('user', ['id' => $session->userid]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -87,6 +87,7 @@ class auth extends base {
|
||||
$new = implode(',', array_flip($plugins));
|
||||
add_to_config_log('auth', $CFG->auth, $new, 'core');
|
||||
set_config('auth', $new);
|
||||
\core\session\manager::destroy_by_auth_plugin($pluginname);
|
||||
// Remove stale sessions.
|
||||
\core\session\manager::gc();
|
||||
// Reset caches.
|
||||
@@ -160,6 +161,7 @@ class auth extends base {
|
||||
$value = implode(',', $auths);
|
||||
add_to_config_log('auth', $CFG->auth, $value, 'core');
|
||||
set_config('auth', $value);
|
||||
\core\session\manager::destroy_by_auth_plugin($this->name);
|
||||
}
|
||||
|
||||
if (!empty($CFG->registerauth) and $CFG->registerauth === $this->name) {
|
||||
|
||||
@@ -14,14 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Database based session handler.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2013 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
use SessionHandlerInterface;
|
||||
@@ -34,7 +26,8 @@ use SessionHandlerInterface;
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class database extends handler implements SessionHandlerInterface {
|
||||
/** @var \stdClass $record session record */
|
||||
|
||||
/** @var int $record session record */
|
||||
protected $recordid = null;
|
||||
|
||||
/** @var \moodle_database $database session database */
|
||||
@@ -62,9 +55,7 @@ class database extends handler implements SessionHandlerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init session handler.
|
||||
*/
|
||||
#[\Override]
|
||||
public function init() {
|
||||
if (!$this->database->session_lock_supported()) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null, 'Database does not support session locking');
|
||||
@@ -76,35 +67,36 @@ class database extends handler implements SessionHandlerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[\Override]
|
||||
public function session_exists($sid) {
|
||||
// It was already checked in the calling code that the record in sessions table exists.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all active sessions, the core sessions table is
|
||||
* purged afterwards.
|
||||
*/
|
||||
public function kill_all_sessions() {
|
||||
// Nothing to do, the sessions table is cleared from core.
|
||||
return;
|
||||
}
|
||||
#[\Override]
|
||||
public function destroy(string $id): bool {
|
||||
if (!$session = $this->get_session_by_sid($id)) {
|
||||
if ($id == session_id()) {
|
||||
$this->recordid = null;
|
||||
$this->lasthash = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one session, the session record is removed afterwards.
|
||||
* @param string $sid
|
||||
*/
|
||||
public function kill_session($sid) {
|
||||
// Nothing to do, the sessions table is purged afterwards.
|
||||
return;
|
||||
if ($this->recordid && ($session->id == $this->recordid)) {
|
||||
try {
|
||||
$this->database->release_session_lock($this->recordid);
|
||||
} catch (\Exception $ex) {
|
||||
// Log and ignore any problems.
|
||||
mtrace('Failed to release session lock: '.$ex->getMessage());
|
||||
}
|
||||
$this->recordid = null;
|
||||
$this->lasthash = null;
|
||||
}
|
||||
|
||||
$this->database->delete_records('sessions', ['id' => $session->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,7 +143,7 @@ class database extends handler implements SessionHandlerInterface {
|
||||
*/
|
||||
public function read(string $sid): string|false {
|
||||
try {
|
||||
if (!$record = $this->database->get_record('sessions', array('sid'=>$sid), 'id')) {
|
||||
if (!$record = $this->get_session_by_sid($sid)) {
|
||||
// Let's cheat and skip locking if this is the first access,
|
||||
// do not create the record here, let the manager do it after session init.
|
||||
$this->failed = false;
|
||||
@@ -252,56 +244,4 @@ class database extends handler implements SessionHandlerInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy session handler.
|
||||
*
|
||||
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
|
||||
*
|
||||
* @param string $id
|
||||
* @return bool success
|
||||
*/
|
||||
public function destroy(string $id): bool {
|
||||
if (!$session = $this->database->get_record('sessions', ['sid' => $id], 'id, sid')) {
|
||||
if ($id == session_id()) {
|
||||
$this->recordid = null;
|
||||
$this->lasthash = null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->recordid && ($session->id == $this->recordid)) {
|
||||
try {
|
||||
$this->database->release_session_lock($this->recordid);
|
||||
} catch (\Exception $ex) {
|
||||
// Ignore problems.
|
||||
}
|
||||
$this->recordid = null;
|
||||
$this->lasthash = null;
|
||||
}
|
||||
|
||||
$this->database->delete_records('sessions', ['id' => $session->id]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* GC session handler.
|
||||
*
|
||||
* {@see http://php.net/manual/en/function.session-set-save-handler.php}
|
||||
*
|
||||
* @param int $max_lifetime moodle uses special timeout rules
|
||||
* @return bool success
|
||||
*/
|
||||
// phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameUnderscore
|
||||
public function gc(int $max_lifetime): int|false {
|
||||
// This should do something only if cron is not running properly...
|
||||
if (!$stalelifetime = ini_get('session.gc_maxlifetime')) {
|
||||
return false;
|
||||
}
|
||||
$params = ['purgebefore' => (time() - $stalelifetime)];
|
||||
$count = $this->database->count_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params);
|
||||
$this->database->delete_records_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params);
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* File based session handler.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2013 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* File based session handler.
|
||||
*
|
||||
@@ -34,6 +24,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class file extends handler {
|
||||
|
||||
/** @var string session dir */
|
||||
protected $sessiondir;
|
||||
|
||||
@@ -50,9 +41,7 @@ class file extends handler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init session handler.
|
||||
*/
|
||||
#[\Override]
|
||||
public function init() {
|
||||
if (preg_match('/^[0-9]+;/', $this->sessiondir)) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null, 'Multilevel session directories are not supported');
|
||||
@@ -75,14 +64,7 @@ class file extends handler {
|
||||
ini_set('session.save_path', $this->sessiondir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[\Override]
|
||||
public function session_exists($sid) {
|
||||
$sid = clean_param($sid, PARAM_FILE);
|
||||
if (!$sid) {
|
||||
@@ -92,30 +74,28 @@ class file extends handler {
|
||||
return file_exists($sessionfile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all active sessions, the core sessions table is
|
||||
* purged afterwards.
|
||||
*/
|
||||
public function kill_all_sessions() {
|
||||
#[\Override]
|
||||
public function destroy_all(): bool {
|
||||
if (is_dir($this->sessiondir)) {
|
||||
foreach (glob("$this->sessiondir/sess_*") as $filename) {
|
||||
@unlink($filename);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one session, the session record is removed afterwards.
|
||||
* @param string $sid
|
||||
*/
|
||||
public function kill_session($sid) {
|
||||
$sid = clean_param($sid, PARAM_FILE);
|
||||
#[\Override]
|
||||
public function destroy(string $id): bool {
|
||||
$sid = clean_param($id, PARAM_FILE);
|
||||
if (!$sid) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
$sessionfile = "$this->sessiondir/sess_$sid";
|
||||
if (file_exists($sessionfile)) {
|
||||
@unlink($sessionfile);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
+217
-21
@@ -14,17 +14,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
use core\clock;
|
||||
use core\di;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Session handler base.
|
||||
@@ -79,6 +73,220 @@ abstract class handler {
|
||||
$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 = $CFG->sessiontimeout;
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete expired sessions for guest user account, give them larger timeout, there is no security risk here.
|
||||
$purgebefore = di::get(clock::class)->time() - ($max_lifetime * 5);
|
||||
$this->destroy_expired_user_sessions($purgebefore, $CFG->siteguest);
|
||||
|
||||
// Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory.
|
||||
$purgebefore = di::get(clock::class)->time() - $max_lifetime;
|
||||
$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.
|
||||
@@ -102,16 +310,4 @@ abstract class handler {
|
||||
* @return bool true if session found.
|
||||
*/
|
||||
abstract public function session_exists($sid);
|
||||
|
||||
/**
|
||||
* Kill all active sessions, the core sessions table is
|
||||
* purged afterwards.
|
||||
*/
|
||||
abstract public function kill_all_sessions();
|
||||
|
||||
/**
|
||||
* Kill one session, the session record is removed afterwards.
|
||||
* @param string $sid
|
||||
*/
|
||||
abstract public function kill_session($sid);
|
||||
}
|
||||
|
||||
+204
-176
@@ -14,18 +14,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Session manager class.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2013 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core\clock;
|
||||
use core\di;
|
||||
use html_writer;
|
||||
|
||||
/**
|
||||
@@ -252,14 +244,14 @@ class manager {
|
||||
global $CFG, $DB;
|
||||
|
||||
if (PHPUNIT_TEST) {
|
||||
return '\core\session\file';
|
||||
return \core\tests\session\mock_handler::class;
|
||||
} else if (!empty($CFG->session_handler_class)) {
|
||||
return $CFG->session_handler_class;
|
||||
} else if (!empty($CFG->dbsessions) and $DB->session_lock_supported()) {
|
||||
return '\core\session\database';
|
||||
} else if (!empty($CFG->dbsessions) && $DB->session_lock_supported()) {
|
||||
return database::class;
|
||||
}
|
||||
|
||||
return '\core\session\file';
|
||||
return file::class;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,6 +265,9 @@ class manager {
|
||||
// Find out which handler to use.
|
||||
$class = self::get_handler_class();
|
||||
self::$handler = new $class();
|
||||
if (!self::$handler instanceof \core\session\handler) {
|
||||
throw new exception("$class must implement the \core\session\handler");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,7 +414,7 @@ class manager {
|
||||
* @param bool $newsid is this a new session in first http request?
|
||||
*/
|
||||
protected static function initialise_user_session($newsid) {
|
||||
global $CFG, $DB;
|
||||
global $CFG;
|
||||
|
||||
$sid = session_id();
|
||||
if (!$sid) {
|
||||
@@ -428,8 +423,8 @@ class manager {
|
||||
self::init_empty_session($newsid);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$record = $DB->get_record('sessions', array('sid'=>$sid), 'id, sid, state, userid, lastip, timecreated, timemodified')) {
|
||||
$record = self::get_session_by_sid($sid);
|
||||
if (!isset($record->sid)) {
|
||||
if (!$newsid) {
|
||||
if (!empty($_SESSION['USER']->id)) {
|
||||
// This should not happen, just log it, we MUST not produce any output here!
|
||||
@@ -456,7 +451,7 @@ class manager {
|
||||
// Ignore guest and not-logged in timeouts, there is very little risk here.
|
||||
$timeout = false;
|
||||
|
||||
} else if ($record->timemodified < time() - $maxlifetime) {
|
||||
} else if ($record->timemodified < di::get(clock::class)->time() - $maxlifetime) {
|
||||
$timeout = true;
|
||||
$authsequence = get_enabled_auth_plugins(); // Auths, in sequence.
|
||||
foreach ($authsequence as $authname) {
|
||||
@@ -474,11 +469,9 @@ class manager {
|
||||
}
|
||||
session_regenerate_id(true);
|
||||
$_SESSION = array();
|
||||
$DB->delete_records('sessions', array('id'=>$record->id));
|
||||
|
||||
self::destroy($record->sid);
|
||||
} else {
|
||||
// Update session tracking record.
|
||||
|
||||
$update = new \stdClass();
|
||||
$updated = false;
|
||||
|
||||
@@ -493,33 +486,34 @@ class manager {
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
$time = di::get(clock::class)->time();
|
||||
|
||||
$updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency;
|
||||
|
||||
if ($record->timemodified == $record->timecreated) {
|
||||
// Always do first update of existing record.
|
||||
$update->timemodified = $record->timemodified = time();
|
||||
$update->timemodified = $record->timemodified = $time;
|
||||
$updated = true;
|
||||
|
||||
} else if ($record->timemodified < time() - $updatefreq) {
|
||||
} else if ($record->timemodified < $time - $updatefreq) {
|
||||
// Update the session modified flag only once every 20 seconds.
|
||||
$update->timemodified = $record->timemodified = time();
|
||||
$update->timemodified = $record->timemodified = $time;
|
||||
$updated = true;
|
||||
}
|
||||
|
||||
if ($updated && (!defined('NO_SESSION_UPDATE') || !NO_SESSION_UPDATE)) {
|
||||
$update->id = $record->id;
|
||||
$DB->update_record('sessions', $update);
|
||||
$update->userid = $record->userid;
|
||||
self::$handler->update_session($update);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if ($record) {
|
||||
// This happens when people switch session handlers...
|
||||
session_regenerate_id(true);
|
||||
$_SESSION = array();
|
||||
$DB->delete_records('sessions', array('id'=>$record->id));
|
||||
}
|
||||
} else if (isset($record->sid)) {
|
||||
// This happens when people switch session handlers...
|
||||
session_regenerate_id(true);
|
||||
$_SESSION = [];
|
||||
self::destroy($record->sid);
|
||||
}
|
||||
unset($record);
|
||||
|
||||
@@ -551,10 +545,10 @@ class manager {
|
||||
// Setup $USER and insert the session tracking record.
|
||||
if ($user) {
|
||||
self::set_user($user);
|
||||
self::add_session_record($user->id);
|
||||
self::add_session($user->id);
|
||||
} else {
|
||||
self::init_empty_session($newsid);
|
||||
self::add_session_record(0);
|
||||
self::add_session(0);
|
||||
}
|
||||
|
||||
if ($timedout) {
|
||||
@@ -562,24 +556,44 @@ class manager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single session record for this session id.
|
||||
*
|
||||
* @param string $sid
|
||||
* @return \stdClass
|
||||
*/
|
||||
public static function get_session_by_sid(string $sid): \stdClass {
|
||||
return self::$handler->get_session_by_sid($sid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the session records for this user id.
|
||||
*
|
||||
* @param int $userid
|
||||
* @return array
|
||||
*/
|
||||
public static function get_sessions_by_userid(int $userid): array {
|
||||
return self::$handler->get_sessions_by_userid($userid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert new empty session record.
|
||||
*
|
||||
* @param int $userid
|
||||
* @return \stdClass the new record
|
||||
*/
|
||||
protected static function add_session_record($userid) {
|
||||
global $DB;
|
||||
$record = new \stdClass();
|
||||
$record->state = 0;
|
||||
$record->sid = session_id();
|
||||
$record->sessdata = null;
|
||||
$record->userid = $userid;
|
||||
$record->timecreated = $record->timemodified = time();
|
||||
$record->firstip = $record->lastip = getremoteaddr();
|
||||
public static function add_session(int $userid): \stdClass {
|
||||
return self::$handler->add_session($userid);
|
||||
}
|
||||
|
||||
$record->id = $DB->insert_record('sessions', $record);
|
||||
|
||||
return $record;
|
||||
/**
|
||||
* Update a session record.
|
||||
*
|
||||
* @param \stdClass $record
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_session(\stdClass $record): bool {
|
||||
return self::$handler->update_session($record);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -619,8 +633,8 @@ class manager {
|
||||
|
||||
$sid = session_id();
|
||||
session_regenerate_id(true);
|
||||
$DB->delete_records('sessions', array('sid'=>$sid));
|
||||
self::add_session_record($user->id);
|
||||
self::destroy($sid);
|
||||
self::add_session($user->id);
|
||||
|
||||
// Let enrol plugins deal with new enrolments if necessary.
|
||||
enrol_check_plugins($user);
|
||||
@@ -679,9 +693,9 @@ class manager {
|
||||
// Write new empty session and make sure the old one is deleted.
|
||||
$sid = session_id();
|
||||
session_regenerate_id(true);
|
||||
$DB->delete_records('sessions', array('sid'=>$sid));
|
||||
self::destroy($sid);
|
||||
self::init_empty_session();
|
||||
self::add_session_record($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
|
||||
self::add_session($_SESSION['USER']->id); // Do not use $USER here because it may not be set up yet.
|
||||
self::write_close();
|
||||
}
|
||||
|
||||
@@ -814,13 +828,14 @@ class manager {
|
||||
}
|
||||
|
||||
// Note: add sessions->state checking here if it gets implemented.
|
||||
if (!$record = $DB->get_record('sessions', array('sid' => $sid), 'id, userid, timemodified')) {
|
||||
$record = self::get_session_by_sid($sid);
|
||||
if (!isset($record->sid)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($record->userid) or isguestuser($record->userid)) {
|
||||
// Ignore guest and not-logged-in timeouts, there is very little risk here.
|
||||
} else if ($record->timemodified < time() - $CFG->sessiontimeout) {
|
||||
} else if ($record->timemodified < di::get(clock::class)->time() - $CFG->sessiontimeout) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -842,7 +857,7 @@ class manager {
|
||||
}
|
||||
|
||||
// Note: add sessions->state checking here if it gets implemented.
|
||||
if (!$record = $DB->get_record('sessions', array('sid' => $sid), 'id, userid, timemodified')) {
|
||||
if (!$record = self::get_session_by_sid($sid)) {
|
||||
return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout];
|
||||
}
|
||||
|
||||
@@ -850,7 +865,10 @@ class manager {
|
||||
// Ignore guest and not-logged-in timeouts, there is very little risk here.
|
||||
return ['userid' => 0, 'timeremaining' => $CFG->sessiontimeout];
|
||||
} else {
|
||||
return ['userid' => $record->userid, 'timeremaining' => $CFG->sessiontimeout - (time() - $record->timemodified)];
|
||||
return [
|
||||
'userid' => $record->userid,
|
||||
'timeremaining' => $CFG->sessiontimeout - (di::get(clock::class)->time() - $record->timemodified),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,64 +877,137 @@ class manager {
|
||||
* @param string $sid
|
||||
*/
|
||||
public static function touch_session($sid) {
|
||||
global $DB;
|
||||
|
||||
// Timeouts depend on core sessions table only, no need to update anything in external stores.
|
||||
|
||||
$sql = "UPDATE {sessions} SET timemodified = :now WHERE sid = :sid";
|
||||
$DB->execute($sql, array('now'=>time(), 'sid'=>$sid));
|
||||
self::$handler->update_session((object) [
|
||||
'sid' => $sid,
|
||||
'timemodified' => di::get(clock::class)->time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all sessions unconditionally.
|
||||
*
|
||||
* @return void
|
||||
* @deprecated since Moodle 4.5 See MDL-66161
|
||||
* @todo Remove in MDL-81848
|
||||
*/
|
||||
public static function kill_all_sessions() {
|
||||
global $DB;
|
||||
|
||||
self::terminate_current();
|
||||
|
||||
self::load_handler();
|
||||
self::$handler->kill_all_sessions();
|
||||
|
||||
try {
|
||||
$DB->delete_records('sessions');
|
||||
} catch (\dml_exception $ignored) {
|
||||
// Do not show any warnings - might be during upgrade/installation.
|
||||
}
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'destroy_all',
|
||||
since: '4.5',
|
||||
)]
|
||||
public static function kill_all_sessions(): void {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
self::destroy_all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate give session unconditionally.
|
||||
*
|
||||
* @param string $sid
|
||||
* @return void
|
||||
* @deprecated since Moodle 4.5 See MDL-66161
|
||||
* @todo Remove in MDL-81848
|
||||
*/
|
||||
public static function kill_session($sid) {
|
||||
global $DB;
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'destroy',
|
||||
since: '4.5',
|
||||
)]
|
||||
public static function kill_session($sid): void {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
self::destroy($sid);
|
||||
}
|
||||
|
||||
self::load_handler();
|
||||
|
||||
if ($sid === session_id()) {
|
||||
self::write_close();
|
||||
}
|
||||
|
||||
self::$handler->kill_session($sid);
|
||||
|
||||
$DB->delete_records('sessions', array('sid'=>$sid));
|
||||
/**
|
||||
* Kill sessions of users with disabled plugins.
|
||||
*
|
||||
* @param string $pluginname
|
||||
* @return void
|
||||
* @deprecated since Moodle 4.5 See MDL-66161
|
||||
* @todo Remove in MDL-81848
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'destroy_by_auth_plugin',
|
||||
since: '4.5',
|
||||
)]
|
||||
public static function kill_sessions_for_auth_plugin(string $pluginname): void {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
self::destroy_by_auth_plugin($pluginname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate all sessions of given user unconditionally.
|
||||
*
|
||||
* @param int $userid
|
||||
* @param string $keepsid keep this sid if present
|
||||
* @deprecated since Moodle 4.5 See MDL-66161
|
||||
* @todo Remove in MDL-81848
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
replacement: 'destroy_user_sessions',
|
||||
since: '4.5',
|
||||
)]
|
||||
public static function kill_user_sessions($userid, $keepsid = null) {
|
||||
\core\deprecation::emit_deprecation_if_present([self::class, __FUNCTION__]);
|
||||
self::destroy_user_sessions($userid, $keepsid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 static function destroy_by_auth_plugin(string $pluginname): void {
|
||||
self::$handler->destroy_by_auth_plugin($pluginname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all sessions, and delete all the session data.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function destroy_all(): bool {
|
||||
self::terminate_current();
|
||||
self::load_handler();
|
||||
|
||||
try {
|
||||
$result = self::$handler->destroy_all();
|
||||
} catch (\moodle_exception $ignored) {
|
||||
// Do not show any warnings - might be during upgrade/installation.
|
||||
$result = true;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a specific session and delete this session record for this session id.
|
||||
*
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*/
|
||||
public static function destroy(string $id): bool {
|
||||
self::load_handler();
|
||||
|
||||
if ($id === session_id()) {
|
||||
self::write_close();
|
||||
}
|
||||
|
||||
return self::$handler->destroy($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy all sessions of given user unconditionally.
|
||||
* @param int $userid
|
||||
* @param string $keepsid keep this sid if present
|
||||
*/
|
||||
public static function kill_user_sessions($userid, $keepsid = null) {
|
||||
global $DB;
|
||||
|
||||
$sessions = $DB->get_records('sessions', array('userid'=>$userid), 'id DESC', 'id, sid');
|
||||
public static function destroy_user_sessions($userid, $keepsid = null) {
|
||||
$sessions = self::get_sessions_by_userid($userid);
|
||||
foreach ($sessions as $session) {
|
||||
if ($keepsid and $keepsid === $session->sid) {
|
||||
continue;
|
||||
}
|
||||
self::kill_session($session->sid);
|
||||
self::destroy($session->sid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,30 +1040,35 @@ class manager {
|
||||
return;
|
||||
}
|
||||
|
||||
$count = $DB->count_records('sessions', array('userid' => $userid));
|
||||
$sessions = self::get_sessions_by_userid($userid);
|
||||
|
||||
$count = count($sessions);
|
||||
|
||||
if ($count <= $CFG->limitconcurrentlogins) {
|
||||
return;
|
||||
}
|
||||
|
||||
$i = 0;
|
||||
$select = "userid = :userid";
|
||||
$params = array('userid' => $userid);
|
||||
if ($sid) {
|
||||
if ($DB->record_exists('sessions', array('sid' => $sid, 'userid' => $userid))) {
|
||||
$select .= " AND sid <> :sid";
|
||||
$params['sid'] = $sid;
|
||||
$i = 1;
|
||||
foreach ($sessions as $key => $session) {
|
||||
if ($session->sid == $sid && $session->userid == $userid) {
|
||||
$i = 1;
|
||||
unset($sessions[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sessions = $DB->get_records_select('sessions', $select, $params, 'timecreated DESC', 'id, sid');
|
||||
// Order records by timecreated DESC.
|
||||
usort($sessions, function($a, $b){
|
||||
return $b->timecreated <=> $a->timecreated;
|
||||
});
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
$i++;
|
||||
if ($i <= $CFG->limitconcurrentlogins) {
|
||||
continue;
|
||||
}
|
||||
self::kill_session($session->sid);
|
||||
self::destroy($session->sid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1006,86 +1102,18 @@ class manager {
|
||||
|
||||
/**
|
||||
* Periodic timed-out session cleanup.
|
||||
*
|
||||
* @param int $maxlifetime Sessions that have not updated for the last max_lifetime seconds will be removed.
|
||||
* @return void
|
||||
*/
|
||||
public static function gc() {
|
||||
global $CFG, $DB;
|
||||
public static function gc(int $maxlifetime = 0): void {
|
||||
global $CFG;
|
||||
|
||||
// This may take a long time...
|
||||
\core_php_time_limit::raise();
|
||||
|
||||
$maxlifetime = $CFG->sessiontimeout;
|
||||
|
||||
try {
|
||||
// Kill all sessions of deleted and suspended users without any hesitation.
|
||||
$rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE deleted <> 0 OR suspended <> 0)", array(), 'id DESC', 'id, sid');
|
||||
foreach ($rs as $session) {
|
||||
self::kill_session($session->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
// Kill sessions of users with disabled plugins.
|
||||
$authsequence = get_enabled_auth_plugins();
|
||||
$authsequence = array_flip($authsequence);
|
||||
unset($authsequence['nologin']); // No login means user cannot login.
|
||||
$authsequence = array_flip($authsequence);
|
||||
|
||||
list($notplugins, $params) = $DB->get_in_or_equal($authsequence, SQL_PARAMS_QM, '', false);
|
||||
$rs = $DB->get_recordset_select('sessions', "userid IN (SELECT id FROM {user} WHERE auth $notplugins)", $params, 'id DESC', 'id, sid');
|
||||
foreach ($rs as $session) {
|
||||
self::kill_session($session->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
// Now get a list of time-out candidates - real users only.
|
||||
$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 = array('purgebefore' => (time() - $maxlifetime), 'guestid'=>$CFG->siteguest);
|
||||
|
||||
$authplugins = array();
|
||||
foreach ($authsequence as $authname) {
|
||||
$authplugins[$authname] = get_auth_plugin($authname);
|
||||
}
|
||||
$rs = $DB->get_recordset_sql($sql, $params);
|
||||
foreach ($rs as $user) {
|
||||
foreach ($authplugins as $authplugin) {
|
||||
/** @var \auth_plugin_base $authplugin*/
|
||||
if ($authplugin->ignore_timeout_hook($user, $user->sid, $user->s_timecreated, $user->s_timemodified)) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
self::kill_session($user->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
// Delete expired sessions for guest user account, give them larger timeout, there is no security risk here.
|
||||
$params = array('purgebefore' => (time() - ($maxlifetime * 5)), 'guestid'=>$CFG->siteguest);
|
||||
$rs = $DB->get_recordset_select('sessions', 'userid = :guestid AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
|
||||
foreach ($rs as $session) {
|
||||
self::kill_session($session->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
// Delete expired sessions for userid = 0 (not logged in), better kill them asap to release memory.
|
||||
$params = array('purgebefore' => (time() - $maxlifetime));
|
||||
$rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified < :purgebefore', $params, 'id DESC', 'id, sid');
|
||||
foreach ($rs as $session) {
|
||||
self::kill_session($session->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
// Cleanup letfovers from the first browser access because it may set multiple cookies and then use only one.
|
||||
$params = array('purgebefore' => (time() - 60*3));
|
||||
$rs = $DB->get_recordset_select('sessions', 'userid = 0 AND timemodified = timecreated AND timemodified < :purgebefore', $params, 'id ASC', 'id, sid');
|
||||
foreach ($rs as $session) {
|
||||
self::kill_session($session->sid);
|
||||
}
|
||||
$rs->close();
|
||||
|
||||
} catch (\Exception $ex) {
|
||||
debugging('Error gc-ing sessions: '.$ex->getMessage(), DEBUG_NORMAL, $ex->getTrace());
|
||||
// If max lifetime is not provided, use the default session timeout.
|
||||
if ($maxlifetime == 0) {
|
||||
$maxlifetime = $CFG->sessiontimeout;
|
||||
}
|
||||
self::$handler->gc($maxlifetime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1214,7 +1242,7 @@ class manager {
|
||||
|
||||
$state = [
|
||||
'token' => random_string(32),
|
||||
'created' => time() // Server time - not user time.
|
||||
'created' => di::get(clock::class)->time(), // Server time - not user time.
|
||||
];
|
||||
|
||||
if (!isset($SESSION->logintoken)) {
|
||||
@@ -1254,7 +1282,7 @@ class manager {
|
||||
}
|
||||
|
||||
// Check token lifespan.
|
||||
if ($state['created'] < (time() - $CFG->sessiontimeout)) {
|
||||
if ($state['created'] < (di::get(clock::class)->time() - $CFG->sessiontimeout)) {
|
||||
$state = self::create_login_token();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Memcached based session handler.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2013 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Memcached based session handler.
|
||||
*
|
||||
@@ -96,10 +86,7 @@ class memcached extends handler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the session.
|
||||
* @return bool success
|
||||
*/
|
||||
#[\Override]
|
||||
public function start() {
|
||||
ini_set('memcached.sess_locking', $this->requires_write_lock() ? '1' : '0');
|
||||
|
||||
@@ -132,9 +119,7 @@ class memcached extends handler {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init session handler.
|
||||
*/
|
||||
#[\Override]
|
||||
public function init() {
|
||||
if (!extension_loaded('memcached')) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null, 'memcached extension is not loaded');
|
||||
@@ -178,14 +163,7 @@ class memcached extends handler {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[\Override]
|
||||
public function session_exists($sid) {
|
||||
if (!$this->servers) {
|
||||
return false;
|
||||
@@ -209,14 +187,11 @@ class memcached extends handler {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all active sessions, the core sessions table is
|
||||
* purged afterwards.
|
||||
*/
|
||||
public function kill_all_sessions() {
|
||||
#[\Override]
|
||||
public function destroy_all(): bool {
|
||||
global $DB;
|
||||
if (!$this->servers) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go through the list of all servers because
|
||||
@@ -245,15 +220,14 @@ class memcached extends handler {
|
||||
foreach ($memcacheds as $memcached) {
|
||||
$memcached->quit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one session, the session record is removed afterwards.
|
||||
* @param string $sid
|
||||
*/
|
||||
public function kill_session($sid) {
|
||||
#[\Override]
|
||||
public function destroy(string $id): bool {
|
||||
if (!$this->servers) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go through the list of all servers because
|
||||
@@ -264,9 +238,11 @@ class memcached extends handler {
|
||||
list($host, $port) = $server;
|
||||
$memcached = new \Memcached();
|
||||
$memcached->addServer($host, $port);
|
||||
$memcached->delete($this->prefix . $sid);
|
||||
$memcached->delete($this->prefix . $id);
|
||||
$memcached->quit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+312
-152
@@ -14,29 +14,21 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Redis based session handler.
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2015 Russell Smith <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core\session;
|
||||
|
||||
use RedisException;
|
||||
use coding_exception;
|
||||
use core\di;
|
||||
use core\clock;
|
||||
use RedisCluster;
|
||||
use RedisClusterException;
|
||||
use RedisException;
|
||||
use SessionHandlerInterface;
|
||||
|
||||
/**
|
||||
* Redis based session handler.
|
||||
*
|
||||
* The default Redis session handler does not handle locking in 2.2.7, so we have written a php session handler
|
||||
* that uses locking. The places where locking is used was modeled from the memcached code that is used in Moodle
|
||||
* https://github.com/php-memcached-dev/php-memcached/blob/master/php_memcached_session.c
|
||||
*
|
||||
* @package core
|
||||
* @copyright 2016 Russell Smith
|
||||
* @copyright Russell Smith
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class redis extends handler implements SessionHandlerInterface {
|
||||
@@ -53,14 +45,11 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
*/
|
||||
const COMPRESSION_ZSTD = 'zstd';
|
||||
|
||||
/**
|
||||
* Minimum version of the Redis extension required.
|
||||
*/
|
||||
public const REDIS_EXTENSION_MIN_VERSION = '2.2.4';
|
||||
/**
|
||||
* Minimum version of the Redis extension required.
|
||||
*/
|
||||
private const REDIS_SERVER_MIN_VERSION = '2.6.12';
|
||||
/** @var string Minimum server version */
|
||||
const REDIS_MIN_SERVER_VERSION = "5.0.0";
|
||||
|
||||
/** @var string Minimum extension version */
|
||||
const REDIS_MIN_EXTENSION_VERSION = "5.1.0";
|
||||
|
||||
/** @var array $host save_path string */
|
||||
protected array $host = [];
|
||||
@@ -74,6 +63,13 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
protected $database = 0;
|
||||
/** @var array $servers list of servers parsed from save_path */
|
||||
protected $prefix = '';
|
||||
|
||||
/** @var string $sessionkeyprefix the prefix for the session key */
|
||||
protected string $sessionkeyprefix = 'session_';
|
||||
|
||||
/** @var string $userkeyprefix the prefix for the user key */
|
||||
protected string $userkeyprefix = 'user_';
|
||||
|
||||
/** @var int $acquiretimeout how long to wait for session lock in seconds */
|
||||
protected $acquiretimeout = 120;
|
||||
/** @var int $acquirewarn how long before warning when waiting for a lock in seconds */
|
||||
@@ -87,14 +83,18 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
/** @var string $lasthash hash of the session data content */
|
||||
protected $lasthash = null;
|
||||
|
||||
/**
|
||||
* @var int $lockexpire how long to wait in seconds before expiring the lock automatically
|
||||
* so that other requests may continue execution, ignored if PECL redis is below version 2.2.0.
|
||||
*/
|
||||
protected $lockexpire;
|
||||
/** @var int $gcbatchsize The number of redis keys that will be processed each time the garbage collector is executed. */
|
||||
protected int $gcbatchsize = 100;
|
||||
|
||||
/** @var Redis|RedisCluster Connection */
|
||||
protected $connection = null;
|
||||
/**
|
||||
* How long to wait in seconds before expiring the lock automatically so that other requests may continue execution.
|
||||
*
|
||||
* @var int $lockexpire
|
||||
*/
|
||||
protected int $lockexpire;
|
||||
|
||||
/** @var \Redis|\RedisCluster|null Connection */
|
||||
protected \Redis|\RedisCluster|null $connection = null;
|
||||
|
||||
/** @var array $locks List of currently held locks by this page. */
|
||||
protected $locks = array();
|
||||
@@ -108,6 +108,12 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
/** @var int Maximum number of retries for cache store operations. */
|
||||
const MAX_RETRIES = 5;
|
||||
|
||||
/** @var int $firstaccesstimeout The initial timeout (seconds) for the first browser access without login. */
|
||||
protected int $firstaccesstimeout = 180;
|
||||
|
||||
/** @var clock A clock instance */
|
||||
protected clock $clock;
|
||||
|
||||
/**
|
||||
* Create new instance of handler.
|
||||
*/
|
||||
@@ -157,12 +163,6 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
$this->serializer = \Redis::SERIALIZER_IGBINARY; // Set igbinary serializer if phpredis supports it.
|
||||
}
|
||||
|
||||
// The following configures the session lifetime in redis to allow some
|
||||
// wriggle room in the user noticing they've been booted off and
|
||||
// letting them log back in before they lose their session entirely.
|
||||
$updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency;
|
||||
$this->timeout = $CFG->sessiontimeout + $updatefreq + MINSECS;
|
||||
|
||||
// This sets the Redis session lock expiry time to whatever is lower, either
|
||||
// the PHP execution time `max_execution_time`, if the value was defined in
|
||||
// the `php.ini` or the globally configured `sessiontimeout`. Setting it to
|
||||
@@ -187,37 +187,33 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
if (isset($CFG->session_redis_compressor)) {
|
||||
$this->compressor = $CFG->session_redis_compressor;
|
||||
}
|
||||
|
||||
$this->clock = di::get(clock::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the session.
|
||||
*
|
||||
* @return bool success
|
||||
*/
|
||||
public function start() {
|
||||
$result = parent::start();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init session handler.
|
||||
*/
|
||||
public function init() {
|
||||
#[\Override]
|
||||
public function init(): bool {
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null, 'redis extension is not loaded');
|
||||
}
|
||||
|
||||
if (empty($this->host)) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null,
|
||||
'$CFG->session_redis_host must be specified in config.php');
|
||||
throw new exception(
|
||||
'sessionhandlerproblem',
|
||||
'error',
|
||||
'',
|
||||
null,
|
||||
'$CFG->session_redis_host must be specified in config.php',
|
||||
);
|
||||
}
|
||||
|
||||
// The session handler requires a version of PHP Redis extension with support for SET command options (at least 2.2.4).
|
||||
$version = phpversion('Redis');
|
||||
if (!$version || version_compare($version, self::REDIS_EXTENSION_MIN_VERSION) <= 0) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null,
|
||||
'redis extension version must be at least ' . self::REDIS_EXTENSION_MIN_VERSION);
|
||||
if (!$version || version_compare($version, self::REDIS_MIN_EXTENSION_VERSION) <= 0) {
|
||||
throw new exception(
|
||||
errorcode: 'sessionhandlerproblem',
|
||||
module: 'error',
|
||||
debuginfo: sprintf('redis extension version must be at least %s', self::REDIS_MIN_EXTENSION_VERSION),
|
||||
);
|
||||
}
|
||||
|
||||
$result = session_set_save_handler($this);
|
||||
@@ -275,12 +271,26 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
try {
|
||||
// Create a $redis object of a RedisCluster or Redis class.
|
||||
if ($this->clustermode) {
|
||||
$this->connection = new \RedisCluster(null, $trimmedservers, 1, 1, true,
|
||||
$this->auth, !empty($opts) ? $opts : null);
|
||||
$this->connection = new \RedisCluster(
|
||||
name: null,
|
||||
seeds: $trimmedservers,
|
||||
timeout: 1,
|
||||
readTimeout: 1,
|
||||
persistent: true,
|
||||
auth: $this->auth,
|
||||
context: !empty($opts) ? $opts : null,
|
||||
);
|
||||
} else {
|
||||
$delay = rand(100, 500);
|
||||
$this->connection = new \Redis();
|
||||
$this->connection->connect($server, $port, 1, null, $delay, 1, $opts);
|
||||
$this->connection->connect(
|
||||
host: $server,
|
||||
port: $port,
|
||||
timeout: 1,
|
||||
retry_interval: $delay,
|
||||
read_timeout: 1,
|
||||
context: $opts,
|
||||
);
|
||||
if ($this->auth !== '' && !$this->connection->auth($this->auth)) {
|
||||
throw new $exceptionclass('Unable to authenticate.');
|
||||
}
|
||||
@@ -295,12 +305,25 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
throw new $exceptionclass('Unable to set the Redis Prefix option.');
|
||||
}
|
||||
}
|
||||
if ($this->sslopts && !$this->connection->ping('Ping')) {
|
||||
// In case of a TLS connection,
|
||||
// if phpredis client does not communicate immediately with the server the connection hangs.
|
||||
// See https://github.com/phpredis/phpredis/issues/2332.
|
||||
throw new $exceptionclass("Ping failed");
|
||||
$info = $this->connection->info('server');
|
||||
if (!$info) {
|
||||
throw new $exceptionclass("Failed to fetch server information");
|
||||
}
|
||||
|
||||
// Check the server version.
|
||||
// Note: In case of a TLS connection,
|
||||
// if phpredis client does not communicate immediately with the server the connection hangs.
|
||||
// See https://github.com/phpredis/phpredis/issues/2332.
|
||||
// This version check satisfies that requirement.
|
||||
$version = $info['redis_version'];
|
||||
if (!$version || version_compare($version, static::REDIS_MIN_SERVER_VERSION) <= 0) {
|
||||
throw new $exceptionclass(sprintf(
|
||||
"Version %s is not supported. The minimum version required is %s.",
|
||||
$version,
|
||||
static::REDIS_MIN_SERVER_VERSION,
|
||||
));
|
||||
}
|
||||
|
||||
if ($this->database !== 0) {
|
||||
if (!$this->connection->select($this->database)) {
|
||||
throw new $exceptionclass('Unable to select the Redis database ' . $this->database . '.');
|
||||
@@ -309,9 +332,9 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
|
||||
// The session handler requires a version of Redis server with support for SET command options (at least 2.6.12).
|
||||
$serverversion = $this->connection->info('server')['redis_version'];
|
||||
if (version_compare($serverversion, self::REDIS_SERVER_MIN_VERSION) <= 0) {
|
||||
if (version_compare($serverversion, self::REDIS_MIN_SERVER_VERSION) <= 0) {
|
||||
throw new exception('sessionhandlerproblem', 'error', '', null,
|
||||
'redis server version must be at least ' . self::REDIS_SERVER_MIN_VERSION);
|
||||
'redis server version must be at least ' . self::REDIS_MIN_SERVER_VERSION);
|
||||
}
|
||||
return true;
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
@@ -334,6 +357,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
if (!$result) {
|
||||
throw new exception('redissessionhandlerproblem', 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +380,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
$this->lasthash = null;
|
||||
try {
|
||||
foreach ($this->locks as $id => $expirytime) {
|
||||
if ($expirytime > $this->time()) {
|
||||
if ($expirytime > $this->clock->time()) {
|
||||
$this->unlock_session($id);
|
||||
}
|
||||
unset($this->locks[$id]);
|
||||
@@ -373,29 +397,45 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
* Read the session data from storage
|
||||
*
|
||||
* @param string $id The session id to read from storage.
|
||||
* @return string The session data for PHP to process.
|
||||
* @return string|false The session data for PHP to process or false.
|
||||
*
|
||||
* @throws RedisException when we are unable to talk to the Redis server.
|
||||
*/
|
||||
public function read(string $id): string|false {
|
||||
try {
|
||||
if ($this->requires_write_lock()) {
|
||||
$this->lock_session($id);
|
||||
$this->lock_session($this->sessionkeyprefix . $id);
|
||||
}
|
||||
$sessiondata = $this->uncompress($this->connection->get($id));
|
||||
|
||||
$keys = $this->connection->hmget($this->sessionkeyprefix . $id, ['userid', 'sessdata']);
|
||||
$userid = $keys['userid'];
|
||||
$sessiondata = $this->uncompress($keys['sessdata']);
|
||||
|
||||
if ($sessiondata === false) {
|
||||
if ($this->requires_write_lock()) {
|
||||
$this->unlock_session($id);
|
||||
$this->unlock_session($this->sessionkeyprefix . $id);
|
||||
}
|
||||
$this->lasthash = sha1('');
|
||||
return '';
|
||||
}
|
||||
$this->connection->expire($id, $this->timeout);
|
||||
|
||||
// Do not update expiry if non-login user (0). This would affect the first access timeout.
|
||||
if ($userid != 0) {
|
||||
$maxlifetime = $this->get_maxlifetime($userid);
|
||||
$this->connection->expire($this->sessionkeyprefix . $id, $maxlifetime);
|
||||
$this->connection->expire($this->userkeyprefix . $userid, $maxlifetime);
|
||||
}
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Update last hash.
|
||||
if ($sessiondata === null) {
|
||||
// As of PHP 8.1 we can't pass null to base64_encode.
|
||||
$sessiondata = '';
|
||||
}
|
||||
|
||||
$this->lasthash = sha1(base64_encode($sessiondata));
|
||||
return $sessiondata;
|
||||
}
|
||||
@@ -406,7 +446,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
private function compress($value) {
|
||||
private function compress($value): string {
|
||||
switch ($this->compressor) {
|
||||
case self::COMPRESSION_NONE:
|
||||
return $value;
|
||||
@@ -455,7 +495,6 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
* @return bool true on write success, false on failure
|
||||
*/
|
||||
public function write(string $id, string $data): bool {
|
||||
|
||||
$hash = sha1(base64_encode($data));
|
||||
|
||||
// If the content has not changed don't bother writing.
|
||||
@@ -475,8 +514,16 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
// address that in the future.
|
||||
try {
|
||||
$data = $this->compress($data);
|
||||
$this->connection->hset($this->sessionkeyprefix . $id, 'sessdata', $data);
|
||||
$keys = $this->connection->hmget($this->sessionkeyprefix . $id, ['userid', 'timecreated', 'timemodified']);
|
||||
$userid = $keys['userid'];
|
||||
|
||||
$this->connection->setex($id, $this->timeout, $data);
|
||||
// Don't update expiry if still first access.
|
||||
if ($keys['timecreated'] != $keys['timemodified']) {
|
||||
$maxlifetime = $this->get_maxlifetime($userid);
|
||||
$this->connection->expire($this->sessionkeyprefix . $id, $maxlifetime);
|
||||
$this->connection->expire($this->userkeyprefix . $userid, $maxlifetime);
|
||||
}
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
return false;
|
||||
@@ -484,16 +531,124 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle destroying a session.
|
||||
*
|
||||
* @param string $id the session id to destroy.
|
||||
* @return bool true if the session was deleted, false otherwise.
|
||||
*/
|
||||
#[\Override]
|
||||
public function get_session_by_sid(string $sid): \stdClass {
|
||||
$keys = ["id", "state", "sid", "userid", "sessdata", "timecreated", "timemodified", "firstip", "lastip"];
|
||||
$sessiondata = $this->connection->hmget($this->sessionkeyprefix . $sid, $keys);
|
||||
|
||||
return (object)$sessiondata;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add_session(int $userid): \stdClass {
|
||||
$timestamp = $this->clock->time();
|
||||
$sid = session_id();
|
||||
$maxlifetime = $this->get_maxlifetime($userid, true);
|
||||
$sessiondata = [
|
||||
'id' => $sid,
|
||||
'state' => '0',
|
||||
'sid' => $sid,
|
||||
'userid' => $userid,
|
||||
'sessdata' => null,
|
||||
'timecreated' => $timestamp,
|
||||
'timemodified' => $timestamp,
|
||||
'firstip' => getremoteaddr(),
|
||||
'lastip' => getremoteaddr(),
|
||||
];
|
||||
|
||||
$userhashkey = $this->userkeyprefix . $userid;
|
||||
$this->connection->hSet($userhashkey, $sid, $timestamp);
|
||||
$this->connection->expire($userhashkey, $maxlifetime);
|
||||
|
||||
$sessionhashkey = $this->sessionkeyprefix . $sid;
|
||||
$this->connection->hmSet($sessionhashkey, $sessiondata);
|
||||
$this->connection->expire($sessionhashkey, $maxlifetime);
|
||||
|
||||
return (object)$sessiondata;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function get_sessions_by_userid(int $userid): array {
|
||||
$this->init_redis_if_required();
|
||||
|
||||
$userhashkey = $this->userkeyprefix . $userid;
|
||||
$sessions = $this->connection->hGetAll($userhashkey);
|
||||
$records = [];
|
||||
foreach (array_keys($sessions) as $session) {
|
||||
$item = $this->connection->hGetAll($this->sessionkeyprefix . $session);
|
||||
if (!empty($item)) {
|
||||
$records[] = (object) $item;
|
||||
}
|
||||
}
|
||||
return $records;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function update_session(\stdClass $record): bool {
|
||||
if (!isset($record->sid) && isset($record->id)) {
|
||||
$record->sid = $record->id;
|
||||
}
|
||||
|
||||
// If record does not have userid set, we need to get it from the session.
|
||||
if (!isset($record->userid)) {
|
||||
$session = $this->get_session_by_sid($record->sid);
|
||||
$record->userid = $session->userid;
|
||||
}
|
||||
|
||||
$sessionhashkey = $this->sessionkeyprefix . $record->sid;
|
||||
$userhashkey = $this->userkeyprefix . $record->userid;
|
||||
|
||||
$recordata = (array) $record;
|
||||
unset($recordata['sid']);
|
||||
$this->connection->hmSet($sessionhashkey, $recordata);
|
||||
|
||||
// Update the expiry time.
|
||||
$maxlifetime = $this->get_maxlifetime($record->userid);
|
||||
$this->connection->expire($sessionhashkey, $maxlifetime);
|
||||
$this->connection->expire($userhashkey, $maxlifetime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#[\Override]
|
||||
public function get_all_sessions(): \Iterator {
|
||||
$sessions = [];
|
||||
$iterator = null;
|
||||
while (false !== ($keys = $this->connection->scan($iterator, '*' . $this->sessionkeyprefix . '*'))) {
|
||||
foreach ($keys as $key) {
|
||||
$sessions[] = $key;
|
||||
}
|
||||
}
|
||||
return new \ArrayIterator($sessions);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function destroy_all(): bool {
|
||||
$this->init_redis_if_required();
|
||||
|
||||
$sessions = $this->get_all_sessions();
|
||||
foreach ($sessions as $session) {
|
||||
// Remove the prefixes from the session id, as destroy expects the raw session id.
|
||||
if (str_starts_with($session, $this->prefix . $this->sessionkeyprefix)) {
|
||||
$session = substr($session, strlen($this->prefix . $this->sessionkeyprefix));
|
||||
}
|
||||
|
||||
$this->destroy($session);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function destroy(string $id): bool {
|
||||
$this->init_redis_if_required();
|
||||
$this->lasthash = null;
|
||||
try {
|
||||
$this->connection->del($id);
|
||||
$sessionhashkey = $this->sessionkeyprefix . $id;
|
||||
$userid = $this->connection->hget($sessionhashkey, "userid");
|
||||
$userhashkey = $this->userkeyprefix . $userid;
|
||||
$this->connection->hDel($userhashkey, $id);
|
||||
$this->connection->unlink($sessionhashkey);
|
||||
$this->unlock_session($id);
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
error_log('Failed talking to redis: '.$e->getMessage());
|
||||
@@ -503,15 +658,53 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
return true;
|
||||
}
|
||||
|
||||
// phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameUnderscore
|
||||
#[\Override]
|
||||
public function gc(int $max_lifetime = 0): int|false {
|
||||
return 0;
|
||||
}
|
||||
// phpcs:enable
|
||||
|
||||
/**
|
||||
* Garbage collect sessions. We don't we any as Redis does it for us.
|
||||
* Get session maximum lifetime in seconds.
|
||||
*
|
||||
* @param integer $max_lifetime All sessions older than this should be removed.
|
||||
* @return bool true, as Redis handles expiry for us.
|
||||
* @param int|null $userid The user id to calculate the max lifetime for.
|
||||
* @param bool $firstbrowseraccess This indicates that this is calculating the expiry when the key is first added.
|
||||
* The first access made by the browser has a shorter timeout to reduce abandoned sessions.
|
||||
* @return float|int
|
||||
*/
|
||||
// phpcs:ignore moodle.NamingConventions.ValidVariableName.VariableNameUnderscore
|
||||
public function gc(int $max_lifetime): int|false {
|
||||
return false;
|
||||
private function get_maxlifetime(?int $userid = null, bool $firstbrowseraccess = false): float|int {
|
||||
global $CFG;
|
||||
|
||||
// Guest user.
|
||||
if ($userid == $CFG->siteguest) {
|
||||
return $CFG->sessiontimeout * 5;
|
||||
}
|
||||
|
||||
// All other users.
|
||||
if ($userid == 0 && $firstbrowseraccess) {
|
||||
$maxlifetime = $this->firstaccesstimeout;
|
||||
} else {
|
||||
// As per MDL-56823 - The following configures the session lifetime in redis to allow some
|
||||
// wriggle room in the user noticing they've been booted off and
|
||||
// letting them log back in before they lose their session entirely.
|
||||
$updatefreq = empty($CFG->session_update_timemodified_frequency) ? 20 : $CFG->session_update_timemodified_frequency;
|
||||
$maxlifetime = (int) $CFG->sessiontimeout + $updatefreq + MINSECS;
|
||||
}
|
||||
|
||||
return $maxlifetime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection will be null if these methods are called from cli or where NO_MOODLE_COOKIES is used.
|
||||
* We need to check for this and initialize the connection if required.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_redis_if_required(): void {
|
||||
if (is_null($this->connection)) {
|
||||
$this->init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,7 +714,7 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
*/
|
||||
protected function unlock_session($id) {
|
||||
if (isset($this->locks[$id])) {
|
||||
$this->connection->del($id.".lock");
|
||||
$this->connection->unlink("{$id}.lock");
|
||||
unset($this->locks[$id]);
|
||||
}
|
||||
}
|
||||
@@ -534,25 +727,30 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
* @throws exception When we are unable to obtain a session lock.
|
||||
*/
|
||||
protected function lock_session($id) {
|
||||
$lockkey = $id.".lock";
|
||||
$lockkey = "{$id}.lock";
|
||||
|
||||
$haslock = isset($this->locks[$id]) && $this->time() < $this->locks[$id];
|
||||
$startlocktime = $this->time();
|
||||
$haslock = isset($this->locks[$id]) && $this->clock->time() < $this->locks[$id];
|
||||
if ($haslock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* To be able to ensure sessions don't write out of order we must obtain an exclusive lock
|
||||
* on the session for the entire time it is open. If another AJAX call, or page is using
|
||||
* the session then we just wait until it finishes before we can open the session.
|
||||
*/
|
||||
$startlocktime = $this->clock->time();
|
||||
|
||||
// To be able to ensure sessions don't write out of order we must obtain an exclusive lock
|
||||
// on the session for the entire time it is open. If another AJAX call, or page is using
|
||||
// the session then we just wait until it finishes before we can open the session.
|
||||
|
||||
// Store the current host, process id and the request URI so it's easy to track who has the lock.
|
||||
$hostname = gethostname();
|
||||
if ($hostname === false) {
|
||||
$hostname = 'UNKNOWN HOST';
|
||||
}
|
||||
|
||||
$pid = getmypid();
|
||||
if ($pid === false) {
|
||||
$pid = 'UNKNOWN';
|
||||
}
|
||||
|
||||
$uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : 'unknown uri';
|
||||
|
||||
$whoami = "[pid {$pid}] {$hostname}:$uri";
|
||||
@@ -563,38 +761,43 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
$haslock = $this->connection->set($lockkey, $whoami, ['nx', 'ex' => $this->lockexpire]);
|
||||
|
||||
if ($haslock) {
|
||||
$this->locks[$id] = $this->time() + $this->lockexpire;
|
||||
$this->locks[$id] = $this->clock->time() + $this->lockexpire;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($this->acquirewarn) && !$haswarned && $this->time() > $startlocktime + $this->acquirewarn) {
|
||||
if (!empty($this->acquirewarn) && !$haswarned && $this->clock->time() > $startlocktime + $this->acquirewarn) {
|
||||
// This is a warning to better inform users.
|
||||
$whohaslock = $this->connection->get($lockkey);
|
||||
// phpcs:ignore
|
||||
error_log("Warning: Cannot obtain session lock for sid: $id within $this->acquirewarn seconds but will keep trying. " .
|
||||
"It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.");
|
||||
error_log(
|
||||
"Warning: Cannot obtain session lock for sid: $id within $this->acquirewarn seconds but will keep trying. " .
|
||||
"It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.",
|
||||
);
|
||||
$haswarned = true;
|
||||
}
|
||||
|
||||
if ($this->time() > $startlocktime + $this->acquiretimeout) {
|
||||
if ($this->clock->time() > $startlocktime + $this->acquiretimeout) {
|
||||
// This is a fatal error, better inform users.
|
||||
// It should not happen very often - all pages that need long time to execute
|
||||
// should close session immediately after access control checks.
|
||||
$whohaslock = $this->connection->get($lockkey);
|
||||
// phpcs:ignore
|
||||
error_log("Error: Cannot obtain session lock for sid: $id within $this->acquiretimeout seconds. " .
|
||||
"It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.");
|
||||
error_log(
|
||||
"Error: Cannot obtain session lock for sid: $id within $this->acquiretimeout seconds. " .
|
||||
"It is likely another page ($whohaslock) has a long session lock, or the session lock was never released.",
|
||||
);
|
||||
$acquiretimeout = format_time($this->acquiretimeout);
|
||||
$lockexpire = format_time($this->lockexpire);
|
||||
$a = (object)[
|
||||
'id' => substr($id, 0, 10),
|
||||
'acquiretimeout' => $acquiretimeout,
|
||||
'whohaslock' => $whohaslock,
|
||||
'lockexpire' => $lockexpire];
|
||||
'lockexpire' => $lockexpire,
|
||||
];
|
||||
throw new exception("sessioncannotobtainlock", 'error', '', $a);
|
||||
}
|
||||
|
||||
if ($this->time() < $startlocktime + 5) {
|
||||
if ($this->clock->time() < $startlocktime + 5) {
|
||||
// We want a random delay to stagger the polling load. Ideally
|
||||
// this delay should be a fraction of the average response
|
||||
// time. If it is too small we will poll too much and if it is
|
||||
@@ -610,63 +813,20 @@ class redis extends handler implements SessionHandlerInterface {
|
||||
|
||||
usleep($delay * 1000);
|
||||
}
|
||||
throw new coding_exception('Unable to lock session');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current time.
|
||||
*
|
||||
* @return int the current time as a unixtimestamp.
|
||||
*/
|
||||
protected function time() {
|
||||
return time();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
#[\Override]
|
||||
public function session_exists($sid) {
|
||||
if (!$this->connection) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return !empty($this->connection->exists($sid));
|
||||
$sessionhashkey = $this->sessionkeyprefix . $sid;
|
||||
return !empty($this->connection->exists($sessionhashkey));
|
||||
} catch (RedisException | RedisClusterException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all active sessions, the core sessions table is purged afterwards.
|
||||
*/
|
||||
public function kill_all_sessions() {
|
||||
global $DB;
|
||||
if (!$this->connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rs = $DB->get_recordset('sessions', array(), 'id DESC', 'id, sid');
|
||||
foreach ($rs as $record) {
|
||||
$this->destroy($record->sid);
|
||||
}
|
||||
$rs->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill one session, the session record is removed afterwards.
|
||||
*
|
||||
* @param string $sid
|
||||
*/
|
||||
public function kill_session($sid) {
|
||||
if (!$this->connection) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->destroy($sid);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -2689,7 +2689,8 @@ function require_logout() {
|
||||
'other' => array('sessionid' => $sid),
|
||||
)
|
||||
);
|
||||
if ($session = $DB->get_record('sessions', array('sid'=>$sid))) {
|
||||
$session = \core\session\manager::get_session_by_sid($sid);
|
||||
if (isset($session->id)) {
|
||||
$event->add_record_snapshot('sessions', $session);
|
||||
}
|
||||
|
||||
@@ -3675,7 +3676,7 @@ function delete_user(stdClass $user) {
|
||||
}
|
||||
|
||||
// Force logout - may fail if file based sessions used, sorry.
|
||||
\core\session\manager::kill_user_sessions($user->id);
|
||||
\core\session\manager::destroy_user_sessions($user->id);
|
||||
|
||||
// Generate username from email address, or a fake email.
|
||||
$delemail = !empty($user->email) ? $user->email : $user->username . '.' . $user->id . '@unknownemail.invalid';
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core\tests\session;
|
||||
|
||||
use core\clock;
|
||||
use core\di;
|
||||
use core\session\database;
|
||||
|
||||
/**
|
||||
* Mock handler methods class.
|
||||
*
|
||||
* @package core
|
||||
* @author Darren Cocco <[email protected]>
|
||||
* @author Trisha Milan <[email protected]>
|
||||
* @copyright 2022 Monash University (http://www.monash.edu)
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class mock_handler extends database {
|
||||
#[\Override]
|
||||
public function init(): bool {
|
||||
// Nothing special to do in the mock.
|
||||
return true;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function session_exists($sid): bool {
|
||||
global $DB;
|
||||
|
||||
return $DB->record_exists('sessions', ['sid' => $sid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new session record to be used in unit tests.
|
||||
*
|
||||
* @param \stdClass $record
|
||||
* @return int Inserted record id.
|
||||
*/
|
||||
public function add_test_session(\stdClass $record): int {
|
||||
global $DB, $USER;
|
||||
|
||||
$data = new \stdClass();
|
||||
$data->state = $record->state ?? 0;
|
||||
$data->sid = $record->sid ?? session_id();
|
||||
$data->sessdata = $record->sessdata ?? null;
|
||||
$data->userid = $record->userid ?? $USER->id;
|
||||
$data->timecreated = $record->timecreated ?? di::get(clock::class)->time();
|
||||
$data->timemodified = $record->timemodified ?? di::get(clock::class)->time();
|
||||
$data->firstip = $record->firstip ?? getremoteaddr();
|
||||
$data->lastip = $record->lastip ?? getremoteaddr();
|
||||
|
||||
return $DB->insert_record('sessions', $data);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function get_all_sessions(): \Iterator {
|
||||
global $DB;
|
||||
|
||||
$records = $DB->get_records('sessions');
|
||||
return new \ArrayIterator($records);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of all sessions stored.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count_sessions(): int {
|
||||
global $DB;
|
||||
|
||||
return $DB->count_records('sessions');
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core;
|
||||
namespace core\session;
|
||||
|
||||
use core\tests\session\mock_handler;
|
||||
|
||||
/**
|
||||
* Unit tests for session manager class.
|
||||
@@ -23,8 +25,18 @@ namespace core;
|
||||
* @category test
|
||||
* @copyright 2013 Petr Skoda {@link http://skodak.org}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core\session\manager
|
||||
*/
|
||||
class session_manager_test extends \advanced_testcase {
|
||||
final class manager_test extends \advanced_testcase {
|
||||
|
||||
/** @var mock_handler $mockhandler Dedicated testing handler. */
|
||||
protected mock_handler $mockhandler;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->mockhandler = new mock_handler();
|
||||
}
|
||||
|
||||
public function test_start(): void {
|
||||
$this->resetAfterTest();
|
||||
// Session must be started only once...
|
||||
@@ -185,23 +197,23 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->sid = $sid;
|
||||
$record->timecreated = time();
|
||||
$record->timemodified = $record->timecreated;
|
||||
$record->id = $DB->insert_record('sessions', $record);
|
||||
$record->id = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$this->assertTrue(\core\session\manager::session_exists($sid));
|
||||
|
||||
$record->timecreated = time() - $CFG->sessiontimeout - 100;
|
||||
$record->timemodified = $record->timecreated + 10;
|
||||
$DB->update_record('sessions', $record);
|
||||
\core\session\manager::update_session($record);
|
||||
|
||||
$this->assertTrue(\core\session\manager::session_exists($sid));
|
||||
|
||||
$record->userid = $guest->id;
|
||||
$DB->update_record('sessions', $record);
|
||||
\core\session\manager::update_session($record);
|
||||
|
||||
$this->assertTrue(\core\session\manager::session_exists($sid));
|
||||
|
||||
$record->userid = $user->id;
|
||||
$DB->update_record('sessions', $record);
|
||||
\core\session\manager::update_session($record);
|
||||
|
||||
$this->assertFalse(\core\session\manager::session_exists($sid));
|
||||
|
||||
@@ -211,7 +223,6 @@ class session_manager_test extends \advanced_testcase {
|
||||
}
|
||||
|
||||
public function test_touch_session(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$sid = md5('hokus');
|
||||
@@ -223,17 +234,23 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$record->id = $DB->insert_record('sessions', $record);
|
||||
$record->id = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$now = time();
|
||||
\core\session\manager::touch_session($sid);
|
||||
$updated = $DB->get_field('sessions', 'timemodified', array('id'=>$record->id));
|
||||
$session = \core\session\manager::get_session_by_sid($sid);
|
||||
|
||||
$this->assertGreaterThanOrEqual($now, $updated);
|
||||
$this->assertLessThanOrEqual(time(), $updated);
|
||||
$this->assertGreaterThanOrEqual($now, $session->timemodified);
|
||||
$this->assertLessThanOrEqual(time(), $session->timemodified);
|
||||
}
|
||||
|
||||
public function test_kill_session(): void {
|
||||
/**
|
||||
* Test destroy method.
|
||||
*
|
||||
* @return void
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function test_destroy(): void {
|
||||
global $DB, $USER;
|
||||
$this->resetAfterTest();
|
||||
|
||||
@@ -249,23 +266,23 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = 0;
|
||||
$record->sid = md5('pokus');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$this->assertEquals(2, $DB->count_records('sessions'));
|
||||
$this->assertEquals(2, $this->mockhandler->count_sessions());
|
||||
|
||||
\core\session\manager::kill_session($sid);
|
||||
|
||||
$this->assertEquals(1, $DB->count_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('sid'=>$sid)));
|
||||
\core\session\manager::destroy($sid);
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertEquals(1, count($sessions));
|
||||
$this->assertFalse($this->contains_session(['sid' => $sid], $sessions));
|
||||
|
||||
$this->assertSame($userid, $USER->id);
|
||||
}
|
||||
|
||||
public function test_kill_user_sessions(): void {
|
||||
public function test_destroy_user_sessions(): void {
|
||||
global $DB, $USER;
|
||||
$this->resetAfterTest();
|
||||
|
||||
@@ -281,40 +298,44 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus2');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = 0;
|
||||
$record->sid = md5('pokus');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$this->assertEquals(3, $DB->count_records('sessions'));
|
||||
|
||||
\core\session\manager::kill_user_sessions($userid);
|
||||
\core\session\manager::destroy_user_sessions($userid);
|
||||
|
||||
$this->assertEquals(1, $DB->count_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $userid)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertEquals(1, count($sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $userid], $sessions));
|
||||
|
||||
$record->userid = $userid;
|
||||
$record->sid = md5('pokus3');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = $userid;
|
||||
$record->sid = md5('pokus4');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = $userid;
|
||||
$record->sid = md5('pokus5');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$this->assertEquals(3, $DB->count_records('sessions', array('userid' => $userid)));
|
||||
$sessions = \core\session\manager::get_sessions_by_userid($userid);
|
||||
$this->assertCount(3, $sessions);
|
||||
|
||||
\core\session\manager::kill_user_sessions($userid, md5('pokus5'));
|
||||
\core\session\manager::destroy_user_sessions($userid, md5('pokus5'));
|
||||
|
||||
$this->assertEquals(1, $DB->count_records('sessions', array('userid' => $userid)));
|
||||
$this->assertEquals(1, $DB->count_records('sessions', array('userid' => $userid, 'sid' => md5('pokus5'))));
|
||||
$sessions = \core\session\manager::get_sessions_by_userid($userid);
|
||||
$session = reset($sessions);
|
||||
$this->assertCount(1, $sessions);
|
||||
$this->assertEquals(md5('pokus5'), $session->sid);
|
||||
}
|
||||
|
||||
public function test_apply_concurrent_login_limit(): void {
|
||||
@@ -334,41 +355,41 @@ class session_manager_test extends \advanced_testcase {
|
||||
|
||||
$record->sid = md5('hokus1');
|
||||
$record->timecreated = 20;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('hokus2');
|
||||
$record->timecreated = 10;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('hokus3');
|
||||
$record->timecreated = 30;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = $user2->id;
|
||||
$record->sid = md5('pokus1');
|
||||
$record->timecreated = 20;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('pokus2');
|
||||
$record->timecreated = 10;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('pokus3');
|
||||
$record->timecreated = 30;
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->timecreated = 10;
|
||||
$record->userid = $guest->id;
|
||||
$record->sid = md5('g1');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('g2');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('g3');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = 0;
|
||||
$record->sid = md5('nl1');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('nl2');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
$record->sid = md5('nl3');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
set_config('limitconcurrentlogins', 0);
|
||||
$this->assertCount(12, $DB->get_records('sessions'));
|
||||
@@ -390,57 +411,103 @@ class session_manager_test extends \advanced_testcase {
|
||||
set_config('limitconcurrentlogins', 2);
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($user1->id);
|
||||
$this->assertCount(11, $DB->get_records('sessions'));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(11, $sessions);
|
||||
$this->assertTrue($this->contains_session(['userid' => $user1->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user1->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $user1->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 10], $sessions));
|
||||
set_config('limitconcurrentlogins', 2);
|
||||
\core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
|
||||
$this->assertCount(10, $DB->get_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(10, $sessions);
|
||||
$this->assertFalse($this->contains_session(['userid' => $user2->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($guest->id);
|
||||
\core\session\manager::apply_concurrent_login_limit(0);
|
||||
$this->assertCount(10, $DB->get_records('sessions'));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(10, $sessions);
|
||||
|
||||
set_config('limitconcurrentlogins', 1);
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($user1->id, md5('grrr'));
|
||||
$this->assertCount(9, $DB->get_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(9, $sessions);
|
||||
$this->assertFalse($this->contains_session(['userid' => $user1->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user1->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $user1->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($user1->id);
|
||||
$this->assertCount(9, $DB->get_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 20)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 30)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user1->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(9, $sessions);
|
||||
$this->assertFalse($this->contains_session(['userid' => $user1->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user1->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $user1->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($user2->id, md5('pokus2'));
|
||||
$this->assertCount(8, $DB->get_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(8, $sessions);
|
||||
$this->assertFalse($this->contains_session(['userid' => $user2->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $user2->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($user2->id);
|
||||
$this->assertCount(8, $DB->get_records('sessions'));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 20)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 30)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('userid' => $user2->id, 'timecreated' => 10)));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(8, $sessions);
|
||||
$this->assertFalse($this->contains_session(['userid' => $user2->id, 'timecreated' => 20], $sessions));
|
||||
$this->assertFalse($this->contains_session(['userid' => $user2->id, 'timecreated' => 30], $sessions));
|
||||
$this->assertTrue($this->contains_session(['userid' => $user2->id, 'timecreated' => 10], $sessions));
|
||||
|
||||
\core\session\manager::apply_concurrent_login_limit($guest->id);
|
||||
\core\session\manager::apply_concurrent_login_limit(0);
|
||||
$this->assertCount(8, $DB->get_records('sessions'));
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertCount(8, $sessions);
|
||||
}
|
||||
|
||||
public function test_kill_all_sessions(): void {
|
||||
/**
|
||||
* Helper method to check if the sessions array contains a session with the given conditions.
|
||||
*
|
||||
* @param array $conditions Conditions to match.
|
||||
* @param null|\Iterator $sessions Sessions to check.
|
||||
* @return bool
|
||||
*/
|
||||
protected function contains_session(array $conditions, ?\Iterator $sessions = null): bool {
|
||||
foreach ($sessions as $session) {
|
||||
if ($this->matches_session($conditions, $session)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if the session matches the given conditions.
|
||||
*
|
||||
* @param array $conditions Conditions to match.
|
||||
* @param \stdClass $session Session to check.
|
||||
* @return bool
|
||||
*/
|
||||
protected function matches_session(array $conditions, \stdClass $session): bool {
|
||||
foreach ($conditions as $key => $value) {
|
||||
if ($session->$key != $value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test destroy_all method.
|
||||
*
|
||||
* @return void
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function test_destroy_all(): void {
|
||||
global $DB, $USER;
|
||||
$this->resetAfterTest();
|
||||
|
||||
@@ -456,25 +523,25 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus2');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->userid = 0;
|
||||
$record->sid = md5('pokus');
|
||||
$DB->insert_record('sessions', $record);
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$this->assertEquals(3, $DB->count_records('sessions'));
|
||||
|
||||
\core\session\manager::kill_all_sessions();
|
||||
\core\session\manager::destroy_all();
|
||||
|
||||
$this->assertEquals(0, $DB->count_records('sessions'));
|
||||
$this->assertSame(0, $USER->id);
|
||||
}
|
||||
|
||||
public function test_gc(): void {
|
||||
global $CFG, $DB, $USER;
|
||||
global $CFG, $USER;
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
@@ -483,6 +550,8 @@ class session_manager_test extends \advanced_testcase {
|
||||
$guestid = $USER->id;
|
||||
$this->setUser(0);
|
||||
|
||||
// Set sessions timeout to 600 (10 minutes) seconds.
|
||||
// We will test if sessions not modified for 600 seconds are removed.
|
||||
$CFG->sessiontimeout = 60*10;
|
||||
|
||||
$record = new \stdClass();
|
||||
@@ -493,53 +562,53 @@ class session_manager_test extends \advanced_testcase {
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$r1 = $DB->insert_record('sessions', $record);
|
||||
$r1 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus2');
|
||||
$record->userid = $adminid;
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 60*20;
|
||||
$r2 = $DB->insert_record('sessions', $record);
|
||||
$r2 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus3');
|
||||
$record->userid = $guestid;
|
||||
$record->timecreated = time() - 60*60*60;
|
||||
$record->timemodified = time() - 60*20;
|
||||
$r3 = $DB->insert_record('sessions', $record);
|
||||
$r3 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus4');
|
||||
$record->userid = $guestid;
|
||||
$record->timecreated = time() - 60*60*60;
|
||||
$record->timemodified = time() - 60*10*5 - 60;
|
||||
$r4 = $DB->insert_record('sessions', $record);
|
||||
$r4 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus5');
|
||||
$record->userid = 0;
|
||||
$record->timecreated = time() - 60*5;
|
||||
$record->timemodified = time() - 60*5;
|
||||
$r5 = $DB->insert_record('sessions', $record);
|
||||
$r5 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus6');
|
||||
$record->userid = 0;
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 60*10 -10;
|
||||
$r6 = $DB->insert_record('sessions', $record);
|
||||
$r6 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('hokus7');
|
||||
$record->userid = 0;
|
||||
$record->timecreated = time() - 60*60;
|
||||
$record->timemodified = time() - 60*9;
|
||||
$r7 = $DB->insert_record('sessions', $record);
|
||||
$r7 = $this->mockhandler->add_test_session($record);
|
||||
|
||||
\core\session\manager::gc();
|
||||
|
||||
$this->assertTrue($DB->record_exists('sessions', array('id'=>$r1)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('id'=>$r2)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('id'=>$r3)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('id'=>$r4)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('id'=>$r5)));
|
||||
$this->assertFalse($DB->record_exists('sessions', array('id'=>$r6)));
|
||||
$this->assertTrue($DB->record_exists('sessions', array('id'=>$r7)));
|
||||
\core\session\manager::gc($CFG->sessiontimeout);
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertTrue($this->contains_session(['id' => $r1], $sessions));
|
||||
$this->assertFalse($this->contains_session(['id' => $r2], $sessions));
|
||||
$this->assertTrue($this->contains_session(['id' => $r3], $sessions));
|
||||
$this->assertFalse($this->contains_session(['id' => $r4], $sessions));
|
||||
$this->assertFalse($this->contains_session(['id' => $r5], $sessions));
|
||||
$this->assertFalse($this->contains_session(['id' => $r6], $sessions));
|
||||
$this->assertTrue($this->contains_session(['id' => $r7], $sessions));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -648,7 +717,7 @@ class session_manager_test extends \advanced_testcase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function pages_sessionlocks() {
|
||||
public function pages_sessionlocks(): array {
|
||||
return [
|
||||
[
|
||||
'url' => '/good.php',
|
||||
@@ -738,7 +807,7 @@ class session_manager_test extends \advanced_testcase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function sessionlock_history() {
|
||||
public function sessionlock_history(): array {
|
||||
return [
|
||||
[
|
||||
'url' => '/good.php',
|
||||
@@ -849,7 +918,7 @@ class session_manager_test extends \advanced_testcase {
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function array_session_diff_provider() {
|
||||
public static function array_session_diff_provider(): array {
|
||||
// Create an instance of this object so the comparison object's identities are the same.
|
||||
// Used in one of the tests below.
|
||||
$compareobjectb = (object) ['array' => 'b'];
|
||||
@@ -920,4 +989,44 @@ class session_manager_test extends \advanced_testcase {
|
||||
$result = $method->invokeArgs(null, [$a, $b]);
|
||||
$this->assertSame($expected, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test destroy by auth plugin method.
|
||||
*/
|
||||
public function test_destroy_by_auth_plugin(): void {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
|
||||
// Create test users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user(['auth' => 'db']);
|
||||
|
||||
// Create sessions for the users.
|
||||
$user1sid = md5('hokus');
|
||||
$record = new \stdClass();
|
||||
$record->state = 0;
|
||||
$record->sid = $user1sid;
|
||||
$record->sessdata = null;
|
||||
$record->userid = $user1->id;
|
||||
$record->timecreated = time() - 60 * 60;
|
||||
$record->timemodified = time() - 30;
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
$record->sid = md5('pokus');
|
||||
$record->userid = $user2->id;
|
||||
$this->mockhandler->add_test_session($record);
|
||||
|
||||
// Check sessions.
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertEquals(2, count($sessions));
|
||||
|
||||
// Destroy the session for the user with manual auth plugin.
|
||||
\core\session\manager::destroy_by_auth_plugin('manual');
|
||||
|
||||
// Check that the session for the user with manual auth plugin is destroyed.
|
||||
$sessions = $this->mockhandler->get_all_sessions();
|
||||
$this->assertEquals(1, count($sessions));
|
||||
$this->assertFalse($this->contains_session(['sid' => $user1sid], $sessions));
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace core;
|
||||
namespace core\session;
|
||||
|
||||
use core\tests\session\mock_handler;
|
||||
use Redis;
|
||||
use RedisException;
|
||||
|
||||
@@ -34,21 +35,21 @@ use RedisException;
|
||||
* @copyright 2016 Russell Smith
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @runClassInSeparateProcess
|
||||
* @covers \core\session\redis
|
||||
*/
|
||||
class session_redis_test extends \advanced_testcase {
|
||||
|
||||
/** @var $keyprefix This key prefix used when testing Redis */
|
||||
protected $keyprefix = null;
|
||||
/** @var $redis The current testing redis connection */
|
||||
protected $redis = null;
|
||||
final class redis_test extends \advanced_testcase {
|
||||
/** @var string $keyprefix This key prefix used when testing Redis */
|
||||
protected string $keyprefix = '';
|
||||
/** @var ?Redis $redis The current testing redis connection */
|
||||
protected ?Redis $redis = null;
|
||||
/** @var bool $encrypted Is the current testing redis connection encrypted*/
|
||||
protected $encrypted = false;
|
||||
protected bool $encrypted = false;
|
||||
/** @var int $acquiretimeout how long we wait for session lock in seconds when testing Redis */
|
||||
protected $acquiretimeout = 1;
|
||||
protected int $acquiretimeout = 1;
|
||||
/** @var int $lockexpire how long to wait in seconds before expiring the lock when testing Redis */
|
||||
protected $lockexpire = 70;
|
||||
|
||||
protected int $lockexpire = 70;
|
||||
|
||||
#[\Override]
|
||||
public function setUp(): void {
|
||||
global $CFG;
|
||||
parent::setUp();
|
||||
@@ -62,8 +63,8 @@ class session_redis_test extends \advanced_testcase {
|
||||
$version = phpversion('Redis');
|
||||
if (!$version) {
|
||||
$this->markTestSkipped('Redis extension version missing');
|
||||
} else if (version_compare($version, \core\session\redis::REDIS_EXTENSION_MIN_VERSION) <= 0) {
|
||||
$this->markTestSkipped('Redis extension version must be at least ' . \core\session\redis::REDIS_EXTENSION_MIN_VERSION .
|
||||
} else if (version_compare($version, \core\session\redis::REDIS_MIN_EXTENSION_VERSION) <= 0) {
|
||||
$this->markTestSkipped('Redis extension version must be at least ' . \core\session\redis::REDIS_MIN_EXTENSION_VERSION .
|
||||
': now running "' . $version . '"');
|
||||
}
|
||||
|
||||
@@ -135,7 +136,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->assertSame('DATA', $sess->read('sess1'));
|
||||
$this->assertTrue($sess->write('sess1', 'DATA-new'));
|
||||
$this->assertTrue($sess->close());
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
}
|
||||
|
||||
public function test_compression_read_and_write_works(): void {
|
||||
@@ -187,16 +188,16 @@ class session_redis_test extends \advanced_testcase {
|
||||
$sessblocked->read('sess1');
|
||||
$this->fail('Session lock must fail to be obtained.');
|
||||
} catch (\core\session\exception $e) {
|
||||
$this->assertStringContainsString("Unable to obtain lock for session id sess1", $e->getMessage());
|
||||
$this->assertStringContainsString("Unable to obtain lock for session id session_se", $e->getMessage());
|
||||
$this->assertStringContainsString('within 1 sec.', $e->getMessage());
|
||||
$this->assertStringContainsString('session lock timeout (1 min 10 secs) ', $e->getMessage());
|
||||
$this->assertStringContainsString('Cannot obtain session lock for sid: sess1', file_get_contents($errorlog));
|
||||
$this->assertStringContainsString('Cannot obtain session lock for sid: session_sess1', file_get_contents($errorlog));
|
||||
}
|
||||
|
||||
$this->assertTrue($sessblocked->close());
|
||||
$this->assertTrue($sess->write('sess1', 'DATA-new'));
|
||||
$this->assertTrue($sess->close());
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
}
|
||||
|
||||
public function test_session_is_destroyed_when_it_does_not_exist(): void {
|
||||
@@ -205,7 +206,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
$sess->set_requires_write_lock(true);
|
||||
$this->assertTrue($sess->open('Not used', 'Not used'));
|
||||
$this->assertTrue($sess->destroy('sess-destroy'));
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
}
|
||||
|
||||
public function test_session_is_destroyed_when_we_have_it_open(): void {
|
||||
@@ -216,7 +217,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->assertSame('', $sess->read('sess-destroy'));
|
||||
$this->assertTrue($sess->destroy('sess-destroy'));
|
||||
$this->assertTrue($sess->close());
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
}
|
||||
|
||||
public function test_multiple_sessions_do_not_interfere_with_each_other(): void {
|
||||
@@ -260,7 +261,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->assertTrue($sess2->close());
|
||||
|
||||
// Read the session again to ensure locking did what it should.
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
}
|
||||
|
||||
public function test_multiple_sessions_work_with_a_single_instance(): void {
|
||||
@@ -279,7 +280,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->assertTrue($sess->destroy('sess2'));
|
||||
|
||||
$this->assertTrue($sess->close());
|
||||
$this->assertSessionNoLocks();
|
||||
$this->assert_session_no_locks();
|
||||
|
||||
$this->assertTrue($sess->close());
|
||||
}
|
||||
@@ -299,12 +300,14 @@ class session_redis_test extends \advanced_testcase {
|
||||
$this->assertFalse($sess->session_exists('sess1'), 'Session should be destroyed.');
|
||||
}
|
||||
|
||||
public function test_kill_sessions_removes_the_session_from_redis(): void {
|
||||
public function test_destroy_removes_the_session_from_redis(): void {
|
||||
global $DB;
|
||||
|
||||
$sess = new \core\session\redis();
|
||||
$sess->init();
|
||||
|
||||
$mockhandler = new mock_handler();
|
||||
|
||||
$this->assertTrue($sess->open('Not used', 'Not used'));
|
||||
$this->assertTrue($sess->write('sess1', 'DATA'));
|
||||
$this->assertTrue($sess->write('sess2', 'DATA'));
|
||||
@@ -316,22 +319,27 @@ class session_redis_test extends \advanced_testcase {
|
||||
$sessiondata->timemodified = time();
|
||||
|
||||
$sessiondata->sid = 'sess1';
|
||||
$DB->insert_record('sessions', $sessiondata);
|
||||
$mockhandler->add_test_session($sessiondata);
|
||||
$sessiondata->sid = 'sess2';
|
||||
$DB->insert_record('sessions', $sessiondata);
|
||||
$mockhandler->add_test_session($sessiondata);
|
||||
$sessiondata->sid = 'sess3';
|
||||
$DB->insert_record('sessions', $sessiondata);
|
||||
$mockhandler->add_test_session($sessiondata);
|
||||
|
||||
$this->assertNotEquals('', $sess->read('sess1'));
|
||||
$sess->kill_session('sess1');
|
||||
$sess->destroy('sess1');
|
||||
$this->assertEquals('', $sess->read('sess1'));
|
||||
|
||||
$this->assertEmpty($this->redis->keys($this->keyprefix.'sess1.lock'));
|
||||
|
||||
$sess->kill_all_sessions();
|
||||
$sess->destroy_all();
|
||||
|
||||
$this->assertEquals(3, $DB->count_records('sessions'), 'Moodle handles session database, plugin must not change it.');
|
||||
$this->assertSessionNoLocks();
|
||||
$mockhandler = new mock_handler();
|
||||
$this->assertEquals(
|
||||
3,
|
||||
$mockhandler->count_sessions(),
|
||||
'Moodle handles session database, plugin must not change it.',
|
||||
);
|
||||
$this->assert_session_no_locks();
|
||||
$this->assertEmpty($this->redis->keys($this->keyprefix.'*'), 'There should be no session data left.');
|
||||
}
|
||||
|
||||
@@ -360,7 +368,7 @@ class session_redis_test extends \advanced_testcase {
|
||||
/**
|
||||
* Assert that we don't have any session locks in Redis.
|
||||
*/
|
||||
protected function assertSessionNoLocks() {
|
||||
protected function assert_session_no_locks(): void {
|
||||
$this->assertEmpty($this->redis->keys($this->keyprefix.'*.lock'));
|
||||
}
|
||||
|
||||
@@ -375,4 +383,208 @@ class session_redis_test extends \advanced_testcase {
|
||||
|
||||
$this->assertEquals($CFG->session_redis_encrypt, $prop->getValue($sess));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the get maxlifetime method.
|
||||
*/
|
||||
public function test_get_maxlifetime(): void {
|
||||
global $CFG;
|
||||
|
||||
// Set the timeout to something known for the test.
|
||||
set_config('sessiontimeout', 100);
|
||||
|
||||
// Generate a test user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// The get_maxlifetime is private, so we need to use reflection to access it.
|
||||
$method = new \ReflectionMethod(\core\session\redis::class, 'get_maxlifetime');
|
||||
|
||||
// Test guest timeout, which should be longer.
|
||||
$result = $method->invoke($session, $CFG->siteguest);
|
||||
$this->assertEquals(500, $result);
|
||||
|
||||
// Test first access timeout.
|
||||
$result = $method->invoke($session, 0, true);
|
||||
$this->assertEquals(180, $result);
|
||||
|
||||
// Test with a real user.
|
||||
$result = $method->invoke($session, $user->id);
|
||||
$this->assertEquals(180, $result);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the add session method.
|
||||
*/
|
||||
public function test_add_session(): void {
|
||||
|
||||
// Set the timeout to something known for the test.
|
||||
set_config('sessiontimeout', 100);
|
||||
|
||||
// Generate a test user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// Create two sessions for the user.
|
||||
session_id('id1');
|
||||
$session1data = $session->add_session($user->id);
|
||||
session_id('id2');
|
||||
$session2data = $session->add_session($user->id);
|
||||
|
||||
$session1 = $session->get_session_by_sid('id1');
|
||||
$session2 = $session->get_session_by_sid('id2');
|
||||
|
||||
// Assert that the sessions were created and have expected data.
|
||||
$this->assertEqualsCanonicalizing((array)$session1data, (array)$session1);
|
||||
$this->assertEqualsCanonicalizing((array)$session2data, (array)$session2);
|
||||
|
||||
// Check that the session hash has a ttl set.
|
||||
$this->assertGreaterThan(-1, $this->redis->ttl($this->keyprefix . 'session_id1'));
|
||||
|
||||
// Check that the session ttl is less or equal to what we set it.
|
||||
$this->assertLessThanOrEqual(180, $this->redis->ttl($this->keyprefix . 'session_id1'));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test writing session data.
|
||||
*/
|
||||
public function test_write(): void {
|
||||
// Set the timeout to something known for the test.
|
||||
set_config('sessiontimeout', 100);
|
||||
|
||||
// Generate a test user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// Create two sessions for the user.
|
||||
session_id('id1');
|
||||
$session->add_session($user->id);
|
||||
session_id('id2');
|
||||
$session->add_session($user->id);
|
||||
|
||||
$testdata = 'some test data';
|
||||
|
||||
// Write some data to the store.
|
||||
$result = $session->write('id2', $testdata);
|
||||
|
||||
// Check that the write was successful.
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Check that the data was written to the store.
|
||||
$getdata = $this->redis->hget($this->keyprefix . 'session_id2', 'sessdata');
|
||||
$this->assertStringContainsString($testdata, $getdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test reading session data.
|
||||
*/
|
||||
public function test_read(): void {
|
||||
// Set the timeout to something known for the test.
|
||||
set_config('sessiontimeout', 100);
|
||||
|
||||
// Generate a test user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// Create two sessions for the user.
|
||||
session_id('id1');
|
||||
$session->add_session($user->id);
|
||||
session_id('id2');
|
||||
$session->add_session($user->id);
|
||||
|
||||
$testdata = 'some test data';
|
||||
|
||||
// Write some session data to the store.
|
||||
$session->write('id2', $testdata);
|
||||
|
||||
// Read the session data.
|
||||
$result = $session->read('id2');
|
||||
|
||||
// Check that the read was successful.
|
||||
$this->assertEquals($result, $testdata);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a session.
|
||||
*/
|
||||
public function test_update_session(): void {
|
||||
// Set the timeout to something known for the test.
|
||||
set_config('sessiontimeout', 100);
|
||||
|
||||
// Generate a test user.
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// Create two sessions for the user.
|
||||
session_id('id1');
|
||||
$session->add_session($user->id);
|
||||
session_id('id2');
|
||||
$sessiondata = $session->add_session($user->id);
|
||||
|
||||
// Update the session data.
|
||||
$sessiondata->lastip = '8.8.8.8';
|
||||
$session->update_session($sessiondata);
|
||||
|
||||
// Check the value was updated.
|
||||
$updatedsession = $session->get_session_by_sid('id2');
|
||||
$this->assertEquals('8.8.8.8', $updatedsession->lastip);
|
||||
|
||||
// Test session update when userid is not set, should not error.
|
||||
unset($sessiondata->userid);
|
||||
$session->update_session($sessiondata);
|
||||
$this->assertDebuggingNotCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test destroying a session by auth plugin.
|
||||
*/
|
||||
public function test_destroy_by_auth_plugin(): void {
|
||||
// Create test users.
|
||||
$user1 = $this->getDataGenerator()->create_user();
|
||||
$user2 = $this->getDataGenerator()->create_user(['auth' => 'db']);
|
||||
|
||||
// Create a new redis session object.
|
||||
$session = new \core\session\redis();
|
||||
$session->init();
|
||||
|
||||
// Create sessions for the users.
|
||||
session_id('id1');
|
||||
$session1data = $session->add_session($user1->id);
|
||||
session_id('id2');
|
||||
$session2data = $session->add_session($user2->id);
|
||||
|
||||
$session1 = $session->get_session_by_sid('id1');
|
||||
$session2 = $session->get_session_by_sid('id2');
|
||||
|
||||
// Assert that the sessions were created and have expected data.
|
||||
$this->assertEqualsCanonicalizing((array) $session1data, (array) $session1);
|
||||
$this->assertEqualsCanonicalizing((array) $session2data, (array) $session2);
|
||||
|
||||
// Destroy the session by auth plugin.
|
||||
$session->destroy_by_auth_plugin('manual');
|
||||
|
||||
// Check that the session was destroyed.
|
||||
$this->assertFalse($session->session_exists('id1'));
|
||||
|
||||
// Check the session with db auth plugin was not destroyed.
|
||||
$this->assertTrue($session->session_exists('id2'));
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ if ($mform->is_cancelled()) {
|
||||
|
||||
// Log out all other sessions if mandated by admin, or if set by the user.
|
||||
if (!empty($CFG->passwordchangelogout) || !empty($data->logoutothersessions)) {
|
||||
\core\session\manager::kill_user_sessions($USER->id, session_id());
|
||||
\core\session\manager::destroy_user_sessions($USER->id, session_id());
|
||||
}
|
||||
|
||||
if (!empty($data->signoutofotherservices)) {
|
||||
|
||||
+1
-1
@@ -288,7 +288,7 @@ function core_login_process_password_set($token) {
|
||||
}
|
||||
user_add_password_history($user->id, $data->password);
|
||||
if (!empty($CFG->passwordchangelogout) || !empty($data->logoutothersessions)) {
|
||||
\core\session\manager::kill_user_sessions($user->id, session_id());
|
||||
\core\session\manager::destroy_user_sessions($user->id, session_id());
|
||||
}
|
||||
// Reset login lockout (if present) before a new password is set.
|
||||
login_unlock_account($user);
|
||||
|
||||
@@ -77,15 +77,15 @@ function report_usersessions_format_ip($ip) {
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
function report_usersessions_kill_session($id) {
|
||||
global $DB, $USER;
|
||||
function report_usersessions_kill_session(int $id): void {
|
||||
global $USER;
|
||||
|
||||
$session = $DB->get_record('sessions', array('id' => $id, 'userid' => $USER->id), 'id, sid');
|
||||
$sessions = \core\session\manager::get_sessions_by_userid($USER->id);
|
||||
$filteredsessions = array_filter($sessions, fn ($session) => $session->id === $id);
|
||||
|
||||
if (!$session or $session->sid === session_id()) {
|
||||
// Do not delete the current session!
|
||||
return;
|
||||
foreach ($filteredsessions as $session) {
|
||||
if ($session->sid !== session_id()) {
|
||||
\core\session\manager::destroy($session->sid);
|
||||
}
|
||||
}
|
||||
|
||||
\core\session\manager::kill_session($session->sid);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ if ($delete && confirm_sesskey()) {
|
||||
|
||||
// Delete all sessions except current.
|
||||
if ($deleteall && confirm_sesskey()) {
|
||||
\core\session\manager::kill_user_sessions($USER->id, session_id());
|
||||
\core\session\manager::destroy_user_sessions($USER->id, session_id());
|
||||
redirect(
|
||||
url: $PAGE->url,
|
||||
message: get_string('logoutothersessionssuccess', 'report_usersessions'),
|
||||
@@ -82,15 +82,13 @@ echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading(get_string('mysessions', 'report_usersessions'));
|
||||
|
||||
$data = array();
|
||||
$sql = "SELECT id, timecreated, timemodified, firstip, lastip, sid
|
||||
FROM {sessions}
|
||||
WHERE userid = :userid
|
||||
ORDER BY timemodified DESC";
|
||||
$params = array('userid' => $USER->id, 'sid' => session_id());
|
||||
|
||||
$sessions = $DB->get_records_sql($sql, $params);
|
||||
$sessions = \core\session\manager::get_sessions_by_userid($USER->id);
|
||||
// Order records by timemodified DESC.
|
||||
usort($sessions, function($a, $b){
|
||||
return $b->timemodified <=> $a->timemodified;
|
||||
});
|
||||
foreach ($sessions as $session) {
|
||||
if ($session->sid === $params['sid']) {
|
||||
if ($session->sid === session_id()) {
|
||||
$lastaccess = get_string('thissession', 'report_usersessions');
|
||||
$deletelink = '';
|
||||
|
||||
|
||||
@@ -294,7 +294,10 @@ class provider implements
|
||||
// Delete user course requests.
|
||||
$DB->delete_records('course_request', ['requester' => $userid]);
|
||||
// Delete sessions.
|
||||
$DB->delete_records('sessions', ['userid' => $userid]);
|
||||
$sessions = \core\session\manager::get_sessions_by_userid($userid);
|
||||
foreach ($sessions as $session) {
|
||||
\core\session\manager::destroy($session->sid);
|
||||
}
|
||||
// Do I delete user preferences? Seems like the right place to do it.
|
||||
$DB->delete_records('user_preferences', ['userid' => $userid]);
|
||||
|
||||
@@ -528,7 +531,7 @@ class provider implements
|
||||
protected static function export_user_session_data(int $userid, \context $context) {
|
||||
global $DB, $SESSION;
|
||||
|
||||
$records = $DB->get_records('sessions', ['userid' => $userid]);
|
||||
$records = \core\session\manager::get_sessions_by_userid($userid);
|
||||
if (!empty($records)) {
|
||||
$sessiondata = (object) array_map(function($record) {
|
||||
return [
|
||||
|
||||
@@ -233,7 +233,7 @@ if ($userform->is_cancelled()) {
|
||||
if (!empty($CFG->passwordchangelogout)) {
|
||||
// We can use SID of other user safely here because they are unique,
|
||||
// the problem here is we do not want to logout admin here when changing own password.
|
||||
\core\session\manager::kill_user_sessions($usernew->id, session_id());
|
||||
\core\session\manager::destroy_user_sessions($usernew->id, session_id());
|
||||
}
|
||||
if (!empty($usernew->signoutofotherservices)) {
|
||||
webservice::delete_user_ws_tokens($usernew->id);
|
||||
@@ -243,7 +243,7 @@ if ($userform->is_cancelled()) {
|
||||
|
||||
// Force logout if user just suspended.
|
||||
if (isset($usernew->suspended) and $usernew->suspended and !$user->suspended) {
|
||||
\core\session\manager::kill_user_sessions($user->id);
|
||||
\core\session\manager::destroy_user_sessions($user->id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ class core_user_external extends \core_external\external_api {
|
||||
useredit_update_user_preference($userpref);
|
||||
}
|
||||
if (isset($user['suspended']) and $user['suspended']) {
|
||||
\core\session\manager::kill_user_sessions($user['id']);
|
||||
\core\session\manager::destroy_user_sessions($user['id']);
|
||||
}
|
||||
|
||||
$transaction->allow_commit();
|
||||
|
||||
@@ -13,33 +13,30 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
/**
|
||||
* Privacy tests for core_user.
|
||||
*
|
||||
* @package core_user
|
||||
* @category test
|
||||
* @copyright 2018 Adrian Greeve <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_user\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
global $CFG;
|
||||
|
||||
use \core_privacy\tests\provider_testcase;
|
||||
use \core_user\privacy\provider;
|
||||
use \core_privacy\local\request\approved_userlist;
|
||||
use \core_privacy\local\request\transform;
|
||||
|
||||
require_once($CFG->dirroot . "/user/lib.php");
|
||||
use core\tests\session\mock_handler;
|
||||
use core_privacy\tests\provider_testcase;
|
||||
use core_privacy\local\request\approved_userlist;
|
||||
use core_privacy\local\request\transform;
|
||||
use core_user\privacy\provider;
|
||||
|
||||
/**
|
||||
* Unit tests for core_user.
|
||||
*
|
||||
* @package core_user
|
||||
* @copyright 2018 Adrian Greeve <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_user\privacy\provider
|
||||
*/
|
||||
class provider_test extends provider_testcase {
|
||||
final class provider_test extends provider_testcase {
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $CFG;
|
||||
parent::setUpBeforeClass();
|
||||
|
||||
require_once($CFG->dirroot . "/user/lib.php");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that context information is returned correctly.
|
||||
@@ -475,7 +472,8 @@ class provider_test extends provider_testcase {
|
||||
'firstip' => '0.0.0.0',
|
||||
'lastip' => '0.0.0.0'
|
||||
];
|
||||
$DB->insert_record('sessions', $usersessions);
|
||||
$mockhandler = new mock_handler();
|
||||
$mockhandler->add_test_session($usersessions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -213,7 +213,7 @@ class core_webservice_external extends \core_external\external_api {
|
||||
$siteinfo['limitconcurrentlogins'] = (int) $CFG->limitconcurrentlogins;
|
||||
if (!empty($CFG->limitconcurrentlogins)) {
|
||||
// For performance, only when enabled.
|
||||
$siteinfo['usersessionscount'] = $DB->count_records('sessions', ['userid' => $USER->id]);
|
||||
$siteinfo['usersessionscount'] = count(\core\session\manager::get_sessions_by_userid($USER->id));
|
||||
}
|
||||
|
||||
$siteinfo['policyagreed'] = $USER->policyagreed;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace core_webservice;
|
||||
|
||||
use core_external\external_api;
|
||||
use externallib_advanced_testcase;
|
||||
use core\tests\session\mock_handler;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
@@ -33,8 +34,8 @@ require_once($CFG->dirroot . '/webservice/tests/helpers.php');
|
||||
* @copyright 2012 Paul Charsley
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class externallib_test extends externallib_advanced_testcase {
|
||||
|
||||
final class externallib_test extends externallib_advanced_testcase {
|
||||
#[\Override]
|
||||
public function setUp(): void {
|
||||
// Calling parent is good, always
|
||||
parent::setUp();
|
||||
@@ -190,7 +191,9 @@ class externallib_test extends externallib_advanced_testcase {
|
||||
$record->firstip = $record->lastip = '10.0.0.1';
|
||||
$record->sid = md5('hokus1');
|
||||
$record->timecreated = time();
|
||||
$DB->insert_record('sessions', $record);
|
||||
|
||||
$mockhandler = new mock_handler();
|
||||
$mockhandler->add_test_session($record);
|
||||
|
||||
$siteinfo = \core_webservice_external::get_site_info();
|
||||
$siteinfo = external_api::clean_returnvalue(\core_webservice_external::get_site_info_returns(), $siteinfo);
|
||||
|
||||
Reference in New Issue
Block a user