From 52f10ae46c314b9d3029c6bda40523d945b21710 Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 24 Apr 2026 19:10:37 +0200 Subject: [PATCH] refactor(actions): centralize core page request/csrf guards --- .../analysis.json | 22 ++++++++ .../execution-report.json | 50 +++++++++++++++++ .../finalize.json | 12 ++++ .../plan.json | 23 ++++++++ .../review-acceptance.json | 37 ++++++++++++ .../review-code.json | 18 ++++++ .../review-security.json | 10 ++++ pages/auth/forgot().php | 3 +- pages/auth/login().php | 5 +- pages/auth/register().php | 3 +- pages/auth/reset().php | 3 +- pages/auth/verify().php | 3 +- pages/auth/verify-email().php | 3 +- pages/flash/dismiss($id).php | 12 ++-- pages/lang().php | 3 +- pages/page/copy-language().php | 7 ++- pages/page/index($slug).php | 12 ++-- .../CorePageRequestGuardContractTest.php | 56 +++++++++++++++++++ 18 files changed, 253 insertions(+), 29 deletions(-) create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/analysis.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/execution-report.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/finalize.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/plan.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/review-acceptance.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/review-code.json create mode 100644 .agents/runs/2026-04-24-action-csrf-centralization-step4/review-security.json create mode 100644 tests/Architecture/CorePageRequestGuardContractTest.php diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/analysis.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/analysis.json new file mode 100644 index 0000000..850822b --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/analysis.json @@ -0,0 +1,22 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "issue_summary": "Nach Abschluss von pages/admin gab es in non-admin Core-Seiten noch direkte requestInput()->method() und Session::checkCsrfToken()-Nutzung. Ziel war die gleiche Zentralisierung auch für pages/auth, pages/page, pages/lang und pages/flash.", + "affected_layers": ["pages", "tests"], + "affected_files": [ + { "path": "pages/lang().php", "role": "Locale switch endpoint (GET/POST mixed)" }, + { "path": "pages/flash/dismiss($id).php", "role": "Flash dismiss POST endpoint" }, + { "path": "pages/page/copy-language().php", "role": "Page content locale copy action" }, + { "path": "pages/page/index($slug).php", "role": "Page edit submit flow with optional JSON response" }, + { "path": "pages/auth/login().php", "role": "Multi-stage login action" }, + { "path": "pages/auth/register().php", "role": "Registration action" }, + { "path": "pages/auth/forgot().php", "role": "Password reset request action" }, + { "path": "pages/auth/verify().php", "role": "Password reset verification code action" }, + { "path": "pages/auth/verify-email().php", "role": "Email verification and resend action" }, + { "path": "pages/auth/reset().php", "role": "Password reset completion action" }, + { "path": "tests/Architecture/CorePageRequestGuardContractTest.php", "role": "Architecture contract to prevent legacy request/csrf checks in non-admin core pages" } + ], + "risks_discovered": [ + "Auth/page flows rely on in-page error handling instead of redirect on CSRF mismatch in several places; helper use must preserve this behavior.", + "Mixed GET/POST endpoints (lang, page/index) require conditional CSRF enforcement without breaking GET rendering paths." + ] +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/execution-report.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/execution-report.json new file mode 100644 index 0000000..207ba8e --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/execution-report.json @@ -0,0 +1,50 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "plan_ref": ".agents/runs/2026-04-24-action-csrf-centralization-step4/plan.json", + "status": "done", + "changed_files": [ + { "path": "pages/lang().php", "summary": "Replaced direct CSRF check with actionRequireCsrf while preserving redirect + flash behavior." }, + { "path": "pages/flash/dismiss($id).php", "summary": "Replaced direct method + CSRF checks with actionRequirePost/actionRequireCsrf." }, + { "path": "pages/page/copy-language().php", "summary": "Added method guard via actionRequirePost and replaced direct CSRF check with helper call." }, + { "path": "pages/page/index($slug).php", "summary": "Replaced direct method/CSRF checks with helper-compatible request/isMethod and actionRequireCsrf." }, + { "path": "pages/auth/login().php", "summary": "Replaced direct method/CSRF checks with requestInput()->isMethod and actionRequireCsrf (non-redirect mode)." }, + { "path": "pages/auth/register().php", "summary": "Replaced direct CSRF check with actionRequireCsrf (non-redirect mode)." }, + { "path": "pages/auth/forgot().php", "summary": "Replaced direct CSRF check with actionRequireCsrf (non-redirect mode)." }, + { "path": "pages/auth/verify().php", "summary": "Replaced direct CSRF check with actionRequireCsrf (non-redirect mode)." }, + { "path": "pages/auth/verify-email().php", "summary": "Replaced direct CSRF check with actionRequireCsrf (non-redirect mode)." }, + { "path": "pages/auth/reset().php", "summary": "Replaced direct CSRF check with actionRequireCsrf (non-redirect mode)." }, + { "path": "tests/Architecture/CorePageRequestGuardContractTest.php", "summary": "Added non-admin regression guard contract test." } + ], + "commands": [ + { "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/CorePageRequestGuardContractTest.php tests/Architecture/AdminActionRequestGuardContractTest.php", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/PostEndpointCsrfContractTest.php tests/Architecture/PostRedirectGetContractTest.php", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/AuthzAdminSettingsContractTest.php tests/Architecture/AuthzAdminMasterDataContractTest.php tests/Architecture/AuthzAdminUsersContractTest.php", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/phpunit", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/phpstan analyse -c phpstan.neon --no-progress", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/phpunit tests/Architecture/CoreStarterkitContractTest.php", "result": "pass" }, + { "cmd": "docker compose exec -T php vendor/bin/php-cs-fixer fix --config=tools/php-cs-fixer/.php-cs-fixer.dist.php --dry-run --diff --verbose", "result": "pass" }, + { "cmd": "bin/docs-link-check.sh", "result": "pass" }, + { "cmd": "bin/docs-drift-check.sh", "result": "pass" }, + { "cmd": "bin/codex-skills-sync.sh --check", "result": "pass" } + ], + "quality_gate_results": [ + { "gate_id": "QG-001", "result": "pass", "notes": "PHPUnit full run green: 2027 tests, 29637 assertions." }, + { "gate_id": "QG-002", "result": "pass", "notes": "PHPStan level 5: no errors." }, + { "gate_id": "QG-003", "result": "pass", "notes": "CoreStarterkitContractTest green: 13 tests, 6986 assertions." }, + { "gate_id": "QG-006", "result": "pass", "notes": "php-cs-fixer dry-run green." }, + { "gate_id": "QG-008", "result": "pass", "notes": "docs-link-check and docs-drift-check green." }, + { "gate_id": "QG-009", "result": "pass", "notes": "codex-skills-sync --check green." } + ], + "metrics": { + "step4_target_files": 10, + "before_requestinput_method_calls": 3, + "after_requestinput_method_calls": 0, + "before_inline_session_csrf_checks": 10, + "after_inline_session_csrf_checks": 0, + "non_admin_inline_method_checks_total_after": 0, + "non_admin_inline_csrf_checks_total_after": 0 + }, + "open_items": [ + "Module pages under modules/*/pages still contain inline method/csrf checks and are intentionally out-of-scope for this step." + ] +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/finalize.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/finalize.json new file mode 100644 index 0000000..81ae42a --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/finalize.json @@ -0,0 +1,12 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "ready_to_finalize": true, + "code_review": "pass", + "security_review": "pass", + "acceptance_review": "pass", + "ci_status": "required-gates-pass", + "final_action": "commit", + "hold_reason": "", + "commit_message": "refactor(actions): centralize core page request/csrf guards", + "notes": "Core non-admin pages now aligned with helper-based request and CSRF guard pattern." +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/plan.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/plan.json new file mode 100644 index 0000000..f648cdb --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/plan.json @@ -0,0 +1,23 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "analysis_ref": ".agents/runs/2026-04-24-action-csrf-centralization-step4/analysis.json", + "summary": "Complete centralization of legacy request method and CSRF checks for all remaining non-admin core pages.", + "scope": { + "in": [ + "Migrate remaining pages/* (excluding pages/admin) off direct requestInput()->method() and Session::checkCsrfToken()", + "Preserve existing error/redirect behavior in auth and page-edit flows", + "Add non-admin architecture contract" + ], + "out": [ + "modules/*/pages", + "service/repository business logic changes" + ] + }, + "success_criteria": [ + { "id": "SC-001", "criterion": "Remaining non-admin method checks use helper-compatible patterns (no direct requestInput()->method())." }, + { "id": "SC-002", "criterion": "Remaining non-admin CSRF checks use actionRequireCsrf (no direct Session::checkCsrfToken())." }, + { "id": "SC-003", "criterion": "New CorePageRequestGuardContractTest prevents regression in non-admin core pages." }, + { "id": "SC-004", "criterion": "Architecture + required gate set remains green." } + ], + "required_quality_gate_ids": ["QG-001", "QG-002", "QG-003", "QG-006", "QG-008", "QG-009"] +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-acceptance.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-acceptance.json new file mode 100644 index 0000000..ec77b54 --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-acceptance.json @@ -0,0 +1,37 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "verdict": "pass", + "checked_criterion_ids": [ + "SC-001", + "SC-002", + "SC-003", + "SC-004" + ], + "checks": [ + { + "criterion_id": "SC-001", + "criterion": "Remaining non-admin method checks use helper-compatible patterns (no direct requestInput()->method()).", + "result": "pass", + "evidence": "Direct requestInput()->method() usage in non-admin pages reduced to 0." + }, + { + "criterion_id": "SC-002", + "criterion": "Remaining non-admin CSRF checks use actionRequireCsrf (no direct Session::checkCsrfToken()).", + "result": "pass", + "evidence": "Direct Session::checkCsrfToken() usage in non-admin pages reduced to 0." + }, + { + "criterion_id": "SC-003", + "criterion": "New CorePageRequestGuardContractTest prevents regression in non-admin core pages.", + "result": "pass", + "evidence": "CorePageRequestGuardContractTest added and passing." + }, + { + "criterion_id": "SC-004", + "criterion": "Architecture + required gate set remains green.", + "result": "pass", + "evidence": "All required gates and targeted architecture tests passed." + } + ], + "missing_or_wrong": [] +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-code.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-code.json new file mode 100644 index 0000000..71ee926 --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-code.json @@ -0,0 +1,18 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "verdict": "pass", + "checked_guard_ids": [ + "GR-CORE-003", + "GR-CORE-012", + "GR-TEST-001" + ], + "checked_quality_gate_ids": [ + "QG-001", + "QG-002", + "QG-003", + "QG-006", + "QG-008", + "QG-009" + ], + "findings": [] +} diff --git a/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-security.json b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-security.json new file mode 100644 index 0000000..4552d59 --- /dev/null +++ b/.agents/runs/2026-04-24-action-csrf-centralization-step4/review-security.json @@ -0,0 +1,10 @@ +{ + "task_id": "2026-04-24-action-csrf-centralization-step4", + "verdict": "pass", + "checked_guard_ids": [ + "GR-SEC-001", + "GR-SEC-008", + "GR-SEC-009" + ], + "findings": [] +} diff --git a/pages/auth/forgot().php b/pages/auth/forgot().php index 25884b2..9b50cfb 100644 --- a/pages/auth/forgot().php +++ b/pages/auth/forgot().php @@ -4,7 +4,6 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; use MintyPHP\Service\Auth\AuthServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -16,7 +15,7 @@ $email = trim((string) $request->body('email', '')); $passwordResetService = (app(AuthServicesFactory::class))->createPasswordResetService(); if ($request->isMethod('POST')) { - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf('password/forgot', flashOnFailure: false, redirectOnFailure: false)) { $errorBag->addGlobal(t('Form expired, please try again')); } elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errorBag->addGlobal(t('Please enter a valid email address')); diff --git a/pages/auth/login().php b/pages/auth/login().php index 5c7c8d1..601bd06 100644 --- a/pages/auth/login().php +++ b/pages/auth/login().php @@ -8,7 +8,6 @@ use MintyPHP\Service\Auth\AuthServicesFactory; use MintyPHP\Service\Security\SecurityServicesFactory; use MintyPHP\Service\Tenant\TenantServicesFactory; use MintyPHP\Service\User\UserServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $requestRuntime = app(RequestRuntimeInterface::class); @@ -172,8 +171,8 @@ $resolveSelectedTenantId = static function (array $candidateByIdValue, int $requ return (int) ($first['id'] ?? 0); }; -if (requestInput()->method() === 'POST') { - if (!Session::checkCsrfToken()) { +if (requestInput()->isMethod('POST')) { + if (!actionRequireCsrf('login', flashOnFailure: false, redirectOnFailure: false)) { $errorBag->addGlobal(t('Form expired, please try again')); $stage = 'resolve_email'; } else { diff --git a/pages/auth/register().php b/pages/auth/register().php index aae299c..aeb86ef 100644 --- a/pages/auth/register().php +++ b/pages/auth/register().php @@ -4,7 +4,6 @@ use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; use MintyPHP\Service\Auth\AuthServicesFactory; use MintyPHP\Service\User\UserServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -17,7 +16,7 @@ if (!allowRegistration()) { } if ($request->isMethod('POST')) { - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf('register', flashOnFailure: false, redirectOnFailure: false)) { $error = t('Form expired, please try again'); } elseif ($request->hasBody('email')) { $authService = (app(AuthServicesFactory::class))->createAuthService(); diff --git a/pages/auth/reset().php b/pages/auth/reset().php index 14ca961..c8eb2c0 100644 --- a/pages/auth/reset().php +++ b/pages/auth/reset().php @@ -5,7 +5,6 @@ use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; use MintyPHP\Service\Auth\AuthServicesFactory; use MintyPHP\Service\User\UserServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -24,7 +23,7 @@ if ($resetId <= 0) { } if ($request->isMethod('POST')) { - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf('password/reset', flashOnFailure: false, redirectOnFailure: false)) { $errorBag->addGlobal(t('Form expired, please try again')); } else { $result = $passwordResetService->resetPassword($resetId, $password, $password2); diff --git a/pages/auth/verify().php b/pages/auth/verify().php index 9e28d0a..f806e29 100644 --- a/pages/auth/verify().php +++ b/pages/auth/verify().php @@ -4,7 +4,6 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; use MintyPHP\Service\Auth\AuthServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -18,7 +17,7 @@ $code = trim((string) $request->body('code', '')); $passwordResetService = (app(AuthServicesFactory::class))->createPasswordResetService(); if ($request->isMethod('POST')) { - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf('password/verify', flashOnFailure: false, redirectOnFailure: false)) { $errorBag->addGlobal(t('Form expired, please try again')); } elseif ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errorBag->addGlobal(t('Please enter a valid email address')); diff --git a/pages/auth/verify-email().php b/pages/auth/verify-email().php index 74df16d..860ee17 100644 --- a/pages/auth/verify-email().php +++ b/pages/auth/verify-email().php @@ -4,7 +4,6 @@ use MintyPHP\Buffer; use MintyPHP\Http\SessionStoreInterface; use MintyPHP\Router; use MintyPHP\Service\Auth\AuthServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -20,7 +19,7 @@ $emailVerificationService = (app(AuthServicesFactory::class))->createEmailVerifi if ($request->isMethod('POST')) { $action = (string) $request->body('action', 'verify'); - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf('verify-email', flashOnFailure: false, redirectOnFailure: false)) { $errorBag->addGlobal(t('Form expired, please try again')); } elseif ($action === 'resend') { // Resend verification code diff --git a/pages/flash/dismiss($id).php b/pages/flash/dismiss($id).php index 39042ca..98a366e 100644 --- a/pages/flash/dismiss($id).php +++ b/pages/flash/dismiss($id).php @@ -2,17 +2,17 @@ use MintyPHP\Http\Request; use MintyPHP\Router; -use MintyPHP\Session; use MintyPHP\Support\Flash; -$target = Request::safeReturnTarget(requestInput()->bodyAll()['return'] ?? ''); +$request = requestInput(); +$target = Request::safeReturnTarget($request->bodyAll()['return'] ?? ''); -if (requestInput()->method() !== 'POST') { - Router::redirect($target); +if (!actionRequirePost($target)) { + return; } -if (!Session::checkCsrfToken()) { - Router::redirect($target); +if (!actionRequireCsrf($target, flashOnFailure: false)) { + return; } $flashId = isset($id) ? trim((string) $id) : ''; diff --git a/pages/lang().php b/pages/lang().php index 9fad56f..8abb062 100644 --- a/pages/lang().php +++ b/pages/lang().php @@ -5,14 +5,13 @@ use MintyPHP\Http\SessionStoreInterface; use MintyPHP\I18n; use MintyPHP\Router; use MintyPHP\Service\User\UserServicesFactory; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); $session = $sessionStore->all(); $request = requestInput(); -if ($request->isMethod('POST') && !Session::checkCsrfToken()) { +if ($request->isMethod('POST') && !actionRequireCsrf('', flashOnFailure: false, redirectOnFailure: false)) { Flash::error(t('Form expired, please try again'), '', 'csrf_expired'); Router::redirect(''); return; diff --git a/pages/page/copy-language().php b/pages/page/copy-language().php index a36e8b3..341f189 100644 --- a/pages/page/copy-language().php +++ b/pages/page/copy-language().php @@ -7,7 +7,6 @@ use MintyPHP\Router; use MintyPHP\Service\Access\SettingsAuthorizationPolicy; use MintyPHP\Service\Access\UiAccessService; use MintyPHP\Service\Content\PageService; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); @@ -21,6 +20,10 @@ if ($currentUserId <= 0) { } $return = Request::safeReturnTarget((string) (requestInput()->bodyAll()['return'] ?? '')); +if (!actionRequirePost($return !== '' ? $return : '')) { + return; +} + $canEdit = app(UiAccessService::class)->allow( $currentUserId, SettingsAuthorizationPolicy::ABILITY_ADMIN_SETTINGS_UPDATE @@ -31,7 +34,7 @@ if (!$canEdit) { Router::redirect($return !== '' ? $return : ''); } -if (!Session::checkCsrfToken()) { +if (!actionRequireCsrf($return !== '' ? $return : '', flashOnFailure: false, redirectOnFailure: false)) { Flash::error(t('Form expired, please try again'), $return, 'page_csrf'); Router::redirect($return !== '' ? $return : ''); } diff --git a/pages/page/index($slug).php b/pages/page/index($slug).php index 1089e5c..de34edf 100644 --- a/pages/page/index($slug).php +++ b/pages/page/index($slug).php @@ -8,11 +8,11 @@ use MintyPHP\Router; use MintyPHP\Service\Access\SettingsAuthorizationPolicy; use MintyPHP\Service\Access\UiAccessService; use MintyPHP\Service\Content\PageService; -use MintyPHP\Session; use MintyPHP\Support\Flash; $sessionStore = app(SessionStoreInterface::class); $session = $sessionStore->all(); +$request = requestInput(); $slug = trim((string) ($slug ?? '')); if ($slug === '') { @@ -38,7 +38,7 @@ $returnPath = Request::path(); if ($returnPath === '') { $returnPath = 'page/' . $slug; } -$requestedReturn = Request::safeReturnTarget((string) (requestInput()->bodyAll()['return'] ?? '')); +$requestedReturn = Request::safeReturnTarget((string) ($request->bodyAll()['return'] ?? '')); if ($requestedReturn !== '') { $returnPath = $requestedReturn; } @@ -62,7 +62,7 @@ if (!empty($session['page_edit_mode']) && is_array($session['page_edit_mode'])) $sessionStore->set('page_edit_mode', $pageEditMode); } -if (requestInput()->method() === 'POST') { +if ($request->isMethod('POST')) { if (!$canEdit || $notFound) { http_response_code(403); Flash::error(t('You are not allowed to edit this page'), $returnPath, 'page_forbidden'); @@ -72,7 +72,7 @@ if (requestInput()->method() === 'POST') { Router::redirect($returnPath); } - if (!Session::checkCsrfToken()) { + if (!actionRequireCsrf($returnPath, flashOnFailure: false, redirectOnFailure: false)) { Flash::error(t('Form expired, please try again'), $returnPath, 'page_csrf'); if ($wantsJson) { Router::json(['ok' => false, 'error' => 'csrf', 'redirect' => $returnPath]); @@ -80,7 +80,7 @@ if (requestInput()->method() === 'POST') { Router::redirect($returnPath); } - $content = (string) (requestInput()->bodyAll()['content'] ?? ''); + $content = (string) ($request->bodyAll()['content'] ?? ''); $result = PageService::updateContentBySlug($slug, $content, $currentUserId, I18n::$locale ?? null); if (!($result['ok'] ?? false)) { @@ -112,6 +112,6 @@ if ($contentJson === '') { $contentJson = json_encode(['blocks' => []], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } -$editMode = $canEdit && (($sessionEdit === true) || ((requestInput()->queryAll()['edit'] ?? '') === '1')); +$editMode = $canEdit && (($sessionEdit === true) || (($request->queryAll()['edit'] ?? '') === '1')); $locales = defined('APP_LOCALES') ? APP_LOCALES : [I18n::$defaultLocale]; $currentLocale = I18n::$locale ?? (I18n::$defaultLocale ?? 'de'); diff --git a/tests/Architecture/CorePageRequestGuardContractTest.php b/tests/Architecture/CorePageRequestGuardContractTest.php new file mode 100644 index 0000000..8f68a7c --- /dev/null +++ b/tests/Architecture/CorePageRequestGuardContractTest.php @@ -0,0 +1,56 @@ +corePageFiles() as $file) { + $content = $this->readProjectFile($file); + $this->assertStringNotContainsString('requestInput()->method()', $content, $file); + $this->assertStringNotContainsString('(requestInput()->method())', $content, $file); + $this->assertStringNotContainsString("strtoupper((string) (requestInput()->method()))", $content, $file); + } + } + + public function testCorePagesDoNotInlineLegacyCsrfChecks(): void + { + foreach ($this->corePageFiles() as $file) { + $content = $this->readProjectFile($file); + $this->assertStringNotContainsString('Session::checkCsrfToken()', $content, $file); + } + } + + /** + * @return array + */ + private function corePageFiles(): array + { + $pagesRoot = $this->projectRootPath() . '/pages'; + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($pagesRoot)); + $files = []; + + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'php') { + continue; + } + + $relativePath = 'pages/' . ltrim(str_replace($pagesRoot, '', $file->getPathname()), '/'); + if (str_starts_with($relativePath, 'pages/admin/')) { + continue; + } + $files[] = $relativePath; + } + + sort($files); + $this->assertNotEmpty($files, 'Expected non-admin core pages.'); + return $files; + } +}