Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exclude sections from static caching (nocache tag) #6231

Merged
merged 71 commits into from
Jul 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
b014e89
Initial replacer logic, with csrf token replacement
jasonvarga Jun 16, 2022
35c569a
Basic half measure caching integration test
jasonvarga Jun 17, 2022
ca3da95
Test for replacers, and allow initial response to also get modified
jasonvarga Jun 17, 2022
ab55d1e
Allow view faking to fall back to real ones
jasonvarga Jun 17, 2022
ef90b48
Tests for no_cache tags
jasonvarga Jun 17, 2022
19015ae
wip
jasonvarga Jun 17, 2022
fe98db9
Initial no cache implementation ...
jasonvarga Jun 17, 2022
81c77ae
wip
jasonvarga Jun 17, 2022
3105ff9
clear and rebind the cascade between requests
jasonvarga Jun 20, 2022
76b44bd
Add the replacer one time
jasonvarga Jun 20, 2022
0bca140
Filter cascade out of context so less is cached. Then merge it back i…
jasonvarga Jun 20, 2022
9f19442
wip
jasonvarga Jun 20, 2022
5b639e9
Apply fixes from StyleCI
StyleCIBot Jun 20, 2022
c528382
rename to nocache
jasonvarga Jun 20, 2022
5e14af2
fix indentation
jasonvarga Jun 20, 2022
9a17bb6
Blade support
jasonvarga Jun 20, 2022
cf61274
Reset only within the test
jasonvarga Jun 20, 2022
faae1a3
Refactor out the manager class
jasonvarga Jun 20, 2022
bcda585
more tests
jasonvarga Jun 20, 2022
e18232c
Support nested nocache tags
jasonvarga Jun 21, 2022
de443bc
Merge branch '3.3' into feature/nocache
jasonvarga Jun 21, 2022
d6011e9
Avoid request by just finding the data by request url
jasonvarga Jun 21, 2022
9419b33
Support view front matter data
jasonvarga Jun 21, 2022
531fcd6
No longer necessary
jasonvarga Jun 21, 2022
146646d
Remove this stack logic until there is test coverage
jasonvarga Jun 21, 2022
cee50d5
Support nocache inside cache when static caching is disabled
jasonvarga Jun 22, 2022
01e3882
tidy
jasonvarga Jun 22, 2022
1fa45fc
Only write/restore nocache sessions when necessary
jasonvarga Jun 22, 2022
3ce62b3
Apply fixes from StyleCI
StyleCIBot Jun 22, 2022
862ec63
Avoid a separate contexts array
jasonvarga Jun 22, 2022
4440e21
Apply fixes from StyleCI
StyleCIBot Jun 22, 2022
59b896c
Fix re-used template chunks from displaying the same thing. e.g. in l…
jasonvarga Jun 22, 2022
eb57e3c
Apply fixes from StyleCI
StyleCIBot Jun 22, 2022
e4a1f80
Support full measure using javascript
jasonvarga Jun 24, 2022
69f8c5a
Apply fixes from StyleCI
StyleCIBot Jun 24, 2022
e6777d9
method didnt exist in old versions
jasonvarga Jun 24, 2022
4328be8
there too
jasonvarga Jun 24, 2022
f35dbf6
The placeholders can be defined by the user, eg a loading state
jasonvarga Jun 24, 2022
32cf637
Indentation
jasonvarga Jun 24, 2022
8dd9be0
import
jasonvarga Jun 24, 2022
25028ec
facade
jasonvarga Jun 24, 2022
126b1f7
Avoid dynamically adding the replacer.
jasonvarga Jun 24, 2022
a1097b3
Add convenience methods for facade usage
jasonvarga Jun 24, 2022
39e0450
tidy
jasonvarga Jun 24, 2022
a7c5e4f
Rename to fragment because having a class named ViewView bothered me …
jasonvarga Jun 24, 2022
84542e9
Rename
jasonvarga Jun 24, 2022
e64a868
Rename to regions
jasonvarga Jun 24, 2022
a91a967
Refactor to region classes
jasonvarga Jun 25, 2022
0bd6159
Refactor to collection
jasonvarga Jun 25, 2022
7eb14ee
Unused
jasonvarga Jun 25, 2022
76cbd78
Refactor out fragment classes and just render the region
jasonvarga Jun 25, 2022
7e3d0fe
Get rid of region interface and just use an abstract class
jasonvarga Jun 25, 2022
d57fa6f
simplify
jasonvarga Jun 25, 2022
948584e
tidy
jasonvarga Jun 25, 2022
4e1cc51
missed these
jasonvarga Jun 25, 2022
6f1cf6e
refactor out the "get" method prefixes
jasonvarga Jun 25, 2022
4d66f7f
Merge branch '3.3' into feature/nocache
jasonvarga Jul 11, 2022
e9aeb8f
move route out to where it bypasses csrf
jasonvarga Jul 11, 2022
a021354
Revert "move route out to where it bypasses csrf"
jasonvarga Jul 11, 2022
10a6a8b
exclude from csrf this way
jasonvarga Jul 11, 2022
1c76793
Avoid a string that the browser will try to render as a tag
jasonvarga Jul 12, 2022
966bbcf
Replace csrf tokens automatically with full measure
jasonvarga Jul 12, 2022
8c178d2
adjust test output
jasonvarga Jul 12, 2022
b70b2f0
put the placeholder directly into the js instead of on ajax
jasonvarga Jul 12, 2022
431d079
tidy
jasonvarga Jul 12, 2022
fe6392a
only update if theres a matching element
jasonvarga Jul 12, 2022
3527a30
Allow the nocache manifests to be flushed via command
jasonvarga Jul 12, 2022
de78e85
test for urls being flushed and tracked
jasonvarga Jul 12, 2022
2398f64
Only output js on the pages that need it
jasonvarga Jul 13, 2022
22e75bb
Only call the methods on the file driver
jasonvarga Jul 13, 2022
d9a174f
Don't try anything if there's no token
jasonvarga Jul 13, 2022
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
15 changes: 15 additions & 0 deletions config/static_caching.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,19 @@

'ignore_query_strings' => false,

/*
|--------------------------------------------------------------------------
| Replacers
|--------------------------------------------------------------------------
|
| Here you may define replacers that dynamically replace content within
| the response. Each replacer must implement the Replacer interface.
|
*/

'replacers' => [
\Statamic\StaticCaching\Replacers\CsrfTokenReplacer::class,
\Statamic\StaticCaching\Replacers\NoCacheReplacer::class,
],

];
4 changes: 4 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
Statamic::additionalActionRoutes();
});

Route::prefix(config('statamic.routes.action'))
->post('nocache', '\Statamic\StaticCaching\NoCache\Controller')
->withoutMiddleware('App\Http\Middleware\VerifyCsrfToken');

if (OAuth::enabled()) {
Route::get(config('statamic.oauth.routes.login'), 'OAuthController@redirectToProvider')->name('oauth.login');
Route::get(config('statamic.oauth.routes.callback'), 'OAuthController@handleProviderCallback')->name('oauth.callback');
Expand Down
4 changes: 2 additions & 2 deletions src/Console/Commands/StaticClear.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Illuminate\Console\Command;
use Statamic\Console\RunsInPlease;
use Statamic\StaticCaching\Cacher as StaticCacher;
use Statamic\Facades\StaticCache;

class StaticClear extends Command
{
Expand All @@ -31,7 +31,7 @@ class StaticClear extends Command
*/
public function handle()
{
app(StaticCacher::class)->flush();
StaticCache::flush();

$this->info('Your static page cache is now so very, very empty.');
}
Expand Down
17 changes: 17 additions & 0 deletions src/Facades/StaticCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Statamic\Facades;

use Illuminate\Support\Facades\Facade;
use Statamic\StaticCaching\StaticCacheManager;

/**
* @see StaticCacheManager
*/
class StaticCache extends Facade
{
protected static function getFacadeAccessor()
{
return StaticCacheManager::class;
}
}
1 change: 1 addition & 0 deletions src/Providers/ExtensionServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ class ExtensionServiceProvider extends ServiceProvider
\Statamic\Auth\Protect\Tags::class,
\Statamic\OAuth\Tags::class,
\Statamic\Search\Tags::class,
\Statamic\StaticCaching\NoCache\Tags::class,
];

protected $widgets = [
Expand Down
81 changes: 81 additions & 0 deletions src/StaticCaching/Cachers/FileCacher.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Statamic\Facades\File;
use Statamic\StaticCaching\Replacers\CsrfTokenReplacer;
use Statamic\Support\Arr;
use Statamic\Support\Str;

Expand All @@ -16,6 +17,21 @@ class FileCacher extends AbstractCacher
*/
private $writer;

/**
* @var bool
*/
private $shouldOutputJs = false;

/**
* @var string
*/
private $nocacheJs;

/**
* @var string
*/
private $nocachePlaceholder;

/**
* @param Writer $writer
* @param Repository $cache
Expand Down Expand Up @@ -166,4 +182,69 @@ private function isLongQueryStringPath($path)
{
return Str::contains($path, '_lqs_');
}

public function setNocacheJs(string $js)
{
$this->nocacheJs = $js;
}

public function getNocacheJs(): string
{
$csrfPlaceholder = CsrfTokenReplacer::REPLACEMENT;

$default = <<<EOT
var els = document.getElementsByClassName('nocache');
var map = {};
for (var i = 0; i < els.length; i++) {
var section = els[i].getAttribute('data-nocache');
map[section] = els[i];
}

fetch('/!/nocache', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: window.location.href,
sections: Object.keys(map)
})
})
.then((response) => response.json())
.then((data) => {
const regions = data.regions;
for (var key in regions) {
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;
}
});
EOT;

return $this->nocacheJs ?? $default;
}

public function shouldOutputJs(): bool
{
return $this->shouldOutputJs;
}

public function includeJs()
{
$this->shouldOutputJs = true;
}

public function setNocachePlaceholder(string $content)
{
$this->nocachePlaceholder = $content;
}

public function getNocachePlaceholder()
{
return $this->nocachePlaceholder ?? '';
}
}
35 changes: 32 additions & 3 deletions src/StaticCaching/Middleware/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
namespace Statamic\StaticCaching\Middleware;

use Closure;
use Illuminate\Support\Collection;
use Statamic\Statamic;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\NoCache\Session;
use Statamic\StaticCaching\Replacer;

class Cache
{
Expand All @@ -13,9 +16,15 @@ class Cache
*/
private $cacher;

public function __construct(Cacher $cacher)
/**
* @var Session
*/
protected $nocache;

public function __construct(Cacher $cacher, Session $nocache)
{
$this->cacher = $cacher;
$this->nocache = $nocache;
}

/**
Expand All @@ -28,18 +37,38 @@ public function __construct(Cacher $cacher)
public function handle($request, Closure $next)
{
if ($this->canBeCached($request) && $this->cacher->hasCachedPage($request)) {
return response($this->cacher->getCachedPage($request));
$response = response($this->cacher->getCachedPage($request));

$this->getReplacers()->each(fn (Replacer $replacer) => $replacer->replaceInCachedResponse($response));

return $response;
}

$response = $next($request);

if ($this->shouldBeCached($request, $response)) {
$this->cacher->cachePage($request, $response);
$this->makeReplacementsAndCacheResponse($request, $response);

$this->nocache->write();
}

return $response;
}

private function makeReplacementsAndCacheResponse($request, $response)
{
$cachedResponse = clone $response;

$this->getReplacers()->each(fn (Replacer $replacer) => $replacer->prepareResponseToCache($cachedResponse, $response));

$this->cacher->cachePage($request, $cachedResponse);
}

private function getReplacers(): Collection
{
return collect(config('statamic.static_caching.replacers'))->map(fn ($class) => app($class));
}

private function canBeCached($request)
{
if ($request->method() !== 'GET') {
Expand Down
23 changes: 23 additions & 0 deletions src/StaticCaching/NoCache/BladeDirective.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Statamic\StaticCaching\NoCache;

class BladeDirective
{
/**
* @var Session
*/
private $nocache;

public function __construct(Session $nocache)
{
$this->nocache = $nocache;
}

public function handle($expression, $context)
{
$view = $expression;

return $this->nocache->pushView($view, $context)->placeholder();
}
}
26 changes: 26 additions & 0 deletions src/StaticCaching/NoCache/Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Statamic\StaticCaching\NoCache;

use Illuminate\Http\Request;
use Statamic\StaticCaching\Replacers\NoCacheReplacer;

class Controller
{
public function __invoke(Request $request, Session $session)
{
$url = $request->input('url'); // todo: maybe strip off query params?

$session = $session->setUrl($url)->restore();

$replacer = new NoCacheReplacer($session);

return [
'csrf' => csrf_token(),
'regions' => $session
->regions()
->map->render()
->map(fn ($contents) => $replacer->replace($contents)),
];
}
}
80 changes: 80 additions & 0 deletions src/StaticCaching/NoCache/Region.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace Statamic\StaticCaching\NoCache;

use Statamic\Support\Arr;

abstract class Region
{
protected $key;
protected $context = [];
protected $session;

public function setSession(Session $session)
{
$this->session = $session;
}

public function placeholder(): string
{
return sprintf('<span class="nocache" data-nocache="%s">NOCACHE_PLACEHOLDER</span>', $this->key());
}

public function context(): array
{
return $this->context;
}

protected function filterContext(array $context)
{
foreach (['__env', 'app', 'errors'] as $var) {
unset($context[$var]);
}

return $this->arrayRecursiveDiff($context, $this->session->cascade());
}

public function fragmentData(): array
{
return array_merge($this->session->cascade(), $this->context());
}

private function arrayRecursiveDiff($a, $b)
{
$data = [];

foreach ($a as $aKey => $aValue) {
if (! is_object($aKey) && is_array($b) && array_key_exists($aKey, $b)) {
if (is_array($aValue)) {
$aRecursiveDiff = $this->arrayRecursiveDiff($aValue, $b[$aKey]);

if (! empty($aRecursiveDiff)) {
$data[$aKey] = $aRecursiveDiff;
}
} else {
if ($aValue != $b[$aKey]) {
$data[$aKey] = $aValue;
}
}
} else {
$data[$aKey] = $aValue;
}
}

return $data;
}

public function __serialize(): array
{
return Arr::except(get_object_vars($this), ['session']);
}

public function __unserialize(array $data)
{
foreach ($data as $key => $value) {
$this->{$key} = $value;
}

$this->session = app(Session::class);
}
}
Loading