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