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