1 <?php
2 
3 namespace ComboStrap;
4 
5 use dokuwiki\Cache\CacheInstructions;
6 use dokuwiki\Cache\CacheParser;
7 use 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  */
19 class 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