Skip to content

Commit 99b22f7

Browse files
aernijasonvarga
andauthored
[6.x] Decouple CSRF token from nocache script (#11014)
Co-authored-by: Jason Varga <jason@pixelfear.com>
1 parent c499e9f commit 99b22f7

File tree

10 files changed

+141
-74
lines changed

10 files changed

+141
-74
lines changed

config/static_caching.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,6 @@
127127

128128
'nocache_db_connection' => env('STATAMIC_NOCACHE_DB_CONNECTION'),
129129

130-
'nocache_js_position' => 'body',
131-
132130
/*
133131
|--------------------------------------------------------------------------
134132
| Replacers

routes/web.php

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
use Statamic\Http\Middleware\CP\HandleInertiaRequests;
2626
use Statamic\Http\Middleware\RedirectIfTwoFactorSetupIncomplete;
2727
use Statamic\Statamic;
28-
use Statamic\StaticCaching\NoCache\Controller as NoCacheController;
28+
use Statamic\StaticCaching\NoCache\CsrfTokenController;
29+
use Statamic\StaticCaching\NoCache\NoCacheController;
2930
use Statamic\StaticCaching\NoCache\NoCacheLocalize;
3031

3132
Route::name('statamic.')->group(function () {
@@ -67,14 +68,16 @@
6768
Route::post('activate', [ActivateAccountController::class, 'reset'])->name('account.activate.action');
6869
});
6970

71+
Route::post('nocache', NoCacheController::class)
72+
->middleware(NoCacheLocalize::class)
73+
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']);
74+
75+
Route::post('csrf', CsrfTokenController::class)
76+
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']);
77+
7078
Statamic::additionalActionRoutes();
7179
});
7280

73-
Route::prefix(config('statamic.routes.action'))
74-
->post('nocache', NoCacheController::class)
75-
->middleware(NoCacheLocalize::class)
76-
->withoutMiddleware(['App\Http\Middleware\VerifyCsrfToken', 'Illuminate\Foundation\Http\Middleware\VerifyCsrfToken']);
77-
7881
if (OAuth::enabled()) {
7982
Route::get(config('statamic.oauth.routes.login'), [OAuthController::class, 'redirectToProvider'])->name('oauth.login');
8083
Route::match(['get', 'post'], config('statamic.oauth.routes.callback'), [OAuthController::class, 'handleProviderCallback'])

src/Facades/StaticCache.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* @method static ApplicationCacher createApplicationDriver(array $config)
1717
* @method static \Illuminate\Cache\Repository cacheStore()
1818
* @method static void flush()
19+
* @method static void csrfTokenJs(string $js)
1920
* @method static void nocacheJs(string $js)
2021
* @method static void nocachePlaceholder(string $placeholder)
2122
* @method static void includeJs()

src/StaticCaching/Cachers/FileCacher.php

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ class FileCacher extends AbstractCacher
2828
*/
2929
private $shouldOutputJs = false;
3030

31+
/**
32+
* @var string
33+
*/
34+
private $csrfTokenJs;
35+
3136
/**
3237
* @var string
3338
*/
@@ -230,16 +235,59 @@ private function isLongQueryStringPath($path)
230235
return Str::contains($path, '_lqs_');
231236
}
232237

238+
public function setCsrfTokenJs(string $js)
239+
{
240+
$this->csrfTokenJs = $js;
241+
}
242+
233243
public function setNocacheJs(string $js)
234244
{
235245
$this->nocacheJs = $js;
236246
}
237247

238-
public function getNocacheJs(): string
248+
public function getCsrfTokenJs(): string
239249
{
240250
$csrfPlaceholder = CsrfTokenReplacer::REPLACEMENT;
241251

242252
$default = <<<EOT
253+
(function() {
254+
fetch('/!/csrf', {
255+
method: 'POST',
256+
headers: { 'Content-Type': 'application/json' },
257+
})
258+
.then((response) => response.json())
259+
.then((data) => {
260+
for (const input of document.querySelectorAll('input[value="$csrfPlaceholder"]')) {
261+
input.value = data.csrf;
262+
}
263+
264+
for (const meta of document.querySelectorAll('meta[content="$csrfPlaceholder"]')) {
265+
meta.content = data.csrf;
266+
}
267+
268+
for (const input of document.querySelectorAll('script[data-csrf="$csrfPlaceholder"]')) {
269+
input.setAttribute('data-csrf', data.csrf);
270+
}
271+
272+
if (window.hasOwnProperty('livewire_token')) {
273+
window.livewire_token = data.csrf
274+
}
275+
276+
if (window.hasOwnProperty('livewireScriptConfig')) {
277+
window.livewireScriptConfig.csrf = data.csrf
278+
}
279+
280+
document.dispatchEvent(new CustomEvent('statamic:csrf.replaced', { detail: data }));
281+
});
282+
})();
283+
EOT;
284+
285+
return $this->csrfTokenJs ?? $default;
286+
}
287+
288+
public function getNocacheJs(): string
289+
{
290+
$default = <<<'EOT'
243291
(function() {
244292
function createMap() {
245293
var map = {};
@@ -270,26 +318,6 @@ function createMap() {
270318
if (map[key]) map[key].outerHTML = regions[key];
271319
}
272320
273-
for (const input of document.querySelectorAll('input[value="$csrfPlaceholder"]')) {
274-
input.value = data.csrf;
275-
}
276-
277-
for (const meta of document.querySelectorAll('meta[content="$csrfPlaceholder"]')) {
278-
meta.content = data.csrf;
279-
}
280-
281-
for (const input of document.querySelectorAll('script[data-csrf="$csrfPlaceholder"]')) {
282-
input.setAttribute('data-csrf', data.csrf);
283-
}
284-
285-
if (window.hasOwnProperty('livewire_token')) {
286-
window.livewire_token = data.csrf
287-
}
288-
289-
if (window.hasOwnProperty('livewireScriptConfig')) {
290-
window.livewireScriptConfig.csrf = data.csrf
291-
}
292-
293321
document.dispatchEvent(new CustomEvent('statamic:nocache.replaced', { detail: data }));
294322
});
295323
})();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Statamic\StaticCaching\NoCache;
4+
5+
class CsrfTokenController
6+
{
7+
public function __invoke()
8+
{
9+
return [
10+
'csrf' => csrf_token(),
11+
];
12+
}
13+
}

src/StaticCaching/NoCache/Controller.php renamed to src/StaticCaching/NoCache/NoCacheController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use Statamic\StaticCaching\Replacers\NoCacheReplacer;
77
use Statamic\Support\Str;
88

9-
class Controller
9+
class NoCacheController
1010
{
1111
public function __invoke(Request $request, Session $session)
1212
{

src/StaticCaching/Replacers/CsrfTokenReplacer.php

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use Illuminate\Http\Response;
66
use Statamic\Facades\StaticCache;
7+
use Statamic\StaticCaching\Cacher;
8+
use Statamic\StaticCaching\Cachers\FileCacher;
79
use Statamic\StaticCaching\Replacer;
810
use Statamic\Support\Str;
911

@@ -12,6 +14,26 @@ class CsrfTokenReplacer implements Replacer
1214
const REPLACEMENT = 'STATAMIC_CSRF_TOKEN';
1315

1416
public function prepareResponseToCache(Response $response, Response $initial)
17+
{
18+
$this->replaceInResponse($response);
19+
20+
$this->modifyFullMeasureResponse($response);
21+
}
22+
23+
public function replaceInCachedResponse(Response $response)
24+
{
25+
if (! $response->getContent()) {
26+
return;
27+
}
28+
29+
$response->setContent(str_replace(
30+
self::REPLACEMENT,
31+
csrf_token(),
32+
$response->getContent()
33+
));
34+
}
35+
36+
private function replaceInResponse(Response $response)
1537
{
1638
if (! $content = $response->getContent()) {
1739
return;
@@ -34,16 +56,30 @@ public function prepareResponseToCache(Response $response, Response $initial)
3456
));
3557
}
3658

37-
public function replaceInCachedResponse(Response $response)
59+
private function modifyFullMeasureResponse(Response $response)
3860
{
39-
if (! $response->getContent()) {
61+
$cacher = app(Cacher::class);
62+
63+
if (! $cacher instanceof FileCacher) {
4064
return;
4165
}
4266

43-
$response->setContent(str_replace(
44-
self::REPLACEMENT,
45-
csrf_token(),
46-
$response->getContent()
47-
));
67+
if (! $cacher->shouldOutputJs()) {
68+
return;
69+
}
70+
71+
$contents = $response->getContent();
72+
73+
$insertBefore = collect([
74+
Str::position($contents, '<link'),
75+
Str::position($contents, '<script'),
76+
Str::position($contents, '</head>'),
77+
])->filter()->min();
78+
79+
$js = "<script>{$cacher->getCsrfTokenJs()}</script>";
80+
81+
$contents = Str::substrReplace($contents, $js, $insertBefore, 0);
82+
83+
$response->setContent($contents);
4884
}
4985
}

src/StaticCaching/Replacers/NoCacheReplacer.php

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Statamic\StaticCaching\Replacers;
44

55
use Illuminate\Http\Response;
6-
use Illuminate\Support\Str;
76
use Statamic\Facades\StaticCache;
87
use Statamic\StaticCaching\Cacher;
98
use Statamic\StaticCaching\Cachers\FileCacher;
@@ -79,40 +78,12 @@ private function modifyFullMeasureResponse(Response $response)
7978
$contents = $response->getContent();
8079

8180
if ($cacher->shouldOutputJs()) {
82-
$contents = match ($pos = $this->insertPosition()) {
83-
'head' => $this->insertJsInHead($contents, $cacher),
84-
'body' => $this->insertJsInBody($contents, $cacher),
85-
default => throw new \Exception('Invalid nocache js insert position ['.$pos.']'),
86-
};
81+
$js = $cacher->getNocacheJs();
82+
$contents = str_replace('</body>', '<script>'.$js.'</script></body>', $contents);
8783
}
8884

8985
$contents = str_replace('NOCACHE_PLACEHOLDER', $cacher->getNocachePlaceholder(), $contents);
9086

9187
$response->setContent($contents);
9288
}
93-
94-
private function insertPosition()
95-
{
96-
return config('statamic.static_caching.nocache_js_position', 'body');
97-
}
98-
99-
private function insertJsInHead($contents, $cacher)
100-
{
101-
$insertBefore = collect([
102-
Str::position($contents, '<link'),
103-
Str::position($contents, '<script'),
104-
Str::position($contents, '</head>'),
105-
])->filter()->min();
106-
107-
$js = "<script>{$cacher->getNocacheJs()}</script>";
108-
109-
return Str::substrReplace($contents, $js, $insertBefore, 0);
110-
}
111-
112-
private function insertJsInBody($contents, $cacher)
113-
{
114-
$js = $cacher->getNocacheJs();
115-
116-
return str_replace('</body>', '<script>'.$js.'</script></body>', $contents);
117-
}
11889
}

src/StaticCaching/StaticCacheManager.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ private function flushNocache()
101101
$this->cacheStore()->forget('nocache::urls');
102102
}
103103

104+
public function csrfTokenJs(string $js)
105+
{
106+
$this->fileDriver()->setCsrfTokenJs($js);
107+
}
108+
104109
public function nocacheJs(string $js)
105110
{
106111
$this->fileDriver()->setNocacheJs($js);

tests/StaticCaching/FullMeasureStaticCachingTest.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPUnit\Framework\Attributes\Test;
66
use Statamic\Facades\File;
77
use Statamic\Facades\StaticCache;
8+
use Statamic\StaticCaching\Cacher;
89
use Statamic\StaticCaching\NoCache\Session;
910
use Tests\FakesContent;
1011
use Tests\FakesViews;
@@ -132,15 +133,16 @@ public function index()
132133
}
133134

134135
#[Test]
135-
public function it_should_add_the_javascript_if_there_is_a_csrf_token()
136+
public function it_adds_the_csrf_and_nocache_scripts()
136137
{
137138
$this->withFakeViews();
138-
$this->viewShouldReturnRaw('layout', '<html><body>{{ template_content }}</body></html>');
139+
$this->viewShouldReturnRaw('layout', '<html><head></head><body>{{ template_content }}</body></html>');
139140
$this->viewShouldReturnRaw('default', '{{ csrf_token }}');
140141

141142
$this->createPage('about');
142143

143-
StaticCache::nocacheJs('js here');
144+
$csrfTokenScript = '<script>'.app(Cacher::class)->getCsrfTokenJs().'</script>';
145+
$nocacheScript = '<script>'.app(Cacher::class)->getNocacheJs().'</script>';
144146

145147
$this->assertFalse(file_exists($this->dir.'/about_.html'));
146148

@@ -149,12 +151,22 @@ public function it_should_add_the_javascript_if_there_is_a_csrf_token()
149151
->assertOk();
150152

151153
// Initial response should be dynamic and not contain javascript.
152-
$this->assertEquals('<html><body>'.csrf_token().'</body></html>', $response->getContent());
154+
$this->assertEquals('<html><head></head><body>'.csrf_token().'</body></html>', $response->getContent());
153155

154156
// The cached response should have the token placeholder, and the javascript.
155157
$this->assertTrue(file_exists($this->dir.'/about_.html'));
156-
$this->assertEquals(vsprintf('<html><body>STATAMIC_CSRF_TOKEN%s</body></html>', [
157-
'<script>js here</script>',
158+
$this->assertEquals(vsprintf("<html><head>{$csrfTokenScript}</head><body>STATAMIC_CSRF_TOKEN%s</body></html>", [
159+
$nocacheScript,
158160
]), file_get_contents($this->dir.'/about_.html'));
159161
}
162+
163+
#[Test]
164+
public function it_can_override_the_csrf_and_nocache_scripts()
165+
{
166+
StaticCache::nocacheJs('nocache');
167+
StaticCache::csrfTokenJs('csrf');
168+
169+
$this->assertEquals(app(Cacher::class)->getNocacheJs(), 'nocache');
170+
$this->assertEquals(app(Cacher::class)->getCsrfTokenJs(), 'csrf');
171+
}
160172
}

0 commit comments

Comments
 (0)