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