toWikiPath(); } catch (ExceptionCast $e) { /** * Not a wiki path, default to the default */ $contextPath = ExecutionContext::getActualOrCreateFromEnv()->getDefaultContextPath(); } } return FetcherMarkup::confRoot() ->setRequestedExecutingPath($executingPath) ->setRequestedContextPath($contextPath) ->setRequestedMimeToXhtml() ->build(); } public static function confRoot(): FetcherMarkupBuilder { return new FetcherMarkupBuilder(); } /** * Use mostly in test * The coutnerpart of {@link \ComboStrap\Test\TestUtility::renderText2XhtmlWithoutP()} * @throws ExceptionNotExists */ public static function createStandaloneExecutionFromStringMarkupToXhtml(string $markup): FetcherMarkup { return self::confRoot() ->setRequestedMarkupString($markup) ->setDeleteRootBlockElement(true) ->setRequestedContextPathWithDefault() ->setRequestedMimeToXhtml() ->setIsStandAloneCodeExecution(true) ->build(); } /** * Starts a child fetcher markup * This is needed for instructions or markup run * Why ? Because the snippets advertised during this run, need to be stored * and we need to know the original request path that is in the parent run. * @return FetcherMarkupBuilder */ public static function confChild(): FetcherMarkupBuilder { $executionContext = ExecutionContext::getActualOrCreateFromEnv(); try { $executing = $executionContext->getExecutingMarkupHandler(); } catch (ExceptionNotFound $e) { if (PluginUtility::isDevOrTest() && $executionContext->getExecutingAction() !== ExecutionContext::PREVIEW_ACTION) { LogUtility::warning("A markup handler is not running, we couldn't create a child."); } $contextPath = $executionContext->getContextPath(); return self::confRoot() ->setRequestedContextPath($contextPath); } return self::confRoot() ->setParentMarkupHandler($executing) ->setRequestedContextPath($executing->getRequestedContextPath()); } /** * Dokuwiki will wrap the markup in a p element * if the first element is not a block * This option permits to delete it. This is used mostly in test to get * the generated html */ public function deleteRootPElementsIfRequested(array &$instructions): void { if (!$this->deleteRootBlockElement) { return; } /** * Delete the p added by {@link Block::process()} * if the plugin of the {@link SyntaxPlugin::getPType() normal} and not in a block * * p_open = document_start in renderer */ if ($instructions[1][0] !== 'p_open') { return; } unset($instructions[1]); /** * The last p position is not fix * We may have other calls due for instance * of {@link \action_plugin_combo_syntaxanalytics} */ $n = 1; while (($lastPBlockPosition = (sizeof($instructions) - $n)) >= 0) { /** * p_open = document_end in renderer */ if ($instructions[$lastPBlockPosition][0] == 'p_close') { unset($instructions[$lastPBlockPosition]); break; } else { $n = $n + 1; } } } /** * * @param Url|null $url * @return Url * * Note: The fetch url is the {@link FetcherCache keyCache} */ function getFetchUrl(Url $url = null): Url { /** * Overwrite default fetcher endpoint * that is {@link UrlEndpoint::createFetchUrl()} */ $url = UrlEndpoint::createDokuUrl(); $url = parent::getFetchUrl($url); try { $wikiPath = $this->getSourcePath()->toWikiPath(); $url->addQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $wikiPath->getWikiId()); $url->addQueryParameter(WikiPath::DRIVE_ATTRIBUTE, $wikiPath->getDrive()); } catch (ExceptionCast|ExceptionNotFound $e) { // not an accessible source path } $url->addQueryParameter("context-id", $this->getRequestedContextPath()->getWikiId()); return $url; } /** * @return Mime */ public function getMime(): Mime { if (isset($this->mime)) { return $this->mime; } // XHTML default try { return Mime::createFromExtension(self::XHTML_MODE); } catch (ExceptionNotFound $e) { // should not happen throw new ExceptionRuntime("Internal error: The XHTML mime was not found.", self::CANONICAL, 1, $e); } } /** * TODO: split We should split fetcherMarkup by object type output and {@link Mime} * @return bool */ private function shouldInstructionProcess(): bool { if (!$this->isPathExecution()) { return true; } if (isset($this->processedInstructions)) { return false; } /** * Edge Case * (as dokuwiki starts the rendering process here * we need to set the execution id) */ $executionContext = ExecutionContext::getActualOrCreateFromEnv()->setExecutingMarkupHandler($this); try { $useCache = $this->instructionsCache->useCache(); } finally { $executionContext->closeExecutingMarkupHandler(); } return ($useCache === false); } public function shouldProcess(): bool { if (!$this->isPathExecution()) { return true; } if ($this->hasExecuted) { return false; } /** * The cache is stored by requested page scope * * We set the environment because * {@link CacheParser::useCache()} may call a parsing of the markup fragment * And the global environment are not always passed * in all actions and is needed to log the {@link CacheResult cache * result} * * Use cache should be always called because it trigger * the event coupled to the cache (ie PARSER_CACHE_USE) */ $depends['age'] = $this->getCacheAge(); if ($this->isFragment()) { /** * Fragment may use variables of the requested page * We have dependency on {@link MarkupCacheDependencies::PAGE_PRIMARY_META_DEPENDENCY} * but as they may be derived such as the {@link PageTitle} * comes from the H1 or the feature image comes from the first image in the section 1 * We can't really use this event. */ try { $depends['files'][] = FetcherMarkup::confRoot() ->setRequestedContextPath($this->getRequestedContextPath()) ->setRequestedExecutingPath($this->getRequestedContextPath()) ->setRequestedMimeToMetadata() ->build() ->getMetadataPath() ->toAbsoluteId(); } catch (ExceptionNotExists|ExceptionNotFound $e) { /** * Computer are hard * At the beginning there is no markup path * We may get this error then * * We don't allow on test */ if (PluginUtility::isTest()) { /** * The first edit, the page does not exists */ $executingAction = ExecutionContext::getActualOrCreateFromEnv()->getExecutingAction(); if (!in_array($executingAction, [ExecutionContext::EDIT_ACTION, ExecutionContext::PREVIEW_ACTION])) { LogUtility::error("The metadata path should be known. " . $e->getMessage(), self::CANONICAL, $e); } } } } /** * Edge Case * (as dokuwiki starts the rendering process here * we need to set the execution id) */ $executionContext = ExecutionContext::getActualOrCreateFromEnv() ->setExecutingMarkupHandler($this); try { $useCache = $this->contentCache->useCache($depends); } finally { $executionContext->closeExecutingMarkupHandler(); } return ($useCache === false); } public function storeSnippets() { /** * Snippet */ $snippets = $this->getSnippets(); $jsonDecodeSnippets = SnippetSystem::toJsonArrayFromSlotSnippets($snippets); /** * Cache file * Using a cache parser, set the page id and will trigger * the parser cache use event in order to log/report the cache usage * At {@link action_plugin_combo_cache::createCacheReport()} */ $snippetCache = $this->getSnippetCacheStore(); $this->outputCacheDependencies->rerouteCacheDestination($snippetCache); if (count($jsonDecodeSnippets) > 0) { $data1 = json_encode($jsonDecodeSnippets); $snippetCache->storeCache($data1); } else { $snippetCache->removeCache(); } } /** * This functon loads the snippets in the global array * by creating them. Not ideal but works for now. * @return Snippet[] */ public function loadSnippets(): array { $snippetCacheStore = $this->getSnippetCacheStore(); $data = $snippetCacheStore->retrieveCache(); $nativeSnippets = []; if (!empty($data)) { $jsonDecodeSnippets = json_decode($data, true); foreach ($jsonDecodeSnippets as $snippet) { try { $nativeSnippets[] = Snippet::createFromJson($snippet); } catch (ExceptionCompile $e) { LogUtility::error("The snippet json array cannot be build into a snippet object. " . $e->getMessage() . "\n" . ArrayUtility::formatAsString($snippet), LogUtility::SUPPORT_CANONICAL,); } } } return $nativeSnippets; } private function removeSnippets() { $snippetCacheFile = $this->getSnippetCacheStore()->cache; if ($snippetCacheFile !== null) { if (file_exists($snippetCacheFile)) { unlink($snippetCacheFile); } } } /** * @return CacheParser - the cache where the snippets are stored * Cache file * Using a cache parser, set the page id and will trigger * the parser cache use event in order to log/report the cache usage * At {@link action_plugin_combo_cache::createCacheReport()} */ public function getSnippetCacheStore(): CacheParser { if (isset($this->snippetCache)) { return $this->snippetCache; } if ($this->isPathExecution()) { throw new ExceptionRuntimeInternal("A source path should be available as this is a path execution"); } throw new ExceptionRuntime("There is no snippet cache store for a non-path execution"); } public function getDependenciesCacheStore(): CacheParser { return $this->outputCacheDependencies->getDependenciesCacheStore(); } public function getDependenciesCachePath(): LocalPath { $cachePath = $this->outputCacheDependencies->getDependenciesCacheStore()->cache; return LocalPath::createFromPathString($cachePath); } /** * @return LocalPath the fetch path - start the process and returns a path. If the cache is on, return the {@link FetcherMarkup::getContentCachePath()} * @throws ExceptionCompile */ function processIfNeededAndGetFetchPath(): LocalPath { $this->processIfNeeded(); /** * The cache path may have change due to the cache key rerouting * We should there always use the {@link FetcherMarkup::getContentCachePath()} * as fetch path */ return $this->getContentCachePath(); } /** * @return $this * @throws ExceptionCompile */ public function process(): FetcherMarkup { $this->hasExecuted = true; /** * Rendering */ $executionContext = (ExecutionContext::getActualOrCreateFromEnv()); $extension = $this->getMime()->getExtension(); switch ($extension) { case MarkupRenderer::METADATA_EXTENSION: /** * The user may ask just for the metadata * and should then use the {@link self::getMetadata()} * function instead */ break; case MarkupRenderer::INSTRUCTION_EXTENSION: /** * The user may ask just for the instuctions * and should then use the {@link self::getInstructions()} * function to get the instructions */ return $this; default: $instructions = $this->getInstructions(); /** * Edge case: We delete here * because the instructions may have been created by dokuwiki * when we test for the cache with {@link CacheParser::useCache()} */ if ($this->deleteRootBlockElement) { self::deleteRootPElementsIfRequested($instructions); } if (!isset($this->builderName)) { $this->builderName = $this->getMime()->getExtension(); } $executionContext->setExecutingMarkupHandler($this); try { if ($this->isDocument()) { $markupRenderer = MarkupRenderer::createFromMarkupInstructions($instructions, $this) ->setRequestedMime($this->getMime()) ->setRendererName($this->builderName); $output = $markupRenderer->getOutput(); if ($output === null && !empty($instructions)) { LogUtility::error("The renderer ({$this->builderName}) seems to have been not found"); } $this->cacheAfterRendering = $markupRenderer->getCacheAfterRendering(); } else { if (!isset($this->markupDynamicRender)) { $this->markupDynamicRender = MarkupDynamicRender::create($this->builderName); } $output = $this->markupDynamicRender->processInstructions($instructions); } } catch (\Exception $e) { /** * Example of errors; * method_exists() expects parameter 2 to be string, array given * inc\parserutils.php:672 */ throw new ExceptionCompile("An error has occurred while getting the output. Error: {$e->getMessage()}", self::CANONICAL, 1, $e); } finally { $executionContext->closeExecutingMarkupHandler(); } if (is_array($output)) { LogUtility::internalError("The output was an array", self::CANONICAL); $this->fetchString = serialize($output); } else { $this->fetchString = $output; } break; } /** * Storage of snippets or dependencies * none if this is not a path execution * and for now, metadata storage is done by dokuwiki */ if (!$this->isPathExecution() || $this->mime->getExtension() === MarkupRenderer::METADATA_EXTENSION) { return $this; } /** * Snippets and cache dependencies are only for HTML rendering * Otherwise, otherwise other type rendering may override them * (such as analtyical json, ...) */ if (in_array($this->getMime()->toString(), [Mime::XHTML, Mime::HTML])) { /** * We make the Snippet store to Html store an atomic operation * * Why ? Because if the rendering of the page is stopped, * the cache of the HTML page may be stored but not the cache of the snippets * leading to a bad page because the next rendering will see then no snippets. */ try { $this->storeSnippets(); } catch (Exception $e) { // if any write os exception LogUtility::msg("Error while storing the xhtml content: {$e->getMessage()}"); $this->removeSnippets(); } /** * Cache output dependencies * Reroute the cache output by runtime dependencies * set during processing */ $this->outputCacheDependencies->storeDependencies(); $this->outputCacheDependencies->rerouteCacheDestination($this->contentCache); } /** * We store always the output in the cache * if the cache is not on, the file is just overwritten * * We don't use * {{@link CacheParser::storeCache()} * because it uses the protected parameter `__nocache` * that will disallow the storage */ io_saveFile($this->contentCache->cache, $this->fetchString); return $this; } function getBuster(): string { // no buster return ""; } public function getFetcherName(): string { return "markup-fetcher"; } private function getCacheAge(): int { $extension = $this->getMime()->getExtension(); switch ($extension) { case self::XHTML_MODE: if (!Site::isHtmlRenderCacheOn()) { return 0; } break; case MarkupRenderer::INSTRUCTION_EXTENSION: // indefinitely return self::MAX_CACHE_AGE; } try { $requestedCache = $this->getRequestedCache(); } catch (ExceptionNotFound $e) { $requestedCache = IFetcherAbs::RECACHE_VALUE; } $cacheAge = $this->getCacheMaxAgeInSec($requestedCache); return $this->cacheAfterRendering ? $cacheAge : 0; } public function __toString() { return parent::__toString() . " ({$this->getSourceName()}, {$this->getMime()->toString()})"; } /** * @throws ExceptionBadArgument */ public function buildFromTagAttributes(TagAttributes $tagAttributes): FetcherMarkup { parent::buildFromTagAttributes($tagAttributes); return $this; } /** * @return LocalPath - the cache path is where the result is stored if the cache is on * The cache path may have change due to the cache key rerouting * We should there always use the {@link FetcherMarkup::getContentCachePath()} * as fetch path */ public function getContentCachePath(): LocalPath { $path = $this->contentCache->cache; return LocalPath::createFromPathString($path); } public function getOutputCacheDependencies(): MarkupCacheDependencies { return $this->outputCacheDependencies; } /** * @return string - with replacement if any * TODO: edit button replacement could be a script tag with a json, permits to do DOM manipulation * @throws ExceptionCompile - if any processing error occurs */ public function getFetchString(): string { $this->processIfNeeded(); if (!$this->isPathExecution()) { return $this->fetchString; } /** * Source path execution * The cache path may have change due to the cache key rerouting * We should there always use the {@link FetcherMarkup::getContentCachePath()} * as fetch path */ $path = $this->getContentCachePath(); try { $text = FileSystems::getContent($path); } catch (ExceptionNotFound $e) { throw new ExceptionRuntime("Internal error: The fetch path should exists.", self::CANONICAL, 1, $e); } /** * Edit button Processing for XHtml * (Path is mandatory to create the buttons) */ if (!in_array($this->getMime()->getExtension(), ["html", "xhtml"])) { return $text; } try { if ($this->getSourcePath()->toWikiPath()->getDrive() !== WikiPath::MARKUP_DRIVE) { // case when this is a default page in the resource/template directory return EditButton::deleteAll($text); } } catch (ExceptionNotFound|ExceptionCast $e) { // not a wiki path } return EditButton::replaceOrDeleteAll($text); } public function getLabel(): string { try { $sourcePath = $this->getSourcePath(); } catch (ExceptionNotFound $e) { return self::MARKUP_DYNAMIC_EXECUTION_NAME; } return ResourceName::getFromPath($sourcePath); } public function getRequestedContextPath(): WikiPath { return $this->requestedContextPath; } /** * @throws ExceptionNotFound */ public function getSourcePath(): Path { if (isset($this->markupSourcePath)) { return $this->markupSourcePath; } throw new ExceptionNotFound("No source path for this markup"); } /** * Utility class that return the source path * @return Path * @throws ExceptionNotFound */ public function getRequestedExecutingPath(): Path { return $this->getSourcePath(); } /** * @return Path|null - utility class to get the source markup path or null (if this is a markup snippet/string rendering) */ public function getExecutingPathOrNull(): ?Path { try { return $this->getSourcePath(); } catch (ExceptionNotFound $e) { return null; } } /** * @param string $componentId * @return Snippet[] */ public function getSnippetsForComponent(string $componentId): array { $snippets = $this->getSnippets(); $snippetsForComponent = []; foreach ($snippets as $snippet) { try { if ($snippet->getComponentId() === $componentId) { $snippetsForComponent[] = $snippet; } } catch (ExceptionNotFound $e) { // } } return $snippetsForComponent; } /** * @return Snippet[] */ public function getSnippets(): array { $snippets = $this->localSnippets; /** * Old ways where snippets were added to the global scope * and not to the fetcher markup via {@link self::addSnippet()} * * During the transition, we support the two * * Note that with the new system where render code * can access this object via {@link ExecutionContext::getExecutingMarkupHandler()} * the code may had snippets without any id * (For the time being, not yet) */ try { $slotId = $this->getSourcePath()->toWikiPath()->getWikiId(); } catch (ExceptionNotFound $e) { // a markup string run return $snippets; } catch (ExceptionCast $e) { // not a wiki path return $snippets; } $snippetManager = PluginUtility::getSnippetManager(); $oldWaySnippets = $snippetManager->getSnippetsForSlot($slotId); return array_merge($oldWaySnippets, $snippets); } /** * @param Snippet $snippet * @return FetcherMarkup */ public function addSnippet(Snippet $snippet): FetcherMarkup { /** * Snippet should be added only when they can be store * (ie when this is path execution) * If this is not a path execution, the snippet cannot be * stored in a cache and are therefore lost if not used */ /** * If there is a parent markup handler * Store the snippets there */ if (isset($this->parentMarkupHandler)) { $this->parentMarkupHandler->addSnippet($snippet); return $this; } if (!$this->isPathExecution() && !$this->isNonPathStandaloneExecution // In preview, there is no parent handler because we didn't take over && ExecutionContext::getActualOrCreateFromEnv()->getExecutingAction() !== ExecutionContext::PREVIEW_ACTION ) { 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."); } if (!in_array($this->getMime()->toString(), [Mime::XHTML, Mime::HTML])) { LogUtility::warning("The execution ($this) is not a HTML execution. The snippet $snippet will not be preserved because they are reserved for XHMTL execution"); } $snippetGuid = $snippet->getPath()->toUriString(); $this->localSnippets[$snippetGuid] = $snippet; return $this; } /** * @return bool true if the markup string comes from a path * This is motsly important for cache as we use the path as the cache key * (Cache: * * of the {@link self::getInstructions() instructions}, * * of the {@link self::getOutputCacheDependencies() output dependencies} * * of the {@link self::getSnippets() snippets} * * of the {@link self::processMetadataIfNotYetDone() metadata} * * The rule is this is a path execution of the {@link self::$markupSourcePath executing source path} is set. * * Ie this is not a path execution, if the input is: * * {@link self::$requestedInstructions} (used for templating) * * a {@link self::$markupString} (used for test or webcode) * */ public function isPathExecution(): bool { if (isset($this->markupSourcePath)) { return true; } return false; } /** * @throws ExceptionCompile - if any processing errors occurs */ public function processIfNeeded(): FetcherMarkup { if (!$this->shouldProcess()) { return $this; } $this->process(); return $this; } /** * @return array - the markup instructions * @throws ExceptionNotExists - if the executing markup file does not exist */ public function getInstructions(): array { if (isset($this->requestedInstructions)) { return $this->requestedInstructions; } /** * We create a fetcher markup to not have the same {@link self::getId()} * on execution */ if (ExecutionContext::getActualOrCreateFromEnv()->hasExecutingMarkupHandler()) { $fetcherMarkupBuilder = FetcherMarkup::confChild(); } else { $fetcherMarkupBuilder = FetcherMarkup::confRoot(); } $fetcherMarkupBuilder = $fetcherMarkupBuilder ->setRequestedMime(Mime::create(Mime::INSTRUCTIONS)) ->setRequestedRenderer(FetcherMarkupInstructions::NAME) ->setIsDocument($this->isDoc) ->setRequestedContextPath($this->getRequestedContextPath()); if ($this->isPathExecution()) { $fetcherMarkupBuilder->setRequestedExecutingPath($this->getExecutingPathOrFail()); } else { $fetcherMarkupBuilder->setRequestedMarkupString($this->markupString); } $fetcherMarkup = $fetcherMarkupBuilder->build(); return $fetcherMarkup->getProcessedInstructions(); } /** * @return bool - a document * * A document will get an {@link Outline} processing * while a {@link self::isFragment() fragment} will not. */ public function isDocument(): bool { return $this->isDoc; } public function getSnippetManager(): SnippetSystem { return PluginUtility::getSnippetManager(); } /** * @throws ExceptionBadSyntax * @throws ExceptionCompile */ public function getFetchStringAsDom(): XmlDocument { return XmlDocument::createXmlDocFromMarkup($this->getFetchString()); } public function getSnippetsAsHtmlString(): string { try { $globalSnippets = SnippetSystem::getFromContext()->getSnippetsForSlot($this->getRequestedExecutingPath()->toAbsoluteId()); } catch (ExceptionNotFound $e) { // string execution $globalSnippets = []; } $allSnippets = array_merge($globalSnippets, $this->localSnippets); return SnippetSystem::toHtmlFromSnippetArray($allSnippets); } public function isFragment(): bool { return $this->isDocument() === false; } private function getMarkupStringToExecute(): string { if (isset($this->markupString)) { return $this->markupString; } else { try { $sourcePath = $this->getSourcePath(); } catch (ExceptionNotFound $e) { throw new ExceptionRuntimeInternal("A markup or a source markup path should be specified."); } try { return FileSystems::getContent($sourcePath); } catch (ExceptionNotFound $e) { 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); return ""; } } } public function getContextData(): array { if (isset($this->contextData)) { return $this->contextData; } $this->contextData = MarkupPath::createPageFromPathObject($this->getRequestedContextPath())->getMetadataForRendering(); return $this->contextData; } public function getToc(): array { if (isset($this->toc)) { return $this->toc; } try { return TOC::createForPage($this->getRequestedExecutingPath())->getValue(); } catch (ExceptionNotFound $e) { // no executing page or no value } /** * Derived TOC from instructions */ return Outline::createFromCallStack(CallStack::createFromInstructions($this->getInstructions()))->toTocDokuwikiFormat(); } public function getInstructionsPath(): LocalPath { $path = $this->instructionsCache->cache; return LocalPath::createFromPathString($path); } public function getOutline(): Outline { $instructions = $this->getInstructions(); $callStack = CallStack::createFromInstructions($instructions); try { $markupPath = MarkupPath::createPageFromPathObject($this->getRequestedExecutingPath()); } catch (ExceptionNotFound $e) { $markupPath = null; } return Outline::createFromCallStack($callStack, $markupPath); } public function getMetadata(): array { $this->processMetadataIfNotYetDone(); return $this->meta; } /** * Adaptation of {@link p_get_metadata()} * to take into account {@link self::getInstructions()} * where we can just pass our own instructions. * * And yes, adaptation of {@link p_get_metadata()} * that process the metadata. Yeah, it calls {@link p_render_metadata()} * and save them * */ public function processMetadataIfNotYetDone(): FetcherMarkup { /** * Already set ? */ if (isset($this->meta)) { return $this; } $actualMeta = []; /** * We wrap the whole block * because {@link CacheRenderer::useCache()} * and the renderer needs it */ $executionContext = ExecutionContext::getActualOrCreateFromEnv()->setExecutingMarkupHandler($this); try { /** * Can we read from the meta file */ if ($this->isPathExecution()) { /** * If the meta file exists */ if (FileSystems::exists($this->getMetaPathOrFail())) { $executingPath = $this->getExecutingPathOrFail(); $actualMeta = MetadataDokuWikiStore::getOrCreateFromResource(MarkupPath::createPageFromPathObject($executingPath)) ->getDataCurrentAndPersistent(); /** * The metadata useCache function has side effect * and triggers a render that fails if the wiki file does not exists */ $depends['files'][] = $this->instructionsCache->cache; $depends['files'][] = $executingPath->toAbsolutePath()->toAbsoluteId(); $useCache = $this->metaCache->useCache($depends); if ($useCache) { $this->meta = $actualMeta; return $this; } } } /** * Process and derived meta */ try { $wikiId = $this->getRequestedExecutingPath()->toWikiPath()->getWikiId(); } catch (ExceptionCast|ExceptionNotFound $e) { // not a wiki path execution $wikiId = null; } /** * Dokuwiki global variable used to see if the process is in rendering mode * See {@link p_get_metadata()} * Store the original metadata in the global $METADATA_RENDERERS * ({@link p_set_metadata()} use it) */ global $METADATA_RENDERERS; $METADATA_RENDERERS[$wikiId] =& $actualMeta; // add an extra key for the event - to tell event handlers the page whose metadata this is $actualMeta['page'] = $wikiId; $evt = new \dokuwiki\Extension\Event('PARSER_METADATA_RENDER', $actualMeta); if ($evt->advise_before()) { // get instructions (from string or file) $instructions = $this->getInstructions(); // set up the renderer $renderer = new Doku_Renderer_metadata(); /** * Runtime/ Derived metadata * The runtime meta are not even deleted * (See {@link p_render_metadata()} */ $renderer->meta =& $actualMeta['current']; /** * The {@link Doku_Renderer_metadata} * will fail if the file and the date modified property does not exist */ try { $path = $this->getRequestedExecutingPath(); if (!FileSystems::exists($path)) { $renderer->meta['date']['modified'] = null; } } catch (ExceptionNotFound $e) { // ok } /** * The persistent data are now available */ $renderer->persistent =& $actualMeta['persistent']; // Loop through the instructions foreach ($instructions as $instruction) { // execute the callback against the renderer call_user_func_array(array(&$renderer, $instruction[0]), (array)$instruction[1]); } $evt->result = array('current' => &$renderer->meta, 'persistent' => &$renderer->persistent); } $evt->advise_after(); $this->meta = $evt->result; /** * Dokuwiki global variable * See {@link p_get_metadata()} */ unset($METADATA_RENDERERS[$wikiId]); /** * Storage */ if ($wikiId !== null) { p_save_metadata($wikiId, $this->meta); $this->metaCache->storeCache(time()); } } finally { $executionContext->closeExecutingMarkupHandler(); } return $this; } /** * @throws ExceptionNotFound */ public function getMetadataPath(): LocalPath { if (isset($this->metaPath)) { return $this->metaPath; } throw new ExceptionNotFound("No meta path for this markup"); } /** * A wrapper from when we are in a code block * were we expect to be a {@link self::isPathExecution()} * All path should then be available * @return Path */ private function getExecutingPathOrFail(): Path { try { return $this->getRequestedExecutingPath(); } catch (ExceptionNotFound $e) { throw new ExceptionRuntime($e); } } /** * A wrapper from when we are in a code block * were we expect to be a {@link self::isPathExecution()} * All path should then be available * @return Path */ private function getMetaPathOrFail() { try { return $this->getMetadataPath(); } catch (ExceptionNotFound $e) { throw new ExceptionRuntime($e); } } /** * Process the instructions * TODO: move to a FetcherMarkup by tree instruction and array/encoding mime * @return $this */ public function processInstructions(): FetcherMarkup { if (isset($this->processedInstructions)) { return $this; } $markup = $this->getMarkupStringToExecute(); $executionContext = ExecutionContext::getActualOrCreateFromEnv() ->setExecutingMarkupHandler($this); try { $markupRenderer = MarkupRenderer::createFromMarkup($markup, $this->getExecutingPathOrNull(), $this->getRequestedContextPath()) ->setRequestedMimeToInstruction(); $instructions = $markupRenderer->getOutput(); if (isset($this->instructionsCache)) { /** * Not a string execution, ie {@link self::isPathExecution()} * a path execution */ $this->instructionsCache->storeCache($instructions); } $this->processedInstructions = $instructions; return $this; } catch (\Exception $e) { throw new ExceptionRuntimeInternal("An error has occurred while processing the instructions. Error: {$e->getMessage()}", self::CANONICAL, 1, $e); } finally { $executionContext->closeExecutingMarkupHandler(); } } public function getSnippetCachePath(): LocalPath { $cache = $this->getSnippetCacheStore()->cache; return LocalPath::createFromPathString($cache); } /** * @return string - an execution id to be sure that we don't execute the same twice in recursion */ public function getId(): string { return "({$this->getSourceName()}) to ({$this->builderName} - {$this->getMime()}) with context ({$this->getRequestedContextPath()->toUriString()})"; } /** * @return string - a name for the source (used in {@link self::__toString()} and {@link self::getId() identification}) */ private function getSourceName(): string { if (!$this->isPathExecution()) { if (isset($this->markupString)) { $md5 = md5($this->markupString); return "Markup String Execution ($md5)"; } elseif (isset($this->requestedInstructions)) { return "Markup Instructions Execution"; } else { LogUtility::internalError("The name of the markup handler is unknown"); return "Markup Unknown Execution"; } } else { try { return $this->getSourcePath()->toUriString(); } catch (ExceptionNotFound $e) { throw new ExceptionRuntimeInternal("A source path should be defined if it's not a markup string execution"); } } } /** * TODO: move to a FetcherMarkup by object type output and mime * @return array */ private function getProcessedInstructions(): array { if (!$this->shouldInstructionProcess()) { $this->processedInstructions = $this->instructionsCache->retrieveCache(); } else { $this->processInstructions(); } return $this->processedInstructions; } /** * @throws ExceptionNotFound */ public function getParent(): FetcherMarkup { if (!isset($this->parentMarkupHandler)) { throw new ExceptionNotFound(); } return $this->parentMarkupHandler; } /** * Hack to not have the {@link MarkupDynamicRender} * stored in {@link FetcherMarkup::$markupDynamicRenderer} * to reset * @param array $requestedInstructions * @param array|null $row * @return FetcherMarkup */ public function setNextIteratorInstructionsWithContext(array $requestedInstructions, array $row = null): FetcherMarkup { $this->hasExecuted = false; if ($row === null) { unset($this->contextData); } else { $this->contextData = $row; } $this->requestedInstructions = $requestedInstructions; return $this; } }