xref: /template/strap/action/cache.php (revision c3437056399326d621a01da73b649707fbb0ae69)
137748cd8SNickeau<?php
237748cd8SNickeau
3*c3437056SNickeauuse ComboStrap\AnalyticsDocument;
4*c3437056SNickeauuse ComboStrap\CacheExpirationDate;
537748cd8SNickeauuse ComboStrap\CacheManager;
61fa8c418SNickeauuse ComboStrap\CacheMedia;
7*c3437056SNickeauuse ComboStrap\Cron;
8*c3437056SNickeauuse ComboStrap\ExceptionCombo;
9*c3437056SNickeauuse ComboStrap\File;
101fa8c418SNickeauuse ComboStrap\Http;
1137748cd8SNickeauuse ComboStrap\Iso8601Date;
12*c3437056SNickeauuse ComboStrap\LogUtility;
13*c3437056SNickeauuse ComboStrap\MetadataDokuWikiStore;
14*c3437056SNickeauuse ComboStrap\Page;
15*c3437056SNickeauuse ComboStrap\PageDescription;
16*c3437056SNickeauuse ComboStrap\PageH1;
17*c3437056SNickeauuse ComboStrap\ResourceName;
18*c3437056SNickeauuse ComboStrap\PageTitle;
1937748cd8SNickeauuse ComboStrap\PluginUtility;
20*c3437056SNickeauuse ComboStrap\TplUtility;
211fa8c418SNickeauuse dokuwiki\Cache\CacheRenderer;
2237748cd8SNickeau
2337748cd8SNickeaurequire_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
2437748cd8SNickeau
2537748cd8SNickeau/**
2637748cd8SNickeau * Can we use the parser cache
2737748cd8SNickeau */
2837748cd8SNickeauclass action_plugin_combo_cache extends DokuWiki_Action_Plugin
2937748cd8SNickeau{
3037748cd8SNickeau    const COMBO_CACHE_PREFIX = "combo:cache:";
3137748cd8SNickeau
321fa8c418SNickeau
331fa8c418SNickeau    const CANONICAL = "cache";
341fa8c418SNickeau    const STATIC_SCRIPT_NAMES = ["/lib/exe/jquery.php", "/lib/exe/js.php", "/lib/exe/css.php"];
351fa8c418SNickeau
361fa8c418SNickeau    /**
37*c3437056SNickeau     * @var string[]
38*c3437056SNickeau     */
39*c3437056SNickeau    private static $sideSlotNames;
40*c3437056SNickeau
41*c3437056SNickeau
42*c3437056SNickeau    private static function getSideSlotNames(): array
43*c3437056SNickeau    {
44*c3437056SNickeau        if (self::$sideSlotNames === null) {
45*c3437056SNickeau            global $conf;
46*c3437056SNickeau
47*c3437056SNickeau            self::$sideSlotNames = [
48*c3437056SNickeau                $conf['sidebar']
49*c3437056SNickeau            ];
50*c3437056SNickeau
51*c3437056SNickeau            /**
52*c3437056SNickeau             * @see {@link \ComboStrap\TplConstant::CONF_SIDEKICK}
53*c3437056SNickeau             */
54*c3437056SNickeau            $loaded = PluginUtility::loadStrapUtilityTemplateIfPresentAndSameVersion();
55*c3437056SNickeau            if ($loaded) {
56*c3437056SNickeau
57*c3437056SNickeau                $sideKickSlotPageName = TplUtility::getSideKickSlotPageName();
58*c3437056SNickeau                if (!empty($sideKickSlotPageName)) {
59*c3437056SNickeau                    self::$sideSlotNames[] = $sideKickSlotPageName;
60*c3437056SNickeau                }
61*c3437056SNickeau
62*c3437056SNickeau            }
63*c3437056SNickeau        }
64*c3437056SNickeau        return self::$sideSlotNames;
65*c3437056SNickeau    }
66*c3437056SNickeau
67*c3437056SNickeau    private static function removeSideSlotCache()
68*c3437056SNickeau    {
69*c3437056SNickeau        $sidebars = self::getSideSlotNames();
70*c3437056SNickeau
71*c3437056SNickeau
72*c3437056SNickeau        /**
73*c3437056SNickeau         * Delete the cache for the sidebar
74*c3437056SNickeau         */
75*c3437056SNickeau        foreach ($sidebars as $sidebarRelativePath) {
76*c3437056SNickeau
77*c3437056SNickeau            $page = Page::createPageFromNonQualifiedPath($sidebarRelativePath);
78*c3437056SNickeau            $page->deleteCache();
79*c3437056SNickeau
80*c3437056SNickeau        }
81*c3437056SNickeau    }
82*c3437056SNickeau
83*c3437056SNickeau    /**
8437748cd8SNickeau     * @param Doku_Event_Handler $controller
8537748cd8SNickeau     */
8637748cd8SNickeau    function register(Doku_Event_Handler $controller)
8737748cd8SNickeau    {
8837748cd8SNickeau
8937748cd8SNickeau        /**
9037748cd8SNickeau         * Log the cache usage and also
9137748cd8SNickeau         */
9237748cd8SNickeau        $controller->register_hook('PARSER_CACHE_USE', 'AFTER', $this, 'logCacheUsage', array());
9337748cd8SNickeau
94*c3437056SNickeau        $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'pageCacheExpiration', array());
9537748cd8SNickeau
9637748cd8SNickeau        /**
97*c3437056SNickeau         * To add the cache result in the HTML
981fa8c418SNickeau         */
99*c3437056SNickeau        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addCacheLogHtmlDataBlock', array());
10037748cd8SNickeau
10137748cd8SNickeau        /**
10237748cd8SNickeau         * To reset the cache manager
10337748cd8SNickeau         * between two run in the test
10437748cd8SNickeau         */
10537748cd8SNickeau        $controller->register_hook('DOKUWIKI_DONE', 'BEFORE', $this, 'close', array());
10637748cd8SNickeau
1071fa8c418SNickeau        /**
1081fa8c418SNickeau         * To delete the VARY on css.php, jquery.php, js.php
1091fa8c418SNickeau         */
1101fa8c418SNickeau        $controller->register_hook('INIT_LANG_LOAD', 'BEFORE', $this, 'deleteVaryFromStaticGeneratedResources', array());
1111fa8c418SNickeau
112*c3437056SNickeau        /**
113*c3437056SNickeau         * To delete sidebar (cache) cache when a page was modified in a namespace
114*c3437056SNickeau         * https://combostrap.com/sideslots
115*c3437056SNickeau         */
116*c3437056SNickeau        $controller->register_hook(MetadataDokuWikiStore::PAGE_METADATA_MUTATION_EVENT, 'AFTER', $this, 'sideSlotsCacheBurstingForMetadataMutation', array());
117*c3437056SNickeau        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'sideSlotsCacheBurstingForPageCreationAndDeletion', array());
1181fa8c418SNickeau
11937748cd8SNickeau    }
12037748cd8SNickeau
12137748cd8SNickeau    /**
12237748cd8SNickeau     *
12337748cd8SNickeau     * @param Doku_Event $event
12437748cd8SNickeau     * @param $params
12537748cd8SNickeau     */
12637748cd8SNickeau    function logCacheUsage(Doku_Event $event, $params)
12737748cd8SNickeau    {
12837748cd8SNickeau
12937748cd8SNickeau        /**
13037748cd8SNickeau         * To log the cache used by bar
13137748cd8SNickeau         * @var \dokuwiki\Cache\CacheParser $data
13237748cd8SNickeau         */
13337748cd8SNickeau        $data = $event->data;
13437748cd8SNickeau        $result = $event->result;
135*c3437056SNickeau        $slotId = $data->page;
13637748cd8SNickeau        $cacheManager = PluginUtility::getCacheManager();
137*c3437056SNickeau        $cacheManager->addSlotForRequestedPage($slotId, $result, $data);
13837748cd8SNickeau
13937748cd8SNickeau
14037748cd8SNickeau    }
14137748cd8SNickeau
14237748cd8SNickeau    /**
14337748cd8SNickeau     *
144*c3437056SNickeau     * Purge the cache if needed
14537748cd8SNickeau     * @param Doku_Event $event
14637748cd8SNickeau     * @param $params
14737748cd8SNickeau     */
148*c3437056SNickeau    function pageCacheExpiration(Doku_Event $event, $params)
14937748cd8SNickeau    {
15037748cd8SNickeau
15137748cd8SNickeau        /**
15237748cd8SNickeau         * No cache for all mode
15337748cd8SNickeau         * (ie xhtml, instruction)
15437748cd8SNickeau         */
15537748cd8SNickeau        $data = &$event->data;
15637748cd8SNickeau        $pageId = $data->page;
1571fa8c418SNickeau
1581fa8c418SNickeau        /**
159*c3437056SNickeau         * For whatever reason, the cache file of XHTML
1601fa8c418SNickeau         * may be empty - No error found on the web server or the log.
1611fa8c418SNickeau         *
1621fa8c418SNickeau         * We just delete it then.
1631fa8c418SNickeau         *
1641fa8c418SNickeau         * It has been seen after the creation of a new page or a `move` of the page.
1651fa8c418SNickeau         */
1661fa8c418SNickeau        if ($data instanceof CacheRenderer) {
1671fa8c418SNickeau            if ($data->mode === "xhtml") {
1681fa8c418SNickeau                if (file_exists($data->cache)) {
1691fa8c418SNickeau                    if (filesize($data->cache) === 0) {
1701fa8c418SNickeau                        $data->depends["purge"] = true;
1711fa8c418SNickeau                    }
1721fa8c418SNickeau                }
1731fa8c418SNickeau            }
1741fa8c418SNickeau        }
17537748cd8SNickeau        /**
17637748cd8SNickeau         * Because of the recursive nature of rendering
17737748cd8SNickeau         * inside dokuwiki, we just handle the first
17837748cd8SNickeau         * rendering for a request.
17937748cd8SNickeau         *
18037748cd8SNickeau         * The first will be purged, the other one not
181*c3437056SNickeau         * because they can't use the first one
18237748cd8SNickeau         */
183*c3437056SNickeau        if (!PluginUtility::getCacheManager()->isCacheLogPresentForSlot($pageId, $data->mode)) {
184*c3437056SNickeau            $page = Page::createPageFromId($pageId);
185*c3437056SNickeau            $cacheExpirationFrequency = $page->getCacheExpirationFrequency();
186*c3437056SNickeau            if ($cacheExpirationFrequency === null) {
187*c3437056SNickeau                return;
188*c3437056SNickeau            }
18937748cd8SNickeau
190*c3437056SNickeau            $expirationDate = CacheExpirationDate::createForPage($page)
191*c3437056SNickeau                ->getValue();
192*c3437056SNickeau
193*c3437056SNickeau            if ($expirationDate === null) {
194*c3437056SNickeau                try {
195*c3437056SNickeau                    $expirationDate = Cron::getDate($cacheExpirationFrequency);
196*c3437056SNickeau                    $page->setCacheExpirationDate($expirationDate);
197*c3437056SNickeau                } catch (ExceptionCombo $e) {
198*c3437056SNickeau                    LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a valid cron expression");
199*c3437056SNickeau                }
200*c3437056SNickeau            }
201*c3437056SNickeau            if ($expirationDate !== null) {
202*c3437056SNickeau
20337748cd8SNickeau                $actualDate = new DateTime();
20437748cd8SNickeau                if ($expirationDate < $actualDate) {
20537748cd8SNickeau                    /**
20637748cd8SNickeau                     * As seen in {@link Cache::makeDefaultCacheDecision()}
20737748cd8SNickeau                     * We request a purge
20837748cd8SNickeau                     */
20937748cd8SNickeau                    $data->depends["purge"] = true;
210*c3437056SNickeau
211*c3437056SNickeau                    /**
212*c3437056SNickeau                     * Calculate a new expiration date
213*c3437056SNickeau                     */
214*c3437056SNickeau                    try {
215*c3437056SNickeau                        $newDate = Cron::getDate($cacheExpirationFrequency);
216*c3437056SNickeau                        if ($newDate < $actualDate) {
217*c3437056SNickeau                            LogUtility::msg("The new calculated date cache expiration frequency ({$newDate->format(Iso8601Date::getFormat())}) is lower than the current date ({$actualDate->format(Iso8601Date::getFormat())})");
218*c3437056SNickeau                        }
219*c3437056SNickeau                        $page->setCacheExpirationDate($newDate);
220*c3437056SNickeau                    } catch (ExceptionCombo $e) {
221*c3437056SNickeau                        LogUtility::msg("The cache expiration frequency ($cacheExpirationFrequency) is not a value cron expression");
222*c3437056SNickeau                    }
22337748cd8SNickeau                }
22437748cd8SNickeau            }
22537748cd8SNickeau        }
22637748cd8SNickeau
22737748cd8SNickeau
22837748cd8SNickeau    }
22937748cd8SNickeau
23037748cd8SNickeau    /**
23137748cd8SNickeau     * Add HTML meta to be able to debug
23237748cd8SNickeau     * @param Doku_Event $event
23337748cd8SNickeau     * @param $params
23437748cd8SNickeau     */
235*c3437056SNickeau    function addCacheLogHtmlDataBlock(Doku_Event $event, $params)
23637748cd8SNickeau    {
23737748cd8SNickeau
23837748cd8SNickeau        $cacheManager = PluginUtility::getCacheManager();
239*c3437056SNickeau        $cacheSlotResults = $cacheManager->getCacheSlotResultsAsHtmlDataBlockArray();
240*c3437056SNickeau        $cacheJson = \ComboStrap\Json::createFromArray($cacheSlotResults);
24137748cd8SNickeau
242*c3437056SNickeau        if (PluginUtility::isDevOrTest()) {
243*c3437056SNickeau            $result = $cacheJson->toPrettyJsonString();
24437748cd8SNickeau        } else {
245*c3437056SNickeau            $result = $cacheJson->toMinifiedJsonString();
24637748cd8SNickeau        }
24737748cd8SNickeau
248*c3437056SNickeau        $event->data["script"][] = array(
249*c3437056SNickeau            "type" => CacheManager::APPLICATION_COMBO_CACHE_JSON,
250*c3437056SNickeau            "_data" => $result,
251*c3437056SNickeau        );
25237748cd8SNickeau
25337748cd8SNickeau    }
25437748cd8SNickeau
25537748cd8SNickeau    function close(Doku_Event $event, $params)
25637748cd8SNickeau    {
257*c3437056SNickeau        CacheManager::reset();
2581fa8c418SNickeau    }
2591fa8c418SNickeau
2601fa8c418SNickeau
2611fa8c418SNickeau    /**
2621fa8c418SNickeau     * Delete the Vary header
2631fa8c418SNickeau     * @param Doku_Event $event
2641fa8c418SNickeau     * @param $params
2651fa8c418SNickeau     */
2661fa8c418SNickeau    public static function deleteVaryFromStaticGeneratedResources(Doku_Event $event, $params)
2671fa8c418SNickeau    {
2681fa8c418SNickeau
2691fa8c418SNickeau        $script = $_SERVER["SCRIPT_NAME"];
2701fa8c418SNickeau        if (in_array($script, self::STATIC_SCRIPT_NAMES)) {
271*c3437056SNickeau            // To be extra sure, they must have the buster key
272*c3437056SNickeau            if (isset($_REQUEST[CacheMedia::CACHE_BUSTER_KEY])) {
2731fa8c418SNickeau                self::deleteVaryHeader();
2741fa8c418SNickeau            }
2751fa8c418SNickeau        }
2761fa8c418SNickeau
2771fa8c418SNickeau    }
2781fa8c418SNickeau
2791fa8c418SNickeau    /**
2801fa8c418SNickeau     *
2811fa8c418SNickeau     * No Vary: Cookie
2821fa8c418SNickeau     * Introduced at
2831fa8c418SNickeau     * https://github.com/splitbrain/dokuwiki/issues/1594
2841fa8c418SNickeau     * But cache problem at:
2851fa8c418SNickeau     * https://github.com/splitbrain/dokuwiki/issues/2520
2861fa8c418SNickeau     *
2871fa8c418SNickeau     */
2881fa8c418SNickeau    public static function deleteVaryHeader(): void
2891fa8c418SNickeau    {
290*c3437056SNickeau        if (PluginUtility::getConfValue(action_plugin_combo_staticresource::CONF_STATIC_CACHE_ENABLED, 1)) {
2911fa8c418SNickeau            Http::removeHeaderIfPresent("Vary");
2921fa8c418SNickeau        }
2931fa8c418SNickeau    }
2941fa8c418SNickeau
295*c3437056SNickeau    function sideSlotsCacheBurstingForMetadataMutation($event)
296*c3437056SNickeau    {
297*c3437056SNickeau
298*c3437056SNickeau        $data = $event->data;
299*c3437056SNickeau        /**
300*c3437056SNickeau         * The side slot cache is deleted only when the
301*c3437056SNickeau         * below property are updated
302*c3437056SNickeau         */
303*c3437056SNickeau        $descriptionProperties = [PageTitle::PROPERTY_NAME, ResourceName::PROPERTY_NAME, PageH1::PROPERTY_NAME, PageDescription::DESCRIPTION_PROPERTY];
304*c3437056SNickeau        if (!in_array($data["name"], $descriptionProperties)) return;
305*c3437056SNickeau
306*c3437056SNickeau        self::removeSideSlotCache();
307*c3437056SNickeau
308*c3437056SNickeau    }
309*c3437056SNickeau
310*c3437056SNickeau    /**
311*c3437056SNickeau     * @param $event
312*c3437056SNickeau     * @throws Exception
313*c3437056SNickeau     * @link https://www.dokuwiki.org/devel:event:io_wikipage_write
314*c3437056SNickeau     */
315*c3437056SNickeau    function sideSlotsCacheBurstingForPageCreationAndDeletion($event)
316*c3437056SNickeau    {
317*c3437056SNickeau
318*c3437056SNickeau        $data = $event->data;
319*c3437056SNickeau        $pageName = $data[2];
320*c3437056SNickeau
321*c3437056SNickeau        /**
322*c3437056SNickeau         * Modification to the side slot is not processed further
323*c3437056SNickeau         */
324*c3437056SNickeau        if (in_array($pageName, self::getSideSlotNames())) return;
325*c3437056SNickeau
326*c3437056SNickeau        /**
327*c3437056SNickeau         * Pointer to see if we need to delete the cache
328*c3437056SNickeau         */
329*c3437056SNickeau        $doWeNeedToDeleteTheSideSlotCache = false;
330*c3437056SNickeau
331*c3437056SNickeau        /**
332*c3437056SNickeau         * File creation
333*c3437056SNickeau         *
334*c3437056SNickeau         * ```
335*c3437056SNickeau         * Page creation may be detected by checking if the file already exists and the revision is false.
336*c3437056SNickeau         * ```
337*c3437056SNickeau         * From https://www.dokuwiki.org/devel:event:io_wikipage_write
338*c3437056SNickeau         *
339*c3437056SNickeau         */
340*c3437056SNickeau        $rev = $data[3];
341*c3437056SNickeau        $filePath = $data[0][0];
342*c3437056SNickeau        $file = File::createFromPath($filePath);
343*c3437056SNickeau        if (!$file->exists() && $rev === false) {
344*c3437056SNickeau            $doWeNeedToDeleteTheSideSlotCache = true;
345*c3437056SNickeau        }
346*c3437056SNickeau
347*c3437056SNickeau        /**
348*c3437056SNickeau         * File deletion
349*c3437056SNickeau         * (No content)
350*c3437056SNickeau         *
351*c3437056SNickeau         * ```
352*c3437056SNickeau         * Page deletion may be detected by checking for empty page content.
353*c3437056SNickeau         * On update to an existing page this event is called twice, once for the transfer of the old version to the attic (rev will have a value)
354*c3437056SNickeau         * and once to write the new version of the page into the wiki (rev is false)
355*c3437056SNickeau         * ```
356*c3437056SNickeau         * From https://www.dokuwiki.org/devel:event:io_wikipage_write
357*c3437056SNickeau         */
358*c3437056SNickeau        $append = $data[0][2];
359*c3437056SNickeau        if (!$append) {
360*c3437056SNickeau
361*c3437056SNickeau            $content = $data[0][1];
362*c3437056SNickeau            if (empty($content) && $rev === false) {
363*c3437056SNickeau                // Deletion
364*c3437056SNickeau                $doWeNeedToDeleteTheSideSlotCache = true;
365*c3437056SNickeau            }
366*c3437056SNickeau
367*c3437056SNickeau        }
368*c3437056SNickeau
369*c3437056SNickeau        if ($doWeNeedToDeleteTheSideSlotCache) self::removeSideSlotCache();
370*c3437056SNickeau
371*c3437056SNickeau    }
37237748cd8SNickeau
37337748cd8SNickeau}
374