Skip to content
Merged
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
122 changes: 27 additions & 95 deletions src/StaticCaching/DefaultInvalidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,36 +167,14 @@ protected function getFormUrls($form)
{
$rules = collect(Arr::get($this->rules, "forms.{$form->handle()}.urls"));

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = Site::all()->map(function ($site) use ($rules) {
return $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($site->url().'/'.$rule));
})->flatten()->all();

return [
...$absoluteUrls,
...$prefixedRelativeUrls,
];
return $this->resolveRuleUrls($rules, Site::all());
}

protected function getAssetUrls($asset)
{
$rules = collect(Arr::get($this->rules, "assets.{$asset->container()->handle()}.urls", []));

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = Site::all()->map(function ($site) use ($rules) {
return $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($site->url().'/'.$rule));
})->flatten()->all();

return [
...$absoluteUrls,
...$prefixedRelativeUrls,
];
return $this->resolveRuleUrls($rules, Site::all());
}

protected function getEntryUrls($entry)
Expand All @@ -212,17 +190,9 @@ protected function getEntryUrls($entry)
->map->absoluteUrl()
->all();

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($entry->site()->url().'/'.$rule))
->all();

return [
...$urls,
...$absoluteUrls,
...$prefixedRelativeUrls,
...$this->resolveRuleUrls($rules, [$entry->site()]),
];
}

Expand All @@ -241,17 +211,9 @@ protected function getTermUrls($term)
->all();
}

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($term->site()->url().'/'.$rule))
->all();

return [
...$urls ?? [],
...$absoluteUrls,
...$prefixedRelativeUrls,
...$this->resolveRuleUrls($rules, [$term->site()]),
];
}

Expand All @@ -262,18 +224,7 @@ protected function getNavUrls($nav)
$nav->toAugmentedCollection()->all()
);

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $nav->sites()->map(function ($site) use ($rules) {
return $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy(Site::get($site)->url().'/'.$rule));
})->flatten()->all();

return [
...$absoluteUrls,
...$prefixedRelativeUrls,
];
return $this->resolveRuleUrls($rules, $nav->sites()->map(fn ($site) => Site::get($site)));
}

protected function getNavTreeUrls($tree)
Expand All @@ -283,17 +234,7 @@ protected function getNavTreeUrls($tree)
$tree->structure()->toAugmentedCollection()->all()
);

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($tree->site()->url().'/'.$rule))
->all();

return [
...$absoluteUrls,
...$prefixedRelativeUrls,
];
return $this->resolveRuleUrls($rules, [$tree->site()]);
}

protected function getGlobalUrls($variables)
Expand All @@ -303,17 +244,7 @@ protected function getGlobalUrls($variables)
$variables->toAugmentedCollection()->all()
);

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($variables->site()->url().'/'.$rule))
->all();

return [
...$absoluteUrls,
...$prefixedRelativeUrls,
];
return $this->resolveRuleUrls($rules, [$variables->site()]);
}

protected function getCollectionUrls($collection)
Expand All @@ -322,18 +253,9 @@ protected function getCollectionUrls($collection)

$urls = $collection->sites()->map(fn ($site) => $collection->absoluteUrl($site))->filter()->all();

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $collection->sites()->map(function ($site) use ($rules) {
return $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy(Site::get($site)->url().'/'.$rule));
})->flatten()->all();

return [
...$urls,
...$absoluteUrls,
...$prefixedRelativeUrls,
...$this->resolveRuleUrls($rules, $collection->sites()->map(fn ($site) => Site::get($site))),
];
}

Expand All @@ -343,17 +265,9 @@ protected function getCollectionTreeUrls($tree)

$urls = $this->getMovedEntryUrls($tree);

$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule))->all();

$prefixedRelativeUrls = $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => URL::tidy($tree->site()->url().'/'.$rule))
->all();

return [
...$urls,
...$absoluteUrls,
...$prefixedRelativeUrls,
...$this->resolveRuleUrls($rules, [$tree->site()]),
];
}

Expand All @@ -369,6 +283,24 @@ private function getMovedEntryUrls($tree)
->all();
}

private function resolveRuleUrls(IlluminateCollection $rules, iterable $sites): array
{
$absoluteUrls = $rules->filter(fn (string $rule) => URL::isAbsolute($rule));

// Prefix with the absolute site URL so the cacher can resolve the domain. A relative
// site URL (e.g. "/de") would otherwise fall back to the cacher's base URL, which
// includes the current site's path and never matches the host-only cached domains.
$prefixedRelativeUrls = collect($sites)->flatMap(fn ($site) => $rules
->reject(fn (string $rule) => URL::isAbsolute($rule))
->map(fn (string $rule) => $site->absoluteUrl().'/'.$rule));

// The cacher removes the final character of wildcard rules, so keep the asterisk last.
return $absoluteUrls->concat($prefixedRelativeUrls)
->map(fn (string $url) => URL::tidy($url, withTrailingSlash: Str::endsWith($url, '*') ? false : null))
->values()
->all();
}

private function parseInvalidationRules(array $rules, array $context = []): IlluminateCollection
{
return collect($rules)
Expand Down
125 changes: 125 additions & 0 deletions tests/StaticCaching/DefaultInvalidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

namespace Tests\StaticCaching;

use Illuminate\Contracts\Cache\Repository;
use Illuminate\Http\Request;
use Illuminate\Routing\Events\ResponsePrepared;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Queue;
use Mockery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use Statamic\Console\Commands\StaticWarmJob;
use Statamic\Contracts\Assets\Asset;
use Statamic\Contracts\Assets\AssetContainer;
use Statamic\Contracts\Entries\Collection;
Expand All @@ -20,6 +26,7 @@
use Statamic\Globals\Variables;
use Statamic\Sites\Site as SiteModel;
use Statamic\StaticCaching\Cacher;
use Statamic\StaticCaching\Cachers\ApplicationCacher;
use Statamic\StaticCaching\DefaultInvalidator as Invalidator;
use Statamic\Structures\CollectionTree;
use Statamic\Structures\CollectionTreeDiff;
Expand Down Expand Up @@ -965,6 +972,124 @@ public function navigation_urls_can_be_invalidated_in_a_multisite()
$this->assertNull($invalidator->invalidate($nav));
}

#[Test]
public function navigation_urls_are_made_absolute_in_a_multisite_with_relative_site_urls()
{
$this->setSites([
'de' => ['url' => '/de/', 'locale' => 'de_DE'],
'fr' => ['url' => '/fr/', 'locale' => 'fr_FR'],
]);

$cacher = tap(Mockery::mock(Cacher::class), function ($cacher) {
$cacher->shouldReceive('invalidateUrls')->with([
'http://localhost/de/*',
'http://localhost/fr/*',
])->once();
});

$nav = tap(Mockery::mock(Nav::class), function ($m) {
$m->shouldReceive('handle')->andReturn('links');
$m->shouldReceive('sites')->andReturn(collect(['de', 'fr']));
$m->shouldReceive('toAugmentedCollection')->andReturn(collect());
});

$invalidator = new Invalidator($cacher, [
'navigation' => [
'links' => [
'urls' => ['/*'],
],
],
]);

$this->assertNull($invalidator->invalidate($nav));
}

#[Test]
public function absolute_navigation_urls_are_tidied()
{
$cacher = tap(Mockery::mock(Cacher::class), function ($cacher) {
$cacher->shouldReceive('invalidateUrls')->with([
'http://localhost/de/*',
'http://localhost/fr',
])->once();
});

$nav = tap(Mockery::mock(Nav::class), function ($m) {
$m->shouldReceive('handle')->andReturn('links');
$m->shouldReceive('sites')->andReturn(collect(['en']));
$m->shouldReceive('toAugmentedCollection')->andReturn(collect());
});

$invalidator = new Invalidator($cacher, [
'navigation' => [
'links' => [
'urls' => [
'http://localhost//de/*',
'http://localhost//fr/',
],
],
],
]);

$this->assertNull($invalidator->invalidate($nav));
}

#[Test]
#[DataProvider('wildcardRulesWithTrailingSlashes')]
public function wildcard_rules_work_with_trailing_slash_enforcement($rule, $path, $method)
{
URL::enforceTrailingSlashes();
config(['statamic.static_caching.background_recache' => true]);
Queue::fake();

$cacher = new ApplicationCacher(app(Repository::class), []);
$request = Request::create('http://localhost'.$path);
$unrelatedRequest = Request::create('http://localhost/other/');

foreach ([$request, $unrelatedRequest] as $cachedRequest) {
$response = response('cached content');
$cacher->cachePage($cachedRequest, $response);
event(new ResponsePrepared($cachedRequest, $response));
$this->assertTrue($cacher->hasCachedPage($cachedRequest));
}

$nav = tap(Mockery::mock(Nav::class), function ($m) {
$m->shouldReceive('handle')->andReturn('links');
$m->shouldReceive('sites')->andReturn(collect(['en']));
$m->shouldReceive('toAugmentedCollection')->andReturn(collect());
});

$invalidator = new Invalidator($cacher, [
'navigation' => ['links' => ['urls' => [$rule]]],
]);

$invalidator->{$method}($nav);

if ($method === 'refresh') {
Queue::assertPushed(StaticWarmJob::class, 1);
Queue::assertPushed(StaticWarmJob::class, fn ($job) => $job->request->getUri()->getPath() === $path);
} else {
$this->assertFalse($cacher->hasCachedPage($request));
Queue::assertNothingPushed();
}

$this->assertTrue($cacher->hasCachedPage($unrelatedRequest));
}

public static function wildcardRulesWithTrailingSlashes()
{
foreach ([
'absolute subtree' => ['http://localhost//blog/*', '/blog/one/'],
'relative subtree' => ['/blog/*', '/blog/one/'],
'absolute prefix' => ['http://localhost/blog*', '/blogroll/'],
'relative prefix' => ['/blog*', '/blogroll/'],
] as $name => [$rule, $path]) {
foreach (['invalidate', 'refresh'] as $method) {
yield "$name $method" => [$rule, $path, $method];
}
}
}

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