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 /** 945 * The template is not part of the execution. 946 * Therefore when doing a page bundle, there is no way to set that this a standalone execution 947 * Hack to not get this message with a {@link self::MARKUP_DYNAMIC_EXECUTION_NAME} inside a page bundle 948 */ 949 if ($_GET["do"] !== "combo_" . FetcherPageBundler::NAME) { 950 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."); 951 } 952 } 953 if (!in_array($this->getMime()->toString(), [Mime::XHTML, Mime::HTML])) { 954 LogUtility::warning("The execution ($this) is not a HTML execution. The snippet $snippet will not be preserved because they are reserved for XHMTL execution"); 955 } 956 957 $snippetGuid = $snippet->getPath()->toUriString(); 958 $this->localSnippets[$snippetGuid] = $snippet; 959 return $this; 960 961 962 } 963 964 /** 965 * @return bool true if the markup string comes from a path 966 * This is motsly important for cache as we use the path as the cache key 967 * (Cache: 968 * * of the {@link self::getInstructions() instructions}, 969 * * of the {@link self::getOutputCacheDependencies() output dependencies} 970 * * of the {@link self::getSnippets() snippets} 971 * * of the {@link self::processMetadataIfNotYetDone() metadata} 972 * 973 * The rule is this is a path execution of the {@link self::$markupSourcePath executing source path} is set. 974 * 975 * Ie this is not a path execution, if the input is: 976 * * {@link self::$requestedInstructions} (used for templating) 977 * * a {@link self::$markupString} (used for test or webcode) 978 * 979 */ 980 public function isPathExecution(): bool 981 { 982 if (isset($this->markupSourcePath)) { 983 return true; 984 } 985 return false; 986 } 987 988 /** 989 * @throws ExceptionCompile - if any processing errors occurs 990 */ 991 public function processIfNeeded(): FetcherMarkup 992 { 993 994 if (!$this->shouldProcess()) { 995 return $this; 996 } 997 998 $this->process(); 999 return $this; 1000 1001 } 1002 1003 1004 /** 1005 * @return array - the markup instructions 1006 * @throws ExceptionNotExists - if the executing markup file does not exist 1007 */ 1008 public function getInstructions(): array 1009 { 1010 1011 if (isset($this->requestedInstructions)) { 1012 1013 return $this->requestedInstructions; 1014 1015 } 1016 1017 /** 1018 * We create a fetcher markup to not have the same {@link self::getId()} 1019 * on execution 1020 */ 1021 if (ExecutionContext::getActualOrCreateFromEnv()->hasExecutingMarkupHandler()) { 1022 $fetcherMarkupBuilder = FetcherMarkup::confChild(); 1023 } else { 1024 $fetcherMarkupBuilder = FetcherMarkup::confRoot(); 1025 } 1026 $fetcherMarkupBuilder = $fetcherMarkupBuilder 1027 ->setRequestedMime(Mime::create(Mime::INSTRUCTIONS)) 1028 ->setRequestedRenderer(FetcherMarkupInstructions::NAME) 1029 ->setIsDocument($this->isDoc) 1030 ->setRequestedContextPath($this->getRequestedContextPath()); 1031 if ($this->isPathExecution()) { 1032 $fetcherMarkupBuilder->setRequestedExecutingPath($this->getExecutingPathOrFail()); 1033 } else { 1034 $fetcherMarkupBuilder->setRequestedMarkupString($this->markupString); 1035 } 1036 $fetcherMarkup = $fetcherMarkupBuilder->build(); 1037 return $fetcherMarkup->getProcessedInstructions(); 1038 1039 1040 } 1041 1042 1043 /** 1044 * @return bool - a document 1045 * 1046 * A document will get an {@link Outline} processing 1047 * while a {@link self::isFragment() fragment} will not. 1048 */ 1049 public function isDocument(): bool 1050 { 1051 1052 return $this->isDoc; 1053 1054 } 1055 1056 public function getSnippetManager(): SnippetSystem 1057 { 1058 return PluginUtility::getSnippetManager(); 1059 } 1060 1061 /** 1062 * @throws ExceptionBadSyntax 1063 * @throws ExceptionCompile 1064 */ 1065 public function getFetchStringAsDom(): XmlDocument 1066 { 1067 return XmlDocument::createXmlDocFromMarkup($this->getFetchString()); 1068 } 1069 1070 public function getSnippetsAsHtmlString(): string 1071 { 1072 1073 try { 1074 $globalSnippets = SnippetSystem::getFromContext()->getSnippetsForSlot($this->getRequestedExecutingPath()->toAbsoluteId()); 1075 } catch (ExceptionNotFound $e) { 1076 // string execution 1077 $globalSnippets = []; 1078 } 1079 $allSnippets = array_merge($globalSnippets, $this->localSnippets); 1080 return SnippetSystem::toHtmlFromSnippetArray($allSnippets); 1081 1082 } 1083 1084 public function isFragment(): bool 1085 { 1086 return $this->isDocument() === false; 1087 } 1088 1089 private function getMarkupStringToExecute(): string 1090 { 1091 if (isset($this->markupString)) { 1092 return $this->markupString; 1093 } else { 1094 try { 1095 $sourcePath = $this->getSourcePath(); 1096 } catch (ExceptionNotFound $e) { 1097 throw new ExceptionRuntimeInternal("A markup or a source markup path should be specified."); 1098 } 1099 try { 1100 return FileSystems::getContent($sourcePath); 1101 } catch (ExceptionNotFound $e) { 1102 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); 1103 return ""; 1104 } 1105 } 1106 } 1107 1108 public function getContextData(): array 1109 { 1110 if (isset($this->contextData)) { 1111 return $this->contextData; 1112 } 1113 $this->contextData = MarkupPath::createPageFromPathObject($this->getRequestedContextPath())->getMetadataForRendering(); 1114 return $this->contextData; 1115 } 1116 1117 1118 public function getToc(): array 1119 { 1120 1121 if (isset($this->toc)) { 1122 return $this->toc; 1123 } 1124 try { 1125 return TOC::createForPage($this->getRequestedExecutingPath())->getValue(); 1126 } catch (ExceptionNotFound $e) { 1127 // no executing page or no value 1128 } 1129 /** 1130 * Derived TOC from instructions 1131 */ 1132 return Outline::createFromCallStack(CallStack::createFromInstructions($this->getInstructions()))->toTocDokuwikiFormat(); 1133 1134 1135 } 1136 1137 public function getInstructionsPath(): LocalPath 1138 { 1139 $path = $this->instructionsCache->cache; 1140 return LocalPath::createFromPathString($path); 1141 } 1142 1143 public function getOutline(): Outline 1144 { 1145 $instructions = $this->getInstructions(); 1146 $callStack = CallStack::createFromInstructions($instructions); 1147 try { 1148 $markupPath = MarkupPath::createPageFromPathObject($this->getRequestedExecutingPath()); 1149 } catch (ExceptionNotFound $e) { 1150 $markupPath = null; 1151 } 1152 return Outline::createFromCallStack($callStack, $markupPath); 1153 } 1154 1155 1156 public function getMetadata(): array 1157 { 1158 1159 $this->processMetadataIfNotYetDone(); 1160 return $this->meta; 1161 1162 } 1163 1164 1165 /** 1166 * Adaptation of {@link p_get_metadata()} 1167 * to take into account {@link self::getInstructions()} 1168 * where we can just pass our own instructions. 1169 * 1170 * And yes, adaptation of {@link p_get_metadata()} 1171 * that process the metadata. Yeah, it calls {@link p_render_metadata()} 1172 * and save them 1173 * 1174 */ 1175 public function processMetadataIfNotYetDone(): FetcherMarkup 1176 { 1177 1178 /** 1179 * Already set ? 1180 */ 1181 if (isset($this->meta)) { 1182 return $this; 1183 } 1184 1185 $actualMeta = []; 1186 1187 /** 1188 * We wrap the whole block 1189 * because {@link CacheRenderer::useCache()} 1190 * and the renderer needs it 1191 */ 1192 $executionContext = ExecutionContext::getActualOrCreateFromEnv()->setExecutingMarkupHandler($this); 1193 try { 1194 1195 /** 1196 * Can we read from the meta file 1197 */ 1198 1199 1200 if ($this->isPathExecution()) { 1201 1202 /** 1203 * If the meta file exists 1204 */ 1205 if (FileSystems::exists($this->getMetaPathOrFail())) { 1206 1207 $executingPath = $this->getExecutingPathOrFail(); 1208 $actualMeta = MetadataDokuWikiStore::getOrCreateFromResource(MarkupPath::createPageFromPathObject($executingPath)) 1209 ->getDataCurrentAndPersistent(); 1210 1211 /** 1212 * The metadata useCache function has side effect 1213 * and triggers a render that fails if the wiki file does not exists 1214 */ 1215 $depends['files'][] = $this->instructionsCache->cache; 1216 $depends['files'][] = $executingPath->toAbsolutePath()->toAbsoluteId(); 1217 $useCache = $this->metaCache->useCache($depends); 1218 if ($useCache) { 1219 $this->meta = $actualMeta; 1220 return $this; 1221 } 1222 } 1223 } 1224 1225 /** 1226 * Process and derived meta 1227 */ 1228 try { 1229 $wikiId = $this->getRequestedExecutingPath()->toWikiPath()->getWikiId(); 1230 } catch (ExceptionCast|ExceptionNotFound $e) { 1231 // not a wiki path execution 1232 $wikiId = null; 1233 } 1234 1235 /** 1236 * Dokuwiki global variable used to see if the process is in rendering mode 1237 * See {@link p_get_metadata()} 1238 * Store the original metadata in the global $METADATA_RENDERERS 1239 * ({@link p_set_metadata()} use it) 1240 */ 1241 global $METADATA_RENDERERS; 1242 $METADATA_RENDERERS[$wikiId] =& $actualMeta; 1243 1244 // add an extra key for the event - to tell event handlers the page whose metadata this is 1245 $actualMeta['page'] = $wikiId; 1246 $evt = new \dokuwiki\Extension\Event('PARSER_METADATA_RENDER', $actualMeta); 1247 if ($evt->advise_before()) { 1248 1249 // get instructions (from string or file) 1250 $instructions = $this->getInstructions(); 1251 1252 // set up the renderer 1253 $renderer = new Doku_Renderer_metadata(); 1254 1255 1256 /** 1257 * Runtime/ Derived metadata 1258 * The runtime meta are not even deleted 1259 * (See {@link p_render_metadata()} 1260 */ 1261 $renderer->meta =& $actualMeta['current']; 1262 1263 /** 1264 * The {@link Doku_Renderer_metadata} 1265 * will fail if the file and the date modified property does not exist 1266 */ 1267 try { 1268 $path = $this->getRequestedExecutingPath(); 1269 if (!FileSystems::exists($path)) { 1270 $renderer->meta['date']['modified'] = null; 1271 } 1272 } catch (ExceptionNotFound $e) { 1273 // ok 1274 } 1275 1276 /** 1277 * The persistent data are now available 1278 */ 1279 $renderer->persistent =& $actualMeta['persistent']; 1280 1281 // Loop through the instructions 1282 foreach ($instructions as $instruction) { 1283 // execute the callback against the renderer 1284 call_user_func_array(array(&$renderer, $instruction[0]), (array)$instruction[1]); 1285 } 1286 1287 $evt->result = array('current' => &$renderer->meta, 'persistent' => &$renderer->persistent); 1288 1289 } 1290 $evt->advise_after(); 1291 1292 $this->meta = $evt->result; 1293 1294 /** 1295 * Dokuwiki global variable 1296 * See {@link p_get_metadata()} 1297 */ 1298 unset($METADATA_RENDERERS[$wikiId]); 1299 1300 /** 1301 * Storage 1302 */ 1303 if ($wikiId !== null) { 1304 p_save_metadata($wikiId, $this->meta); 1305 $this->metaCache->storeCache(time()); 1306 } 1307 1308 } finally { 1309 $executionContext->closeExecutingMarkupHandler(); 1310 } 1311 return $this; 1312 1313 } 1314 1315 /** 1316 * @throws ExceptionNotFound 1317 */ 1318 public function getMetadataPath(): LocalPath 1319 { 1320 if (isset($this->metaPath)) { 1321 return $this->metaPath; 1322 } 1323 throw new ExceptionNotFound("No meta path for this markup"); 1324 } 1325 1326 /** 1327 * A wrapper from when we are in a code block 1328 * were we expect to be a {@link self::isPathExecution()} 1329 * All path should then be available 1330 * @return Path 1331 */ 1332 private 1333 function getExecutingPathOrFail(): Path 1334 { 1335 try { 1336 return $this->getRequestedExecutingPath(); 1337 } catch (ExceptionNotFound $e) { 1338 throw new ExceptionRuntime($e); 1339 } 1340 } 1341 1342 /** 1343 * A wrapper from when we are in a code block 1344 * were we expect to be a {@link self::isPathExecution()} 1345 * All path should then be available 1346 * @return Path 1347 */ 1348 private 1349 function getMetaPathOrFail() 1350 { 1351 try { 1352 return $this->getMetadataPath(); 1353 } catch (ExceptionNotFound $e) { 1354 throw new ExceptionRuntime($e); 1355 } 1356 } 1357 1358 /** 1359 * Process the instructions 1360 * TODO: move to a FetcherMarkup by tree instruction and array/encoding mime 1361 * @return $this 1362 */ 1363 public function processInstructions(): FetcherMarkup 1364 { 1365 if (isset($this->processedInstructions)) { 1366 return $this; 1367 } 1368 1369 $markup = $this->getMarkupStringToExecute(); 1370 $executionContext = ExecutionContext::getActualOrCreateFromEnv() 1371 ->setExecutingMarkupHandler($this); 1372 try { 1373 $markupRenderer = MarkupRenderer::createFromMarkup($markup, $this->getExecutingPathOrNull(), $this->getRequestedContextPath()) 1374 ->setRequestedMimeToInstruction(); 1375 $instructions = $markupRenderer->getOutput(); 1376 if (isset($this->instructionsCache)) { 1377 /** 1378 * Not a string execution, ie {@link self::isPathExecution()} 1379 * a path execution 1380 */ 1381 $this->instructionsCache->storeCache($instructions); 1382 } 1383 $this->processedInstructions = $instructions; 1384 return $this; 1385 } catch (\Exception $e) { 1386 throw new ExceptionRuntimeInternal("An error has occurred while processing the instructions. Error: {$e->getMessage()}", self::CANONICAL, 1, $e); 1387 } finally { 1388 $executionContext->closeExecutingMarkupHandler(); 1389 } 1390 } 1391 1392 public function getSnippetCachePath(): LocalPath 1393 { 1394 $cache = $this->getSnippetCacheStore()->cache; 1395 return LocalPath::createFromPathString($cache); 1396 1397 } 1398 1399 /** 1400 * @return string - an execution id to be sure that we don't execute the same twice in recursion 1401 */ 1402 public function getId(): string 1403 { 1404 1405 return "({$this->getSourceName()}) to ({$this->builderName} - {$this->getMime()}) with context ({$this->getRequestedContextPath()->toUriString()})"; 1406 } 1407 1408 /** 1409 * @return string - a name for the source (used in {@link self::__toString()} and {@link self::getId() identification}) 1410 */ 1411 private function getSourceName(): string 1412 { 1413 if (!$this->isPathExecution()) { 1414 if (isset($this->markupString)) { 1415 $md5 = md5($this->markupString); 1416 return "Markup String Execution ($md5)"; 1417 } elseif (isset($this->requestedInstructions)) { 1418 return "Markup Instructions Execution"; 1419 } else { 1420 LogUtility::internalError("The name of the markup handler is unknown"); 1421 return "Markup Unknown Execution"; 1422 1423 } 1424 } else { 1425 try { 1426 return $this->getSourcePath()->toUriString(); 1427 } catch (ExceptionNotFound $e) { 1428 throw new ExceptionRuntimeInternal("A source path should be defined if it's not a markup string execution"); 1429 } 1430 } 1431 } 1432 1433 1434 /** 1435 * TODO: move to a FetcherMarkup by object type output and mime 1436 * @return array 1437 */ 1438 private function getProcessedInstructions(): array 1439 { 1440 1441 1442 if (!$this->shouldInstructionProcess()) { 1443 1444 $this->processedInstructions = $this->instructionsCache->retrieveCache(); 1445 1446 } else { 1447 1448 $this->processInstructions(); 1449 1450 } 1451 return $this->processedInstructions; 1452 1453 } 1454 1455 /** 1456 * @throws ExceptionNotFound 1457 */ 1458 public function getParent(): FetcherMarkup 1459 { 1460 if (!isset($this->parentMarkupHandler)) { 1461 throw new ExceptionNotFound(); 1462 } 1463 return $this->parentMarkupHandler; 1464 } 1465 1466 /** 1467 * Hack to not have the {@link MarkupDynamicRender} 1468 * stored in {@link FetcherMarkup::$markupDynamicRenderer} 1469 * to reset 1470 * @param array $requestedInstructions 1471 * @param array|null $row 1472 * @return FetcherMarkup 1473 */ 1474 public function setNextIteratorInstructionsWithContext(array $requestedInstructions, array $row = null): FetcherMarkup 1475 { 1476 $this->hasExecuted = false; 1477 if ($row === null) { 1478 unset($this->contextData); 1479 } else { 1480 $this->contextData = $row; 1481 } 1482 $this->requestedInstructions = $requestedInstructions; 1483 return $this; 1484 } 1485 1486 1487} 1488