Skip to content

Commit

Permalink
Log active user sessions to database (#5339)
Browse files Browse the repository at this point in the history
Co-authored-by: rex-bot <78283150+rex-bot@users.noreply.github.com>
  • Loading branch information
bloep and rex-bot committed Oct 13, 2022
1 parent 519286e commit 2b6d00c
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 12 deletions.
Binary file modified .github/tests-visual/backup_export--dark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/tests-visual/backup_export.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 6 additions & 9 deletions .tools/psalm/baseline.xml
Expand Up @@ -4758,9 +4758,8 @@
<code>$sessionConfig['samesite']</code>
<code>$sessionConfig['secure']</code>
</MixedArrayAccess>
<MixedAssignment occurrences="2">
<MixedAssignment occurrences="1">
<code>$sessionConfig</code>
<code>$userId</code>
</MixedAssignment>
<PossiblyFalseReference occurrences="2">
<code>format</code>
Expand Down Expand Up @@ -4817,25 +4816,23 @@
<MixedArrayOffset occurrences="1">
<code>$cookieParams[$name]</code>
</MixedArrayOffset>
<MixedAssignment occurrences="14">
<MixedAssignment occurrences="11">
<code>$cookieParams[$name]</code>
<code>$impersonator</code>
<code>$name</code>
<code>$password</code>
<code>$password</code>
<code>$rexSessId</code>
<code>$sessUid</code>
<code>$sessionConfig</code>
<code>$sessionStartTime</code>
<code>$this-&gt;DB</code>
<code>$this-&gt;cache</code>
<code>$this-&gt;logout</code>
<code>$this-&gt;sessionMaxOverallDuration</code>
<code>$value</code>
</MixedAssignment>
<MixedOperand occurrences="1">
<code>$sessionStartTime</code>
</MixedOperand>
<MixedReturnStatement occurrences="2">
<code>$_SESSION[static::getSessionNamespace()][$this-&gt;systemId][$varname]</code>
<code>$default</code>
</MixedReturnStatement>
<MixedReturnTypeCoercion occurrences="2">
<code>$cookieParams</code>
<code>array{lifetime: ?int, path: ?string, domain: ?string, secure: ?bool, httponly: ?bool, samesite: ?string}</code>
Expand Down
14 changes: 12 additions & 2 deletions redaxo/src/addons/backup/pages/export.php
Expand Up @@ -154,9 +154,19 @@
$tables = rex_sql::factory()->getTables();
foreach ($tables as $table) {
$tableSelect->addOption($table, $table);
if ($table != rex::getTable('user') && str_starts_with($table, rex::getTablePrefix()) && !str_starts_with($table, rex::getTablePrefix() . rex::getTempPrefix())) {
$tableSelect->setSelected($table);
if ($table === rex::getTable('user') || $table === rex::getTable('user_session')) {
continue;
}
// skip non rex_ tables
if (!str_starts_with($table, rex::getTablePrefix())) {
continue;
}
// skip rex_tmp_ tables
if (str_starts_with($table, rex::getTablePrefix().rex::getTempPrefix())) {
continue;
}

$tableSelect->setSelected($table);
}

$formElements = [];
Expand Down
10 changes: 10 additions & 0 deletions redaxo/src/core/install.php
Expand Up @@ -58,3 +58,13 @@
->setRawValue('password_changed', 'updatedate')
->update();
}

rex_sql_table::get(rex::getTable('user_session'))
->ensureColumn(new rex_sql_column('session_id', 'varchar(255)'))
->ensureColumn(new rex_sql_column('user_id', 'int(10) unsigned'))
->ensureColumn(new rex_sql_column('ip', 'varchar(39)')) // max for ipv6
->ensureColumn(new rex_sql_column('useragent', 'varchar(255)'))
->ensureColumn(new rex_sql_column('starttime', 'datetime'))
->ensureColumn(new rex_sql_column('last_activity', 'datetime'))
->setPrimaryKey('session_id')
->ensure();
9 changes: 9 additions & 0 deletions redaxo/src/core/lang/de_de.lang
Expand Up @@ -415,6 +415,15 @@ backend_language = Backendsprache
user_data_updated = Benutzerdaten wurden aktualisiert!
theme = Backend-Theme

# src\core\pages\profile.sessions.php
session_caption = Liste aller offenen Sitzungen
session_id = Sitzungs-ID
ip = IP-Adresse
user_agent = User-Agent
last_activity = Letzte Aktivität
starttime = Startzeit
active_session = Aktive Sitzung

password_change_required = Das Passwort ist abgelaufen. Um fortzufahren, bitte ein neues Passwort angeben!
password_not_changed = Das neue Passwort muss sich vom vorherigen unterscheiden!
password_already_used = Das Passwort wurde schon benutzt und kann nicht erneut verwendet werden!
Expand Down
13 changes: 13 additions & 0 deletions redaxo/src/core/lib/login/backend_login.php
Expand Up @@ -114,6 +114,7 @@ public function checkLogin()
}
array_push($params, rex_sql::datetime(), rex_sql::datetime(), session_id(), $this->userLogin);
$sql->setQuery('UPDATE ' . $this->tableName . ' SET ' . $add . 'login_tries=0, lasttrydate=?, lastlogin=?, session_id=? WHERE login=? LIMIT 1', $params);
rex_user_session::getInstance()->storeCurrentSession();
}

assert($this->user instanceof rex_sql);
Expand All @@ -133,6 +134,7 @@ public function checkLogin()
}
}
}
rex_user_session::getInstance()->updateLastActivity();
} else {
// fehlversuch speichern | login_tries++
if ('' != $this->userLogin) {
Expand All @@ -154,9 +156,20 @@ public function checkLogin()
}
}

// check if session was killed only if the user is logged in
if ($check) {
$sql->setQuery('SELECT 1 FROM '.rex::getTable('user_session').' where session_id = ?', [session_id()]);
if (0 === $sql->getRows()) {
$check = false;
$this->message = rex_i18n::msg('login_session_expired');
rex_csrf_token::removeAll();
}
}

if ($this->isLoggedOut() && '' != $userId) {
$sql->setQuery('UPDATE ' . $this->tableName . ' SET session_id="" WHERE id=? LIMIT 1', [$userId]);
self::deleteStayLoggedInCookie();
rex_user_session::getInstance()->clearCurrentSession();
}

return $check;
Expand Down
13 changes: 12 additions & 1 deletion redaxo/src/core/lib/login/login.php
Expand Up @@ -266,6 +266,7 @@ public function checkLogin()
if (1 == $this->user->getRows() && self::passwordVerify($this->userPassword, $this->user->getValue($this->passwordColumn), true)) {
$ok = true;
self::regenerateSessionId();
$this->setSessionVar(self::SESSION_START_TIME, time());
$this->setSessionVar(self::SESSION_USER_ID, $this->user->getValue($this->idColumn));
$this->setSessionVar(self::SESSION_PASSWORD, $this->user->getValue($this->passwordColumn));
} else {
Expand Down Expand Up @@ -455,7 +456,17 @@ public function setSessionVar($varname, $value)
*
* @param string $varname
* @param mixed $default
* @return mixed
* @return mixed
* @psalm-return (
* $varname is 'starttime' ? int|null :
* ($varname is 'STAMP' ? int|null :
* ($varname is 'UID' ? int|null :
* ($varname is 'password' ? string|null :
* ($varname is 'impersonator' ? int|null :
* ($varname is 'last_db_update' ? int|null :
* mixed
* )))))
* )
*/
public function getSessionVar($varname, $default = '')
{
Expand Down
77 changes: 77 additions & 0 deletions redaxo/src/core/lib/login/user_session.php
@@ -0,0 +1,77 @@
<?php

/**
* @package redaxo\core\login
*/
class rex_user_session
{
use rex_singleton_trait;

private const SESSION_VAR_LAST_DB_UPDATE = 'last_db_update';

private function __construct()
{
rex_extension::register('RESPONSE_SHUTDOWN', [self::class, 'clearExpiredSessions']);
}

public function storeCurrentSession(): void
{
if (false === session_id()) {
return;
}

$login = new rex_backend_login();
$userId = $login->getSessionVar(rex_login::SESSION_IMPERSONATOR, null);
if (null === $userId) {
$userId = $login->getSessionVar(rex_login::SESSION_USER_ID);
}

rex_sql::factory()
->setTable(rex::getTable('user_session'))
->setValue('session_id', session_id())
->setValue('user_id', $userId)
->setValue('ip', rex_request::server('REMOTE_ADDR', 'string'))
->setValue('useragent', rex_request::server('HTTP_USER_AGENT', 'string'))
->setValue('starttime', rex_sql::datetime($login->getSessionVar(rex_login::SESSION_START_TIME, time())))
->setValue('last_activity', rex_sql::datetime($login->getSessionVar(rex_login::SESSION_LAST_ACTIVITY)))
->insertOrUpdate();

$login->setSessionVar(self::SESSION_VAR_LAST_DB_UPDATE, time());
}

public function clearCurrentSession(): void
{
if (false === session_id()) {
return;
}

rex_sql::factory()
->setTable(rex::getTable('user_session'))
->setWhere('session_id = ?', [session_id()])
->delete();
}

public function updateLastActivity(): void
{
if (false === session_id()) {
return;
}

$login = new rex_backend_login();

// only once a minute
if ($login->getSessionVar(self::SESSION_VAR_LAST_DB_UPDATE, 0) > (time() - 60)) {
return;
}

$this->storeCurrentSession();
}

public static function clearExpiredSessions(): void
{
rex_sql::factory()
->setTable(rex::getTable('user_session'))
->setWhere('UNIX_TIMESTAMP(last_activity) < ?', [time() - (int) rex::getProperty('session_duration')])
->delete();
}
}
2 changes: 2 additions & 0 deletions redaxo/src/core/pages/profile.php
Expand Up @@ -294,3 +294,5 @@
</form>';

echo $content;

require rex_path::core('pages/profile.sessions.php');
25 changes: 25 additions & 0 deletions redaxo/src/core/pages/profile.sessions.php
@@ -0,0 +1,25 @@
<?php

$list = rex_list::factory('Select session_id, ip, useragent, starttime, last_activity from rex_user_session where user_id = '.rex::requireUser()->getId());

$list->setColumnLabel('session_id', rex_i18n::msg('session_id'));
$list->setColumnLabel('ip', rex_i18n::msg('ip'));
$list->setColumnLabel('useragent', rex_i18n::msg('user_agent'));
$list->setColumnLabel('starttime', rex_i18n::msg('starttime'));
$list->setColumnLabel('last_activity', rex_i18n::msg('last_activity'));

$list->setColumnFormat('last_activity', 'custom', static function () use ($list) {
if (session_id() === $list->getValue('session_id')) {
return rex_i18n::msg('active_session');
}
return rex_formatter::date((string) $list->getValue('last_activity'), 'd.m.Y H:i');
});
$list->setColumnFormat('starttime', 'custom', static function () use ($list) {
return rex_formatter::date((string) $list->getValue('starttime'), 'd.m.Y H:i');
});
$content = $list->get();

$fragment = new rex_fragment();
$fragment->setVar('title', rex_i18n::msg('session_caption'));
$fragment->setVar('content', $content, false);
echo $fragment->parse('core/page/section.php');

0 comments on commit 2b6d00c

Please sign in to comment.