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