Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ public function boot()

$this->registerElevatedSessionMacros();

$this->registerCascadeHydrationForErrorViews();

if (config('statamic.system.handle_scheduled_entries')) {
$this->app->make(Schedule::class)->job(HandleEntrySchedule::class)->everyMinute();
}
Expand Down Expand Up @@ -349,6 +351,44 @@ private function sitesAboutCommandInfo()
return $sites->count().' ('.$summary.')';
}

// Statamic's own exceptions hydrate the cascade themselves via RendersHttpExceptions::contents().
// A generic HTTP exception thrown outside Statamic's stack (e.g. Livewire's default 404) never
// goes through that path, so the error template's globals would be unresolved. This closes that gap.
private function registerCascadeHydrationForErrorViews()
{
$handler = $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class);

if (! method_exists($handler, 'renderable')) {
return;
}

$handler->renderable(
function (\Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e, Request $request) {
if ($request->expectsJson() || Statamic::isCpRoute() || Statamic::isApiRoute()) {
return null;
}

$status = $e->getStatusCode();

if (! view()->exists('errors.'.$status)) {
return null;
}

Facades\Cascade::hydrated(function ($cascade) use ($status) {
$cascade->set('response_code', $status);
});

$layouts = collect(['errors.layout', 'layouts.layout', config('statamic.system.layout', 'layout'), 'statamic::blank']);
$layout = $layouts->filter()->first(fn ($layout) => view()->exists($layout));

return response(
$this->app->make(\Statamic\View\View::class)->template('errors.'.$status)->layout($layout)->render(),
$status
);
}
);
}

private function registerElevatedSessionMacros()
{
Request::macro('hasElevatedSession', function () {
Expand Down
24 changes: 24 additions & 0 deletions tests/FrontendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Auth\Protect\ProtectorManager;
Expand All @@ -17,9 +18,11 @@
use Statamic\Facades\Blueprint;
use Statamic\Facades\Cascade;
use Statamic\Facades\Collection;
use Statamic\Facades\GlobalSet;
use Statamic\Facades\User;
use Statamic\Tags\Tags;
use Statamic\View\Antlers\Language\Utilities\StringUtilities;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException as SymfonyNotFoundHttpException;

class FrontendTest extends TestCase
{
Expand Down Expand Up @@ -698,6 +701,27 @@ public function the_404_page_is_treated_like_a_template()
// todo: test cascade vars are in the debugbar
}

#[Test]
public function it_hydrates_the_cascade_for_a_404_thrown_outside_of_statamics_own_exception_stack()
{
// Simulates something like Livewire's default 404 handling, which throws Symfony's
// stock NotFoundHttpException rather than Statamic's own subclass. See GH-14167.
Route::get('/non-statamic-404', function () {
throw new SymfonyNotFoundHttpException;
});

$global = GlobalSet::make('site_settings')->save();
$global->in('en')->data(['site_name' => 'Test Site'])->save();

$this->withFakeViews();
$this->viewShouldReturnRaw('layout', '{{ template_content }}');
$this->viewShouldReturnRaw('errors.404', 'Not found: {{ site_settings:site_name }}');

$this->get('/non-statamic-404')
->assertNotFound()
->assertSee('Not found: Test Site');
}

#[Test]
public function it_sets_the_translation_locale_based_on_site()
{
Expand Down
Loading