diff --git a/README.md b/README.md index bda68a8..3ba7545 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,8 @@ Notes: - Header month/year links target `chronological:YYYY:MM` and `chronological:YYYY`. - Prev/next month buttons update the widget in-place without a full page reload. - Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node. +- Calendar output is marked as non-cacheable to keep missing/existing link styling and + current-day highlighting up to date. ### 0.4) Virtual chronological day pages @@ -300,6 +302,18 @@ The virtual page includes: The page is only created once you edit and save actual content. +### 0.5) Cache invalidation + +luxtools provides an admin-only **Invalidate Cache** action in the page tools menu. + +- Purges the **entire** DokuWiki cache directory (`data/cache/*`). +- This covers rendered pages, parsed instructions, CSS/JS bundles, and all + luxtools-specific caches (thumbnails, pagelink mapping). +- Useful after deploying plugin or template changes (replaces the old + `touch conf/local.php` step in `deploy.sh` which often failed due to + permission errors). +- Also useful when actively adding external photos to the current day page. + ### 1) List files by glob pattern The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table: diff --git a/action.php b/action.php index f6d7ed7..009897d 100644 --- a/action.php +++ b/action.php @@ -3,11 +3,13 @@ use dokuwiki\Extension\ActionPlugin; use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; +use dokuwiki\plugin\luxtools\CacheInvalidation; use dokuwiki\plugin\luxtools\ChronoID; use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget; use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker; use dokuwiki\plugin\luxtools\ChronologicalDayTemplate; use dokuwiki\plugin\luxtools\ChronologicalIcsEvents; +use dokuwiki\plugin\luxtools\MenuItem\InvalidateCache; require_once(__DIR__ . '/autoload.php'); /** @@ -69,6 +71,18 @@ class action_plugin_luxtools extends ActionPlugin $this, "handleCalendarWidgetAjax", ); + $controller->register_hook( + "ACTION_ACT_PREPROCESS", + "BEFORE", + $this, + "handleInvalidateCacheAction", + ); + $controller->register_hook( + "MENU_ITEMS_ASSEMBLY", + "AFTER", + $this, + "addInvalidateCacheMenuItem", + ); $controller->register_hook( "TOOLBAR_DEFINE", "AFTER", @@ -137,6 +151,8 @@ class action_plugin_luxtools extends ActionPlugin return; } + $this->sendNoStoreHeaders(); + $html = ChronologicalCalendarWidget::render($year, $month, $baseNs); if ($html === '') { http_status(500); @@ -391,6 +407,9 @@ class action_plugin_luxtools extends ActionPlugin if ($id === '') return; if (!ChronoID::isDayId($id)) return; + + $this->sendNoStoreHeaders(); + if (function_exists('page_exists') && page_exists($id)) return; $wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? ''; @@ -578,4 +597,99 @@ class action_plugin_luxtools extends ActionPlugin "block" => false, ]; } + + /** + * Add admin-only page-tools item to invalidate luxtools caches. + * + * @param Event $event + * @param mixed $param + * @return void + */ + public function addInvalidateCacheMenuItem(Event $event, $param) + { + if (!is_array($event->data)) return; + if (($event->data['view'] ?? '') !== 'page') return; + if (!function_exists('auth_isadmin') || !auth_isadmin()) return; + if (!isset($event->data['items']) || !is_array($event->data['items'])) return; + + $label = (string)$this->getLang('cache_invalidate_button'); + if ($label === '') $label = 'Invalidate Cache'; + + $title = (string)$this->getLang('cache_invalidate_button_title'); + if ($title === '') $title = 'Invalidate luxtools cache for this page'; + + $event->data['items'][] = new InvalidateCache($label, $title); + } + + /** + * Handle manual cache invalidation action. + * + * @param Event $event + * @param mixed $param + * @return void + */ + public function handleInvalidateCacheAction(Event $event, $param) + { + if (!is_string($event->data) || $event->data !== 'show') return; + + global $INPUT; + if (!$INPUT->bool('luxtools_invalidate_cache')) return; + + global $ID; + $id = is_string($ID) ? $ID : ''; + if (function_exists('cleanID')) { + $id = (string)cleanID($id); + } + + if (!function_exists('auth_isadmin') || !auth_isadmin()) { + $message = (string)$this->getLang('cache_invalidate_denied'); + if ($message === '') $message = 'Only admins can invalidate cache.'; + msg($message, -1); + $this->redirectToShow($id); + return; + } + + if (!checkSecurityToken()) { + $message = (string)$this->getLang('cache_invalidate_badtoken'); + if ($message === '') $message = 'Security token mismatch. Please retry.'; + msg($message, -1); + $this->redirectToShow($id); + return; + } + + $removed = CacheInvalidation::purgeAll(); + $message = (string)$this->getLang('cache_invalidate_success'); + if ($message === '') { + $message = 'DokuWiki cache invalidated.'; + } + msg($message . ' (' . $removed . ')', 1); + + $this->redirectToShow($id); + } + + /** + * Redirect to normal show view for the given page. + * + * @param string $id + * @return void + */ + protected function redirectToShow(string $id): void + { + $params = ['do' => 'show']; + send_redirect(wl($id, $params, true, '&')); + } + + /** + * Send no-store cache headers for highly dynamic responses. + * + * @return void + */ + protected function sendNoStoreHeaders(): void + { + if (headers_sent()) return; + + header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); + header('Pragma: no-cache'); + header('Expires: 0'); + } } diff --git a/deploy.sh b/deploy.sh index bac9d6f..70340d1 100755 --- a/deploy.sh +++ b/deploy.sh @@ -93,20 +93,4 @@ echo "Deploying luxtools to: $TARGET/" rsync "${RSYNC_ARGS[@]}" "$SRC_DIR/" "$TARGET/" -# Invalidate DokuWiki cache by touching conf/local.php -# This forces DokuWiki to rebuild JavaScript/CSS bundles -CONF_LOCAL="$(dirname "$TARGET")/../../conf/local.php" -if [[ -f "$CONF_LOCAL" ]]; then - if ((DRY_RUN)); then - echo "(dry-run) Would touch $CONF_LOCAL to invalidate cache" - elif touch "$CONF_LOCAL" 2>/dev/null; then - echo "Cache invalidated (touched conf/local.php)" - else - echo "Note: Cannot touch conf/local.php (permission denied)." - echo " Run 'touch conf/local.php' on the server to clear cache." - fi -else - echo "Note: conf/local.php not found at expected path, skip cache invalidation." -fi - echo "Done." diff --git a/lang/de/lang.php b/lang/de/lang.php index 4a47c86..57c9a8b 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -85,3 +85,8 @@ $lang["pagelink_unlinked"] = "Seite nicht verknüpft"; $lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft"; $lang["chronological_photos_title"] = "Fotos"; $lang["chronological_events_title"] = "Termine"; +$lang["cache_invalidate_button"] = "Cache invalidieren"; +$lang["cache_invalidate_button_title"] = "Gesamten DokuWiki-Cache leeren"; +$lang["cache_invalidate_success"] = "DokuWiki-Cache invalidiert."; +$lang["cache_invalidate_denied"] = "Nur Admins dürfen den Cache invalidieren."; +$lang["cache_invalidate_badtoken"] = "Sicherheits-Token ungültig. Bitte erneut versuchen."; diff --git a/lang/en/lang.php b/lang/en/lang.php index e3efdfb..ef0d308 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -86,3 +86,8 @@ $lang["pagelink_multi_warning"] = "Multiple folders linked"; $lang["calendar_err_badmonth"] = "Invalid calendar month. Use YYYY-MM."; $lang["chronological_photos_title"] = "Photos"; $lang["chronological_events_title"] = "Events"; +$lang["cache_invalidate_button"] = "Invalidate Cache"; +$lang["cache_invalidate_button_title"] = "Purge the entire DokuWiki cache"; +$lang["cache_invalidate_success"] = "DokuWiki cache invalidated."; +$lang["cache_invalidate_denied"] = "Only admins can invalidate cache."; +$lang["cache_invalidate_badtoken"] = "Security token mismatch. Please retry."; diff --git a/src/CacheInvalidation.php b/src/CacheInvalidation.php new file mode 100644 index 0000000..b3016b0 --- /dev/null +++ b/src/CacheInvalidation.php @@ -0,0 +1,71 @@ +id = is_string($ID) ? $ID : ''; + $this->type = 'show'; + $this->method = 'get'; + $this->params = [ + 'do' => 'show', + 'luxtools_invalidate_cache' => 1, + ]; + $this->nofollow = true; + $this->label = $label; + $this->title = $title; + $this->svg = DOKU_INC . 'lib/images/menu/calendar-clock.svg'; + + if (function_exists('getSecurityToken')) { + $this->params['sectok'] = getSecurityToken(); + } + } + + /** + * Keep a distinct class hook for template styling. + * + * @return string + */ + public function getType() + { + return 'luxtools-invalidate-cache'; + } +} diff --git a/syntax/calendar.php b/syntax/calendar.php index 1280bd9..6ba22a3 100644 --- a/syntax/calendar.php +++ b/syntax/calendar.php @@ -72,6 +72,8 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin if ($format !== 'xhtml') return false; if (!($renderer instanceof Doku_Renderer_xhtml)) return false; + $renderer->nocache(); + if (!($data['ok'] ?? false)) { $message = (string)$this->getLang((string)($data['error'] ?? 'calendar_err_badmonth')); if ($message === '') $message = 'Invalid calendar month. Use YYYY-MM.';