Page context menu: Purge from SSR cache

SsrPurgeItemProvider adds the item behind Clear cache for real pages (doktype < 199) whenever the purge endpoint is configured. The callback module posts the page uid to the new vitec_ssr_purge_page ajax route, which resolves the page path through the site router, purges it via SsrPurgeService (trigger contextmenu:username, logged as usual) and reports back as a toast notification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
orascheandClaude Fable 5 committed 2026-11-09 21:10:30 +01:00
1 parent fa63871da7
commit 5b524bfce9
4 files changed
+132

No files matched your search

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\ContextMenu;
use TYPO3\CMS\Backend\ContextMenu\ItemProviders\AbstractProvider;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Core\Environment;
/**
* Page tree context menu: "Purge from SSR cache" - evicts exactly this
* page from the SSR node's full-page cache, with toast feedback.
* Shown only for real pages and only while the purge endpoint is
* configured (var/vitec-ssr-purge.env).
*/
final class SsrPurgeItemProvider extends AbstractProvider
{
protected $itemsConfiguration = [
'ssrPurge' => [
'type' => 'item',
'label' => 'Purge from SSR cache',
'iconIdentifier' => 'actions-system-cache-clear-impact-medium',
'callbackAction' => 'ssrPurge',
],
];
public function canHandle(): bool
{
return $this->table === 'pages';
}
/**
* Below the core PageProvider (100): we run after it and splice our
* item into the finished menu, right behind "Clear cache".
*/
public function getPriority(): int
{
return 42;
}
public function addItems(array $items): array
{
if (!is_readable(Environment::getVarPath() . '/vitec-ssr-purge.env')) {
return $items;
}
$page = BackendUtility::getRecord('pages', (int)$this->identifier, 'uid,doktype');
if ($page === null || (int)$page['doktype'] >= 199) {
// Spacers, folders, recycler - nothing cached for those.
return $items;
}
$this->initDisabledItems();
$localItems = $this->prepareItems($this->itemsConfiguration);
if (isset($items['clearCache'])) {
$new = [];
foreach ($items as $key => $item) {
$new[$key] = $item;
if ($key === 'clearCache') {
$new = array_merge($new, $localItems);
}
}
return $new;
}
return array_merge($items, $localItems);
}
protected function getAdditionalAttributes(string $itemName): array
{
return ['data-callback-module' => '@evomedien/vitec/ssr-context-menu.js'];
}
}
@@ -12,9 +12,11 @@ use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\JsonResponse;
use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Http\RequestFactory;
use TYPO3\CMS\Core\Messaging\FlashMessage; use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Messaging\FlashMessageService; use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -100,6 +102,32 @@ final class SsrController
return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'), 303); return new RedirectResponse((string)$this->uriBuilder->buildUriFromRoute('web_vitecssr'), 303);
} }
/**
* Ajax route vitec_ssr_purge_page: purge exactly one page, called by
* the page tree context menu item (ssr-context-menu.js).
*/
public function purgePage(ServerRequestInterface $request): ResponseInterface
{
$body = (array)($request->getParsedBody() ?? []);
$pageUid = (int)($body['pageUid'] ?? ($request->getQueryParams()['pageUid'] ?? 0));
if ($pageUid <= 0) {
return new JsonResponse(['success' => false, 'message' => 'No page uid given'], 400);
}
try {
$site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($pageUid);
$path = $site->getRouter()->generateUri($pageUid)->getPath() ?: '/';
} catch (\Throwable) {
return new JsonResponse(['success' => false, 'message' => 'Page has no routable URL (folder?)']);
}
$user = (string)($GLOBALS['BE_USER']->user['username'] ?? '?');
$result = $this->purgeService->purge([$path], 'contextmenu:' . $user);
return new JsonResponse([
'success' => $result['success'],
'path' => $path,
'message' => 'HTTP ' . $result['httpCode'] . ' - ' . $result['message'],
]);
}
private function rows(string $table, int $limit): array private function rows(string $table, int $limit): array
{ {
try { try {
@@ -27,4 +27,10 @@ return [
'path' => '/vitec/product/field/save', 'path' => '/vitec/product/field/save',
'target' => ProductTextImportController::class . '::fieldSave', 'target' => ProductTextImportController::class . '::fieldSave',
], ],
// Context menu "Purge from SSR cache" (SsrPurgeItemProvider +
// Resources/Public/Javascript/ssr-context-menu.js).
'vitec_ssr_purge_page' => [
'path' => '/vitec/ssr/purge-page',
'target' => \Evomedien\Vitec\Controller\Backend\SsrController::class . '::purgePage',
],
]; ];
@@ -0,0 +1,27 @@
/**
* Context menu action "Purge from SSR cache" (SsrPurgeItemProvider).
* Posts the page uid to the vitec_ssr_purge_page ajax route and shows
* the outcome as a backend notification.
*/
import AjaxRequest from '@typo3/core/ajax/ajax-request.js';
import Notification from '@typo3/backend/notification.js';
class SsrContextMenuActions {
ssrPurge(table, uid) {
new AjaxRequest(TYPO3.settings.ajaxUrls.vitec_ssr_purge_page)
.post({ pageUid: uid })
.then(async (response) => {
const data = await response.resolve();
if (data.success) {
Notification.success('SSR cache purged', data.path + ' (' + data.message + ')', 5);
} else {
Notification.error('SSR purge failed', data.message || 'Unknown error', 8);
}
})
.catch(() => {
Notification.error('SSR purge failed', 'Request error - see purge protocol in the VITEC SSR module', 8);
});
}
}
export default new SsrContextMenuActions();