From a27b6a96ffdaa720d305fc938fd1457cd6420fde Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 13 Mar 2026 18:56:19 +0100 Subject: [PATCH] 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 --- i18n/default_de.json | 1 + i18n/default_en.json | 1 + lib/Support/Flash.php | 24 ++++++++++++++++++++++ tests/Architecture/AuthzUiContractTest.php | 8 ++++---- web/index.php | 12 +++++++---- 5 files changed, 38 insertions(+), 8 deletions(-) diff --git a/i18n/default_de.json b/i18n/default_de.json index 885eadc..d2ff8de 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -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", diff --git a/i18n/default_en.json b/i18n/default_en.json index a5e45ab..583431a 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -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", diff --git a/lib/Support/Flash.php b/lib/Support/Flash.php index 72d434f..74b0296 100644 --- a/lib/Support/Flash.php +++ b/lib/Support/Flash.php @@ -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) { diff --git a/tests/Architecture/AuthzUiContractTest.php b/tests/Architecture/AuthzUiContractTest.php index f296427..bda85b9 100644 --- a/tests/Architecture/AuthzUiContractTest.php +++ b/tests/Architecture/AuthzUiContractTest.php @@ -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; } diff --git a/web/index.php b/web/index.php index 81ef5af..37ec0d6 100644 --- a/web/index.php +++ b/web/index.php @@ -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;