From 5b524bfce9dce53cd3423faadc3e4b7be5e61114 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Mon, 9 Nov 2026 21:10:30 +0100 Subject: [PATCH] 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 --- .../ContextMenu/SsrPurgeItemProvider.php | 71 +++++++++++++++++++ .../Controller/Backend/SsrController.php | 28 ++++++++ .../Configuration/Backend/AjaxRoutes.php | 6 ++ .../Public/Javascript/ssr-context-menu.js | 27 +++++++ 4 files changed, 132 insertions(+) create mode 100644 packages/vitec/Classes/ContextMenu/SsrPurgeItemProvider.php create mode 100644 packages/vitec/Resources/Public/Javascript/ssr-context-menu.js diff --git a/packages/vitec/Classes/ContextMenu/SsrPurgeItemProvider.php b/packages/vitec/Classes/ContextMenu/SsrPurgeItemProvider.php new file mode 100644 index 0000000..729f418 --- /dev/null +++ b/packages/vitec/Classes/ContextMenu/SsrPurgeItemProvider.php @@ -0,0 +1,71 @@ + [ + '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']; + } +} diff --git a/packages/vitec/Classes/Controller/Backend/SsrController.php b/packages/vitec/Classes/Controller/Backend/SsrController.php index a4bc4e3..b10a7b5 100644 --- a/packages/vitec/Classes/Controller/Backend/SsrController.php +++ b/packages/vitec/Classes/Controller/Backend/SsrController.php @@ -12,9 +12,11 @@ use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Core\Core\Environment; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Http\JsonResponse; use TYPO3\CMS\Core\Http\RedirectResponse; use TYPO3\CMS\Core\Http\RequestFactory; use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Site\SiteFinder; use TYPO3\CMS\Core\Messaging\FlashMessageService; use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -100,6 +102,32 @@ final class SsrController 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 { try { diff --git a/packages/vitec/Configuration/Backend/AjaxRoutes.php b/packages/vitec/Configuration/Backend/AjaxRoutes.php index d85a000..d62acd4 100755 --- a/packages/vitec/Configuration/Backend/AjaxRoutes.php +++ b/packages/vitec/Configuration/Backend/AjaxRoutes.php @@ -27,4 +27,10 @@ return [ 'path' => '/vitec/product/field/save', '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', + ], ]; diff --git a/packages/vitec/Resources/Public/Javascript/ssr-context-menu.js b/packages/vitec/Resources/Public/Javascript/ssr-context-menu.js new file mode 100644 index 0000000..9aedf37 --- /dev/null +++ b/packages/vitec/Resources/Public/Javascript/ssr-context-menu.js @@ -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();