xref: /plugin/combo/ComboStrap/FetcherMarkup.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1<?php
2
3
4namespace ComboStrap;
5
6
7use ComboStrap\Meta\Store\MetadataDokuWikiStore;
8use ComboStrap\Web\Url;
9use ComboStrap\Web\UrlEndpoint;
10use ComboStrap\Xml\XmlDocument;
11use Doku_Renderer_metadata;
12use dokuwiki\Cache\CacheInstructions;
13use dokuwiki\Cache\CacheParser;
14use dokuwiki\Cache\CacheRenderer;
15use Exception;
16
17
18/**
19 * A class that renders a markup fragment
20 * This is the context object
21 * during parsing and rendering is determined by {@link FetcherMarkup}
22 *
23 * You can get it in any place via {@link ExecutionContext::getExecutingMarkupHandler()}
24 *
25 * It:
26 * * does not output any full page (HTML document) but only fragment.
27 * * manage the dependencies (snippets, cache)
28 *
29 * This is not really a {@link IFetcher function} because it should not be called
30 * from the outside but to be able to use the {@link FetcherCache} we need to.
31 * (as fetcher cache uses the url as unique identifier)
32 *
33 *
34 * TODO: {@link MarkupRenderer} could be one with {@link FetcherMarkup} ?
35 *
36 * Not all properties are public to support
37 * the {@link FetcherMarkupBuilder} pattern.
38 * Php does not support internal class and protected does not
39 * work for class on the same namespace.
40 */
41class FetcherMarkup extends IFetcherAbs implements IFetcherSource, IFetcherString
42{
43
44
45    const XHTML_MODE = "xhtml";
46    const MAX_CACHE_AGE = 999999;
47
48    const CANONICAL = "markup-fragment-fetcher";
49
50    /**
51     * When the rendering is done from:
52     * * a string
53     * * or an instructions (template)
54     * but not from a file
55     */
56    public const MARKUP_DYNAMIC_EXECUTION_NAME = "markup-dynamic-execution";
57
58    /**
59     * @var array - toc in a dokuwiki format
60     */
61    public array $toc;
62
63    /**
64     * @var CacheParser cache file (may be not set if this is not a {@link self::isPathExecution() execution}
65     */
66    public CacheParser $contentCache;
67
68    /**
69     * @var string the type of object (known as renderer in Dokuwiki)
70     */
71    public string $builderName;
72
73    public array $requestedInstructions;
74    public array $contextData;
75
76    public CacheInstructions $instructionsCache;
77
78    /**
79     * @var CacheRenderer This cache file stores the last render timestamp (see {@link p_get_metadata()}
80     */
81    public CacheRenderer $metaCache;
82    public LocalPath $metaPath;
83
84    /**
85     * @var CacheParser
86     */
87    public CacheParser $snippetCache;
88
89    /**
90     * @var FetcherMarkup - the parent (a instructions run may run inside a path run, ie {@link \syntax_plugin_combo_iterator)
91     */
92    public FetcherMarkup $parentMarkupHandler;
93
94    /**
95     * @var bool threat the markup as a document (not as a fragment)
96     */
97    public bool $isDoc;
98
99
100    public Mime $mime;
101    private bool $cacheAfterRendering = true;
102    public MarkupCacheDependencies $outputCacheDependencies;
103
104
105    /**
106     * @var Snippet[]
107     */
108    private array $localSnippets = [];
109
110    public bool $deleteRootBlockElement = false;
111
112    /**
113     * @var WikiPath the context path, it's important to resolve relative link and to create cache for each context namespace for instance
114     */
115    public WikiPath $requestedContextPath;
116
117    /**
118     * @var Path the source path of the markup (may be not set if we render a markup string for instance)
119     */
120    public Path $markupSourcePath;
121
122
123    public string $markupString;
124
125    /**
126     * @var bool true if this fetcher has already run
127     * (
128     * Fighting file modified time, even if we cache has been stored,
129     * the modified time is not always good, this indicator will
130     * make the processing not run twice)
131     */
132    private bool $hasExecuted = false;
133
134    /**
135     * The result
136     * @var string
137     */
138    private string $fetchString;
139
140
141    /**
142     * @var array
143     */
144    private array $meta;
145
146    /**
147     * @var bool - when a execution is not a {@link self::isPathExecution()}, the snippet will not be stored automatically.
148     * To avoid this problem, a warning is send if the calling code does not set explicitly that this is specifically a
149     * standalone execution
150     */
151    public bool $isNonPathStandaloneExecution = false;
152    /**
153     * @var array
154     */
155    private array $processedInstructions;
156
157
158    /**
159     * @param Path $executingPath - the path where we can find the markup
160     * @param ?WikiPath $contextPath - the context path, the requested path in the browser url (from where relative component are resolved (ie links, ...))
161     * @return FetcherMarkup
162     * @throws ExceptionNotExists
163     */
164    public static function createXhtmlMarkupFetcherFromPath(Path $executingPath, WikiPath $contextPath = null): FetcherMarkup
165    {
166        if ($contextPath === null) {
167            try {
168                $contextPath = $executingPath->toWikiPath();
169            } catch (ExceptionCast $e) {
170                /**
171                 * Not a wiki path, default to the default
172                 */
173                $contextPath = ExecutionContext::getActualOrCreateFromEnv()->getDefaultContextPath();
174            }
175        }
176        return FetcherMarkup::confRoot()
177            ->setRequestedExecutingPath($executingPath)
178            ->setRequestedContextPath($contextPath)
179            ->setRequestedMimeToXhtml()
180            ->build();
181    }
182
183
184    public static function confRoot(): FetcherMarkupBuilder
185    {
186        return new FetcherMarkupBuilder();
187    }
188
189    /**
190     * Use mostly in test
191     * The coutnerpart of {@link \ComboStrap\Test\TestUtility::renderText2XhtmlWithoutP()}
192     * @throws ExceptionNotExists
193     */
194    public static function createStandaloneExecutionFromStringMarkupToXhtml(string $markup): FetcherMarkup
195    {
196        return self::confRoot()
197            ->setRequestedMarkupString($markup)
198            ->setDeleteRootBlockElement(true)
199            ->setRequestedContextPathWithDefault()
200            ->setRequestedMimeToXhtml()
201            ->setIsStandAloneCodeExecution(true)
202            ->build();
203    }
204
205    /**
206     * Starts a child fetcher markup
207     * This is needed for instructions or markup run
208     * Why ? Because the snippets advertised during this run, need to be stored
209     * and we need to know the original request path that is in the parent run.
210     * @return FetcherMarkupBuilder
211     */
212    public static function confChild(): FetcherMarkupBuilder
213    {
214        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
215        try {
216            $executing = $executionContext->getExecutingMarkupHandler();
217        } catch (ExceptionNotFound $e) {
218            if (PluginUtility::isDevOrTest() && $executionContext->getExecutingAction() !== ExecutionContext::PREVIEW_ACTION) {
219                LogUtility::warning("A markup handler is not running, we couldn't create a child.");
220            }
221            return self::confRoot();
222        }
223        return self::confRoot()
224            ->setParentMarkupHandler($executing)
225            ->setRequestedContextPath($executing->getRequestedContextPath());
226    }
227
228
229    /**
230     * Dokuwiki will wrap the markup in a p element
231     * if the first element is not a block
232     * This option permits to delete it. This is used mostly in test to get
233     * the generated html
234     */
235    public function deleteRootPElementsIfRequested(array &$instructions): void
236    {
237
238        if (!$this->deleteRootBlockElement) {
239            return;
240        }
241
242        /**
243         * Delete the p added by {@link Block::process()}
244         * if the plugin of the {@link SyntaxPlugin::getPType() normal} and not in a block
245         *
246         * p_open = document_start in renderer
247         */
248        if ($instructions[1][0] !== 'p_open') {
249            return;
250        }
251        unset($instructions[1]);
252
253        /**
254         * The last p position is not fix
255         * We may have other calls due for instance
256         * of {@link \action_plugin_combo_syntaxanalytics}
257         */
258        $n = 1;
259        while (($lastPBlockPosition = (sizeof($instructions) - $n)) >= 0) {
260
261            /**
262             * p_open = document_end in renderer
263             */
264            if ($instructions[$lastPBlockPosition][0] == 'p_close') {
265                unset($instructions[$lastPBlockPosition]);
266                break;
267            } else {
268                $n = $n + 1;
269            }
270        }
271
272    }
273
274    /**
275     *
276     * @param Url|null $url
277     * @return Url
278     *
279     * Note: The fetch url is the {@link FetcherCache keyCache}
280     */
281    function getFetchUrl(Url $url = null): Url
282    {
283        /**
284         * Overwrite default fetcher endpoint
285         * that is {@link UrlEndpoint::createFetchUrl()}
286         */
287        $url = UrlEndpoint::createDokuUrl();
288        $url = parent::getFetchUrl($url);
289        try {
290            $wikiPath = $this->getSourcePath()->toWikiPath();
291            $url->addQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $wikiPath->getWikiId());
292            $url->addQueryParameter(WikiPath::DRIVE_ATTRIBUTE, $wikiPath->getDrive());
293        } catch (ExceptionCast|ExceptionNotFound $e) {
294            // not an accessible source path
295        }
296        $url->addQueryParameter("context-id", $this->getRequestedContextPath()->getWikiId());
297        return $url;
298
299    }
300
301
302    /**
303     * @return Mime
304     */
305    public function getMime(): Mime
306    {
307        if (isset($this->mime)) {
308            return $this->mime;
309        }
310
311        // XHTML default
312        try {
313            return Mime::createFromExtension(self::XHTML_MODE);
314        } catch (ExceptionNotFound $e) {
315            // should not happen
316            throw new ExceptionRuntime("Internal error: The XHTML mime was not found.", self::CANONICAL, 1, $e);
317        }
318    }
319
320    /**
321     * TODO: split We should split fetcherMarkup by object type output and {@link Mime}
322     * @return bool
323     */
324    private function shouldInstructionProcess(): bool
325    {
326
327        if (!$this->isPathExecution()) {
328            return true;
329        }
330
331        if (isset($this->processedInstructions)) {
332            return false;
333        }
334
335        /**
336         * Edge Case
337         * (as dokuwiki starts the rendering process here
338         * we need to set the execution id)
339         */
340        $executionContext = ExecutionContext::getActualOrCreateFromEnv()->setExecutingMarkupHandler($this);
341        try {
342            $useCache = $this->instructionsCache->useCache();
343        } finally {
344            $executionContext->closeExecutingMarkupHandler();
345        }
346        return ($useCache === false);
347    }
348
349    public function shouldProcess(): bool
350    {
351
352        if (!$this->isPathExecution()) {
353            return true;
354        }
355
356        if ($this->hasExecuted) {
357            return false;
358        }
359
360        /**
361         * The cache is stored by requested page scope
362         *
363         * We set the environment because
364         * {@link CacheParser::useCache()} may call a parsing of the markup fragment
365         * And the global environment are not always passed
366         * in all actions and is needed to log the {@link  CacheResult cache
367         * result}
368         *
369         * Use cache should be always called because it trigger
370         * the event coupled to the cache (ie PARSER_CACHE_USE)
371         */
372        $depends['age'] = $this->getCacheAge();
373        if ($this->isFragment()) {
374            /**
375             * Fragment may use variables of the requested page
376             * We have dependency on {@link MarkupCacheDependencies::PAGE_PRIMARY_META_DEPENDENCY}
377             * but as they may be derived such as the {@link PageTitle}
378             * comes from the H1 or the feature image comes from the first image in the section 1
379             * We can't really use this event.
380             */
381            try {
382                $depends['files'][] = FetcherMarkup::confRoot()
383                    ->setRequestedContextPath($this->getRequestedContextPath())
384                    ->setRequestedExecutingPath($this->getRequestedContextPath())
385                    ->setRequestedMimeToMetadata()
386                    ->build()
387                    ->getMetadataPath()
388                    ->toAbsoluteId();
389            } catch (ExceptionNotExists|ExceptionNotFound $e) {
390                /**
391                 * Computer are hard
392                 * At the beginning there is no markup path
393                 * We may get this error then
394                 *
395                 * We don't allow on test
396                 */
397                if (PluginUtility::isTest()) {
398                    /**
399                     * The first edit, the page does not exists
400                     */
401                    $executingAction = ExecutionContext::getActualOrCreateFromEnv()->getExecutingAction();
402                    if (!in_array($executingAction, [ExecutionContext::EDIT_ACTION, ExecutionContext::PREVIEW_ACTION])) {
403                        LogUtility::error("The metadata path should be known. " . $e->getMessage(), self::CANONICAL, $e);
404                    }
405                }
406            }
407        }
408        /**
409         * Edge Case
410         * (as dokuwiki starts the rendering process here
411         * we need to set the execution id)
412         */
413        $executionContext = ExecutionContext::getActualOrCreateFromEnv()
414            ->setExecutingMarkupHandler($this);
415        try {
416            $useCache = $this->contentCache->useCache($depends);
417        } finally {
418            $executionContext->closeExecutingMarkupHandler();
419        }
420        return ($useCache === false);
421
422    }
423
424
425    public
426    function storeSnippets()
427    {
428
429        /**
430         * Snippet
431         */
432        $snippets = $this->getSnippets();
433        $jsonDecodeSnippets = SnippetSystem::toJsonArrayFromSlotSnippets($snippets);
434
435        /**
436         * Cache file
437         * Using a cache parser, set the page id and will trigger
438         * the parser cache use event in order to log/report the cache usage
439         * At {@link action_plugin_combo_cache::createCacheReport()}
440         */
441        $snippetCache = $this->getSnippetCacheStore();
442        $this->outputCacheDependencies->rerouteCacheDestination($snippetCache);
443
444        if (count($jsonDecodeSnippets) > 0) {
445            $data1 = json_encode($jsonDecodeSnippets);
446            $snippetCache->storeCache($data1);
447        } else {
448            $snippetCache->removeCache();
449        }
450
451    }
452
453    /**
454     * This functon loads the snippets in the global array
455     * by creating them. Not ideal but works for now.
456     * @return Snippet[]
457     */
458    public
459    function loadSnippets(): array
460    {
461
462        $snippetCacheStore = $this->getSnippetCacheStore();
463        $data = $snippetCacheStore->retrieveCache();
464        $nativeSnippets = [];
465        if (!empty($data)) {
466            $jsonDecodeSnippets = json_decode($data, true);
467            foreach ($jsonDecodeSnippets as $snippet) {
468                try {
469                    $nativeSnippets[] = Snippet::createFromJson($snippet);
470                } catch (ExceptionCompile $e) {
471                    LogUtility::error("The snippet json array cannot be build into a snippet object. " . $e->getMessage() . "\n" . ArrayUtility::formatAsString($snippet), LogUtility::SUPPORT_CANONICAL,);
472                }
473            }
474        }
475        return $nativeSnippets;
476
477    }
478
479    private
480    function removeSnippets()
481    {
482        $snippetCacheFile = $this->getSnippetCacheStore()->cache;
483        if ($snippetCacheFile !== null) {
484            if (file_exists($snippetCacheFile)) {
485                unlink($snippetCacheFile);
486            }
487        }
488    }
489
490    /**
491     * @return CacheParser - the cache where the snippets are stored
492     * Cache file
493     * Using a cache parser, set the page id and will trigger
494     * the parser cache use event in order to log/report the cache usage
495     * At {@link action_plugin_combo_cache::createCacheReport()}
496     */
497    public
498    function getSnippetCacheStore(): CacheParser
499    {
500        if (isset($this->snippetCache)) {
501            return $this->snippetCache;
502        }
503        if ($this->isPathExecution()) {
504            throw new ExceptionRuntimeInternal("A source path should be available as this is a path execution");
505        }
506        throw new ExceptionRuntime("There is no snippet cache store for a non-path execution");
507
508    }
509
510
511    public
512    function getDependenciesCacheStore(): CacheParser
513    {
514        return $this->outputCacheDependencies->getDependenciesCacheStore();
515    }
516
517    public
518    function getDependenciesCachePath(): LocalPath
519    {
520        $cachePath = $this->outputCacheDependencies->getDependenciesCacheStore()->cache;
521        return LocalPath::createFromPathString($cachePath);
522    }
523
524    /**
525     * @return LocalPath the fetch path - start the process and returns a path. If the cache is on, return the {@link FetcherMarkup::getContentCachePath()}
526     * @throws ExceptionCompile
527     */
528    function processIfNeededAndGetFetchPath(): LocalPath
529    {
530        $this->processIfNeeded();
531
532        /**
533         * The cache path may have change due to the cache key rerouting
534         * We should there always use the {@link FetcherMarkup::getContentCachePath()}
535         * as fetch path
536         */
537        return $this->getContentCachePath();
538
539    }
540
541
542    /**
543     * @return $this
544     * @throws ExceptionCompile
545     */
546    public function process(): FetcherMarkup
547    {
548
549        $this->hasExecuted = true;
550
551        /**
552         * Rendering
553         */
554        $executionContext = (ExecutionContext::getActualOrCreateFromEnv());
555
556        $extension = $this->getMime()->getExtension();
557        switch ($extension) {
558
559            case MarkupRenderer::METADATA_EXTENSION:
560                /**
561                 * The user may ask just for the metadata
562                 * and should then use the {@link self::getMetadata()}
563                 * function instead
564                 */
565                break;
566            case MarkupRenderer::INSTRUCTION_EXTENSION:
567                /**
568                 * The user may ask just for the instuctions
569                 * and should then use the {@link self::getInstructions()}
570                 * function to get the instructions
571                 */
572                return $this;
573            default:
574
575                $instructions = $this->getInstructions();
576
577                /**
578                 * Edge case: We delete here
579                 * because the instructions may have been created by dokuwiki
580                 * when we test for the cache with {@link CacheParser::useCache()}
581                 */
582                if ($this->deleteRootBlockElement) {
583                    self::deleteRootPElementsIfRequested($instructions);
584                }
585
586                if (!isset($this->builderName)) {
587                    $this->builderName = $this->getMime()->getExtension();
588                }
589
590                $executionContext->setExecutingMarkupHandler($this);
591                try {
592                    if ($this->isDocument()) {
593                        $markupRenderer = MarkupRenderer::createFromMarkupInstructions($instructions, $this)
594                            ->setRequestedMime($this->getMime())
595                            ->setRendererName($this->builderName);
596
597                        $output = $markupRenderer->getOutput();
598                        if ($output === null && !empty($instructions)) {
599                            LogUtility::error("The renderer ({$this->builderName}) seems to have been not found");
600                        }
601                        $this->cacheAfterRendering = $markupRenderer->getCacheAfterRendering();
602                    } else {
603                        $output = MarkupDynamicRender::create($this->builderName)->processInstructions($instructions);
604                    }
605                } catch (\Exception $e) {
606                    /**
607                     * Example of errors;
608                     * method_exists() expects parameter 2 to be string, array given
609                     * inc\parserutils.php:672
610                     */
611                    throw new ExceptionCompile("An error has occurred while getting the output. Error: {$e->getMessage()}", self::CANONICAL, 1, $e);
612
613                } finally {
614                    $executionContext->closeExecutingMarkupHandler();
615                }
616                if (is_array($output)) {
617                    LogUtility::internalError("The output was an array", self::CANONICAL);
618                    $this->fetchString = serialize($output);
619                } else {
620                    $this->fetchString = $output;
621                }
622
623                break;
624        }
625
626        /**
627         * Storage of snippets or dependencies
628         * none if this is not a path execution
629         * and for now, metadata storage is done by dokuwiki
630         */
631        if (!$this->isPathExecution() || $this->mime->getExtension() === MarkupRenderer::METADATA_EXTENSION) {
632            return $this;
633        }
634
635
636        /**
637         * Snippets and cache dependencies are only for HTML rendering
638         * Otherwise, otherwise other type rendering may override them
639         * (such as analtyical json, ...)
640         */
641        if (in_array($this->getMime()->toString(), [Mime::XHTML, Mime::HTML])) {
642
643            /**
644             * We make the Snippet store to Html store an atomic operation
645             *
646             * Why ? Because if the rendering of the page is stopped,
647             * the cache of the HTML page may be stored but not the cache of the snippets
648             * leading to a bad page because the next rendering will see then no snippets.
649             */
650            try {
651                $this->storeSnippets();
652            } catch (Exception $e) {
653                // if any write os exception
654                LogUtility::msg("Error while storing the xhtml content: {$e->getMessage()}");
655                $this->removeSnippets();
656            }
657
658            /**
659             * Cache output dependencies
660             * Reroute the cache output by runtime dependencies
661             * set during processing
662             */
663            $this->outputCacheDependencies->storeDependencies();
664            $this->outputCacheDependencies->rerouteCacheDestination($this->contentCache);
665
666        }
667
668        /**
669         * We store always the output in the cache
670         * if the cache is not on, the file is just overwritten
671         *
672         * We don't use
673         * {{@link CacheParser::storeCache()}
674         * because it uses the protected parameter `__nocache`
675         * that will disallow the storage
676         */
677        io_saveFile($this->contentCache->cache, $this->fetchString);
678
679        return $this;
680    }
681
682
683    function getBuster(): string
684    {
685        // no buster
686        return "";
687    }
688
689    public
690    function getFetcherName(): string
691    {
692        return "markup-fetcher";
693    }
694
695
696    private function getCacheAge(): int
697    {
698
699        $extension = $this->getMime()->getExtension();
700        switch ($extension) {
701            case self::XHTML_MODE:
702                if (!Site::isHtmlRenderCacheOn()) {
703                    return 0;
704                }
705                break;
706            case MarkupRenderer::INSTRUCTION_EXTENSION:
707                // indefinitely
708                return self::MAX_CACHE_AGE;
709        }
710        try {
711            $requestedCache = $this->getRequestedCache();
712        } catch (ExceptionNotFound $e) {
713            $requestedCache = IFetcherAbs::RECACHE_VALUE;
714        }
715        $cacheAge = $this->getCacheMaxAgeInSec($requestedCache);
716        return $this->cacheAfterRendering ? $cacheAge : 0;
717
718    }
719
720
721    public function __toString()
722    {
723
724        return parent::__toString() . " ({$this->getSourceName()}, {$this->getMime()->toString()})";
725    }
726
727
728    /**
729     * @throws ExceptionBadArgument
730     */
731    public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherMarkup
732    {
733        parent::buildFromTagAttributes($tagAttributes);
734        return $this;
735    }
736
737
738    /**
739     * @return LocalPath - the cache path is where the result is stored if the cache is on
740     * The cache path may have change due to the cache key rerouting
741     * We should there always use the {@link FetcherMarkup::getContentCachePath()}
742     * as fetch path
743     */
744    public function getContentCachePath(): LocalPath
745    {
746        $path = $this->contentCache->cache;
747        return LocalPath::createFromPathString($path);
748    }
749
750
751    public function getOutputCacheDependencies(): MarkupCacheDependencies
752    {
753        return $this->outputCacheDependencies;
754    }
755
756
757    /**
758     * @return string - with replacement if any
759     * TODO: edit button replacement could be a script tag with a json, permits to do DOM manipulation
760     * @throws ExceptionCompile - if any processing error occurs
761     */
762    public function getFetchString(): string
763    {
764        $this->processIfNeeded();
765
766        if (!$this->isPathExecution()) {
767            return $this->fetchString;
768        }
769
770        /**
771         * Source path execution
772         * The cache path may have change due to the cache key rerouting
773         * We should there always use the {@link FetcherMarkup::getContentCachePath()}
774         * as fetch path
775         */
776        $path = $this->getContentCachePath();
777        try {
778            $text = FileSystems::getContent($path);
779        } catch (ExceptionNotFound $e) {
780            throw new ExceptionRuntime("Internal error: The fetch path should exists.", self::CANONICAL, 1, $e);
781        }
782
783        /**
784         * Edit button Processing for XHtml
785         * (Path is mandatory to create the buttons)
786         */
787        if (!in_array($this->getMime()->getExtension(), ["html", "xhtml"])) {
788            return $text;
789        }
790        try {
791            if ($this->getSourcePath()->toWikiPath()->getDrive() !== WikiPath::MARKUP_DRIVE) {
792                // case when this is a default page in the resource/template directory
793                return EditButton::deleteAll($text);
794            }
795        } catch (ExceptionNotFound|ExceptionCast $e) {
796            // not a wiki path
797        }
798        return EditButton::replaceOrDeleteAll($text);
799
800    }
801
802
803    public function getLabel(): string
804    {
805        try {
806            $sourcePath = $this->getSourcePath();
807        } catch (ExceptionNotFound $e) {
808            return self::MARKUP_DYNAMIC_EXECUTION_NAME;
809        }
810        return ResourceName::getFromPath($sourcePath);
811    }
812
813
814    public function getRequestedContextPath(): WikiPath
815    {
816        return $this->requestedContextPath;
817    }
818
819
820    /**
821     * @throws ExceptionNotFound
822     */
823    public function getSourcePath(): Path
824    {
825        if (isset($this->markupSourcePath)) {
826            return $this->markupSourcePath;
827        }
828        throw new ExceptionNotFound("No source path for this markup");
829    }
830
831    /**
832     * Utility class that return the source path
833     * @return Path
834     * @throws ExceptionNotFound
835     */
836    public function getRequestedExecutingPath(): Path
837    {
838        return $this->getSourcePath();
839    }
840
841    /**
842     * @return Path|null - utility class to get the source markup path or null (if this is a markup snippet/string rendering)
843     */
844    public function getExecutingPathOrNull(): ?Path
845    {
846        try {
847            return $this->getSourcePath();
848        } catch (ExceptionNotFound $e) {
849            return null;
850        }
851    }
852
853
854    /**
855     * @param string $componentId
856     * @return Snippet[]
857     */
858    public function getSnippetsForComponent(string $componentId): array
859    {
860
861        $snippets = $this->getSnippets();
862        $snippetsForComponent = [];
863        foreach ($snippets as $snippet) {
864            try {
865                if ($snippet->getComponentId() === $componentId) {
866                    $snippetsForComponent[] = $snippet;
867                }
868            } catch (ExceptionNotFound $e) {
869                //
870            }
871        }
872        return $snippetsForComponent;
873
874    }
875
876    /**
877     * @return Snippet[]
878     */
879    public function getSnippets(): array
880    {
881
882        $snippets = $this->localSnippets;
883
884        /**
885         * Old ways where snippets were added to the global scope
886         * and not to the fetcher markup via {@link self::addSnippet()}
887         *
888         * During the transition, we support the two
889         *
890         * Note that with the new system where render code
891         * can access this object via {@link ExecutionContext::getExecutingMarkupHandler()}
892         * the code may had snippets without any id
893         * (For the time being, not yet)
894         */
895        try {
896            $slotId = $this->getSourcePath()->toWikiPath()->getWikiId();
897        } catch (ExceptionNotFound $e) {
898            // a markup string run
899            return $snippets;
900        } catch (ExceptionCast $e) {
901            // not a wiki path
902            return $snippets;
903        }
904
905        $snippetManager = PluginUtility::getSnippetManager();
906        $oldWaySnippets = $snippetManager->getSnippetsForSlot($slotId);
907        return array_merge($oldWaySnippets, $snippets);
908
909    }
910
911    /**
912     * @param Snippet $snippet
913     * @return FetcherMarkup
914     */
915    public function addSnippet(Snippet $snippet): FetcherMarkup
916    {
917        /**
918         * Snippet should be added only when they can be store
919         * (ie when this is path execution)
920         * If this is not a path execution, the snippet cannot be
921         * stored in a cache and are therefore lost if not used
922         */
923
924        /**
925         * If there is a parent markup handler
926         * Store the snippets there
927         */
928        if (isset($this->parentMarkupHandler)) {
929            $this->parentMarkupHandler->addSnippet($snippet);
930            return $this;
931        }
932
933        if (!$this->isPathExecution()
934            && !$this->isNonPathStandaloneExecution
935            // In preview, there is no parent handler because we didn't take over
936            && ExecutionContext::getActualOrCreateFromEnv()->getExecutingAction() !== ExecutionContext::PREVIEW_ACTION
937        ) {
938            LogUtility::warning("The execution ($this) is not a path execution. The snippet $snippet will not be preserved after initial rendering. Set the execution as standalone or set a parent markup handler.");
939        }
940        if (!in_array($this->getMime()->toString(), [Mime::XHTML, Mime::HTML])) {
941            LogUtility::warning("The execution ($this) is not a HTML execution. The snippet $snippet will not be preserved because they are reserved for XHMTL execution");
942        }
943
944        $snippetGuid = $snippet->getPath()->toUriString();
945        $this->localSnippets[$snippetGuid] = $snippet;
946        return $this;
947
948
949    }
950
951    /**
952     * @return bool true if the markup string comes from a path
953     * This is motsly important for cache as we use the path as the cache key
954     * (Cache:
955     * * of the {@link self::getInstructions() instructions},
956     * * of the {@link self::getOutputCacheDependencies() output dependencies}
957     * * of the {@link self::getSnippets() snippets}
958     * * of the {@link self::processMetadataIfNotYetDone() metadata}
959     *
960     * The rule is this is a path execution of the {@link self::$markupSourcePath executing source path} is set.
961     *
962     * Ie this is not a path execution, if the input is:
963     * * {@link self::$requestedInstructions} (used for templating)
964     * * a {@link self::$markupString} (used for test or webcode)
965     *
966     */
967    public function isPathExecution(): bool
968    {
969        if (isset($this->markupSourcePath)) {
970            return true;
971        }
972        return false;
973    }
974
975    /**
976     * @throws ExceptionCompile - if any processing errors occurs
977     */
978    public function processIfNeeded(): FetcherMarkup
979    {
980
981        if (!$this->shouldProcess()) {
982            return $this;
983        }
984
985        $this->process();
986        return $this;
987
988    }
989
990
991    /**
992     * @return array - the markup instructions
993     * @throws ExceptionNotExists - if the executing markup file does not exist
994     */
995    public function getInstructions(): array
996    {
997
998        if (isset($this->requestedInstructions)) {
999
1000            return $this->requestedInstructions;
1001
1002        }
1003
1004        /**
1005         * We create a fetcher markup to not have the same {@link self::getId()}
1006         * on execution
1007         */
1008        if (ExecutionContext::getActualOrCreateFromEnv()->hasExecutingMarkupHandler()) {
1009            $fetcherMarkupBuilder = FetcherMarkup::confChild();
1010        } else {
1011            $fetcherMarkupBuilder = FetcherMarkup::confRoot();
1012        }
1013        $fetcherMarkupBuilder = $fetcherMarkupBuilder
1014            ->setRequestedMime(Mime::create(Mime::INSTRUCTIONS))
1015            ->setRequestedRenderer(FetcherMarkupInstructions::NAME)
1016            ->setIsDocument($this->isDoc)
1017            ->setRequestedContextPath($this->getRequestedContextPath());
1018        if ($this->isPathExecution()) {
1019            $fetcherMarkupBuilder->setRequestedExecutingPath($this->getExecutingPathOrFail());
1020        } else {
1021            $fetcherMarkupBuilder->setRequestedMarkupString($this->markupString);
1022        }
1023        $fetcherMarkup = $fetcherMarkupBuilder->build();
1024        return $fetcherMarkup->getProcessedInstructions();
1025
1026
1027    }
1028
1029
1030    /**
1031     * @return bool - a document
1032     *
1033     * A document will get an {@link Outline} processing
1034     * while a {@link self::isFragment() fragment} will not.
1035     */
1036    public function isDocument(): bool
1037    {
1038
1039        return $this->isDoc;
1040
1041    }
1042
1043    public function getSnippetManager(): SnippetSystem
1044    {
1045        return PluginUtility::getSnippetManager();
1046    }
1047
1048    /**
1049     * @throws ExceptionBadSyntax
1050     * @throws ExceptionCompile
1051     */
1052    public function getFetchStringAsDom(): XmlDocument
1053    {
1054        return XmlDocument::createXmlDocFromMarkup($this->getFetchString());
1055    }
1056
1057    public function getSnippetsAsHtmlString(): string
1058    {
1059
1060        try {
1061            $globalSnippets = SnippetSystem::getFromContext()->getSnippetsForSlot($this->getRequestedExecutingPath()->toAbsoluteId());
1062        } catch (ExceptionNotFound $e) {
1063            // string execution
1064            $globalSnippets = [];
1065        }
1066        $allSnippets = array_merge($globalSnippets, $this->localSnippets);
1067        return SnippetSystem::toHtmlFromSnippetArray($allSnippets);
1068
1069    }
1070
1071    public function isFragment(): bool
1072    {
1073        return $this->isDocument() === false;
1074    }
1075
1076    private function getMarkupStringToExecute(): string
1077    {
1078        if (isset($this->markupString)) {
1079            return $this->markupString;
1080        } else {
1081            try {
1082                $sourcePath = $this->getSourcePath();
1083            } catch (ExceptionNotFound $e) {
1084                throw new ExceptionRuntimeInternal("A markup or a source markup path should be specified.");
1085            }
1086            try {
1087                return FileSystems::getContent($sourcePath);
1088            } catch (ExceptionNotFound $e) {
1089                LogUtility::error("The path ($sourcePath) does not exist, we have set the markup to the empty string during rendering. If you want to delete the cache path, ask it via the cache path function", self::CANONICAL, $e);
1090                return "";
1091            }
1092        }
1093    }
1094
1095    public function getContextData(): array
1096    {
1097        if (isset($this->contextData)) {
1098            return $this->contextData;
1099        }
1100        $this->contextData = MarkupPath::createPageFromPathObject($this->getRequestedContextPath())->getMetadataForRendering();
1101        return $this->contextData;
1102    }
1103
1104
1105    public function getToc(): array
1106    {
1107
1108        if (isset($this->toc)) {
1109            return $this->toc;
1110        }
1111        try {
1112            return TOC::createForPage($this->getRequestedExecutingPath())->getValue();
1113        } catch (ExceptionNotFound $e) {
1114            // no executing page or no value
1115        }
1116        /**
1117         * Derived TOC from instructions
1118         */
1119        return Outline::createFromCallStack(CallStack::createFromInstructions($this->getInstructions()))->toTocDokuwikiFormat();
1120
1121
1122    }
1123
1124    public function getInstructionsPath(): LocalPath
1125    {
1126        $path = $this->instructionsCache->cache;
1127        return LocalPath::createFromPathString($path);
1128    }
1129
1130    public function getOutline(): Outline
1131    {
1132        $instructions = $this->getInstructions();
1133        $callStack = CallStack::createFromInstructions($instructions);
1134        try {
1135            $markupPath = MarkupPath::createPageFromPathObject($this->getRequestedExecutingPath());
1136        } catch (ExceptionNotFound $e) {
1137            $markupPath = null;
1138        }
1139        return Outline::createFromCallStack($callStack, $markupPath);
1140    }
1141
1142
1143    public function getMetadata(): array
1144    {
1145
1146        $this->processMetadataIfNotYetDone();
1147        return $this->meta;
1148
1149    }
1150
1151
1152    /**
1153     * Adaptation of {@link p_get_metadata()}
1154     * to take into account {@link self::getInstructions()}
1155     * where we can just pass our own instructions.
1156     *
1157     * And yes, adaptation of {@link p_get_metadata()}
1158     * that process the metadata. Yeah, it calls {@link p_render_metadata()}
1159     * and save them
1160     *
1161     */
1162    public function processMetadataIfNotYetDone(): FetcherMarkup
1163    {
1164
1165        /**
1166         * Already set ?
1167         */
1168        if (isset($this->meta)) {
1169            return $this;
1170        }
1171
1172        $actualMeta = [];
1173
1174        /**
1175         * We wrap the whole block
1176         * because {@link CacheRenderer::useCache()}
1177         * and the renderer needs it
1178         */
1179        $executionContext = ExecutionContext::getActualOrCreateFromEnv()->setExecutingMarkupHandler($this);
1180        try {
1181
1182            /**
1183             * Can we read from the meta file
1184             */
1185
1186
1187            if ($this->isPathExecution()) {
1188
1189                /**
1190                 * If the meta file exists
1191                 */
1192                if (FileSystems::exists($this->getMetaPathOrFail())) {
1193
1194                    $executingPath = $this->getExecutingPathOrFail();
1195                    $actualMeta = MetadataDokuWikiStore::getOrCreateFromResource(MarkupPath::createPageFromPathObject($executingPath))
1196                        ->getDataCurrentAndPersistent();
1197
1198                    /**
1199                     * The metadata useCache function has side effect
1200                     * and triggers a render that fails if the wiki file does not exists
1201                     */
1202                    $depends['files'][] = $this->instructionsCache->cache;
1203                    $depends['files'][] = $executingPath->toAbsolutePath()->toAbsoluteId();
1204                    $useCache = $this->metaCache->useCache($depends);
1205                    if ($useCache) {
1206                        $this->meta = $actualMeta;
1207                        return $this;
1208                    }
1209                }
1210            }
1211
1212            /**
1213             * Process and derived meta
1214             */
1215            try {
1216                $wikiId = $this->getRequestedExecutingPath()->toWikiPath()->getWikiId();
1217            } catch (ExceptionCast|ExceptionNotFound $e) {
1218                // not a wiki path execution
1219                $wikiId = null;
1220            }
1221
1222            /**
1223             * Dokuwiki global variable used to see if the process is in rendering mode
1224             * See {@link p_get_metadata()}
1225             * Store the original metadata in the global $METADATA_RENDERERS
1226             * ({@link p_set_metadata()} use it)
1227             */
1228            global $METADATA_RENDERERS;
1229            $METADATA_RENDERERS[$wikiId] =& $actualMeta;
1230
1231            // add an extra key for the event - to tell event handlers the page whose metadata this is
1232            $actualMeta['page'] = $wikiId;
1233            $evt = new \dokuwiki\Extension\Event('PARSER_METADATA_RENDER', $actualMeta);
1234            if ($evt->advise_before()) {
1235
1236                // get instructions (from string or file)
1237                $instructions = $this->getInstructions();
1238
1239                // set up the renderer
1240                $renderer = new Doku_Renderer_metadata();
1241
1242
1243                /**
1244                 * Runtime/ Derived metadata
1245                 * The runtime meta are not even deleted
1246                 * (See {@link p_render_metadata()}
1247                 */
1248                $renderer->meta =& $actualMeta['current'];
1249
1250                /**
1251                 * The {@link Doku_Renderer_metadata}
1252                 * will fail if the file and the date modified property does not exist
1253                 */
1254                try {
1255                    $path = $this->getRequestedExecutingPath();
1256                    if (!FileSystems::exists($path)) {
1257                        $renderer->meta['date']['modified'] = null;
1258                    }
1259                } catch (ExceptionNotFound $e) {
1260                    // ok
1261                }
1262
1263                /**
1264                 * The persistent data are now available
1265                 */
1266                $renderer->persistent =& $actualMeta['persistent'];
1267
1268                // Loop through the instructions
1269                foreach ($instructions as $instruction) {
1270                    // execute the callback against the renderer
1271                    call_user_func_array(array(&$renderer, $instruction[0]), (array)$instruction[1]);
1272                }
1273
1274                $evt->result = array('current' => &$renderer->meta, 'persistent' => &$renderer->persistent);
1275
1276            }
1277            $evt->advise_after();
1278
1279            $this->meta = $evt->result;
1280
1281            /**
1282             * Dokuwiki global variable
1283             * See {@link p_get_metadata()}
1284             */
1285            unset($METADATA_RENDERERS[$wikiId]);
1286
1287            /**
1288             * Storage
1289             */
1290            if ($wikiId !== null) {
1291                p_save_metadata($wikiId, $this->meta);
1292                $this->metaCache->storeCache(time());
1293            }
1294
1295        } finally {
1296            $executionContext->closeExecutingMarkupHandler();
1297        }
1298        return $this;
1299
1300    }
1301
1302    /**
1303     * @throws ExceptionNotFound
1304     */
1305    public function getMetadataPath(): LocalPath
1306    {
1307        if (isset($this->metaPath)) {
1308            return $this->metaPath;
1309        }
1310        throw new ExceptionNotFound("No meta path for this markup");
1311    }
1312
1313    /**
1314     * A wrapper from when we are in a code block
1315     * were we expect to be a {@link self::isPathExecution()}
1316     * All path should then be available
1317     * @return Path
1318     */
1319    private
1320    function getExecutingPathOrFail(): Path
1321    {
1322        try {
1323            return $this->getRequestedExecutingPath();
1324        } catch (ExceptionNotFound $e) {
1325            throw new ExceptionRuntime($e);
1326        }
1327    }
1328
1329    /**
1330     * A wrapper from when we are in a code block
1331     * were we expect to be a {@link self::isPathExecution()}
1332     * All path should then be available
1333     * @return Path
1334     */
1335    private
1336    function getMetaPathOrFail()
1337    {
1338        try {
1339            return $this->getMetadataPath();
1340        } catch (ExceptionNotFound $e) {
1341            throw new ExceptionRuntime($e);
1342        }
1343    }
1344
1345    /**
1346     * Process the instructions
1347     * TODO: move to a FetcherMarkup by tree instruction and array/encoding mime
1348     * @return $this
1349     */
1350    public function processInstructions(): FetcherMarkup
1351    {
1352        if (isset($this->processedInstructions)) {
1353            return $this;
1354        }
1355
1356        $markup = $this->getMarkupStringToExecute();
1357        $executionContext = ExecutionContext::getActualOrCreateFromEnv()
1358            ->setExecutingMarkupHandler($this);
1359        try {
1360            $markupRenderer = MarkupRenderer::createFromMarkup($markup, $this->getExecutingPathOrNull(), $this->getRequestedContextPath())
1361                ->setRequestedMimeToInstruction();
1362            $instructions = $markupRenderer->getOutput();
1363            if (isset($this->instructionsCache)) {
1364                /**
1365                 * Not a string execution, ie {@link self::isPathExecution()}
1366                 * a path execution
1367                 */
1368                $this->instructionsCache->storeCache($instructions);
1369            }
1370            $this->processedInstructions = $instructions;
1371            return $this;
1372        } catch (\Exception $e) {
1373            throw new ExceptionRuntimeInternal("An error has occurred while getting the output. Error: {$e->getMessage()}", self::CANONICAL, 1, $e);
1374        } finally {
1375            $executionContext->closeExecutingMarkupHandler();
1376        }
1377    }
1378
1379    public function getSnippetCachePath(): LocalPath
1380    {
1381        $cache = $this->getSnippetCacheStore()->cache;
1382        return LocalPath::createFromPathString($cache);
1383
1384    }
1385
1386    /**
1387     * @return string - an execution id to be sure that we don't execute the same twice in recursion
1388     */
1389    public function getId(): string
1390    {
1391
1392        return "({$this->getSourceName()}) to ({$this->builderName} - {$this->getMime()}) with context ({$this->getRequestedContextPath()->toUriString()})";
1393    }
1394
1395    /**
1396     * @return string - a name for the source (used in {@link self::__toString()} and {@link self::getId() identification})
1397     */
1398    private function getSourceName(): string
1399    {
1400        if (!$this->isPathExecution()) {
1401            if (isset($this->markupString)) {
1402                $md5 = md5($this->markupString);
1403                return "Markup String Execution ($md5)";
1404            } elseif (isset($this->requestedInstructions)) {
1405                return "Markup Instructions Execution";
1406            } else {
1407                LogUtility::internalError("The name of the markup handler is unknown");
1408                return "Markup Unknown Execution";
1409
1410            }
1411        } else {
1412            try {
1413                return $this->getSourcePath()->toUriString();
1414            } catch (ExceptionNotFound $e) {
1415                throw new ExceptionRuntimeInternal("A source path should be defined if it's not a markup string execution");
1416            }
1417        }
1418    }
1419
1420
1421    /**
1422     * TODO: move to a FetcherMarkup by object type output and mime
1423     * @return array
1424     */
1425    private function getProcessedInstructions(): array
1426    {
1427
1428
1429        if (!$this->shouldInstructionProcess()) {
1430
1431            $this->processedInstructions = $this->instructionsCache->retrieveCache();
1432
1433        } else {
1434
1435            $this->processInstructions();
1436
1437        }
1438        return $this->processedInstructions;
1439
1440    }
1441
1442    /**
1443     * @throws ExceptionNotFound
1444     */
1445    public function getParent(): FetcherMarkup
1446    {
1447        if (!isset($this->parentMarkupHandler)) {
1448            throw new ExceptionNotFound();
1449        }
1450        return $this->parentMarkupHandler;
1451    }
1452
1453
1454}
1455