xref: /plugin/combo/ComboStrap/MarkupCacheDependencies.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1*04fd306cSNickeau<?php
2*04fd306cSNickeau
3*04fd306cSNickeau
4*04fd306cSNickeaunamespace ComboStrap;
5*04fd306cSNickeau
6*04fd306cSNickeauuse dokuwiki\Cache\CacheParser;
7*04fd306cSNickeauuse dokuwiki\Cache\CacheRenderer;
8*04fd306cSNickeauuse splitbrain\slika\Exception;
9*04fd306cSNickeau
10*04fd306cSNickeau/**
11*04fd306cSNickeau *
12*04fd306cSNickeau * @package ComboStrap
13*04fd306cSNickeau *
14*04fd306cSNickeau * Manage the cache dependencies for a slot level (not instructions cache).
15*04fd306cSNickeau *
16*04fd306cSNickeau * The dependencies are stored on a file system.
17*04fd306cSNickeau *
18*04fd306cSNickeau * Cache dependencies are used:
19*04fd306cSNickeau *   * to generate the cache key output
20*04fd306cSNickeau *   * to add cache validity dependency such as requested page,
21*04fd306cSNickeau *
22*04fd306cSNickeau * For cache key generation, this is mostly used on
23*04fd306cSNickeau *   * side slots to have several output of a list {@link \syntax_plugin_combo_pageexplorer navigation pane} for different namespace (ie there is one cache by namespace)
24*04fd306cSNickeau *   * header and footer main slot to have one output for each requested main page
25*04fd306cSNickeau */
26*04fd306cSNickeauclass MarkupCacheDependencies
27*04fd306cSNickeau{
28*04fd306cSNickeau    /**
29*04fd306cSNickeau     * The dependency value is the requested page path
30*04fd306cSNickeau     * (used for syntax mostly used in the header and footer of the main slot for instance)
31*04fd306cSNickeau     */
32*04fd306cSNickeau    public const REQUESTED_PAGE_DEPENDENCY = "requested_page";
33*04fd306cSNickeau    /**
34*04fd306cSNickeau     * The special scope value current means the namespace of the requested page
35*04fd306cSNickeau     * The real scope value is then calculated before retrieving the cache
36*04fd306cSNickeau     */
37*04fd306cSNickeau    public const REQUESTED_NAMESPACE_DEPENDENCY = "requested_namespace";
38*04fd306cSNickeau    /**
39*04fd306cSNickeau     * @deprecated use the {@link MarkupCacheDependencies::REQUESTED_NAMESPACE_DEPENDENCY}
40*04fd306cSNickeau     */
41*04fd306cSNickeau    public const NAMESPACE_OLD_VALUE = "current";
42*04fd306cSNickeau
43*04fd306cSNickeau    /**
44*04fd306cSNickeau     * This dependencies have an impact on the
45*04fd306cSNickeau     * output location of the cache
46*04fd306cSNickeau     * {@link MarkupCacheDependencies::getOrCalculateDependencyKey()}
47*04fd306cSNickeau     */
48*04fd306cSNickeau    public const OUTPUT_DEPENDENCIES = [self::REQUESTED_PAGE_DEPENDENCY, self::REQUESTED_NAMESPACE_DEPENDENCY];
49*04fd306cSNickeau
50*04fd306cSNickeau    /**
51*04fd306cSNickeau     * This dependencies have an impact on the freshness
52*04fd306cSNickeau     * of the cache
53*04fd306cSNickeau     */
54*04fd306cSNickeau    public const validityDependencies = [
55*04fd306cSNickeau        self::BACKLINKS_DEPENDENCY,
56*04fd306cSNickeau        self::SQL_DEPENDENCY,
57*04fd306cSNickeau        self::PAGE_PRIMARY_META_DEPENDENCY,
58*04fd306cSNickeau        self::PAGE_SYSTEM_DEPENDENCY
59*04fd306cSNickeau    ];
60*04fd306cSNickeau
61*04fd306cSNickeau    /**
62*04fd306cSNickeau     * Backlinks are printed in the page
63*04fd306cSNickeau     * {@link \action_plugin_combo_backlinkmutation}
64*04fd306cSNickeau     * If a referent page add or delete a link,
65*04fd306cSNickeau     * the slot should be refreshed / cache should be deleted
66*04fd306cSNickeau     */
67*04fd306cSNickeau    const BACKLINKS_DEPENDENCY = "backlinks";
68*04fd306cSNickeau
69*04fd306cSNickeau    /**
70*04fd306cSNickeau     * A page sql is in the page
71*04fd306cSNickeau     * (The page should be refreshed by default once a day)
72*04fd306cSNickeau     */
73*04fd306cSNickeau    const SQL_DEPENDENCY = "sql";
74*04fd306cSNickeau
75*04fd306cSNickeau    /**
76*04fd306cSNickeau     * If the name, the title, the h1 or the description
77*04fd306cSNickeau     * of a page changes, the cache should be invalidated
78*04fd306cSNickeau     * See {@link \action_plugin_combo_pageprimarymetamutation}
79*04fd306cSNickeau     */
80*04fd306cSNickeau    const PAGE_PRIMARY_META_DEPENDENCY = "page_primary_meta";
81*04fd306cSNickeau
82*04fd306cSNickeau    /**
83*04fd306cSNickeau     * If a page is added or deleted
84*04fd306cSNickeau     * See {@link \action_plugin_combo_pagesystemmutation}
85*04fd306cSNickeau     */
86*04fd306cSNickeau    const PAGE_SYSTEM_DEPENDENCY = "page_system";
87*04fd306cSNickeau
88*04fd306cSNickeau    const CANONICAL = "cache:dependency";
89*04fd306cSNickeau
90*04fd306cSNickeau
91*04fd306cSNickeau    /**
92*04fd306cSNickeau     * @var CacheParser
93*04fd306cSNickeau     */
94*04fd306cSNickeau    private $dependenciesCacheStore;
95*04fd306cSNickeau
96*04fd306cSNickeau
97*04fd306cSNickeau    /**
98*04fd306cSNickeau     * @var array list of dependencies to calculate the cache key
99*04fd306cSNickeau     *
100*04fd306cSNickeau     * In a general pattern, a dependency is a series of function that would output runtime data
101*04fd306cSNickeau     * that should go into the render cache key such as user logged in, requested page, namespace of the requested page, ...
102*04fd306cSNickeau     *
103*04fd306cSNickeau     * The cache dependencies data are saved alongside the page (same as snippets)
104*04fd306cSNickeau     *
105*04fd306cSNickeau     */
106*04fd306cSNickeau    private $runtimeAddedDependencies = null;
107*04fd306cSNickeau    /**
108*04fd306cSNickeau     * The stored runtime dependencies
109*04fd306cSNickeau     * @var array
110*04fd306cSNickeau     */
111*04fd306cSNickeau    private $runtimeStoreDependencies;
112*04fd306cSNickeau
113*04fd306cSNickeau
114*04fd306cSNickeau    /**
115*04fd306cSNickeau     * @var string the first key captured
116*04fd306cSNickeau     */
117*04fd306cSNickeau    private $firstActualKey;
118*04fd306cSNickeau    private FetcherMarkup $markupFetcher;
119*04fd306cSNickeau
120*04fd306cSNickeau
121*04fd306cSNickeau    /**
122*04fd306cSNickeau     * CacheManagerForSlot constructor.
123*04fd306cSNickeau     *
124*04fd306cSNickeau     */
125*04fd306cSNickeau    private function __construct(FetcherMarkup $markupFetcher)
126*04fd306cSNickeau    {
127*04fd306cSNickeau
128*04fd306cSNickeau        $this->markupFetcher = $markupFetcher;
129*04fd306cSNickeau        $executingPath = $markupFetcher->getExecutingPathOrNull();
130*04fd306cSNickeau        if ($executingPath !== null) {
131*04fd306cSNickeau            $data = $this->getDependenciesCacheStore()->retrieveCache();
132*04fd306cSNickeau            if (!empty($data)) {
133*04fd306cSNickeau                $this->runtimeStoreDependencies = json_decode($data, true);
134*04fd306cSNickeau            }
135*04fd306cSNickeau        }
136*04fd306cSNickeau
137*04fd306cSNickeau    }
138*04fd306cSNickeau
139*04fd306cSNickeau    public static function create(FetcherMarkup $fetcherMarkup): MarkupCacheDependencies
140*04fd306cSNickeau    {
141*04fd306cSNickeau        return new MarkupCacheDependencies($fetcherMarkup);
142*04fd306cSNickeau    }
143*04fd306cSNickeau
144*04fd306cSNickeau    /**
145*04fd306cSNickeau     * Rerender for now only the secondary slot if it has cache dependency
146*04fd306cSNickeau     * (ie {@link MarkupCacheDependencies::PAGE_SYSTEM_DEPENDENCY} or {@link MarkupCacheDependencies::PAGE_PRIMARY_META_DEPENDENCY})
147*04fd306cSNickeau     * @param $pathAddedOrDeleted
148*04fd306cSNickeau     * @param string $dependency -  a {@link MarkupCacheDependencies} ie
149*04fd306cSNickeau     * @param string $event
150*04fd306cSNickeau     */
151*04fd306cSNickeau    public static function reRenderSideSlotIfNeeded($pathAddedOrDeleted, string $dependency, string $event)
152*04fd306cSNickeau    {
153*04fd306cSNickeau
154*04fd306cSNickeau        /**
155*04fd306cSNickeau         * Rerender secondary slot if needed
156*04fd306cSNickeau         */
157*04fd306cSNickeau        $page = MarkupPath::createMarkupFromStringPath($pathAddedOrDeleted);
158*04fd306cSNickeau        $pageWikiPath = $page->getPathObject();
159*04fd306cSNickeau        if (!($pageWikiPath instanceof WikiPath)) {
160*04fd306cSNickeau            LogUtility::errorIfDevOrTest("The path should be a wiki path");
161*04fd306cSNickeau            return;
162*04fd306cSNickeau        }
163*04fd306cSNickeau        $slots = [$page->getSideSlot()];
164*04fd306cSNickeau        foreach ($slots as $slot) {
165*04fd306cSNickeau            if ($slot === null) {
166*04fd306cSNickeau                continue;
167*04fd306cSNickeau            }
168*04fd306cSNickeau            try {
169*04fd306cSNickeau                $slotFetcher = FetcherMarkup::confRoot()
170*04fd306cSNickeau                    ->setRequestedMimeToXhtml()
171*04fd306cSNickeau                    ->setRequestedContextPath($pageWikiPath)
172*04fd306cSNickeau                    ->setRequestedExecutingPath($slot)
173*04fd306cSNickeau                    ->build();
174*04fd306cSNickeau            } catch (ExceptionNotExists $e) {
175*04fd306cSNickeau                // layout fragment does not exists
176*04fd306cSNickeau                continue;
177*04fd306cSNickeau            }
178*04fd306cSNickeau            $cacheDependencies = $slotFetcher->getOutputCacheDependencies();
179*04fd306cSNickeau            if ($cacheDependencies->hasDependency($dependency)) {
180*04fd306cSNickeau                $link = PluginUtility::getDocumentationHyperLink("cache:slot", "Slot Dependency", false);
181*04fd306cSNickeau                $message = "$link ($dependency) was met with the primary slot ($pathAddedOrDeleted).";
182*04fd306cSNickeau                CacheLog::deleteCacheIfExistsAndLog(
183*04fd306cSNickeau                    $slotFetcher,
184*04fd306cSNickeau                    $event,
185*04fd306cSNickeau                    $message
186*04fd306cSNickeau                );
187*04fd306cSNickeau                CacheLog::renderCacheAndLog(
188*04fd306cSNickeau                    $slotFetcher,
189*04fd306cSNickeau                    $event,
190*04fd306cSNickeau                    $message
191*04fd306cSNickeau                );
192*04fd306cSNickeau            }
193*04fd306cSNickeau
194*04fd306cSNickeau        }
195*04fd306cSNickeau
196*04fd306cSNickeau    }
197*04fd306cSNickeau
198*04fd306cSNickeau
199*04fd306cSNickeau    /**
200*04fd306cSNickeau     * @return string - output the namespace used in the cache key
201*04fd306cSNickeau     *
202*04fd306cSNickeau     * For example:
203*04fd306cSNickeau     *   * the ':sidebar' html output may be dependent to the namespace `ns` or `ns2`
204*04fd306cSNickeau     */
205*04fd306cSNickeau    public function getValueForKey($dependenciesValue): string
206*04fd306cSNickeau    {
207*04fd306cSNickeau
208*04fd306cSNickeau        /**
209*04fd306cSNickeau         * Set the logical id
210*04fd306cSNickeau         * When no $ID is set (for instance, test),
211*04fd306cSNickeau         * the logical id is the id
212*04fd306cSNickeau         *
213*04fd306cSNickeau         * The logical id depends on the namespace attribute of the {@link \syntax_plugin_combo_pageexplorer}
214*04fd306cSNickeau         * stored in the `scope` metadata.
215*04fd306cSNickeau         *
216*04fd306cSNickeau         * Scope is directory/namespace based
217*04fd306cSNickeau         */
218*04fd306cSNickeau        $path = $this->markupFetcher->getRequestedContextPath();
219*04fd306cSNickeau        $requestedPage = MarkupPath::createPageFromPathObject($path);
220*04fd306cSNickeau        switch ($dependenciesValue) {
221*04fd306cSNickeau            case MarkupCacheDependencies::NAMESPACE_OLD_VALUE:
222*04fd306cSNickeau            case MarkupCacheDependencies::REQUESTED_NAMESPACE_DEPENDENCY:
223*04fd306cSNickeau                try {
224*04fd306cSNickeau                    $parentPath = $requestedPage->getPathObject()->getParent();
225*04fd306cSNickeau                    return $parentPath->toAbsoluteId();
226*04fd306cSNickeau                } catch (ExceptionNotFound $e) {
227*04fd306cSNickeau                    // root
228*04fd306cSNickeau                    return ":";
229*04fd306cSNickeau                }
230*04fd306cSNickeau            case MarkupCacheDependencies::REQUESTED_PAGE_DEPENDENCY:
231*04fd306cSNickeau                return $requestedPage->getPathObject()->toAbsoluteId();
232*04fd306cSNickeau            default:
233*04fd306cSNickeau                throw new ExceptionRuntimeInternal("The requested dependency value ($dependenciesValue) has no calculation");
234*04fd306cSNickeau        }
235*04fd306cSNickeau
236*04fd306cSNickeau
237*04fd306cSNickeau    }
238*04fd306cSNickeau
239*04fd306cSNickeau    /**
240*04fd306cSNickeau     * @return string
241*04fd306cSNickeau     *
242*04fd306cSNickeau     * Cache is now managed by dependencies function that creates a unique key
243*04fd306cSNickeau     * for the instruction document and the output document
244*04fd306cSNickeau     *
245*04fd306cSNickeau     * See the discussion at: https://github.com/splitbrain/dokuwiki/issues/3496
246*04fd306cSNickeau     * @throws ExceptionCompile
247*04fd306cSNickeau     * @var $actualKey
248*04fd306cSNickeau     */
249*04fd306cSNickeau    public function getOrCalculateDependencyKey($actualKey): string
250*04fd306cSNickeau    {
251*04fd306cSNickeau        /**
252*04fd306cSNickeau         * We should wrap a call only once
253*04fd306cSNickeau         * We capture therefore the first actual key passed
254*04fd306cSNickeau         */
255*04fd306cSNickeau        if ($this->firstActualKey === null) {
256*04fd306cSNickeau            $this->firstActualKey = $actualKey;
257*04fd306cSNickeau        }
258*04fd306cSNickeau        $dependencyKey = $this->firstActualKey;
259*04fd306cSNickeau        $runtimeDependencies = $this->getDependencies();
260*04fd306cSNickeau
261*04fd306cSNickeau        foreach ($runtimeDependencies as $dependency) {
262*04fd306cSNickeau            if (in_array($dependency, self::OUTPUT_DEPENDENCIES)) {
263*04fd306cSNickeau                $dependencyKey .= $this->getValueForKey($dependency);
264*04fd306cSNickeau            }
265*04fd306cSNickeau        }
266*04fd306cSNickeau        return $dependencyKey;
267*04fd306cSNickeau    }
268*04fd306cSNickeau
269*04fd306cSNickeau
270*04fd306cSNickeau    /**
271*04fd306cSNickeau     * @param string $dependencyName
272*04fd306cSNickeau     * @return MarkupCacheDependencies
273*04fd306cSNickeau     */
274*04fd306cSNickeau    public function addDependency(string $dependencyName): MarkupCacheDependencies
275*04fd306cSNickeau    {
276*04fd306cSNickeau        if (PluginUtility::isDevOrTest()) {
277*04fd306cSNickeau            if (!in_array($dependencyName, self::OUTPUT_DEPENDENCIES) &&
278*04fd306cSNickeau                !in_array($dependencyName, self::validityDependencies)
279*04fd306cSNickeau            ) {
280*04fd306cSNickeau                throw new ExceptionRuntime("Unknown dependency value ($dependencyName)");
281*04fd306cSNickeau            }
282*04fd306cSNickeau        }
283*04fd306cSNickeau        $this->runtimeAddedDependencies[$dependencyName] = "";
284*04fd306cSNickeau        return $this;
285*04fd306cSNickeau    }
286*04fd306cSNickeau
287*04fd306cSNickeau    public
288*04fd306cSNickeau    function getDependencies(): array
289*04fd306cSNickeau    {
290*04fd306cSNickeau        if ($this->runtimeAddedDependencies !== null) {
291*04fd306cSNickeau            return array_keys($this->runtimeAddedDependencies);
292*04fd306cSNickeau        }
293*04fd306cSNickeau        if ($this->runtimeStoreDependencies === null) {
294*04fd306cSNickeau            return [];
295*04fd306cSNickeau        }
296*04fd306cSNickeau        return array_keys($this->runtimeStoreDependencies);
297*04fd306cSNickeau    }
298*04fd306cSNickeau
299*04fd306cSNickeau    /**
300*04fd306cSNickeau     * The default key as seen in {@link CacheParser}
301*04fd306cSNickeau     * Used for test purpose
302*04fd306cSNickeau     * @return string
303*04fd306cSNickeau     */
304*04fd306cSNickeau    public
305*04fd306cSNickeau    function getDefaultKey(): string
306*04fd306cSNickeau    {
307*04fd306cSNickeau        try {
308*04fd306cSNickeau            try {
309*04fd306cSNickeau                // dokuwiki cache key compatible
310*04fd306cSNickeau                $wikiId = $this->markupFetcher->getRequestedExecutingPath()->toWikiPath()->getWikiId();
311*04fd306cSNickeau                $absoluteString = wikiFN($wikiId);
312*04fd306cSNickeau            } catch (ExceptionCast|ExceptionNotFound $e) {
313*04fd306cSNickeau                $absoluteString = $this->markupFetcher->getRequestedExecutingPath()->toAbsoluteId();
314*04fd306cSNickeau            }
315*04fd306cSNickeau            $keyDokuWikiCompliant = str_replace("\\", "/", $absoluteString);
316*04fd306cSNickeau            return $keyDokuWikiCompliant . $_SERVER['HTTP_HOST'] . $_SERVER['SERVER_PORT'];
317*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
318*04fd306cSNickeau            throw new ExceptionRuntimeInternal("No executing path to calculate the cache key");
319*04fd306cSNickeau        }
320*04fd306cSNickeau
321*04fd306cSNickeau    }
322*04fd306cSNickeau
323*04fd306cSNickeau    /**
324*04fd306cSNickeau     * Snippet.json, Cache dependency are data dependent
325*04fd306cSNickeau     *
326*04fd306cSNickeau     * For instance, the carrousel may add glide or grid as snippet. It depends on the the number of backlinks.
327*04fd306cSNickeau     *
328*04fd306cSNickeau     * Therefore the output should be unique by rendered fragment
329*04fd306cSNickeau     * Therefore we reroute (recalculate the cache key to be the same than the html file)
330*04fd306cSNickeau     *
331*04fd306cSNickeau     * @param CacheParser $cache
332*04fd306cSNickeau     * @return void
333*04fd306cSNickeau     */
334*04fd306cSNickeau    public
335*04fd306cSNickeau    function rerouteCacheDestination(CacheParser &$cache)
336*04fd306cSNickeau    {
337*04fd306cSNickeau
338*04fd306cSNickeau        try {
339*04fd306cSNickeau
340*04fd306cSNickeau            $cache->key = $this->getOrCalculateDependencyKey($cache->key);
341*04fd306cSNickeau            $cache->cache = getCacheName($cache->key, '.' . $cache->mode);
342*04fd306cSNickeau
343*04fd306cSNickeau        } catch (ExceptionCompile $e) {
344*04fd306cSNickeau            LogUtility::msg("Error while trying to reroute the content cache destination for the fetcher ({$this->markupFetcher}). You may have cache problem. Error: {$e->getMessage()}");
345*04fd306cSNickeau        }
346*04fd306cSNickeau
347*04fd306cSNickeau    }
348*04fd306cSNickeau
349*04fd306cSNickeau
350*04fd306cSNickeau    /**
351*04fd306cSNickeau     */
352*04fd306cSNickeau    public function storeDependencies()
353*04fd306cSNickeau    {
354*04fd306cSNickeau
355*04fd306cSNickeau        /**
356*04fd306cSNickeau         * Cache file
357*04fd306cSNickeau         * Using a cache parser, set the page id and will trigger
358*04fd306cSNickeau         * the parser cache use event in order to log/report the cache usage
359*04fd306cSNickeau         * At {@link action_plugin_combo_cache::createCacheReport()}
360*04fd306cSNickeau         */
361*04fd306cSNickeau        $dependencies = $this->getDependenciesCacheStore();
362*04fd306cSNickeau        $deps = $this->runtimeAddedDependencies;
363*04fd306cSNickeau        if ($deps !== null) {
364*04fd306cSNickeau            $jsonDeps = json_encode($deps);
365*04fd306cSNickeau            $dependencies->storeCache($jsonDeps);
366*04fd306cSNickeau        } else {
367*04fd306cSNickeau            // dependencies does not exist or were removed
368*04fd306cSNickeau            $dependencies->removeCache();
369*04fd306cSNickeau        }
370*04fd306cSNickeau
371*04fd306cSNickeau
372*04fd306cSNickeau    }
373*04fd306cSNickeau
374*04fd306cSNickeau    public
375*04fd306cSNickeau    function getDependenciesCacheStore(): CacheParser
376*04fd306cSNickeau    {
377*04fd306cSNickeau        if ($this->dependenciesCacheStore !== null) {
378*04fd306cSNickeau            return $this->dependenciesCacheStore;
379*04fd306cSNickeau        }
380*04fd306cSNickeau
381*04fd306cSNickeau        /**
382*04fd306cSNickeau         * The local path to calculate the full qualified Os path
383*04fd306cSNickeau         */
384*04fd306cSNickeau        try {
385*04fd306cSNickeau            $executingPath = $this->markupFetcher->getRequestedExecutingPath();
386*04fd306cSNickeau        } catch (ExceptionNotFound $e) {
387*04fd306cSNickeau            throw new ExceptionRuntimeInternal("There is no executing path, you can create a cache dependencies store", self::CANONICAL);
388*04fd306cSNickeau        }
389*04fd306cSNickeau
390*04fd306cSNickeau        list($wikiId, $localPath) = FetcherMarkupBuilder::getWikiIdAndLocalFileDokuwikiCompliant($executingPath);
391*04fd306cSNickeau        $this->dependenciesCacheStore = new CacheParser($wikiId, $localPath, "deps.json");
392*04fd306cSNickeau        return $this->dependenciesCacheStore;
393*04fd306cSNickeau    }
394*04fd306cSNickeau
395*04fd306cSNickeau    public
396*04fd306cSNickeau    function hasDependency(string $dependencyName): bool
397*04fd306cSNickeau    {
398*04fd306cSNickeau        $dependencies = $this->getDependencies();
399*04fd306cSNickeau        return in_array($dependencyName, $dependencies);
400*04fd306cSNickeau    }
401*04fd306cSNickeau
402*04fd306cSNickeau}
403