1<?php
2
3namespace ComboStrap;
4
5use dokuwiki\Cache\CacheInstructions;
6use dokuwiki\Cache\CacheParser;
7use dokuwiki\Cache\CacheRenderer;
8
9/**
10 * Builder class for {@link FetcherMarkup}
11 * Php does not allow for nested class
12 * We therefore need to get the builder class out.
13 *
14 * We extends just to get access to protected members class
15 * and to mimic a builder pattern
16 *
17 * @internal
18 */
19class FetcherMarkupBuilder
20{
21
22    /**
23     * Private are they may be null
24     */
25    private ?string $builderMarkupString = null;
26    private ?Path $builderMarkupSourcePath = null;
27    private ?array $builderRequestedInstructions = null;
28
29    protected WikiPath $requestedContextPath;
30    protected Mime $mime;
31    protected bool $deleteRootBlockElement = false;
32    protected string $rendererName;
33
34
35    protected bool $isDoc;
36    protected array $builderContextData;
37    private bool $isCodeStandAloneExecution = false;
38    /**
39     * @var FetcherMarkup - a parent if any
40     */
41    private FetcherMarkup $parentMarkupHandler;
42
43
44    public function __construct()
45    {
46    }
47
48    /**
49     * The local path is part of the key cache and should be the same
50     * than dokuwiki
51     *
52     * For whatever reason, Dokuwiki uses:
53     *   * `/` as separator on Windows
54     *   * and Windows short path `GERARD~1` not gerardnico
55     * See {@link wikiFN()}
56     * There is also a cache in the function
57     *
58     * We can't use our {@link Path} class to be compatible because the
59     * path is on windows format without the short path format
60     */
61    public static function getWikiIdAndLocalFileDokuwikiCompliant(Path $sourcePath): array
62    {
63
64        try {
65            $markuSourceWikiPath = $sourcePath->toWikiPath();
66
67            if ($markuSourceWikiPath->getDrive() === WikiPath::MARKUP_DRIVE) {
68                /**
69                 * Dokuwiki special function
70                 * that should be the same to conform to the cache key
71                 */
72                $wikiId = $markuSourceWikiPath->getWikiId();
73                $localFile = wikiFN($wikiId);
74            } else {
75                $localFile = $markuSourceWikiPath->toLocalPath();
76                $wikiId = $markuSourceWikiPath->toUriString();
77            }
78        } catch (ExceptionCast $e) {
79            $wikiId = $sourcePath->toAbsoluteId();
80            try {
81                $localFile = $sourcePath->toLocalPath();
82            } catch (ExceptionCast $e) {
83                throw new ExceptionRuntimeInternal("The source path ({$sourcePath}) is not supported as markup source path.", $e);
84            }
85        }
86        return [$wikiId, $localFile];
87    }
88
89    /**
90     * @param string $markupString - the markup is a string format
91     * @return FetcherMarkupBuilder
92     */
93    public function setRequestedMarkupString(string $markupString): FetcherMarkupBuilder
94    {
95        $this->builderMarkupString = $markupString;
96        return $this;
97    }
98
99    /**
100     * Delete the first P instructions
101     * (The parser will add a p block element)
102     * @param bool $b
103     * @return $this
104     */
105    public function setDeleteRootBlockElement(bool $b): FetcherMarkupBuilder
106    {
107        $this->deleteRootBlockElement = $b;
108        return $this;
109    }
110
111    /**
112     * The source where the markup is stored (null if dynamic)
113     * It's a duplicate of {@link FetcherMarkup::setSourcePath()}
114     * @param ?Path $executingPath
115     * @return $this
116     */
117    public function setRequestedExecutingPath(?Path $executingPath): FetcherMarkupBuilder
118    {
119
120        if ($executingPath == null) {
121            return $this;
122        }
123
124        try {
125            /**
126             * Normalize to wiki path if possible
127             * Why ?
128             * Because the parent path may be used a {@link MarkupCacheDependencies::getValueForKey()  cache key}
129             * and they will have different value if the path type is different
130             * * With {@link LocalPath Local Path}: `C:\Users\gerardnico\AppData\Local\Temp\dwtests-1676386702.9751\data\pages\ns_without_scope`
131             * * With {@link WikiPath Wiki Path}: `ns_without_scope`
132             * It will then make the cache file path different (ie the md5 output key is the file name)
133             */
134            $this->builderMarkupSourcePath = $executingPath->toWikiPath();
135        } catch (ExceptionCast $e) {
136            $this->builderMarkupSourcePath = $executingPath;
137        }
138        return $this;
139
140    }
141
142    /**
143     * The page context in which this fragment was requested
144     *
145     * Note that it may or may be not the main requested markup page.
146     * You can have a markup rendering inside another markup rendering.
147     *
148     * @param WikiPath $contextPath
149     * @return $this
150     */
151    public function setRequestedContextPath(WikiPath $contextPath): FetcherMarkupBuilder
152    {
153        $this->requestedContextPath = $contextPath;
154        return $this;
155    }
156
157    /**
158     */
159    public function setRequestedMime(Mime $mime): FetcherMarkupBuilder
160    {
161        $this->mime = $mime;
162        return $this;
163    }
164
165    public function setRequestedMimeToXhtml(): FetcherMarkupBuilder
166    {
167        try {
168            return $this->setRequestedMime(Mime::createFromExtension("xhtml"));
169        } catch (ExceptionNotFound $e) {
170            throw new ExceptionRuntime("Internal error", 0, $e);
171        }
172
173    }
174
175
176    /**
177     * Technically, you could set the mime to whatever you want
178     * and still get the instructions via {@link FetcherMarkup::getInstructions()}
179     * Setting the mime to instructions will just not do any render processing.
180     * @return $this
181     */
182    public function setRequestedMimeToInstructions(): FetcherMarkupBuilder
183    {
184        try {
185            $this->setRequestedMime(Mime::createFromExtension(MarkupRenderer::INSTRUCTION_EXTENSION));
186        } catch (ExceptionNotFound $e) {
187            throw new ExceptionRuntime("Internal error: the mime is internal and should be good");
188        }
189        return $this;
190
191    }
192
193
194    /**
195     * @throws ExceptionNotExists
196     */
197    public function build(): FetcherMarkup
198    {
199
200        /**
201         * One input should be given
202         */
203        if ($this->builderMarkupSourcePath === null && $this->builderMarkupString === null && $this->builderRequestedInstructions === null) {
204            throw new ExceptionRuntimeInternal("A markup source path, a markup string or instructions should be given");
205        }
206        /**
207         * Only one input should be given
208         */
209        $foundInput = "";
210        if ($this->builderMarkupSourcePath !== null) {
211            $foundInput = "markup path";
212        }
213        if ($this->builderMarkupString !== null) {
214            if (!empty($foundInput)) {
215                throw new ExceptionRuntimeInternal("Only one input should be given, we have found 2 inputs ($foundInput and markup string)");
216            }
217            $foundInput = "markup string";
218
219        }
220        if ($this->builderRequestedInstructions !== null) {
221            if (!empty($foundInput)) {
222                throw new ExceptionRuntimeInternal("Only one input should be given, we have found 2 inputs ($foundInput and instructions)");
223            }
224        }
225
226        /**
227         * Other Mandatory
228         */
229        if (!isset($this->mime)) {
230            throw new ExceptionRuntimeInternal("A mime is mandatory");
231        }
232        if (!isset($this->requestedContextPath)) {
233            throw new ExceptionRuntimeInternal("A context path is mandatory");
234        }
235
236        /**
237         * The object type is mandatory
238         */
239        if (!isset($this->rendererName)) {
240            switch ($this->mime->toString()) {
241                case Mime::XHTML:
242                    /**
243                     * ie last name of {@link \Doku_Renderer_xhtml}
244                     */
245                    $rendererName = MarkupRenderer::XHTML_RENDERER;
246                    break;
247                case Mime::META:
248                    /**
249                     * ie last name of {@link \Doku_Renderer_metadata}
250                     */
251                    $rendererName = "metadata";
252                    break;
253                case Mime::INSTRUCTIONS:
254                    /**
255                     * Does not exist yet bu that the future
256                     */
257                    $rendererName = FetcherMarkupInstructions::NAME;
258                    break;
259                default:
260                    throw new ExceptionRuntimeInternal("A renderer name (ie builder name/output object type) is mandatory");
261            }
262        } else {
263            $rendererName = $this->rendererName;
264        }
265
266        /**
267         * Building
268         */
269        $newFetcherMarkup = new FetcherMarkup();
270        $newFetcherMarkup->builderName = $rendererName;
271
272        $newFetcherMarkup->requestedContextPath = $this->requestedContextPath;
273        if ($this->builderMarkupString !== null) {
274            $newFetcherMarkup->markupString = $this->builderMarkupString;
275        }
276        if ($this->builderMarkupSourcePath !== null) {
277            $newFetcherMarkup->markupSourcePath = $this->builderMarkupSourcePath;
278            if (!FileSystems::exists($this->builderMarkupSourcePath)) {
279                /**
280                 * Too much edge case for now with dokuwiki
281                 * The {@link \Doku_Renderer_metadata} for instance throws an error if the file does not exist
282                 * ... etc ....
283                 */
284                throw new ExceptionNotExists("The executing source file ({$this->builderMarkupSourcePath}) does not exist");
285            }
286        }
287        if ($this->builderRequestedInstructions !== null) {
288            $newFetcherMarkup->requestedInstructions = $this->builderRequestedInstructions;
289        }
290        $newFetcherMarkup->mime = $this->mime;
291        $newFetcherMarkup->deleteRootBlockElement = $this->deleteRootBlockElement;
292
293
294        $newFetcherMarkup->isDoc = $this->getIsDocumentExecution();
295        if (isset($this->builderContextData)) {
296            $newFetcherMarkup->contextData = $this->builderContextData;
297        }
298
299        if (isset($this->parentMarkupHandler)) {
300            $newFetcherMarkup->parentMarkupHandler = $this->parentMarkupHandler;
301        }
302        $newFetcherMarkup->isNonPathStandaloneExecution = $this->isCodeStandAloneExecution;
303
304
305        /**
306         * We build the cache dependencies even if there is no source markup path (therefore no cache store)
307         * (Why ? for test purpose, where we want to check if the dependencies was applied)
308         * !!! Attention, the build of the dependencies should happen after that the markup source path is set !!!
309         */
310        $newFetcherMarkup->outputCacheDependencies = MarkupCacheDependencies::create($newFetcherMarkup);
311
312        /**
313         * The cache object depends on the running request
314         * We build it then just
315         *
316         * A request is also send by dokuwiki to check the cache validity
317         *
318         */
319        if ($this->builderMarkupSourcePath !== null) {
320
321
322            list($wikiId, $localFile) = self::getWikiIdAndLocalFileDokuwikiCompliant($this->builderMarkupSourcePath);
323
324            /**
325             * Instructions cache
326             */
327            $newFetcherMarkup->instructionsCache = new CacheInstructions($wikiId, $localFile);
328
329            /**
330             * Content cache
331             */
332            $extension = $this->mime->getExtension();
333            $newFetcherMarkup->contentCache = new CacheRenderer($wikiId, $localFile, $extension);
334            $newFetcherMarkup->outputCacheDependencies->rerouteCacheDestination($newFetcherMarkup->contentCache);
335
336            /**
337             * Snippet Cache
338             * Snippet.json is data dependent
339             *
340             * For instance, the carrousel may add glide or grid as snippet. It depends on the the number of backlinks.
341             *
342             * Therefore the output should be unique by rendered slot
343             * Therefore we reroute (recalculate the cache key to the same than the html file)
344             */
345            $newFetcherMarkup->snippetCache = new CacheParser($wikiId, $localFile, "snippet.json");
346            $newFetcherMarkup->outputCacheDependencies->rerouteCacheDestination($newFetcherMarkup->snippetCache);
347
348
349            /**
350             * Runtime Meta cache
351             * (Technically, it's derived from the instructions)
352             */
353            $newFetcherMarkup->metaPath = LocalPath::createFromPathString(metaFN($wikiId, '.meta'));
354            $newFetcherMarkup->metaCache = new CacheRenderer($wikiId, $localFile, 'metadata');
355
356        }
357
358        return $newFetcherMarkup;
359
360    }
361
362    public function setRequestedMimeToMetadata(): FetcherMarkupBuilder
363    {
364        try {
365            return $this->setRequestedMime(Mime::createFromExtension(MarkupRenderer::METADATA_EXTENSION));
366        } catch (ExceptionNotFound $e) {
367            throw new ExceptionRuntime("Internal error", 0, $e);
368        }
369    }
370
371    public function setRequestedRenderer(string $rendererName): FetcherMarkupBuilder
372    {
373        $this->rendererName = $rendererName;
374        return $this;
375    }
376
377    public function setRequestedContextPathWithDefault(): FetcherMarkupBuilder
378    {
379        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
380        try {
381            // do we have an executing handler
382            $this->requestedContextPath = $executionContext
383                ->getExecutingMarkupHandler()
384                ->getRequestedExecutingPath()
385                ->toWikiPath();
386        } catch (ExceptionCast|ExceptionNotFound $e) {
387            $this->requestedContextPath = $executionContext->getConfig()->getDefaultContextPath();
388        }
389        return $this;
390    }
391
392    /**
393     * @param bool $isDoc - if the markup is a document or a fragment
394     * @return $this
395     * If the markup is a document, an outline is added, a toc is calculated.
396     *
397     * The default is execution parameters dependent if not set
398     * and is calculated at {@link FetcherMarkupBuilder::getIsDocumentExecution()}
399     */
400    public function setIsDocument(bool $isDoc): FetcherMarkupBuilder
401    {
402        $this->isDoc = $isDoc;
403        return $this;
404    }
405
406    /**
407     * @param array $instructions
408     * @return FetcherMarkupBuilder
409     */
410    public function setRequestedInstructions(array $instructions): FetcherMarkupBuilder
411    {
412        $this->builderRequestedInstructions = $instructions;
413        return $this;
414    }
415
416    /**
417     * @param array|null $contextData
418     * @return $this
419     */
420    public function setContextData(?array $contextData): FetcherMarkupBuilder
421    {
422        if ($contextData == null) {
423            return $this;
424        }
425        $this->builderContextData = $contextData;
426        return $this;
427    }
428
429    /**
430     * @param bool $isStandAlone
431     * @return $this
432     *
433     * when a execution is not a {@link FetcherMarkup::isPathExecution()}, the snippet will not be stored automatically.
434     * To avoid this problem, a warning is send if the calling code does not set explicitly that this is specifically a
435     * standalone execution
436     */
437    public function setIsStandAloneCodeExecution(bool $isStandAlone): FetcherMarkupBuilder
438    {
439        $this->isCodeStandAloneExecution = $isStandAlone;
440        return $this;
441    }
442
443    /**
444     * Determins if the run is a fragment or document execution
445     *
446     * Note: in dokuwiki term, a {@link ExecutionContext::PREVIEW_ACTION}
447     * preview action is a fragment
448     *
449     * @return bool true if this is a document execution
450     */
451    private function getIsDocumentExecution(): bool
452    {
453
454        if (isset($this->isDoc)) {
455            return $this->isDoc;
456        }
457
458        /**
459         * By default, a string is not a whole doc
460         * (in test, this is almost always the case)
461         */
462        if ($this->builderMarkupString !== null) {
463            return false;
464        }
465
466        /**
467         * By default, a instructions array is not a whole doc
468         * (in test and rendering, this is almost always the case)
469         */
470        if ($this->builderRequestedInstructions !== null) {
471            return false;
472        }
473
474        try {
475
476            /**
477             * What fucked up is fucked up
478             * Fragment such as sidebar may run in their own context (ie when editing for instance)
479             * but are not a document
480             */
481            $executingWikiPath = $this->builderMarkupSourcePath->toWikiPath();
482            $isFragmentMarkup = MarkupPath::createPageFromPathObject($executingWikiPath)->isSlot();
483            if ($isFragmentMarkup) {
484                return false;
485            }
486
487            /**
488             * If the context and executing path are:
489             * * the same, this is a document run
490             * * not the same, this is a fragment run
491             */
492            if ($this->requestedContextPath->toUriString() !== $executingWikiPath->toUriString()) {
493                return false;
494            }
495
496        } catch (ExceptionCast $e) {
497            // no executing path, not a wiki path
498        }
499
500        return true;
501
502    }
503
504    public function setParentMarkupHandler(FetcherMarkup $parentMarkupHandler): FetcherMarkupBuilder
505    {
506        $this->parentMarkupHandler = $parentMarkupHandler;
507        return $this;
508    }
509
510
511}
512