fix: dismiss stale session-timeout flash on auto-login, resolve PHPStan and i18n issues

- Add Flash::dismissByKey() to clear flash messages by key+scope
- Dismiss 'session_timeout' flash after successful remember-me auto-login
  so the message doesn't persist through manual logout
- Suppress PHPStan false positives for dynamically defined MINTY_ALLOW_OUTPUT
- Remove unnecessary ?? [] fallbacks on preg_match_all results in tests
- Add missing i18n key 'No active roles are configured yet.'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 18:56:19 +01:00
parent e1c211069e
commit a27b6a96ff
5 changed files with 38 additions and 8 deletions

View File

@@ -159,6 +159,7 @@
"Select permissions": "Berechtigungen auswählen",
"No permissions available": "Keine Berechtigungen verfügbar",
"No active permissions are configured yet.": "Es sind noch keine aktiven Berechtigungen konfiguriert.",
"No active roles are configured yet.": "Es sind noch keine aktiven Rollen konfiguriert.",
"Permission denied": "Keine Berechtigung",
"No active tenant assigned": "Kein aktiver Mandant zugewiesen",
"Please select at least one tenant": "Bitte mindestens einen Mandanten auswählen",

View File

@@ -159,6 +159,7 @@
"Select permissions": "Select permissions",
"No permissions available": "No permissions available",
"No active permissions are configured yet.": "No active permissions are configured yet.",
"No active roles are configured yet.": "No active roles are configured yet.",
"Permission denied": "Permission denied",
"No active tenant assigned": "No active tenant assigned",
"Please select at least one tenant": "Please select at least one tenant",

View File

@@ -95,6 +95,30 @@ class Flash
}
}
/**
* Remove all flash messages matching a given key (and optionally scope).
*/
public static function dismissByKey(string $key, ?string $scope = null): void
{
self::ensureSession();
$messages = $_SESSION[self::SESSION_KEY] ?? [];
$messages = array_values(array_filter($messages, function ($message) use ($key, $scope) {
if (($message['key'] ?? null) !== $key) {
return true;
}
if ($scope !== null && ($message['scope'] ?? null) !== $scope) {
return true;
}
return false;
}));
if ($messages) {
$_SESSION[self::SESSION_KEY] = $messages;
} else {
unset($_SESSION[self::SESSION_KEY]);
}
}
// Prevents messages from being cleared on the next render — useful when a redirect chain crosses multiple pages.
public static function keep(int $times = 1)
{

View File

@@ -305,7 +305,7 @@ class AuthzUiContractTest extends TestCase
private function extractPageAuthKeys(string $content): array
{
preg_match_all('/\\$pageAuth\\[[\'"]([a-z_]+)[\'"]\\]/', $content, $matches);
$keys = array_values(array_unique($matches[1] ?? []));
$keys = array_values(array_unique($matches[1]));
sort($keys);
return $keys;
}
@@ -317,9 +317,9 @@ class AuthzUiContractTest extends TestCase
{
$keys = [];
preg_match_all('/\\$viewAuth\\[\'page\'\\]\\s*=\\s*\\[(.*?)\\];/s', $content, $blocks);
foreach ($blocks[1] ?? [] as $block) {
foreach ($blocks[1] as $block) {
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', $block, $matches);
foreach ($matches[1] ?? [] as $key) {
foreach ($matches[1] as $key) {
$keys[] = $key;
}
}
@@ -348,7 +348,7 @@ class AuthzUiContractTest extends TestCase
);
preg_match_all('/[\'"]([a-z_]+)[\'"]\\s*=>/', (string) ($constantMatch[1] ?? ''), $keyMatches);
$keys = array_values(array_unique($keyMatches[1] ?? []));
$keys = array_values(array_unique($keyMatches[1]));
sort($keys);
return $keys;
}

View File

@@ -55,6 +55,10 @@ if ($defaultLocale !== null) {
if (empty($_SESSION['user']['id'])) {
$autoLoginResult = $rememberMeService->autoLoginFromCookie();
if ($autoLoginResult) {
// The session timeout flash is no longer relevant — the user was
// seamlessly re-authenticated and never saw the login page.
Flash::dismissByKey('session_timeout', 'login');
// After auto-login (e.g. session timeout with active remember-me cookie),
// honour ?return_to= so the user lands on the page they were on.
$returnToParam = trim((string) ($_GET['return_to'] ?? ''));
@@ -209,13 +213,13 @@ if (Router::getTemplateAction()) {
}
if (ob_get_contents()) {
ob_end_flush();
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) {
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanNot.alwaysFalse
trigger_error('MintyPHP template action"' . Router::getTemplateAction() . '" should not send output. Error raised ', E_USER_WARNING);
}
} else {
ob_end_clean();
}
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) {
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanAnd.rightAlwaysTrue
Session::end();
DB::close();
exit;
@@ -228,13 +232,13 @@ if (Router::getAction()) {
}
if (ob_get_contents()) {
ob_end_flush();
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) {
if (!defined('MINTY_ALLOW_OUTPUT') || !MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanNot.alwaysFalse
trigger_error('MintyPHP action "' . Router::getAction() . '" should not send output. Error raised ', E_USER_WARNING);
}
} else {
ob_end_clean();
}
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) {
if (defined('MINTY_ALLOW_OUTPUT') && MINTY_ALLOW_OUTPUT) { // @phpstan-ignore booleanAnd.rightAlwaysTrue
Session::end();
DB::close();
exit;