diff --git a/composer.json b/composer.json index 2557f10..14319f2 100644 --- a/composer.json +++ b/composer.json @@ -30,14 +30,15 @@ "prefer-stable": true, "require": { "php": "^8.2", - "leafs/anchor": "^5.0", - "leafs/http": "^5.0", + "leafs/anchor": "^1.6 || ^5.0", + "leafs/http": "^4.0 || ^5.0", "leafs/session": "^5.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "*", "pestphp/pest": "*", - "leafs/alchemy": "^5.0" + "leafs/alchemy": "^5.0", + "leafs/exception": "^5.0" }, "scripts": { "test": "@php vendor/bin/alchemy test", diff --git a/src/CSRF.php b/src/CSRF.php index 4980e11..9fadaac 100644 --- a/src/CSRF.php +++ b/src/CSRF.php @@ -188,8 +188,11 @@ public static function validate() ? 'tokenNotFound' : 'tokenInvalid' ); exit(); // failsafe to prevent further execution - } else { + } elseif (class_exists(\Leaf\Crash\Pages::class)) { \Leaf\Crash\Pages::csrf(static::$errors['token']); + } else { + // Leaf 4 (leafs/exception <5) has no Crash\Pages view + (new \Leaf\Http\Response())->exit(static::$errors['token'], 400); } } } diff --git a/tests/fixtures/validate.php b/tests/fixtures/validate.php new file mode 100644 index 0000000..87c2695 --- /dev/null +++ b/tests/fixtures/validate.php @@ -0,0 +1,51 @@ + [no-pages] +// "no-pages" hides Leaf\Crash\* from the autoloader to mimic a Leaf 4 app, +// where leafs/exception <5 has no Crash\Pages class. + +require $argv[1]; + +if (($argv[2] ?? '') === 'no-pages') { + $loaders = spl_autoload_functions(); + + foreach ($loaders as $loader) { + spl_autoload_unregister($loader); + } + + spl_autoload_register(function ($class) use ($loaders) { + if (strpos($class, 'Leaf\\Crash\\') === 0) { + return; + } + + foreach ($loaders as $loader) { + $loader($class); + + if (class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false)) { + return; + } + } + }); +} + +register_shutdown_function(function () { + fwrite(STDERR, 'STATUS=' . var_export(http_response_code(), true)); +}); + +$_SERVER['REQUEST_METHOD'] = 'POST'; +$_SERVER['REQUEST_URI'] = '/submit'; +$_SESSION = []; + +\Leaf\Anchor\CSRF::config([ + 'secret' => 'test-secret', + 'except' => [], + 'methods' => ['POST', 'PUT', 'PATCH', 'DELETE'], + 'rotate' => false, + 'cookie' => false, +]); + +\Leaf\Anchor\CSRF::validate(); + +echo 'NOT-STOPPED'; diff --git a/tests/validate.test.php b/tests/validate.test.php new file mode 100644 index 0000000..b973e02 --- /dev/null +++ b/tests/validate.test.php @@ -0,0 +1,42 @@ + ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); + $out = stream_get_contents($pipes[1]); + $err = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($proc); + + return [$out, $err]; +} + +test('validate renders the Crash csrf page when leafs/exception v5 is present', function () { + if (!class_exists(\Leaf\Crash\Pages::class)) { + $this->markTestSkipped('leafs/exception v5 not installed'); + } + + [$out, $err] = runCsrfValidate(); + + expect($out)->toContain('Invalid request'); + expect($out)->toContain('not->toContain('NOT-STOPPED'); + expect($err)->toContain('STATUS=400'); +}); + +test('validate falls back to a plain 400 when Crash\Pages is unavailable (Leaf 4)', function () { + [$out, $err] = runCsrfValidate('no-pages'); + + expect($out)->toContain(\Leaf\Anchor\CSRF::config()['messages.tokenNotFound'] ?? 'Token not found.'); + expect($out)->not->toContain('not->toContain('NOT-STOPPED'); + expect($err)->toContain('STATUS=400'); +});