diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e6fcd89..b48d6ba 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -8,6 +8,6 @@ jobs: with: enable_backend_testing: false enable_phpstan: true - php_versions: '["8.0", "8.1", "8.2", "8.3"]' + php_versions: '["8.1", "8.2", "8.3"]' backend_directory: . diff --git a/composer.json b/composer.json index bd946b8..04ab8fd 100644 --- a/composer.json +++ b/composer.json @@ -22,12 +22,17 @@ ], "require": { "flarum/core": "^1.8", - "php": ">=8.0" + "php": ">=8.1" }, "require-dev": { "flarum/phpstan": "^1.8", - "flarum/tags": "*", - "v17development/flarum-blog": "^0.7.7" + "flarum/tags": "^1.8", + "v17development/flarum-blog": "^0.7.7", + "flarum/approval": "^1.8", + "flarum/likes": "^1.8", + "fof/masquerade": "^2.1", + "clarkwinkelmann/flarum-ext-author-change": "^1.0", + "sycho/flarum-move-posts": "^0.1.7" }, "suggest": { "blomstra/flarum-redis": "This library allows using Redis as cache, session and for the queue. https://github.com/blomstra/flarum-redis#set-up" @@ -41,7 +46,7 @@ ], "autoload": { "psr-4": { - "ACPL\\FlarumCache\\": "src/" + "ACPL\\FlarumLSCache\\": "src/" } }, "extra": { @@ -54,9 +59,13 @@ "color": "#fff" }, "optional-dependencies": [ + "flarum/approval", "flarum/tags", "flarum/likes", - "fof/masquerade" + "fof/masquerade", + "v17development/flarum-blog", + "clarkwinkelmann/flarum-ext-author-change", + "sycho/flarum-move-posts" ] }, "flarum-cli": { diff --git a/extend.php b/extend.php index 8519688..b4ddcf1 100644 --- a/extend.php +++ b/extend.php @@ -9,22 +9,32 @@ * file that was distributed with this source code. */ -namespace ACPL\FlarumCache; +namespace ACPL\FlarumLSCache; -use ACPL\FlarumCache\Api\Controller\LSCacheCsrfResponseController; -use ACPL\FlarumCache\Api\Controller\PurgeLSCacheController; -use ACPL\FlarumCache\Command\LSCacheClearCommand; -use ACPL\FlarumCache\Compatibility\Flarum\Likes\FlarumLikesPurgeMiddleware; -use ACPL\FlarumCache\Compatibility\Flarum\Tags\FlarumTagsPurgeMiddleware; -use ACPL\FlarumCache\Compatibility\FriendsOfFlarum\Masquerade\FofMasqueradePurgeMiddleware; -use ACPL\FlarumCache\Compatibility\v17development\FlarumBlog\FlarumBlogPurgeMiddleware; -use ACPL\FlarumCache\Listener\ClearingCacheListener; -use ACPL\FlarumCache\Middleware\LoginMiddleware; -use ACPL\FlarumCache\Middleware\LogoutMiddleware; -use ACPL\FlarumCache\Middleware\LSCacheControlMiddleware; -use ACPL\FlarumCache\Middleware\LSCachePurgeMiddleware; -use ACPL\FlarumCache\Middleware\LSTagsMiddleware; -use ACPL\FlarumCache\Middleware\VaryCookieMiddleware; +use ACPL\FlarumLSCache\Api\Controller\{LSCacheCsrfResponseController, PurgeLSCacheController}; +use ACPL\FlarumLSCache\Command\LSCachePurgeCommand; +use ACPL\FlarumLSCache\Compatibility\{ + ClarkWinkelmann\AuthorChange\ClarkWinkelmannAuthorChangeEventSubscriber, + Flarum\Likes\FlarumLikesEventSubscriber, + Flarum\Tags\FlarumTagsEventSubscriber, + FriendsOfFlarum\Masquerade\FofMasqueradePurgeCacheMiddleware, + SychO\MovePosts\SychOMovePostsSubscriber, + v17development\FlarumBlog\FlarumBlogEventSubscriber +}; +use ACPL\FlarumLSCache\Listener\{ + ClearingCacheListener, + DiscussionEventSubscriber, + PostEventSubscriber, + UserEventSubscriber +}; +use ACPL\FlarumLSCache\Middleware\{ + CacheControlMiddleware, + CacheTagsMiddleware, + LoginMiddleware, + LogoutMiddleware, + PurgeCacheMiddleware, + VaryCookieMiddleware +}; use Flarum\Extend; use Flarum\Foundation\Event\ClearingCache; use Flarum\Http\Middleware\CheckCsrfToken; @@ -41,7 +51,7 @@ ->default('acpl-lscache.public_cache_ttl', 604_800) ->default('acpl-lscache.clearing_cache_listener', true) ->default('acpl-lscache.drop_qs', implode("\n", LSCache::DEFAULT_DROP_QS)), - (new Extend\Event())->listen(Saved::class, Listener\UpdateSettings::class), + (new Extend\Event())->listen(Saved::class, Listener\UpdateSettingsListener::class), // Vary cookie (new Extend\Middleware('forum'))->insertAfter(CheckCsrfToken::class, VaryCookieMiddleware::class), @@ -53,43 +63,47 @@ (new Extend\Middleware('forum'))->insertAfter(VaryCookieMiddleware::class, LogoutMiddleware::class), // Tag routes - (new Extend\Middleware('forum'))->add(LSTagsMiddleware::class), - (new Extend\Middleware('api'))->add(LSTagsMiddleware::class), + (new Extend\Middleware('forum'))->add(CacheTagsMiddleware::class), + (new Extend\Middleware('api'))->add(CacheTagsMiddleware::class), // Cache routes - (new Extend\Middleware('forum'))->insertAfter(VaryCookieMiddleware::class, LSCacheControlMiddleware::class), - (new Extend\Middleware('api'))->insertAfter(VaryCookieMiddleware::class, LSCacheControlMiddleware::class), + (new Extend\Middleware('forum'))->insertAfter(VaryCookieMiddleware::class, CacheControlMiddleware::class), + (new Extend\Middleware('api'))->insertAfter(VaryCookieMiddleware::class, CacheControlMiddleware::class), // A workaround for the CSRF cache issue. The JS script fetches this path to update the CSRF (new Extend\Routes('api'))->get('/lscache-csrf', 'lscache.csrf', LSCacheCsrfResponseController::class), // Purge cache on update - (new Extend\Middleware('forum'))->add(LSCachePurgeMiddleware::class), - (new Extend\Middleware('admin'))->add(LSCachePurgeMiddleware::class), - (new Extend\Middleware('api'))->add(LSCachePurgeMiddleware::class), + (new Extend\Middleware('forum'))->add(PurgeCacheMiddleware::class), + (new Extend\Middleware('admin'))->add(PurgeCacheMiddleware::class), + (new Extend\Middleware('api'))->add(PurgeCacheMiddleware::class), // Purge cache (new Extend\Routes('api'))->get('/lscache-purge', 'lscache.purge', PurgeLSCacheController::class), - (new Extend\Console())->command(LSCacheClearCommand::class), - (new Extend\Event())->listen(ClearingCache::class, ClearingCacheListener::class), + (new Extend\Console)->command(LSCachePurgeCommand::class), + (new Extend\Event)->listen(ClearingCache::class, ClearingCacheListener::class), + + (new Extend\Event)->subscribe(DiscussionEventSubscriber::class), + (new Extend\Event)->subscribe(PostEventSubscriber::class), + (new Extend\Event)->subscribe(UserEventSubscriber::class), - // Compatibility with extensions (new Extend\Conditional) - ->whenExtensionEnabled('flarum-tags', [ - (new Extend\Middleware('api'))->add(FlarumTagsPurgeMiddleware::class), - ]) ->whenExtensionEnabled('flarum-likes', [ - (new Extend\Middleware('api'))->add(FlarumLikesPurgeMiddleware::class), + (new Extend\Event)->subscribe(FlarumLikesEventSubscriber::class), + ]) + ->whenExtensionEnabled('flarum-tags', [ + (new Extend\Event)->subscribe(FlarumTagsEventSubscriber::class), ]) ->whenExtensionEnabled('fof-masquerade', [ - (new Extend\Middleware('api'))->add(FofMasqueradePurgeMiddleware::class), + (new Extend\Middleware('api'))->add(FofMasqueradePurgeCacheMiddleware::class), ]) ->whenExtensionEnabled('v17development-blog', [ - // Using insertBefore enables reading headers set by LSCachePurgeMiddleware, while insertAfter does not. - // This suggests Flarum processes middleware in a reverse order 🤔. - (new Extend\Middleware('api'))->insertBefore( - LSCachePurgeMiddleware::class, - FlarumBlogPurgeMiddleware::class - ), + (new Extend\Event)->subscribe(FlarumBlogEventSubscriber::class), + ]) + ->whenExtensionEnabled('clarkwinkelmann-author-change', [ + (new Extend\Event)->subscribe(ClarkWinkelmannAuthorChangeEventSubscriber::class), ]) + ->whenExtensionEnabled('sycho-move-posts', [ + (new Extend\Event)->subscribe(SychOMovePostsSubscriber::class), + ]), ]; diff --git a/locale/en.yml b/locale/en.yml index 7760c5f..ad236c0 100644 --- a/locale/en.yml +++ b/locale/en.yml @@ -11,8 +11,8 @@ acpl-lscache: serve_stale_label: "Serve Stale Content" serve_stale_help: "If enabled, an outdated version of a cached page will be served to visitors until a fresh cache copy is generated. This reduces server load. If disabled, the page will be dynamically generated during the cache update, which may increase wait times. By design, this option can serve out-of-date content. Please do not enable this if you find that unacceptable." - purge_on_discussion_update_label: "Purge URLs or Tags on Discussion or Post Update" - purge_on_discussion_update_help: "Enter the URLs or Tags you want to purge when a discussion or post is updated, one per line. URL should start with /, e.g. /rankings, and cache Tag should start with tag=, e.g. tag=rankings. For multiple routes, adding a rule in .htaccess with a regular expression that tags routes and entering only this tag here is faster. Learn more. By default, the cache for the homepage and updated discussions is purged." + purge_on_discussion_update_label: "Purge URLs or cache Tags on Discussion Update" + purge_on_discussion_update_help: "Enter the URLs or cache Tags you want to purge when a discussion is updated, one per line. URL should start with /, e.g. /rankings, and cache Tag should start with tag=, e.g. tag=rankings. For multiple routes, adding a rule in .htaccess with a regular expression that tags routes and entering only this tag here is faster. Learn more. By default, the cache for the homepage and updated discussions is purged." cache_exclude_label: "Exclude Paths from Caching" cache_exclude_help: "Paths containing these strings will not be cached. For /mypath/mypage?aa=bb, you can use mypage?aa=. To match the beginning, add ^ at the start. For an exact match, add $ at the end of the URL. One per line." diff --git a/migrations/2023_05_16_000001_update_htaccess.php b/migrations/2023_05_16_000001_update_htaccess.php index 5cc8d33..4a34517 100644 --- a/migrations/2023_05_16_000001_update_htaccess.php +++ b/migrations/2023_05_16_000001_update_htaccess.php @@ -1,6 +1,6 @@ function () { $htaccessManager = lsCacheGetHtaccessManager(); $htaccessManager->removeLsCacheBlock(); - } + }, ]; diff --git a/src/Abstract/PurgeMiddleware.php b/src/Abstract/PurgeMiddleware.php deleted file mode 100644 index fdfb387..0000000 --- a/src/Abstract/PurgeMiddleware.php +++ /dev/null @@ -1,81 +0,0 @@ -settings = $settings; - } - - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $response = $handler->handle($request); - - if ( - ! in_array($request->getMethod(), ['POST', 'PUT', 'PATCH', 'DELETE']) - || $response->getStatusCode() >= 400 - ) { - return $response; - } - - $this->currentRouteName = $request->getAttribute('routeName'); - $this->isDiscussion = str_starts_with($this->currentRouteName, 'discussions'); - $this->isPost = str_starts_with($this->currentRouteName, 'posts'); - - // If this is just an update of the last read post, there is no point in clearing the public cache - if ($this->isDiscussion && Arr::get( - $request->getParsedBody(), - 'data.attributes.lastReadPostNumber' - ) - ) { - return $response; - } - - return $this->processPurge($request, $handler, $response); - } - - abstract protected function processPurge( - ServerRequestInterface $request, - RequestHandlerInterface $handler, - ResponseInterface $response - ): ResponseInterface; - - protected function addPurgeParamsToResponse(ResponseInterface $response, array $newPurgeParams): ResponseInterface - { - if ($response->hasHeader(LSCacheHeadersEnum::PURGE)) { - $existingPurgeParams = explode(',', $response->getHeaderLine(LSCacheHeadersEnum::PURGE)); - $newPurgeParams = array_unique(array_merge($existingPurgeParams, $newPurgeParams)); - } - - if (count($newPurgeParams) < 1) { - return $response; - } - - if ($this->settings->get('acpl-lscache.serve_stale') && ! array_key_exists('stale', $newPurgeParams)) { - array_unshift($newPurgeParams, 'stale'); - } - - return $response->withHeader(LSCacheHeadersEnum::PURGE, implode(',', $newPurgeParams)); - } - - protected function getRouteParams(ServerRequestInterface $request): array - { - return $request->getAttribute('routeParameters'); - } -} diff --git a/src/Api/Controller/LSCacheCsrfResponseController.php b/src/Api/Controller/LSCacheCsrfResponseController.php index 75d3c96..325f5a2 100644 --- a/src/Api/Controller/LSCacheCsrfResponseController.php +++ b/src/Api/Controller/LSCacheCsrfResponseController.php @@ -1,6 +1,6 @@ withHeader(LSCacheHeadersEnum::PURGE, $purgeStr); + return (new EmptyResponse())->withHeader(LSCacheHeader::PURGE, $purgeStr); } } diff --git a/src/Command/LSCacheClearCommand.php b/src/Command/LSCachePurgeCommand.php similarity index 96% rename from src/Command/LSCacheClearCommand.php rename to src/Command/LSCachePurgeCommand.php index ab1436f..f69f867 100644 --- a/src/Command/LSCacheClearCommand.php +++ b/src/Command/LSCachePurgeCommand.php @@ -1,6 +1,6 @@ addPurgeListener($events, DiscussionCreateDateChanged::class, [$this, 'handleDiscussion']); + $this->addPurgeListener($events, DiscussionUserChanged::class, [$this, 'handleDiscussionUserChanged']); + $this->addPurgeListener($events, PostCreateDateChanged::class, [$this, 'handlePost']); + $this->addPurgeListener($events, PostEditDateChanged::class, [$this, 'handlePost']); + $this->addPurgeListener($events, PostUserChanged::class, [$this, 'handlePostUserChanged']); + } + + protected function handleDiscussion(DiscussionCreateDateChanged|DiscussionUserChanged $event): void + { + $this->handleDiscussionRelatedPurge(); + $this->purger->addPurgeTags([ + "discussion_{$event->discussion->id}", + "user_{$event->discussion->user->id}", + "user_{$event->discussion->user->username}", + ]); + } + + protected function handleDiscussionUserChanged(DiscussionUserChanged $event): void + { + $this->handleDiscussion($event); + $this->purger->addPurgeTags([ + "user_{$event->oldUser->id}", + "user_{$event->oldUser->username}", + ]); + } + + protected function handlePost(PostCreateDateChanged|PostEditDateChanged|PostUserChanged $event): void + { + $this->purger->addPurgeTags([ + "discussion_{$event->post->discussion->id}", + 'posts.index', + "post_{$event->post->id}", + "user_{$event->post->user->id}", + "user_{$event->post->user->username}", + ]); + } + + protected function handlePostUserChanged(PostUserChanged $event): void + { + $this->handlePost($event); + $this->purger->addPurgeTags([ + "user_{$event->oldUser->id}", + "user_{$event->oldUser->username}", + ]); + } +} diff --git a/src/Compatibility/Flarum/Likes/FlarumLikesEventSubscriber.php b/src/Compatibility/Flarum/Likes/FlarumLikesEventSubscriber.php new file mode 100644 index 0000000..ba41cea --- /dev/null +++ b/src/Compatibility/Flarum/Likes/FlarumLikesEventSubscriber.php @@ -0,0 +1,30 @@ +addPurgeListener($events, $event, [$this, 'handle']); + } + } + + protected function handle(PostWasLiked|PostWasUnliked $event): void + { + $this->purger->addPurgeTags([ + 'posts', + "post_{$event->post->id}", + "discussion_{$event->post->discussion->id}", + "user_{$event->post->user->id}", + "user_{$event->post->user->username}", + "user_{$event->user->id}", + "user_{$event->user->username}", + ]); + } +} diff --git a/src/Compatibility/Flarum/Likes/FlarumLikesPurgeMiddleware.php b/src/Compatibility/Flarum/Likes/FlarumLikesPurgeMiddleware.php deleted file mode 100644 index 4e7383f..0000000 --- a/src/Compatibility/Flarum/Likes/FlarumLikesPurgeMiddleware.php +++ /dev/null @@ -1,30 +0,0 @@ -currentRouteName !== 'posts.update') { - return $response; - } - - $requestBody = $request->getParsedBody(); - - if (! Arr::has($requestBody, 'data.attributes.isLiked')) { - return $response; - } - - return $this->addPurgeParamsToResponse($response, ['users.index']); - } -} diff --git a/src/Compatibility/Flarum/Tags/FlarumTagsEventSubscriber.php b/src/Compatibility/Flarum/Tags/FlarumTagsEventSubscriber.php new file mode 100644 index 0000000..ac8a572 --- /dev/null +++ b/src/Compatibility/Flarum/Tags/FlarumTagsEventSubscriber.php @@ -0,0 +1,90 @@ +addPurgeListener($events, DiscussionWasTagged::class, [$this, 'handleDiscussionWasTagged']); + + $discussionEvents = [ + DiscussionDeleted::class, DiscussionHidden::class, DiscussionRenamed::class, DiscussionRestored::class, + DiscussionStarted::class, + ]; + foreach ($discussionEvents as $event) { + $this->addPurgeListener($events, $event, [$this, 'handleDiscussionEvents']); + } + + $postEvents = [PostDeleted::class, PostHidden::class, Posted::class, PostRestored::class]; + foreach ($postEvents as $event) { + $this->addPurgeListener($events, $event, [$this, 'handlePostEvents']); + } + } + + public function handleDiscussionWasTagged(DiscussionWasTagged $event): void + { + $this->handleDiscussionRelatedPurge(); + $this->purger->addPurgeTags([ + "discussion_{$event->discussion->id}", + "user_{$event->discussion->user->id}", + "user_{$event->discussion->user->username}", + 'tags', + ...$this->generateCacheTagsForDiscussionTags($event->discussion), + ]); + } + + public function handleDiscussionEvents( + DiscussionDeleted|DiscussionHidden|DiscussionRenamed|DiscussionRestored|DiscussionStarted $event, + ): void { + if ( + ($event instanceof DiscussionDeleted && $event->discussion->hidden_at !== null) + /** @phpstan-ignore-next-line Access to an undefined property Flarum\Discussion\Discussion::$is_approved. */ + || $event->discussion->is_approved === false + ) { + return; + } + + $this->purger->addPurgeTags([ + 'tags', + ...$this->generateCacheTagsForDiscussionTags($event->discussion), + ]); + } + + public function handlePostEvents(PostDeleted|PostHidden|Posted|PostRestored $event): void + { + if ( + ($event instanceof PostRestored && $event->post->discussion->hidden_at !== null) + /** @phpstan-ignore-next-line Access to an undefined property Flarum\Discussion\Discussion::$is_approved. */ + || $event->post->discussion->is_approved === false + ) { + return; + } + + $this->purger->addPurgeTags([ + 'tags', + ...$this->generateCacheTagsForDiscussionTags($event->post->discussion), + ]); + } + + protected function generateCacheTagsForDiscussionTags(Discussion $discussion): array + { + /** @phpstan-ignore-next-line Access to an undefined property Flarum\Discussion\Discussion::$is_approved. */ + return $discussion->tags->map(fn (Tag $tag) => "tag_$tag->slug")->toArray(); + } +} diff --git a/src/Compatibility/Flarum/Tags/FlarumTagsPurgeMiddleware.php b/src/Compatibility/Flarum/Tags/FlarumTagsPurgeMiddleware.php deleted file mode 100644 index 90c0d59..0000000 --- a/src/Compatibility/Flarum/Tags/FlarumTagsPurgeMiddleware.php +++ /dev/null @@ -1,91 +0,0 @@ -url = $url; - parent::__construct($settings); - } - - protected function processPurge( - ServerRequestInterface $request, - RequestHandlerInterface $handler, - ResponseInterface $response - ): ResponseInterface { - $isDiscussion = $this->isDiscussion; - $isPost = $this->isPost; - - if (! $isDiscussion && ! $isPost) { - return $response; - } - - $body = $request->getParsedBody(); - $routeName = $this->currentRouteName; - - // When a post is edited, there is no need to purge tags cache unless the post is being hidden - if ($routeName === 'posts.update' && ! Arr::has($body, 'data.attributes.isHidden')) { - return $response; - } - - $response->getBody()->rewind(); - $payload = $response->getBody()->getContents(); - $payload = json_decode($payload, true); - - if ($isDiscussion) { - $discussionId = Arr::get($payload, 'data.id'); - } else { - $discussionId = Arr::get($payload, 'data.relationships.discussion.data.id'); - if (! $discussionId) { - $postId = Arr::get($payload, 'data.id'); - if (! $postId) { - return $response; - } - - $discussionId = Post::find($postId)->discussion_id; - } - } - - if (! $discussionId) { - return $response; - } - - $discussion = Discussion::find($discussionId); - if (! $discussion) { - return $response; - } - - /** - * @var Tag[] $tags - * @phpstan-ignore-next-line - */ - $tags = $discussion->tags; - - if (! $tags) { - return $response; - } - - $purgeParams = ['tags.index', 'tags']; - - foreach ($tags as $tag) { - $purgeParams[] = "tag_$tag->slug"; - } - - return $this->addPurgeParamsToResponse($response, $purgeParams); - } -} diff --git a/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeCacheMiddleware.php b/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeCacheMiddleware.php new file mode 100644 index 0000000..76cb2e0 --- /dev/null +++ b/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeCacheMiddleware.php @@ -0,0 +1,26 @@ +currentRouteName === 'masquerade.api.configure.save') { + $userID = $this->getRouteParams($request)['id']; + $user = User::find($userID); + + if ($user) { + $this->cachePurger->addPurgeTags([ + "user_$user->id", + "user_$user->username", + "masquerade_$user->id", + ]); + } + } + } +} diff --git a/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeMiddleware.php b/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeMiddleware.php deleted file mode 100644 index 24dc74a..0000000 --- a/src/Compatibility/FriendsOfFlarum/Masquerade/FofMasqueradePurgeMiddleware.php +++ /dev/null @@ -1,37 +0,0 @@ -currentRouteName === 'masquerade.api.configure.save') { - $userID = $this->getRouteParams($request)['id']; - $user = User::find($userID); - - return $this->addPurgeParamsToResponse( - $response, - [ - "tag=user_$user->id", - "tag=users_$user->id", - "tag=user_$user->username", - "tag=users_$user->username", - "tag=masquerade_$user->id", - ] - ); - } - - return $response; - } -} diff --git a/src/Compatibility/SychO/MovePosts/SychOMovePostsSubscriber.php b/src/Compatibility/SychO/MovePosts/SychOMovePostsSubscriber.php new file mode 100644 index 0000000..84db67f --- /dev/null +++ b/src/Compatibility/SychO/MovePosts/SychOMovePostsSubscriber.php @@ -0,0 +1,37 @@ +addPurgeListener($events, PostsMoved::class, [$this, 'handlePostsMoved']); + } + + protected function handlePostsMoved(PostsMoved $event): void + { + $cacheTags = []; + $event->posts->each(function ($post) use (&$cacheTags) { + /** @var CommentPost $post */ + $cacheTags[] = "post_{$post->id}"; + $cacheTags[] = "user_{$post->user->id}"; + $cacheTags[] = "user_{$post->user->username}"; + }); + + $this->handleDiscussionRelatedPurge(); + $this->purger->addPurgeTags([ + "discussion_{$event->sourceDiscussion->id}", + "discussion_{$event->targetDiscussion->id}", + ...$cacheTags, + ]); + } +} diff --git a/src/Compatibility/v17development/FlarumBlog/FlarumBlogEventSubscriber.php b/src/Compatibility/v17development/FlarumBlog/FlarumBlogEventSubscriber.php new file mode 100644 index 0000000..611bdb2 --- /dev/null +++ b/src/Compatibility/v17development/FlarumBlog/FlarumBlogEventSubscriber.php @@ -0,0 +1,42 @@ +addPurgeListener($events, BlogMetaSaving::class, [$this, 'handle']); + $this->addPurgeListener($events, LSCachePurging::class, [$this, 'handleLSCachePurging']); + } + + public function handle(BlogMetaSaving $event): void + { + $this->purger->addPurgeTags([ + 'blog.overview', + /** @phpstan-ignore-next-line Access to an undefined property V17Development\FlarumBlog\BlogMeta\BlogMeta::$discussion_id. */ + "blog_{$event->blogMeta->discussion_id}", + ]); + } + + /** + * If discussion is detected, also purge blog, because blog is a discussion. + */ + public function handleLSCachePurging(LSCachePurging $event): void + { + if (in_array('index', $event->data['tags'])) { + $this->purger->addPurgeTag('blog.overview'); + } + + $discussion = Arr::first($event->data['tags'], fn (string $tag) => str_starts_with($tag, 'discussion_')); + if ($discussion) { + $this->purger->addPurgeTag('blog_'.explode('_', $discussion)[1]); + } + } +} diff --git a/src/Compatibility/v17development/FlarumBlog/FlarumBlogPurgeMiddleware.php b/src/Compatibility/v17development/FlarumBlog/FlarumBlogPurgeMiddleware.php deleted file mode 100644 index f330cb0..0000000 --- a/src/Compatibility/v17development/FlarumBlog/FlarumBlogPurgeMiddleware.php +++ /dev/null @@ -1,67 +0,0 @@ -isDiscussion; - $isPost = $this->isPost; - - if (! ($isDiscussion || $isPost)) { - return $response; - } - - if ($this->currentRouteName === 'discussions.create') { - $body = $request->getParsedBody(); - if (Arr::has($body, 'data.attributes.blogMeta')) { - return $this->addPurgeParamsToResponse($response, ['tag=blog.overview']); - } - } - - $currentPurgeParams = $response->getHeaderLine(LSCacheHeadersEnum::PURGE); - if (empty($currentPurgeParams)) { - return $response; - } - - $newPurgeParams = []; - $currentPurgeParams = explode(',', $currentPurgeParams); - - // Blog extension is using default Flarum discussion api routes, so we can just reuse previous middleware to get the blog post id - $discussionParam = Arr::first( - $currentPurgeParams, - fn (string $param) => Str::startsWith($param, ['tag=discussion_', 'tag=discussions_']) - ); - if (empty($discussionParam)) { - return $response; - } - - if (preg_match('/(\d+)/', $discussionParam, $matches)) { - $discussionId = $matches[1]; - $newPurgeParams[] = 'tag=blog_'.$discussionId; - - // If the previous response wants to purge the index page and this is a blog post, we need to purge the blog overview page as well - if ( - in_array('tag=index', $currentPurgeParams) - && BlogMeta::where('discussion_id', '=', $discussionId)->first() - ) { - $newPurgeParams[] = 'tag=blog.overview'; - } - } - - return $this->addPurgeParamsToResponse($response, $newPurgeParams); - } -} diff --git a/src/Event/LSCachePurging.php b/src/Event/LSCachePurging.php new file mode 100644 index 0000000..3336661 --- /dev/null +++ b/src/Event/LSCachePurging.php @@ -0,0 +1,21 @@ + [], 'tags' => []]) + { + } + + public function handle(LSCachePurgeCommand $command): void + { + $input = []; + + if (! empty($this->data['paths'])) { + $input['--path'] = $this->data['paths']; + } + + if (! empty($this->data['tags'])) { + $input['--tag'] = $this->data['tags']; + } + + $command->run(new ArrayInput($input), new NullOutput()); + } +} diff --git a/src/LSCache.php b/src/LSCache.php index 17a7ec0..f6a7bdc 100644 --- a/src/LSCache.php +++ b/src/LSCache.php @@ -1,6 +1,8 @@ addPurgeData($event); + $this->purger->executePurge(); + } + + abstract protected function addPurgeData($event): void; +} diff --git a/src/Listener/AbstractCachePurgeSubscriber.php b/src/Listener/AbstractCachePurgeSubscriber.php new file mode 100644 index 0000000..d14df5c --- /dev/null +++ b/src/Listener/AbstractCachePurgeSubscriber.php @@ -0,0 +1,29 @@ +listen($event, function ($eventInstance) use ($handler) { + $handler($eventInstance); + + // Prevent infinite loop when something listens to LSCachePurging event + if (! $eventInstance instanceof LSCachePurging) { + $this->purger->executePurge(); + } + }); + } +} diff --git a/src/Listener/ClearingCacheListener.php b/src/Listener/ClearingCacheListener.php index 43b4529..dd2d00e 100644 --- a/src/Listener/ClearingCacheListener.php +++ b/src/Listener/ClearingCacheListener.php @@ -1,31 +1,16 @@ command = $command; - $this->settings = $settings; - } - - /** - * @throws ExceptionInterface - */ - public function handle(): void + /** @param ClearingCache $event */ + protected function addPurgeData($event): void { if ($this->settings->get('acpl-lscache.clearing_cache_listener')) { - $this->command->run(new ArrayInput([]), new NullOutput()); + $this->purger->addPurgePath('*'); } } } diff --git a/src/Listener/DiscussionCachePurgeTrait.php b/src/Listener/DiscussionCachePurgeTrait.php new file mode 100644 index 0000000..d5c0cdc --- /dev/null +++ b/src/Listener/DiscussionCachePurgeTrait.php @@ -0,0 +1,37 @@ +purger->addPurgeTags([ + 'default', + 'index', + 'discussions', + ]); + + $purgeList = $this->settings->get('acpl-lscache.purge_on_discussion_update'); + if (! empty($purgeList)) { + $purgeList = explode("\n", $purgeList); + + $paths = Arr::where($purgeList, fn ($item) => str_starts_with($item, '/')); + if (! empty($paths)) { + $this->purger->addPurgePaths($paths); + } + + $tags = Arr::where($purgeList, fn ($item) => str_starts_with($item, 'tag=')); + if (! empty($tags)) { + $this->purger->addPurgeTags($tags); + } + } + } +} diff --git a/src/Listener/DiscussionEventSubscriber.php b/src/Listener/DiscussionEventSubscriber.php new file mode 100644 index 0000000..0a6948c --- /dev/null +++ b/src/Listener/DiscussionEventSubscriber.php @@ -0,0 +1,53 @@ +addPurgeListener($events, $event, [$this, 'handle']); + } + + $this->addPurgeListener($events, Deleted::class, [$this, 'handleDeleted']); + } + + protected function handle(Deleted|Hidden|Started|Restored|Renamed $event): void + { + if (! $this->shouldPurge($event)) { + return; + } + + $this->handleDiscussionRelatedPurge(); + $this->purger->addPurgeTags([ + "discussion_{$event->discussion->id}", + "user_{$event->discussion->user->id}", + "user_{$event->discussion->user->username}", + ]); + } + + protected function handleDeleted(Deleted $event): void + { + // If discussion was hidden before, there is no need to purge cache, because it is not visible for guests anyway + if ($event->discussion->hidden_at === null && $this->shouldPurge($event)) { + $this->handle($event); + } + } + + protected function shouldPurge( + Deleted|Hidden|Started|Restored|Renamed $event, + ): bool { + return ! ( + $event->discussion->is_private + /** @phpstan-ignore-next-line Access to an undefined property Flarum\Discussion\Discussion::$is_approved. */ + || $event->discussion->is_approved === false + ); + } +} diff --git a/src/Listener/PostEventSubscriber.php b/src/Listener/PostEventSubscriber.php new file mode 100644 index 0000000..230b15f --- /dev/null +++ b/src/Listener/PostEventSubscriber.php @@ -0,0 +1,47 @@ +addPurgeListener($events, $event, [$this, 'handle']); + } + } + + protected function handle(Hidden|Posted|Restored|PostWasApproved|Revised $event): void + { + if (! $this->shouldPurge($event)) { + return; + } + + $this->purger->addPurgeTags([ + 'posts', + "discussion_{$event->post->discussion_id}", + "user_{$event->post->user_id}", + "user_{$event->post->user_id}", + ]); + + if (! $event instanceof Revised) { + $this->handleDiscussionRelatedPurge(); + } + } + + protected function shouldPurge(Deleted|Hidden|Posted|Restored|Revised|PostWasApproved $event): bool + { + return ! ( + $event->post->discussion->is_private + /** @phpstan-ignore-next-line Access to an undefined property Flarum\Post\Post::$is_approved. */ + || $event->post->is_approved === false + ); + } +} diff --git a/src/Listener/UpdateSettings.php b/src/Listener/UpdateSettings.php deleted file mode 100644 index ee26d6d..0000000 --- a/src/Listener/UpdateSettings.php +++ /dev/null @@ -1,38 +0,0 @@ -htaccessManager = $htaccessManager; - $this->cacheClearCommand = $command; - } - - /** - * @throws FileNotFoundException|ExceptionInterface - */ - public function handle(Saved $event): void - { - if (isset($event->settings['acpl-lscache.drop_qs'])) { - $this->htaccessManager->updateHtaccess(); - } - - // If the LSCache is being disabled, initiate a cache clear operation. - if (isset($event->settings['acpl-lscache.cache_enabled']) && ! $event->settings['acpl-lscache.cache_enabled']) { - $this->cacheClearCommand->run(new ArrayInput([]), new NullOutput()); - } - } -} diff --git a/src/Listener/UpdateSettingsListener.php b/src/Listener/UpdateSettingsListener.php new file mode 100644 index 0000000..709db84 --- /dev/null +++ b/src/Listener/UpdateSettingsListener.php @@ -0,0 +1,31 @@ +settings['acpl-lscache.drop_qs'])) { + $this->htaccessManager->updateHtaccess(); + } + + // If the LSCache is being disabled, initiate a cache purge operation. + if (isset($event->settings['acpl-lscache.cache_enabled']) && ! $event->settings['acpl-lscache.cache_enabled']) { + $this->purger->addPurgePath('*'); + $this->purger->executePurge(); + } + } +} diff --git a/src/Listener/UserEventSubscriber.php b/src/Listener/UserEventSubscriber.php new file mode 100644 index 0000000..0ad9c09 --- /dev/null +++ b/src/Listener/UserEventSubscriber.php @@ -0,0 +1,33 @@ +addPurgeListener($events, $event, [$this, 'handleUserWithPosts']); + } + } + + /** Purge discussions where user has posted. */ + public function handleUserWithPosts(AvatarChanged|Deleting|GroupsChanged|Renamed $event): void + { + $this->purger->addPurgeTags([ + "user_{$event->user->id}", + "user_{$event->user->username}", + 'posts', + 'discussions', + // TODO: If user has a lot of discussions chunk it and push to the queue job + ...array_map( + fn ($id) => "discussion_$id", + $event->user->posts()->pluck('discussion_id')->toArray(), + ), + ]); + } +} diff --git a/src/Abstract/CacheTagsMiddleware.php b/src/Middleware/AbstractCacheTagsMiddleware.php similarity index 60% rename from src/Abstract/CacheTagsMiddleware.php rename to src/Middleware/AbstractCacheTagsMiddleware.php index 4131189..5f02389 100644 --- a/src/Abstract/CacheTagsMiddleware.php +++ b/src/Middleware/AbstractCacheTagsMiddleware.php @@ -1,21 +1,19 @@ hasHeader(LSCacheHeadersEnum::TAG)) { + if ($response->hasHeader(LSCacheHeader::TAG)) { $newTags = array_merge( - explode(',', $response->getHeaderLine(LSCacheHeadersEnum::TAG)), - $newTags + explode(',', $response->getHeaderLine(LSCacheHeader::TAG)), + $newTags, ); } - $newTags = array_unique($newTags); - - return $response->withHeader(LSCacheHeadersEnum::TAG, implode(',', $newTags)); + return $response->withHeader(LSCacheHeader::TAG, implode(',', array_unique($newTags))); } } diff --git a/src/Middleware/AbstractPurgeCacheMiddleware.php b/src/Middleware/AbstractPurgeCacheMiddleware.php new file mode 100644 index 0000000..dba06b9 --- /dev/null +++ b/src/Middleware/AbstractPurgeCacheMiddleware.php @@ -0,0 +1,97 @@ +handle($request); + $this->currentRouteName = $request->getAttribute('routeName'); + + if ($this->shouldProcessPurge($request, $response)) { + $this->preparePurgeData($request); + $this->dispatchLSCachePurgingEvent($request); + $response = $this->addPurgeParamsToResponse($response); + } + + return $response; + } + + protected function shouldProcessPurge(ServerRequestInterface $request, ResponseInterface $response): bool + { + return in_array($request->getMethod(), ['POST', 'PUT', 'PATCH', 'DELETE']) + && $response->getStatusCode() < 400; + } + + abstract protected function preparePurgeData(ServerRequestInterface $request): void; + + protected function addPurgeParamsToResponse(ResponseInterface $response): ResponseInterface + { + $purgeData = $this->cachePurger->getPurgeData(); + $newPurgeParams = $this->formatPurgeParams($purgeData); + + if ($response->hasHeader(LSCacheHeader::PURGE)) { + $existingPurgeParams = explode(',', $response->getHeaderLine(LSCacheHeader::PURGE)); + $newPurgeParams = array_merge($existingPurgeParams, $newPurgeParams); + } + + if (empty($newPurgeParams)) { + return $response; + } + + $this->addStaleParamIfNeeded($newPurgeParams); + $this->cachePurger->clearPurgeData(); + + return $response->withHeader(LSCacheHeader::PURGE, implode(',', array_unique($newPurgeParams))); + } + + protected function formatPurgeParams(array $purgeData): array + { + $params = $purgeData['paths'] ?? []; + if (! empty($purgeData['tags'])) { + $params = array_merge( + $params, + array_map(fn (string $tag) => "tag=$tag", $purgeData['tags']), + ); + } + + return $params; + } + + protected function addStaleParamIfNeeded(array &$params): void + { + if ($this->settings->get('acpl-lscache.serve_stale') && ! in_array('stale', $params)) { + array_unshift($params, 'stale'); + } + } + + protected function getRouteParams(ServerRequestInterface $request): array + { + return $request->getAttribute('routeParameters'); + } + + protected function dispatchLSCachePurgingEvent(ServerRequestInterface $request): void + { + $purgeData = $this->cachePurger->getPurgeData(); + $this->events->dispatch(new LSCachePurging($purgeData), RequestUtil::getActor($request)); + } +} diff --git a/src/Middleware/LSCacheControlMiddleware.php b/src/Middleware/CacheControlMiddleware.php similarity index 78% rename from src/Middleware/LSCacheControlMiddleware.php rename to src/Middleware/CacheControlMiddleware.php index a2ab400..c6403e1 100644 --- a/src/Middleware/LSCacheControlMiddleware.php +++ b/src/Middleware/CacheControlMiddleware.php @@ -1,18 +1,16 @@ withCacheControlHeader($response, 'no-cache'); } - if (! in_array($method, ['GET', 'HEAD']) || $response->hasHeader(LSCacheHeadersEnum::CACHE_CONTROL)) { + if (! in_array($method, ['GET', 'HEAD']) || $response->hasHeader(LSCacheHeader::CACHE_CONTROL)) { return $response; } $routeName = $request->getAttribute('routeName'); - //Exclude FriendsOfFlarum/OAuth routes + // Exclude FriendsOfFlarum/OAuth routes if (Str::startsWith($routeName, ['auth', 'fof-oauth'])) { return $this->withCacheControlHeader($response, 'no-cache'); } - //Exclude paths specified in settings + // Exclude paths specified in settings $excludedPaths = Str::of($this->settings->get('acpl-lscache.cache_exclude')); if ($excludedPaths->isNotEmpty()) { $excludedPathsArr = $excludedPaths->explode("\n"); @@ -56,25 +54,25 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } } - //Exclude purge API route + // Exclude purge API route if ($routeName === 'lscache.purge') { return $this->withCacheControlHeader($response, 'no-cache'); } - //Cache CSRF privately + // Cache CSRF privately if ($routeName === 'lscache.csrf') { // Subtract 2 minutes (120 seconds) // from the session lifetime to set the cache to expire before the actual session does. // This is to prevent a potential issue where an expired CSRF token might be served from the cache. return $this->withCacheControlHeader( $response, - 'private,max-age='.(($this->session['lifetime'] * 60) - 120) + 'private,max-age='.(($this->session['lifetime'] * 60) - 120), ); } $lscacheParams = []; - //Guest only cache for now + // Guest-only cache $user = RequestUtil::getActor($request); if ($user->isGuest()) { $lscacheParams[] = 'public'; @@ -85,14 +83,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $lscacheParams[] = 'no-cache'; } - //TODO user group cache vary https://docs.litespeedtech.com/lscache/devguide/#cache-vary - //TODO private cache - return $this->withCacheControlHeader($response, implode(',', $lscacheParams)); } private function withCacheControlHeader(ResponseInterface $response, string $paramsStr): ResponseInterface { - return $response->withHeader(LSCacheHeadersEnum::CACHE_CONTROL, $paramsStr); + return $response->withHeader(LSCacheHeader::CACHE_CONTROL, $paramsStr); } } diff --git a/src/Middleware/LSTagsMiddleware.php b/src/Middleware/CacheTagsMiddleware.php similarity index 68% rename from src/Middleware/LSTagsMiddleware.php rename to src/Middleware/CacheTagsMiddleware.php index e2d82d6..920ee7b 100644 --- a/src/Middleware/LSTagsMiddleware.php +++ b/src/Middleware/CacheTagsMiddleware.php @@ -1,27 +1,29 @@ currentRouteName; $params = $request->getAttribute('routeParameters'); - $tagParams = [$routeName]; + if (str_ends_with($routeName, '.index')) { + $tagParams = [LSCache::extractRootRouteName($routeName)]; + } else { + $tagParams = [$routeName]; + } if (! empty($params)) { - $rootRouteName = LSCache::extractRootRouteName($routeName); + $rootRouteName = LSCache::extractRootRouteSingularName($routeName); // Discussion if (! empty($params['id'])) { diff --git a/src/Middleware/LSCachePurgeMiddleware.php b/src/Middleware/LSCachePurgeMiddleware.php deleted file mode 100644 index 40c82d6..0000000 --- a/src/Middleware/LSCachePurgeMiddleware.php +++ /dev/null @@ -1,100 +0,0 @@ -currentRouteName; - - $purgeParams = []; - - $params = $this->getRouteParams($request); - - $isDiscussion = $this->isDiscussion; - $isPost = $this->isPost; - - $body = $request->getParsedBody(); - - if ($isDiscussion || $isPost) { - $purgeList = $this->settings->get('acpl-lscache.purge_on_discussion_update'); - if (! empty($purgeList)) { - $purgeList = explode("\n", $purgeList); - // Get only valid items - $purgeList = array_filter($purgeList, fn ($item) => Str::startsWith($item, ['/', 'tag='])); - $purgeParams = array_merge($purgeParams, $purgeList); - } - - // If this is a post update, we don't need to clear the home page cache unless the post is hidden - $isPostUpdate = $routeName === 'posts.update'; - if (($isPostUpdate && Arr::has($body, 'data.attributes.isHidden')) || ! $isPostUpdate) { - array_push($purgeParams, 'tag=default', 'tag=index', 'tag=discussions.index'); - } - - // User profile cache - $response->getBody()->rewind(); - $payload = json_decode($response->getBody()->getContents(), true); - - if (isset($payload, $payload['included'])) { - $userData = Arr::first($payload['included'], fn ($value, $key) => $value['type'] === 'users'); - if ($userData) { - $userId = $userData['id']; - $userName = Arr::get($userData, 'attributes.username'); - - array_push( - $purgeParams, - "tag=user_$userId", - "tag=user_$userName", - "tag=users_$userId", - "tag=users_$userName" - ); - } - } - } - - if ($isPost) { - $discussionId = Arr::get($body, 'data.relationships.discussion.data.id'); - - if (! $discussionId) { - // When an existing post is edited or deleted - $postId = Arr::get($body, 'data.id'); - - if ($postId) { - $discussionId = Post::find($postId)->discussion_id; - } - } - - if ($discussionId) { - array_push($purgeParams, "tag=discussions_$discussionId", "tag=discussion_$discussionId"); - } - } - - if (Str::endsWith($routeName, ['.create', '.update', '.delete'])) { - $rootRouteName = LSCache::extractRootRouteName($routeName); - - // discussions.index is handled earlier - if (! $isDiscussion) { - $purgeParams[] = "tag=$rootRouteName.index"; - } - - if (! empty($params) && ! empty($params['id'])) { - $purgeParams[] = "tag={$rootRouteName}_{$params['id']}"; - } - } - - return $this->addPurgeParamsToResponse($response, $purgeParams); - } -} diff --git a/src/Middleware/LoginMiddleware.php b/src/Middleware/LoginMiddleware.php index 11e320d..0738586 100644 --- a/src/Middleware/LoginMiddleware.php +++ b/src/Middleware/LoginMiddleware.php @@ -1,26 +1,21 @@ cookie = $cookie; $this->session = $config->get('session'); } @@ -43,7 +38,7 @@ private function withVaryCookie(Response $response, Session $session): Response { return FigResponseCookies::set( $response, - $this->cookie->make(LSCache::VARY_COOKIE, $session->token(), $this->session['lifetime'] * 60) + $this->cookie->make(LSCache::VARY_COOKIE, $session->token(), $this->session['lifetime'] * 60), ); } } diff --git a/src/Middleware/LogoutMiddleware.php b/src/Middleware/LogoutMiddleware.php index b181e91..599e717 100644 --- a/src/Middleware/LogoutMiddleware.php +++ b/src/Middleware/LogoutMiddleware.php @@ -1,33 +1,27 @@ cookie = $cookie; } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); if ($request->getAttribute('routeName') === 'logout' && $response instanceof RedirectResponse) { - $response = $response->withHeader(LSCacheHeadersEnum::CACHE_CONTROL, 'no-cache'); + $response = $response->withHeader(LSCacheHeader::CACHE_CONTROL, 'no-cache'); return $this->withExpiredVaryCookie($response, $request->getAttribute('session')); } @@ -39,7 +33,7 @@ private function withExpiredVaryCookie(Response $response, Session $session): Re { return FigResponseCookies::set( FigResponseCookies::remove($response, LSCache::VARY_COOKIE), - $this->cookie->make(LSCache::VARY_COOKIE, $session->token())->expire() + $this->cookie->make(LSCache::VARY_COOKIE, $session->token())->expire(), ); } } diff --git a/src/Middleware/PurgeCacheMiddleware.php b/src/Middleware/PurgeCacheMiddleware.php new file mode 100644 index 0000000..853eb31 --- /dev/null +++ b/src/Middleware/PurgeCacheMiddleware.php @@ -0,0 +1,27 @@ +currentRouteName; + $rootRouteName = LSCache::extractRootRouteSingularName($routeName); + $params = $this->getRouteParams($request); + + if (! empty($params['id']) && $this->shouldPurgeRoute($rootRouteName, $routeName)) { + $this->cachePurger->addPurgeTag("tag={$rootRouteName}_{$params['id']}"); + } + } + + private function shouldPurgeRoute(string $rootRouteName, string $routeName): bool + { + return ! $this->cachePurger::isResourceSupportedByEvent($rootRouteName) + && Str::endsWith($routeName, ['.create', '.update', '.delete']); + } +} diff --git a/src/Middleware/VaryCookieMiddleware.php b/src/Middleware/VaryCookieMiddleware.php index 2ea2679..da4ff8d 100644 --- a/src/Middleware/VaryCookieMiddleware.php +++ b/src/Middleware/VaryCookieMiddleware.php @@ -1,28 +1,23 @@ cookie = $cookie; $this->session = $config->get('session'); } @@ -35,7 +30,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $response = $handler->handle($request); $response = $response->withHeader( - LSCacheHeadersEnum::VARY, + LSCacheHeader::VARY, "cookie={$this->cookie->getName(LSCache::VARY_COOKIE)},cookie={$this->cookie->getName('remember')},cookie=locale", ); @@ -48,7 +43,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $this->withVaryCookie($response, $session); } - private function withVaryCookie(Response $response, ?Session $session): Response + private function withVaryCookie(ResponseInterface $response, ?Session $session): ResponseInterface { if (! $session) { return $response; @@ -56,7 +51,7 @@ private function withVaryCookie(Response $response, ?Session $session): Response return FigResponseCookies::set( $response, - $this->cookie->make(LSCache::VARY_COOKIE, $session->token(), $this->session['lifetime'] * 60) + $this->cookie->make(LSCache::VARY_COOKIE, $session->token(), $this->session['lifetime'] * 60), ); } } diff --git a/src/Utility/HtaccessManager.php b/src/Utility/HtaccessManager.php index f113d95..72d4e0b 100644 --- a/src/Utility/HtaccessManager.php +++ b/src/Utility/HtaccessManager.php @@ -1,8 +1,8 @@ cookie->getName(LSCache::VARY_COOKIE), $this->cookie->getName('remember'), - 'locale' - ]).'"]' + 'locale', + ]).'"]', ); // In the extend.php, there is an extender for default settings, diff --git a/src/Utility/LSCachePurger.php b/src/Utility/LSCachePurger.php new file mode 100644 index 0000000..6811458 --- /dev/null +++ b/src/Utility/LSCachePurger.php @@ -0,0 +1,94 @@ + [], + 'tags' => [], + ]; + + /** + * @var array|string[] + */ + public static array $resourcesSupportedByEvent = ['discussion', 'post', 'user']; + + public function __construct(protected Dispatcher $events, protected Queue $queue) + { + } + + public function addPurgePath(string $purgePath): void + { + self::$purgeData['paths'][] = $purgePath; + } + + /** + * @param array $paths + */ + public function addPurgePaths(array $paths): void + { + self::$purgeData['paths'] = array_merge(self::$purgeData['paths'] ?? [], $paths); + } + + public function addPurgeTag(string $tag): void + { + self::$purgeData['tags'][] = $tag; + } + + /** + * @param array $tags + */ + public function addPurgeTags(array $tags): void + { + self::$purgeData['tags'] = array_merge(self::$purgeData['tags'] ?? [], $tags); + } + + public function getPurgeData(): array + { + return self::$purgeData; + } + + public function clearPurgeData(): void + { + self::$purgeData = [ + 'paths' => [], + 'tags' => [], + ]; + } + + public function executePurge(): void + { + if (empty(self::$purgeData) || (empty(self::$purgeData['paths']) && empty(self::$purgeData['tags']))) { + return; + } + + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + $this->events->dispatch(new LSCachePurging(self::$purgeData)); + $this->clearPurgeData(); + $this->queue->push(new PurgeCacheViaCliJob(self::$purgeData)); + } // else purge will be handled by middleware + } + + public static function isResourceSupportedByEvent(string $resource): bool + { + return in_array($resource, self::$resourcesSupportedByEvent); + } +}