Skip to content
31 changes: 20 additions & 11 deletions src/StaticCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,21 @@
* The values are comma-separated and can be in several formats:
*
* - Added by the gateway:
* - `{environmentId}`
* - `{environmentId}:{uri}` (URI has a leading and no trailing slash)
* - `{environmentId}` (legacy)
* - `{environmentId}:{uri}` (legacy; non-homepage URI has a leading and no trailing slash)
* - `{environmentId}:uri`
* - `{environmentId}:uri:{uri}` (homepage URI is `/`, otherwise with a leading and no trailing slash)
* - Added by the CDN:
* - `cdn:{environmentId}`
* - `cdn:{environmentId}:{objectKey}` (object key has no leading slash)
* - `cdn:{environmentId}` (legacy)
* - `cdn:{environmentId}:{objectKey}` (legacy; object key has no leading slash)
* - `{environmentId}:cdn`
* - `{environmentId}:cdn:{objectKey}` (object key has no leading slash)
* - Added by Craft:
* - `{environmentShortId}{hashed}`
* - `{environmentId}:overflow` (when the response has too many cache tags)
* - `{environmentId}:overflow` (when the response has too many tags or a selector is too long)
*/
class StaticCache extends \yii\base\Component
{
public const CDN_PREFIX = 'cdn:';

/**
* @event PurgeEvent The event that is triggered before static cache tags are purged.
*/
Expand All @@ -51,7 +53,7 @@ class StaticCache extends \yii\base\Component
* @see https://developers.cloudflare.com/workers/cache/configuration/
*/
private const MAX_TAG_HEADER_VALUE_LENGTH = 16 * 1024;
private const MAX_TAG_VALUE_LENGTH = 1024;
public const MAX_TAG_VALUE_LENGTH = 1024;
private const MAX_TAG_COUNT = 1000;
private ?int $cacheDuration = null;
private Collection $tags;
Expand Down Expand Up @@ -225,9 +227,10 @@ public function purgeAll(): void

public function purgeOrigin(): void
{
$environmentId = Module::getInstance()->getConfig()->environmentId;
$tags = $this->beforePurge(
null,
Module::getInstance()->getConfig()->environmentId,
"$environmentId:uri",
);
$this->tagsToPurge->push(...$tags);
}
Expand All @@ -246,9 +249,10 @@ public function purgeGateway(): void

public function purgeCdn(): void
{
$environmentId = Module::getInstance()->getConfig()->environmentId;
$tags = $this->beforePurge(
null,
self::CDN_PREFIX . Module::getInstance()->getConfig()->environmentId,
"$environmentId:cdn",
);
$this->tagsToPurge->push(...$tags);
}
Expand All @@ -266,7 +270,12 @@ private function purgeElementUri(ElementInterface $element): bool
: Path::new($uri)->withLeadingSlash()->withoutTrailingSlash();

$environmentId = Module::getInstance()->getConfig()->environmentId;
$tags = $this->beforePurge($element, "$environmentId:$uri");
$tag = StaticCacheTag::create("$environmentId:uri:$uri");
$overflowTag = $this->overflowTag();
$tags = $this->beforePurge(
$element,
strlen($tag->getValue()) > self::MAX_TAG_VALUE_LENGTH ? $overflowTag : $tag,
);
$this->tagsToPurge = $tags->concat($this->tagsToPurge);

return $tags->isNotEmpty();
Expand Down
11 changes: 6 additions & 5 deletions src/fs/Fs.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,13 @@ protected function createAdapter(): AwsS3V3Adapter
protected function invalidateCdnPath(string $path): bool
{
try {
$prefix = StaticCache::CDN_PREFIX . Module::getInstance()->getConfig()->environmentId . ':';
$tag = StaticCacheTag::create($this->createBucketPath($path)->toString())
->minify(false)
->withPrefix($prefix);
$environmentId = Module::getInstance()->getConfig()->environmentId;
$objectKey = $this->createBucketPath($path)->toString();
$cdnTag = StaticCacheTag::create("$environmentId:cdn:$objectKey");

Module::getInstance()->getStaticCache()->purgeTags($tag);
Module::getInstance()->getStaticCache()->purgeTags(
strlen($cdnTag->getValue()) > StaticCache::MAX_TAG_VALUE_LENGTH ? "$environmentId:overflow" : $cdnTag,
);

return true;
} catch (\Throwable $e) {
Expand Down
87 changes: 84 additions & 3 deletions tests/unit/StaticCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Codeception\Test\Unit;
use Craft;
use craft\cloud\events\PurgeEvent;
use craft\cloud\fs\AssetsFs;
use craft\cloud\HeaderEnum;
use craft\cloud\Module;
use craft\cloud\signing\RequestSigner;
Expand Down Expand Up @@ -218,6 +219,50 @@ public function testPurgeTagsKeepExistingHeaderTags(): void
);
}

public function testPurgeAllUsesOriginAndCdnTags(): void
{
$staticCache = new StaticCache();

$staticCache->purgeAll();

$this->assertSame(
['123-environment-id:uri', '123-environment-id:cdn'],
$this->collectionProperty($staticCache, 'tagsToPurge')
->map(fn(StaticCacheTag $tag) => $tag->getValue())
->all(),
);
}

public function testAssetCdnPurgeUsesEnvironmentFirstTag(): void
{
$staticCache = new StaticCache();
Module::getInstance()->set('staticCache', $staticCache);
$fs = new AssetsFs();
$method = new ReflectionMethod($fs, 'invalidateCdnPath');
$method->setAccessible(true);

$this->assertTrue($method->invoke($fs, 'image.jpg'));
Comment thread
timkelty marked this conversation as resolved.
$this->assertSame(
'123-environment-id:cdn:123-environment-id/assets/image.jpg',
Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value),
);
}

public function testOverlongAssetCdnPurgeUsesOverflowTag(): void
{
$staticCache = new StaticCache();
Module::getInstance()->set('staticCache', $staticCache);
$fs = new AssetsFs();
$method = new ReflectionMethod($fs, 'invalidateCdnPath');
$method->setAccessible(true);

$this->assertTrue($method->invoke($fs, str_repeat('x', 1024)));
$this->assertSame(
'123-environment-id:overflow',
Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value),
);
}

public function testBeforePurgeEventCanCancelPurge(): void
{
$staticCache = new StaticCache();
Expand Down Expand Up @@ -266,7 +311,7 @@ public function testFailedFetchRequestFallsBackToPurgeHeader(): void
}

$this->assertSame(
'123-environment-id:/news',
'123-environment-id:uri:/news',
Craft::$app->getResponse()->getHeaders()->get(HeaderEnum::CACHE_PURGE_TAG->value),
);
}
Expand Down Expand Up @@ -313,7 +358,43 @@ public function testBeforePurgeEventReceivesElement(): void
$this->saveElement($staticCache, $element);

$this->assertSame($element, $purgeEvent->element);
$this->assertSame('123-environment-id:/news', $purgeEvent->tags[0]->getValue());
$this->assertSame(
['123-environment-id:uri:/news'],
array_map(fn(StaticCacheTag $tag) => $tag->getValue(), $purgeEvent->tags),
);
}

public function testHomepagePurgeUsesUriTag(): void
{
$staticCache = new StaticCache();
$element = new Entry(['uri' => Entry::HOMEPAGE_URI]);

$this->saveElement($staticCache, $element);

$this->assertSame(
['123-environment-id:uri:/'],
$this->collectionProperty($staticCache, 'tagsToPurge')
->map(fn(StaticCacheTag $tag) => $tag->getValue())
->all(),
);
}

public function testOverlongElementUriPurgeUsesOverflowTag(): void
{
$staticCache = new StaticCache();
$element = new FetchableElement(['uri' => str_repeat('x', 1024)]);
$element->fetchUrl = 'https://example.com/overlong';

$this->saveElement($staticCache, $element);
$this->sendPendingPurgeTags($staticCache);

$payload = json_decode(
(string) $this->gatewayRequest?->getBody(),
true,
flags: JSON_THROW_ON_ERROR,
);

$this->assertSame(['123-environment-id:overflow'], $payload['tags']);
}

public function testCancelledElementPurgeDoesNotQueueFetch(): void
Expand Down Expand Up @@ -369,7 +450,7 @@ public function testSavedElementPurgeRequestIncludesFetchUrls(): void
flags: JSON_THROW_ON_ERROR,
);

$this->assertSame(['123-environment-id:/news'], $payload['tags']);
$this->assertSame(['123-environment-id:uri:/news'], $payload['tags']);
$this->assertSame([
'https://example.com/news',
'https://example.com/fr/nouvelles',
Expand Down
Loading