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