diff --git a/config/static_caching.php b/config/static_caching.php index 758c7f633b9..b06932f4da4 100644 --- a/config/static_caching.php +++ b/config/static_caching.php @@ -127,8 +127,6 @@ 'nocache_db_connection' => env('STATAMIC_NOCACHE_DB_CONNECTION'), - 'nocache_js_position' => 'body', - /* |-------------------------------------------------------------------------- | Replacers diff --git a/routes/web.php b/routes/web.php index 388076c38e6..9871e439c4a 100755 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,8 @@ use Statamic\Http\Middleware\CP\HandleInertiaRequests; use Statamic\Http\Middleware\RedirectIfTwoFactorSetupIncomplete; use Statamic\Statamic; -use Statamic\StaticCaching\NoCache\Controller as NoCacheController; +use Statamic\StaticCaching\NoCache\CsrfTokenController; +use Statamic\StaticCaching\NoCache\NoCacheController; use Statamic\StaticCaching\NoCache\NoCacheLocalize; Route::name('statamic.')->group(function () { @@ -67,14 +68,16 @@ Route::post('activate', [ActivateAccountController::class, 'reset'])->name('account.activate.action'); }); + Route::post('nocache', NoCacheController::class) + ->middleware(NoCacheLocalize::class) + ->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']); + + Route::post('csrf', CsrfTokenController::class) + ->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']); + Statamic::additionalActionRoutes(); }); - Route::prefix(config('statamic.routes.action')) - ->post('nocache', NoCacheController::class) - ->middleware(NoCacheLocalize::class) - ->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']); - if (OAuth::enabled()) { Route::get(config('statamic.oauth.routes.login'), [OAuthController::class, 'redirectToProvider'])->name('oauth.login'); Route::match(['get', 'post'], config('statamic.oauth.routes.callback'), [OAuthController::class, 'handleProviderCallback']) diff --git a/src/Facades/StaticCache.php b/src/Facades/StaticCache.php index d70550234a3..de8ad7f8dd7 100644 --- a/src/Facades/StaticCache.php +++ b/src/Facades/StaticCache.php @@ -16,6 +16,7 @@ * @method static ApplicationCacher createApplicationDriver(array $config) * @method static \Illuminate\Cache\Repository cacheStore() * @method static void flush() + * @method static void csrfTokenJs(string $js) * @method static void nocacheJs(string $js) * @method static void nocachePlaceholder(string $placeholder) * @method static void includeJs() diff --git a/src/StaticCaching/Cachers/FileCacher.php b/src/StaticCaching/Cachers/FileCacher.php index f0816308f33..f0a1bcfb67a 100644 --- a/src/StaticCaching/Cachers/FileCacher.php +++ b/src/StaticCaching/Cachers/FileCacher.php @@ -28,6 +28,11 @@ class FileCacher extends AbstractCacher */ private $shouldOutputJs = false; + /** + * @var string + */ + private $csrfTokenJs; + /** * @var string */ @@ -230,16 +235,59 @@ private function isLongQueryStringPath($path) return Str::contains($path, '_lqs_'); } + public function setCsrfTokenJs(string $js) + { + $this->csrfTokenJs = $js; + } + public function setNocacheJs(string $js) { $this->nocacheJs = $js; } - public function getNocacheJs(): string + public function getCsrfTokenJs(): string { $csrfPlaceholder = CsrfTokenReplacer::REPLACEMENT; $default = << response.json()) + .then((data) => { + for (const input of document.querySelectorAll('input[value="$csrfPlaceholder"]')) { + input.value = data.csrf; + } + + for (const meta of document.querySelectorAll('meta[content="$csrfPlaceholder"]')) { + meta.content = data.csrf; + } + + for (const input of document.querySelectorAll('script[data-csrf="$csrfPlaceholder"]')) { + input.setAttribute('data-csrf', data.csrf); + } + + if (window.hasOwnProperty('livewire_token')) { + window.livewire_token = data.csrf + } + + if (window.hasOwnProperty('livewireScriptConfig')) { + window.livewireScriptConfig.csrf = data.csrf + } + + document.dispatchEvent(new CustomEvent('statamic:csrf.replaced', { detail: data })); + }); +})(); +EOT; + + return $this->csrfTokenJs ?? $default; + } + + public function getNocacheJs(): string + { + $default = <<<'EOT' (function() { function createMap() { var map = {}; @@ -270,26 +318,6 @@ function createMap() { if (map[key]) map[key].outerHTML = regions[key]; } - for (const input of document.querySelectorAll('input[value="$csrfPlaceholder"]')) { - input.value = data.csrf; - } - - for (const meta of document.querySelectorAll('meta[content="$csrfPlaceholder"]')) { - meta.content = data.csrf; - } - - for (const input of document.querySelectorAll('script[data-csrf="$csrfPlaceholder"]')) { - input.setAttribute('data-csrf', data.csrf); - } - - if (window.hasOwnProperty('livewire_token')) { - window.livewire_token = data.csrf - } - - if (window.hasOwnProperty('livewireScriptConfig')) { - window.livewireScriptConfig.csrf = data.csrf - } - document.dispatchEvent(new CustomEvent('statamic:nocache.replaced', { detail: data })); }); })(); diff --git a/src/StaticCaching/NoCache/CsrfTokenController.php b/src/StaticCaching/NoCache/CsrfTokenController.php new file mode 100644 index 00000000000..b42933cedb4 --- /dev/null +++ b/src/StaticCaching/NoCache/CsrfTokenController.php @@ -0,0 +1,13 @@ + csrf_token(), + ]; + } +} diff --git a/src/StaticCaching/NoCache/Controller.php b/src/StaticCaching/NoCache/NoCacheController.php similarity index 97% rename from src/StaticCaching/NoCache/Controller.php rename to src/StaticCaching/NoCache/NoCacheController.php index f3e4e51a952..4d3e58f8d8c 100644 --- a/src/StaticCaching/NoCache/Controller.php +++ b/src/StaticCaching/NoCache/NoCacheController.php @@ -6,7 +6,7 @@ use Statamic\StaticCaching\Replacers\NoCacheReplacer; use Statamic\Support\Str; -class Controller +class NoCacheController { public function __invoke(Request $request, Session $session) { diff --git a/src/StaticCaching/Replacers/CsrfTokenReplacer.php b/src/StaticCaching/Replacers/CsrfTokenReplacer.php index e7c0d562eff..e04e8fa2762 100644 --- a/src/StaticCaching/Replacers/CsrfTokenReplacer.php +++ b/src/StaticCaching/Replacers/CsrfTokenReplacer.php @@ -4,6 +4,8 @@ use Illuminate\Http\Response; use Statamic\Facades\StaticCache; +use Statamic\StaticCaching\Cacher; +use Statamic\StaticCaching\Cachers\FileCacher; use Statamic\StaticCaching\Replacer; use Statamic\Support\Str; @@ -12,6 +14,26 @@ class CsrfTokenReplacer implements Replacer const REPLACEMENT = 'STATAMIC_CSRF_TOKEN'; public function prepareResponseToCache(Response $response, Response $initial) + { + $this->replaceInResponse($response); + + $this->modifyFullMeasureResponse($response); + } + + public function replaceInCachedResponse(Response $response) + { + if (! $response->getContent()) { + return; + } + + $response->setContent(str_replace( + self::REPLACEMENT, + csrf_token(), + $response->getContent() + )); + } + + private function replaceInResponse(Response $response) { if (! $content = $response->getContent()) { return; @@ -34,16 +56,30 @@ public function prepareResponseToCache(Response $response, Response $initial) )); } - public function replaceInCachedResponse(Response $response) + private function modifyFullMeasureResponse(Response $response) { - if (! $response->getContent()) { + $cacher = app(Cacher::class); + + if (! $cacher instanceof FileCacher) { return; } - $response->setContent(str_replace( - self::REPLACEMENT, - csrf_token(), - $response->getContent() - )); + if (! $cacher->shouldOutputJs()) { + return; + } + + $contents = $response->getContent(); + + $insertBefore = collect([ + Str::position($contents, ''), + ])->filter()->min(); + + $js = ""; + + $contents = Str::substrReplace($contents, $js, $insertBefore, 0); + + $response->setContent($contents); } } diff --git a/src/StaticCaching/Replacers/NoCacheReplacer.php b/src/StaticCaching/Replacers/NoCacheReplacer.php index a242992f598..4b0dec3938a 100644 --- a/src/StaticCaching/Replacers/NoCacheReplacer.php +++ b/src/StaticCaching/Replacers/NoCacheReplacer.php @@ -3,7 +3,6 @@ namespace Statamic\StaticCaching\Replacers; use Illuminate\Http\Response; -use Illuminate\Support\Str; use Statamic\Facades\StaticCache; use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\Cachers\FileCacher; @@ -79,40 +78,12 @@ private function modifyFullMeasureResponse(Response $response) $contents = $response->getContent(); if ($cacher->shouldOutputJs()) { - $contents = match ($pos = $this->insertPosition()) { - 'head' => $this->insertJsInHead($contents, $cacher), - 'body' => $this->insertJsInBody($contents, $cacher), - default => throw new \Exception('Invalid nocache js insert position ['.$pos.']'), - }; + $js = $cacher->getNocacheJs(); + $contents = str_replace('', '', $contents); } $contents = str_replace('NOCACHE_PLACEHOLDER', $cacher->getNocachePlaceholder(), $contents); $response->setContent($contents); } - - private function insertPosition() - { - return config('statamic.static_caching.nocache_js_position', 'body'); - } - - private function insertJsInHead($contents, $cacher) - { - $insertBefore = collect([ - Str::position($contents, ''), - ])->filter()->min(); - - $js = ""; - - return Str::substrReplace($contents, $js, $insertBefore, 0); - } - - private function insertJsInBody($contents, $cacher) - { - $js = $cacher->getNocacheJs(); - - return str_replace('', '', $contents); - } } diff --git a/src/StaticCaching/StaticCacheManager.php b/src/StaticCaching/StaticCacheManager.php index 9af77ab541d..4fb2716de0f 100644 --- a/src/StaticCaching/StaticCacheManager.php +++ b/src/StaticCaching/StaticCacheManager.php @@ -101,6 +101,11 @@ private function flushNocache() $this->cacheStore()->forget('nocache::urls'); } + public function csrfTokenJs(string $js) + { + $this->fileDriver()->setCsrfTokenJs($js); + } + public function nocacheJs(string $js) { $this->fileDriver()->setNocacheJs($js); diff --git a/tests/StaticCaching/FullMeasureStaticCachingTest.php b/tests/StaticCaching/FullMeasureStaticCachingTest.php index b75a0ac54ec..336c349d749 100644 --- a/tests/StaticCaching/FullMeasureStaticCachingTest.php +++ b/tests/StaticCaching/FullMeasureStaticCachingTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\File; use Statamic\Facades\StaticCache; +use Statamic\StaticCaching\Cacher; use Statamic\StaticCaching\NoCache\Session; use Tests\FakesContent; use Tests\FakesViews; @@ -132,15 +133,16 @@ public function index() } #[Test] - public function it_should_add_the_javascript_if_there_is_a_csrf_token() + public function it_adds_the_csrf_and_nocache_scripts() { $this->withFakeViews(); - $this->viewShouldReturnRaw('layout', '{{ template_content }}'); + $this->viewShouldReturnRaw('layout', '{{ template_content }}'); $this->viewShouldReturnRaw('default', '{{ csrf_token }}'); $this->createPage('about'); - StaticCache::nocacheJs('js here'); + $csrfTokenScript = ''; + $nocacheScript = ''; $this->assertFalse(file_exists($this->dir.'/about_.html')); @@ -149,12 +151,22 @@ public function it_should_add_the_javascript_if_there_is_a_csrf_token() ->assertOk(); // Initial response should be dynamic and not contain javascript. - $this->assertEquals(''.csrf_token().'', $response->getContent()); + $this->assertEquals(''.csrf_token().'', $response->getContent()); // The cached response should have the token placeholder, and the javascript. $this->assertTrue(file_exists($this->dir.'/about_.html')); - $this->assertEquals(vsprintf('STATAMIC_CSRF_TOKEN%s', [ - '', + $this->assertEquals(vsprintf("{$csrfTokenScript}STATAMIC_CSRF_TOKEN%s", [ + $nocacheScript, ]), file_get_contents($this->dir.'/about_.html')); } + + #[Test] + public function it_can_override_the_csrf_and_nocache_scripts() + { + StaticCache::nocacheJs('nocache'); + StaticCache::csrfTokenJs('csrf'); + + $this->assertEquals(app(Cacher::class)->getNocacheJs(), 'nocache'); + $this->assertEquals(app(Cacher::class)->getCsrfTokenJs(), 'csrf'); + } }