From 1f61a1677d1c056056659eed0dc57e29e070ce5f Mon Sep 17 00:00:00 2001 From: Michael Darko Date: Mon, 9 Nov 2026 16:46:02 +0000 Subject: [PATCH] fix: fall back to a plain 403 when Leaf\Crash\Pages is unavailable The is/isNot/can/cannot middlewares called Pages::error() unguarded, which fatals on Leaf 4 apps (leafs/exception <5 has no Crash\Pages). Leaf 5 output is unchanged; Leaf 4 now gets response()->exit(message, 403). --- src/Auth.php | 45 +++++++++++++++++++--------------- tests/deny.test.php | 42 +++++++++++++++++++++++++++++++ tests/fixtures/deny-access.php | 40 ++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 20 deletions(-) create mode 100644 tests/deny.test.php create mode 100644 tests/fixtures/deny-access.php diff --git a/src/Auth.php b/src/Auth.php index 67f7958..f961d3b 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -59,35 +59,19 @@ public function __construct() }); $this->middleware('is', function ($role) { - Pages::error( - 'Page not found', - 'The page you are looking for could not be found.', - 403 - ); + static::denyAccess(); }); $this->middleware('isNot', function () { - Pages::error( - 'Page not found', - 'The page you are looking for could not be found.', - 403 - ); + static::denyAccess(); }); $this->middleware('can', function () { - Pages::error( - 'Page not found', - 'The page you are looking for could not be found.', - 403 - ); + static::denyAccess(); }); $this->middleware('cannot', function () { - Pages::error( - 'Page not found', - 'The page you are looking for could not be found.', - 403 - ); + static::denyAccess(); }); $this->middleware('auth.verified', function () { @@ -118,6 +102,27 @@ public function __construct() } } + /** + * Stop a request that failed a role or permission middleware. + * Leaf 5 (leafs/exception ^5) renders its Pages error view; Leaf 4 + * has no Leaf\Crash\Pages, so fall back to a plain 403 response. + * @return void + */ + protected static function denyAccess() + { + if (class_exists(Pages::class)) { + Pages::error( + 'Page not found', + 'The page you are looking for could not be found.', + 403 + ); + + return; + } + + response()->exit('The page you are looking for could not be found.', 403); + } + /** * Read an environment value, with or without leaf core around. * Reads live rather than through _env()'s per-process cache: these diff --git a/tests/deny.test.php b/tests/deny.test.php new file mode 100644 index 0000000..1ba51f9 --- /dev/null +++ b/tests/deny.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('role middleware denial renders the Crash error page when leafs/exception v5 is present', function () { + if (!class_exists(\Leaf\Crash\Pages::class)) { + $this->markTestSkipped('leafs/exception v5 not installed'); + } + + [$out, $err] = runDenyAccess(); + + expect($out)->toContain('Page not found'); + expect($out)->toContain('not->toContain('NOT-STOPPED'); + expect($err)->toContain('STATUS=403'); +}); + +test('role middleware denial falls back to a plain 403 when Crash\Pages is unavailable (Leaf 4)', function () { + [$out, $err] = runDenyAccess('no-pages'); + + expect($out)->toContain('The page you are looking for could not be found.'); + expect($out)->not->toContain('not->toContain('NOT-STOPPED'); + expect($err)->toContain('STATUS=403'); +}); diff --git a/tests/fixtures/deny-access.php b/tests/fixtures/deny-access.php new file mode 100644 index 0000000..a3e806f --- /dev/null +++ b/tests/fixtures/deny-access.php @@ -0,0 +1,40 @@ + [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)); +}); + +$deny = new ReflectionMethod(Leaf\Auth::class, 'denyAccess'); +$deny->setAccessible(true); +$deny->invoke(null); + +echo 'NOT-STOPPED';