1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeaunamespace ComboStrap; 4*04fd306cSNickeau 5*04fd306cSNickeau 6*04fd306cSNickeauuse ComboStrap\Meta\Field\PageTemplateName; 7*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 8*04fd306cSNickeauuse ComboStrap\Web\UrlEndpoint; 9*04fd306cSNickeauuse ComboStrap\Xml\XmlDocument; 10*04fd306cSNickeauuse ComboStrap\Xml\XmlElement; 11*04fd306cSNickeauuse Symfony\Component\Yaml\Yaml; 12*04fd306cSNickeau 13*04fd306cSNickeau/** 14*04fd306cSNickeau * A page template is the object 15*04fd306cSNickeau * that generates a HTML page 16*04fd306cSNickeau * (ie the templating engine) 17*04fd306cSNickeau * 18*04fd306cSNickeau * It's used by Fetcher that creates pages such 19*04fd306cSNickeau * as {@link FetcherPage}, {@link FetcherMarkupWebcode} or {@link FetcherPageBundler} 20*04fd306cSNickeau */ 21*04fd306cSNickeauclass TemplateForWebPage 22*04fd306cSNickeau{ 23*04fd306cSNickeau 24*04fd306cSNickeau 25*04fd306cSNickeau /** 26*04fd306cSNickeau * An internal configuration 27*04fd306cSNickeau * to tell if the page is social 28*04fd306cSNickeau * (ie seo, search engine, friendly) 29*04fd306cSNickeau */ 30*04fd306cSNickeau const CONF_INTERNAL_IS_SOCIAL = "web-page-is-social"; 31*04fd306cSNickeau 32*04fd306cSNickeau /** 33*04fd306cSNickeau * DocType is required by bootstrap and chrome 34*04fd306cSNickeau * https://developer.chrome.com/docs/lighthouse/best-practices/doctype/ 35*04fd306cSNickeau * https://getbootstrap.com/docs/5.0/getting-started/introduction/#html5-doctype 36*04fd306cSNickeau * <!doctype html> 37*04fd306cSNickeau */ 38*04fd306cSNickeau const DOCTYPE = "<!DOCTYPE html>"; 39*04fd306cSNickeau 40*04fd306cSNickeau private array $templateDefinition; 41*04fd306cSNickeau const CANONICAL = "template"; 42*04fd306cSNickeau 43*04fd306cSNickeau 44*04fd306cSNickeau public const UTF_8_CHARSET_VALUE = "utf-8"; 45*04fd306cSNickeau public const VIEWPORT_RESPONSIVE_VALUE = "width=device-width, initial-scale=1"; 46*04fd306cSNickeau public const TASK_RUNNER_ID = "task-runner"; 47*04fd306cSNickeau public const APPLE_TOUCH_ICON_REL_VALUE = "apple-touch-icon"; 48*04fd306cSNickeau 49*04fd306cSNickeau public const PRELOAD_TAG = "preload"; 50*04fd306cSNickeau 51*04fd306cSNickeau private string $templateName; 52*04fd306cSNickeau 53*04fd306cSNickeau 54*04fd306cSNickeau private string $requestedTitle; 55*04fd306cSNickeau 56*04fd306cSNickeau 57*04fd306cSNickeau private bool $requestedEnableTaskRunner = true; 58*04fd306cSNickeau private WikiPath $requestedContextPath; 59*04fd306cSNickeau private Lang $requestedLang; 60*04fd306cSNickeau private Toc $toc; 61*04fd306cSNickeau private bool $isSocial; 62*04fd306cSNickeau private string $mainContent; 63*04fd306cSNickeau private string $templateString; 64*04fd306cSNickeau private array $model; 65*04fd306cSNickeau private bool $hadMessages = false; 66*04fd306cSNickeau private string $requestedTheme; 67*04fd306cSNickeau private bool $isIframe = false; 68*04fd306cSNickeau private array $slots; 69*04fd306cSNickeau 70*04fd306cSNickeau 71*04fd306cSNickeau public static function create(): TemplateForWebPage 72*04fd306cSNickeau { 73*04fd306cSNickeau return new TemplateForWebPage(); 74*04fd306cSNickeau } 75*04fd306cSNickeau 76*04fd306cSNickeau public static function config(): TemplateForWebPage 77*04fd306cSNickeau { 78*04fd306cSNickeau return new TemplateForWebPage(); 79*04fd306cSNickeau } 80*04fd306cSNickeau 81*04fd306cSNickeau public static function getPoweredBy(): string 82*04fd306cSNickeau { 83*04fd306cSNickeau $domain = PluginUtility::$URL_APEX; 84*04fd306cSNickeau $version = PluginUtility::$INFO_PLUGIN['version'] . " (" . PluginUtility::$INFO_PLUGIN['date'] . ")"; 85*04fd306cSNickeau $poweredBy = "<div class=\"mx-auto\" style=\"width: 300px;text-align: center;margin-bottom: 1rem\">"; 86*04fd306cSNickeau $poweredBy .= " <small><i>Powered by <a href=\"$domain\" title=\"ComboStrap " . $version . "\" style=\"color:#495057\">ComboStrap</a></i></small>"; 87*04fd306cSNickeau $poweredBy .= '</div>'; 88*04fd306cSNickeau return $poweredBy; 89*04fd306cSNickeau } 90*04fd306cSNickeau 91*04fd306cSNickeau 92*04fd306cSNickeau /** 93*04fd306cSNickeau * @throws ExceptionNotFound 94*04fd306cSNickeau */ 95*04fd306cSNickeau public function getHtmlTemplatePath(): LocalPath 96*04fd306cSNickeau { 97*04fd306cSNickeau return $this->getEngine()->searchTemplateByName($this->templateName . "." . TemplateEngine::EXTENSION_HBS); 98*04fd306cSNickeau } 99*04fd306cSNickeau 100*04fd306cSNickeau public function setTemplateString(string $templateString): TemplateForWebPage 101*04fd306cSNickeau { 102*04fd306cSNickeau $this->templateString = $templateString; 103*04fd306cSNickeau return $this; 104*04fd306cSNickeau } 105*04fd306cSNickeau 106*04fd306cSNickeau public function setModel(array $model): TemplateForWebPage 107*04fd306cSNickeau { 108*04fd306cSNickeau $this->model = $model; 109*04fd306cSNickeau return $this; 110*04fd306cSNickeau } 111*04fd306cSNickeau 112*04fd306cSNickeau /** 113*04fd306cSNickeau * @return WikiPath from where the markup slot should be searched 114*04fd306cSNickeau * @throws ExceptionNotFound 115*04fd306cSNickeau */ 116*04fd306cSNickeau public function getRequestedContextPath(): WikiPath 117*04fd306cSNickeau { 118*04fd306cSNickeau if (!isset($this->requestedContextPath)) { 119*04fd306cSNickeau throw new ExceptionNotFound("A requested context path was not found"); 120*04fd306cSNickeau } 121*04fd306cSNickeau return $this->requestedContextPath; 122*04fd306cSNickeau } 123*04fd306cSNickeau 124*04fd306cSNickeau /** 125*04fd306cSNickeau * 126*04fd306cSNickeau * @return string - the page as html string (not dom because that's not how works dokuwiki) 127*04fd306cSNickeau * 128*04fd306cSNickeau */ 129*04fd306cSNickeau public function render(): string 130*04fd306cSNickeau { 131*04fd306cSNickeau 132*04fd306cSNickeau $executionContext = (ExecutionContext::getActualOrCreateFromEnv()) 133*04fd306cSNickeau ->setExecutingPageTemplate($this); 134*04fd306cSNickeau try { 135*04fd306cSNickeau 136*04fd306cSNickeau 137*04fd306cSNickeau $pageTemplateEngine = $this->getEngine(); 138*04fd306cSNickeau if ($this->isTemplateStringExecutionMode()) { 139*04fd306cSNickeau $template = $this->templateString; 140*04fd306cSNickeau } else { 141*04fd306cSNickeau $pageTemplateEngine = $this->getEngine(); 142*04fd306cSNickeau $template = $this->getTemplateName(); 143*04fd306cSNickeau if (!$pageTemplateEngine->templateExists($template)) { 144*04fd306cSNickeau $defaultTemplate = PageTemplateName::HOLY_TEMPLATE_VALUE; 145*04fd306cSNickeau LogUtility::warning("The template ($template) was not found, the default template ($defaultTemplate) was used instead."); 146*04fd306cSNickeau $template = $defaultTemplate; 147*04fd306cSNickeau $this->setRequestedTemplateName($template); 148*04fd306cSNickeau } 149*04fd306cSNickeau } 150*04fd306cSNickeau 151*04fd306cSNickeau /** 152*04fd306cSNickeau * Get model should came after template validation 153*04fd306cSNickeau * as the template definition is named dependent 154*04fd306cSNickeau * (Create a builder, nom de dieu) 155*04fd306cSNickeau */ 156*04fd306cSNickeau $model = $this->getModel(); 157*04fd306cSNickeau 158*04fd306cSNickeau 159*04fd306cSNickeau return self::DOCTYPE . $pageTemplateEngine->renderWebPage($template, $model); 160*04fd306cSNickeau 161*04fd306cSNickeau 162*04fd306cSNickeau } finally { 163*04fd306cSNickeau $executionContext 164*04fd306cSNickeau ->closeExecutingPageTemplate(); 165*04fd306cSNickeau } 166*04fd306cSNickeau 167*04fd306cSNickeau } 168*04fd306cSNickeau 169*04fd306cSNickeau /** 170*04fd306cSNickeau * @return string[] 171*04fd306cSNickeau */ 172*04fd306cSNickeau public function getElementIds(): array 173*04fd306cSNickeau { 174*04fd306cSNickeau $definition = $this->getDefinition(); 175*04fd306cSNickeau $elements = $definition['elements']; 176*04fd306cSNickeau if ($elements == null) { 177*04fd306cSNickeau return []; 178*04fd306cSNickeau } 179*04fd306cSNickeau return $elements; 180*04fd306cSNickeau 181*04fd306cSNickeau } 182*04fd306cSNickeau 183*04fd306cSNickeau 184*04fd306cSNickeau /** 185*04fd306cSNickeau * @throws ExceptionNotFound 186*04fd306cSNickeau */ 187*04fd306cSNickeau private function getRequestedLang(): Lang 188*04fd306cSNickeau { 189*04fd306cSNickeau if (!isset($this->requestedLang)) { 190*04fd306cSNickeau throw new ExceptionNotFound("No requested lang"); 191*04fd306cSNickeau } 192*04fd306cSNickeau return $this->requestedLang; 193*04fd306cSNickeau } 194*04fd306cSNickeau 195*04fd306cSNickeau 196*04fd306cSNickeau public function getTemplateName(): string 197*04fd306cSNickeau { 198*04fd306cSNickeau if (isset($this->templateName)) { 199*04fd306cSNickeau return $this->templateName; 200*04fd306cSNickeau } 201*04fd306cSNickeau try { 202*04fd306cSNickeau $requestedPath = $this->getRequestedContextPath(); 203*04fd306cSNickeau return PageTemplateName::createFromPage(MarkupPath::createPageFromPathObject($requestedPath)) 204*04fd306cSNickeau ->getValueOrDefault(); 205*04fd306cSNickeau } catch (ExceptionNotFound $e) { 206*04fd306cSNickeau // no requested path 207*04fd306cSNickeau } 208*04fd306cSNickeau return ExecutionContext::getActualOrCreateFromEnv() 209*04fd306cSNickeau ->getConfig() 210*04fd306cSNickeau ->getDefaultLayoutName(); 211*04fd306cSNickeau } 212*04fd306cSNickeau 213*04fd306cSNickeau 214*04fd306cSNickeau public function __toString() 215*04fd306cSNickeau { 216*04fd306cSNickeau return $this->templateName; 217*04fd306cSNickeau } 218*04fd306cSNickeau 219*04fd306cSNickeau /** 220*04fd306cSNickeau * @throws ExceptionNotFound 221*04fd306cSNickeau */ 222*04fd306cSNickeau public function getCssPath(): LocalPath 223*04fd306cSNickeau { 224*04fd306cSNickeau return $this->getEngine()->searchTemplateByName("$this->templateName.css"); 225*04fd306cSNickeau } 226*04fd306cSNickeau 227*04fd306cSNickeau /** 228*04fd306cSNickeau * @throws ExceptionNotFound 229*04fd306cSNickeau */ 230*04fd306cSNickeau public function getJsPath(): LocalPath 231*04fd306cSNickeau { 232*04fd306cSNickeau $jsPath = $this->getEngine()->searchTemplateByName("$this->templateName.js"); 233*04fd306cSNickeau if (!FileSystems::exists($jsPath)) { 234*04fd306cSNickeau throw new ExceptionNotFound("No js file"); 235*04fd306cSNickeau } 236*04fd306cSNickeau return $jsPath; 237*04fd306cSNickeau } 238*04fd306cSNickeau 239*04fd306cSNickeau public function hasMessages(): bool 240*04fd306cSNickeau { 241*04fd306cSNickeau return $this->hadMessages; 242*04fd306cSNickeau } 243*04fd306cSNickeau 244*04fd306cSNickeau public function setRequestedTheme(string $themeName): TemplateForWebPage 245*04fd306cSNickeau { 246*04fd306cSNickeau $this->requestedTheme = $themeName; 247*04fd306cSNickeau return $this; 248*04fd306cSNickeau } 249*04fd306cSNickeau 250*04fd306cSNickeau public function hasElement(string $elementId): bool 251*04fd306cSNickeau { 252*04fd306cSNickeau return in_array($elementId, $this->getElementIds()); 253*04fd306cSNickeau } 254*04fd306cSNickeau 255*04fd306cSNickeau public function isSocial(): bool 256*04fd306cSNickeau { 257*04fd306cSNickeau if (isset($this->isSocial)) { 258*04fd306cSNickeau return $this->isSocial; 259*04fd306cSNickeau } 260*04fd306cSNickeau try { 261*04fd306cSNickeau $path = $this->getRequestedContextPath(); 262*04fd306cSNickeau if (!FileSystems::exists($path)) { 263*04fd306cSNickeau return false; 264*04fd306cSNickeau } 265*04fd306cSNickeau $markup = MarkupPath::createPageFromPathObject($path); 266*04fd306cSNickeau if ($markup->isSlot()) { 267*04fd306cSNickeau // slot are not social 268*04fd306cSNickeau return false; 269*04fd306cSNickeau } 270*04fd306cSNickeau } catch (ExceptionNotFound $e) { 271*04fd306cSNickeau // not a path run 272*04fd306cSNickeau return false; 273*04fd306cSNickeau } 274*04fd306cSNickeau if ($this->isIframe) { 275*04fd306cSNickeau return false; 276*04fd306cSNickeau } 277*04fd306cSNickeau return ExecutionContext::getActualOrCreateFromEnv() 278*04fd306cSNickeau ->getConfig() 279*04fd306cSNickeau ->getBooleanValue(self::CONF_INTERNAL_IS_SOCIAL, true); 280*04fd306cSNickeau 281*04fd306cSNickeau } 282*04fd306cSNickeau 283*04fd306cSNickeau public function setIsIframe(bool $isIframe): TemplateForWebPage 284*04fd306cSNickeau { 285*04fd306cSNickeau $this->isIframe = $isIframe; 286*04fd306cSNickeau return $this; 287*04fd306cSNickeau } 288*04fd306cSNickeau 289*04fd306cSNickeau /** 290*04fd306cSNickeau * @return TemplateSlot[] 291*04fd306cSNickeau */ 292*04fd306cSNickeau public function getSlots(): array 293*04fd306cSNickeau { 294*04fd306cSNickeau if (isset($this->slots)) { 295*04fd306cSNickeau return $this->slots; 296*04fd306cSNickeau } 297*04fd306cSNickeau $this->slots = []; 298*04fd306cSNickeau foreach ($this->getElementIds() as $elementId) { 299*04fd306cSNickeau if ($elementId === TemplateSlot::MAIN_TOC_ID) { 300*04fd306cSNickeau /** 301*04fd306cSNickeau * Main toc element is not a slot 302*04fd306cSNickeau */ 303*04fd306cSNickeau continue; 304*04fd306cSNickeau } 305*04fd306cSNickeau 306*04fd306cSNickeau try { 307*04fd306cSNickeau $this->slots[] = TemplateSlot::createFromElementId($elementId, $this->getRequestedContextPath()); 308*04fd306cSNickeau } catch (ExceptionNotFound $e) { 309*04fd306cSNickeau LogUtility::internalError("This template is not for a markup path, it cannot have slots then."); 310*04fd306cSNickeau } 311*04fd306cSNickeau } 312*04fd306cSNickeau return $this->slots; 313*04fd306cSNickeau } 314*04fd306cSNickeau 315*04fd306cSNickeau 316*04fd306cSNickeau /** 317*04fd306cSNickeau * Character set 318*04fd306cSNickeau * Note: avoid using {@link Html::encode() character entities} in your HTML, 319*04fd306cSNickeau * provided their encoding matches that of the document (generally UTF-8) 320*04fd306cSNickeau */ 321*04fd306cSNickeau private function checkCharSetMeta(XmlElement $head) 322*04fd306cSNickeau { 323*04fd306cSNickeau $charsetValue = TemplateForWebPage::UTF_8_CHARSET_VALUE; 324*04fd306cSNickeau try { 325*04fd306cSNickeau $metaCharset = $head->querySelector("meta[charset]"); 326*04fd306cSNickeau $charsetActualValue = $metaCharset->getAttribute("charset"); 327*04fd306cSNickeau if ($charsetActualValue !== $charsetValue) { 328*04fd306cSNickeau LogUtility::warning("The actual charset ($charsetActualValue) should be $charsetValue"); 329*04fd306cSNickeau } 330*04fd306cSNickeau } catch (ExceptionBadSyntax|ExceptionNotFound $e) { 331*04fd306cSNickeau try { 332*04fd306cSNickeau $metaCharset = $head->getDocument() 333*04fd306cSNickeau ->createElement("meta") 334*04fd306cSNickeau ->setAttribute("charset", $charsetValue); 335*04fd306cSNickeau $head->appendChild($metaCharset); 336*04fd306cSNickeau } catch (\DOMException $e) { 337*04fd306cSNickeau throw new ExceptionRuntimeInternal("Bad local name meta, should not occur", self::CANONICAL, 1, $e); 338*04fd306cSNickeau } 339*04fd306cSNickeau } 340*04fd306cSNickeau } 341*04fd306cSNickeau 342*04fd306cSNickeau /** 343*04fd306cSNickeau * @param XmlElement $head 344*04fd306cSNickeau * @return void 345*04fd306cSNickeau * Adapted from {@link TplUtility::renderFaviconMetaLinks()} 346*04fd306cSNickeau */ 347*04fd306cSNickeau private function getPageIconHeadLinkHtml(): string 348*04fd306cSNickeau { 349*04fd306cSNickeau $html = $this->getShortcutFavIconHtmlLink(); 350*04fd306cSNickeau $html .= $this->getIconHtmlLink(); 351*04fd306cSNickeau $html .= $this->getAppleTouchIconHtmlLink(); 352*04fd306cSNickeau return $html; 353*04fd306cSNickeau } 354*04fd306cSNickeau 355*04fd306cSNickeau /** 356*04fd306cSNickeau * Add a favIcon.ico 357*04fd306cSNickeau * 358*04fd306cSNickeau */ 359*04fd306cSNickeau private function getShortcutFavIconHtmlLink(): string 360*04fd306cSNickeau { 361*04fd306cSNickeau 362*04fd306cSNickeau $internalFavIcon = WikiPath::createComboResource('images:favicon.ico'); 363*04fd306cSNickeau $iconPaths = array( 364*04fd306cSNickeau WikiPath::createMediaPathFromId(':favicon.ico'), 365*04fd306cSNickeau WikiPath::createMediaPathFromId(':wiki:favicon.ico'), 366*04fd306cSNickeau $internalFavIcon 367*04fd306cSNickeau ); 368*04fd306cSNickeau try { 369*04fd306cSNickeau /** 370*04fd306cSNickeau * @var WikiPath $icoWikiPath - we give wiki paths, we get wiki path 371*04fd306cSNickeau */ 372*04fd306cSNickeau $icoWikiPath = FileSystems::getFirstExistingPath($iconPaths); 373*04fd306cSNickeau } catch (ExceptionNotFound $e) { 374*04fd306cSNickeau LogUtility::internalError("The internal fav icon ($internalFavIcon) should be at minimal found", self::CANONICAL); 375*04fd306cSNickeau return ""; 376*04fd306cSNickeau } 377*04fd306cSNickeau 378*04fd306cSNickeau return TagAttributes::createEmpty() 379*04fd306cSNickeau ->addOutputAttributeValue("rel", "shortcut icon") 380*04fd306cSNickeau ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($icoWikiPath)->getFetchUrl()->toAbsoluteUrl()->toString()) 381*04fd306cSNickeau ->toHtmlEmptyTag("link"); 382*04fd306cSNickeau 383*04fd306cSNickeau } 384*04fd306cSNickeau 385*04fd306cSNickeau /** 386*04fd306cSNickeau * Add Icon Png (16x16 and 32x32) 387*04fd306cSNickeau * @return string 388*04fd306cSNickeau */ 389*04fd306cSNickeau private function getIconHtmlLink(): string 390*04fd306cSNickeau { 391*04fd306cSNickeau 392*04fd306cSNickeau $html = ""; 393*04fd306cSNickeau $sizeValues = ["32x32", "16x16"]; 394*04fd306cSNickeau foreach ($sizeValues as $sizeValue) { 395*04fd306cSNickeau 396*04fd306cSNickeau $internalIcon = WikiPath::createComboResource(":images:favicon-$sizeValue.png"); 397*04fd306cSNickeau $iconPaths = array( 398*04fd306cSNickeau WikiPath::createMediaPathFromId(":favicon-$sizeValue.png"), 399*04fd306cSNickeau WikiPath::createMediaPathFromId(":wiki:favicon-$sizeValue.png"), 400*04fd306cSNickeau $internalIcon 401*04fd306cSNickeau ); 402*04fd306cSNickeau try { 403*04fd306cSNickeau /** 404*04fd306cSNickeau * @var WikiPath $iconPath - to say to the linter that this is a wiki path 405*04fd306cSNickeau */ 406*04fd306cSNickeau $iconPath = FileSystems::getFirstExistingPath($iconPaths); 407*04fd306cSNickeau } catch (ExceptionNotFound $e) { 408*04fd306cSNickeau LogUtility::internalError("The internal icon ($internalIcon) should be at minimal found", self::CANONICAL); 409*04fd306cSNickeau continue; 410*04fd306cSNickeau } 411*04fd306cSNickeau $html .= TagAttributes::createEmpty() 412*04fd306cSNickeau ->addOutputAttributeValue("rel", "icon") 413*04fd306cSNickeau ->addOutputAttributeValue("sizes", $sizeValue) 414*04fd306cSNickeau ->addOutputAttributeValue("type", Mime::PNG) 415*04fd306cSNickeau ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($iconPath)->getFetchUrl()->toAbsoluteUrl()->toString()) 416*04fd306cSNickeau ->toHtmlEmptyTag("link"); 417*04fd306cSNickeau } 418*04fd306cSNickeau return $html; 419*04fd306cSNickeau } 420*04fd306cSNickeau 421*04fd306cSNickeau /** 422*04fd306cSNickeau * Add Apple touch icon 423*04fd306cSNickeau * 424*04fd306cSNickeau * @return string 425*04fd306cSNickeau */ 426*04fd306cSNickeau private function getAppleTouchIconHtmlLink(): string 427*04fd306cSNickeau { 428*04fd306cSNickeau 429*04fd306cSNickeau $internalIcon = WikiPath::createComboResource(":images:apple-touch-icon.png"); 430*04fd306cSNickeau $iconPaths = array( 431*04fd306cSNickeau WikiPath::createMediaPathFromId(":apple-touch-icon.png"), 432*04fd306cSNickeau WikiPath::createMediaPathFromId(":wiki:apple-touch-icon.png"), 433*04fd306cSNickeau $internalIcon 434*04fd306cSNickeau ); 435*04fd306cSNickeau try { 436*04fd306cSNickeau /** 437*04fd306cSNickeau * @var WikiPath $iconPath - to say to the linter that this is a wiki path 438*04fd306cSNickeau */ 439*04fd306cSNickeau $iconPath = FileSystems::getFirstExistingPath($iconPaths); 440*04fd306cSNickeau } catch (ExceptionNotFound $e) { 441*04fd306cSNickeau LogUtility::internalError("The internal apple icon ($internalIcon) should be at minimal found", self::CANONICAL); 442*04fd306cSNickeau return ""; 443*04fd306cSNickeau } 444*04fd306cSNickeau try { 445*04fd306cSNickeau $fetcherLocalPath = FetcherRaster::createImageRasterFetchFromPath($iconPath); 446*04fd306cSNickeau $sizesValue = "{$fetcherLocalPath->getIntrinsicWidth()}x{$fetcherLocalPath->getIntrinsicHeight()}"; 447*04fd306cSNickeau 448*04fd306cSNickeau return TagAttributes::createEmpty() 449*04fd306cSNickeau ->addOutputAttributeValue("rel", self::APPLE_TOUCH_ICON_REL_VALUE) 450*04fd306cSNickeau ->addOutputAttributeValue("sizes", $sizesValue) 451*04fd306cSNickeau ->addOutputAttributeValue("type", Mime::PNG) 452*04fd306cSNickeau ->addOutputAttributeValue("href", $fetcherLocalPath->getFetchUrl()->toAbsoluteUrl()->toString()) 453*04fd306cSNickeau ->toHtmlEmptyTag("link"); 454*04fd306cSNickeau } catch (\Exception $e) { 455*04fd306cSNickeau LogUtility::internalError("The file ($iconPath) should be found and the local name should be good. Error: {$e->getMessage()}"); 456*04fd306cSNickeau return ""; 457*04fd306cSNickeau } 458*04fd306cSNickeau } 459*04fd306cSNickeau 460*04fd306cSNickeau public 461*04fd306cSNickeau function getModel(): array 462*04fd306cSNickeau { 463*04fd306cSNickeau 464*04fd306cSNickeau $executionConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig(); 465*04fd306cSNickeau 466*04fd306cSNickeau /** 467*04fd306cSNickeau * Mandatory HTML attributes 468*04fd306cSNickeau */ 469*04fd306cSNickeau $model = 470*04fd306cSNickeau [ 471*04fd306cSNickeau PageTitle::PROPERTY_NAME => $this->getRequestedTitleOrDefault(), 472*04fd306cSNickeau Lang::PROPERTY_NAME => $this->getRequestedLangOrDefault()->getValueOrDefault(), 473*04fd306cSNickeau // The direction is not yet calculated from the page, we let the browser determine it from the lang 474*04fd306cSNickeau // dokuwiki has a direction config also ... 475*04fd306cSNickeau // "dir" => $this->getRequestedLangOrDefault()->getDirection() 476*04fd306cSNickeau ]; 477*04fd306cSNickeau 478*04fd306cSNickeau if (isset($this->model)) { 479*04fd306cSNickeau return array_merge($model, $this->model); 480*04fd306cSNickeau } 481*04fd306cSNickeau 482*04fd306cSNickeau /** 483*04fd306cSNickeau * The width of the layout 484*04fd306cSNickeau */ 485*04fd306cSNickeau $container = $executionConfig->getValue(ContainerTag::DEFAULT_LAYOUT_CONTAINER_CONF, ContainerTag::DEFAULT_LAYOUT_CONTAINER_DEFAULT_VALUE); 486*04fd306cSNickeau $containerClass = ContainerTag::getClassName($container); 487*04fd306cSNickeau $model["layout-container-class"] = $containerClass; 488*04fd306cSNickeau 489*04fd306cSNickeau 490*04fd306cSNickeau /** 491*04fd306cSNickeau * The rem 492*04fd306cSNickeau */ 493*04fd306cSNickeau try { 494*04fd306cSNickeau $model["rem-size"] = $executionConfig->getRemFontSize(); 495*04fd306cSNickeau } catch (ExceptionNotFound $e) { 496*04fd306cSNickeau // ok none 497*04fd306cSNickeau } 498*04fd306cSNickeau 499*04fd306cSNickeau 500*04fd306cSNickeau /** 501*04fd306cSNickeau * Body class 502*04fd306cSNickeau * {@link tpl_classes} will add the dokuwiki class. 503*04fd306cSNickeau * See https://www.dokuwiki.org/devel:templates#dokuwiki_class 504*04fd306cSNickeau * dokuwiki__top ID is needed for the "Back to top" utility 505*04fd306cSNickeau * used also by some plugins 506*04fd306cSNickeau * dokwuiki as class is also needed as it's used by the linkwizard 507*04fd306cSNickeau * to locate where to add the node (ie .appendTo('.dokuwiki:first')) 508*04fd306cSNickeau */ 509*04fd306cSNickeau $bodyDokuwikiClass = tpl_classes(); 510*04fd306cSNickeau try { 511*04fd306cSNickeau $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("{$this->getTheme()}-{$this->getTemplateName()}"); 512*04fd306cSNickeau } catch (\Exception $e) { 513*04fd306cSNickeau $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("template-string"); 514*04fd306cSNickeau } 515*04fd306cSNickeau // position relative is for the toast and messages that are in the corner 516*04fd306cSNickeau $model['body-classes'] = "$bodyDokuwikiClass position-relative $bodyTemplateIdentifierClass"; 517*04fd306cSNickeau 518*04fd306cSNickeau /** 519*04fd306cSNickeau * Data coupled to a page 520*04fd306cSNickeau */ 521*04fd306cSNickeau try { 522*04fd306cSNickeau 523*04fd306cSNickeau $contextPath = $this->getRequestedContextPath(); 524*04fd306cSNickeau $markupPath = MarkupPath::createPageFromPathObject($contextPath); 525*04fd306cSNickeau /** 526*04fd306cSNickeau * Meta 527*04fd306cSNickeau */ 528*04fd306cSNickeau $metadata = $markupPath->getMetadataForRendering(); 529*04fd306cSNickeau $model = array_merge($metadata, $model); 530*04fd306cSNickeau 531*04fd306cSNickeau 532*04fd306cSNickeau /** 533*04fd306cSNickeau * Railbar 534*04fd306cSNickeau * You can define the layout type by page 535*04fd306cSNickeau * This is not a handelbars helper because it needs some css snippet. 536*04fd306cSNickeau */ 537*04fd306cSNickeau $railBarLayout = $this->getRailbarLayout(); 538*04fd306cSNickeau try { 539*04fd306cSNickeau $model["railbar-html"] = FetcherRailBar::createRailBar() 540*04fd306cSNickeau ->setRequestedLayout($railBarLayout) 541*04fd306cSNickeau ->setRequestedPath($contextPath) 542*04fd306cSNickeau ->getFetchString(); 543*04fd306cSNickeau } catch (ExceptionBadArgument $e) { 544*04fd306cSNickeau LogUtility::error("Error while creating the railbar layout"); 545*04fd306cSNickeau } 546*04fd306cSNickeau 547*04fd306cSNickeau /** 548*04fd306cSNickeau * Css Variables Colors 549*04fd306cSNickeau * Added for now in `head-partial.hbs` 550*04fd306cSNickeau */ 551*04fd306cSNickeau try { 552*04fd306cSNickeau $primaryColor = $executionConfig->getPrimaryColor(); 553*04fd306cSNickeau $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = $primaryColor->toCssValue(); 554*04fd306cSNickeau $model[BrandingColors::PRIMARY_COLOR_TEXT_ATTRIBUTE] = ColorSystem::toTextColor($primaryColor); 555*04fd306cSNickeau $model[BrandingColors::PRIMARY_COLOR_TEXT_HOVER_ATTRIBUTE] = ColorSystem::toTextHoverColor($primaryColor); 556*04fd306cSNickeau } catch (ExceptionNotFound $e) { 557*04fd306cSNickeau // not found 558*04fd306cSNickeau $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = null; 559*04fd306cSNickeau } 560*04fd306cSNickeau try { 561*04fd306cSNickeau $secondaryColor = $executionConfig->getSecondaryColor(); 562*04fd306cSNickeau $model[BrandingColors::SECONDARY_COLOR_TEMPLATE_ATTRIBUTE] = $secondaryColor->toCssValue(); 563*04fd306cSNickeau } catch (ExceptionNotFound $e) { 564*04fd306cSNickeau // not found 565*04fd306cSNickeau } 566*04fd306cSNickeau 567*04fd306cSNickeau 568*04fd306cSNickeau /** 569*04fd306cSNickeau * Main 570*04fd306cSNickeau */ 571*04fd306cSNickeau if (isset($this->mainContent)) { 572*04fd306cSNickeau $model["main-content-html"] = $this->mainContent; 573*04fd306cSNickeau } else { 574*04fd306cSNickeau try { 575*04fd306cSNickeau if (!$markupPath->isSlot()) { 576*04fd306cSNickeau $requestedContextPathForMain = $this->getRequestedContextPath(); 577*04fd306cSNickeau } else { 578*04fd306cSNickeau try { 579*04fd306cSNickeau $markupContextPath = SlotSystem::getContextPath(); 580*04fd306cSNickeau SlotSystem::sendContextPathMessage($markupContextPath); 581*04fd306cSNickeau $requestedContextPathForMain = $markupContextPath->toWikiPath(); 582*04fd306cSNickeau } catch (ExceptionNotFound|ExceptionCast $e) { 583*04fd306cSNickeau $requestedContextPathForMain = $this->getRequestedContextPath(); 584*04fd306cSNickeau } 585*04fd306cSNickeau } 586*04fd306cSNickeau $model["main-content-html"] = FetcherMarkup::confRoot() 587*04fd306cSNickeau ->setRequestedMimeToXhtml() 588*04fd306cSNickeau ->setRequestedContextPath($requestedContextPathForMain) 589*04fd306cSNickeau ->setRequestedExecutingPath($this->getRequestedContextPath()) 590*04fd306cSNickeau ->build() 591*04fd306cSNickeau ->getFetchString(); 592*04fd306cSNickeau } catch (ExceptionCompile|ExceptionNotExists|ExceptionNotExists $e) { 593*04fd306cSNickeau LogUtility::error("Error while rendering the page content.", self::CANONICAL, $e); 594*04fd306cSNickeau $model["main-content-html"] = "An error has occured. " . $e->getMessage(); 595*04fd306cSNickeau } 596*04fd306cSNickeau } 597*04fd306cSNickeau 598*04fd306cSNickeau /** 599*04fd306cSNickeau * Toc (after main execution please) 600*04fd306cSNickeau */ 601*04fd306cSNickeau $model['toc-class'] = Toc::getClass(); 602*04fd306cSNickeau $model['toc-html'] = $this->getTocOrDefault()->toXhtml(); 603*04fd306cSNickeau 604*04fd306cSNickeau /** 605*04fd306cSNickeau * Slots 606*04fd306cSNickeau */ 607*04fd306cSNickeau foreach ($this->getSlots() as $slot) { 608*04fd306cSNickeau 609*04fd306cSNickeau $elementId = $slot->getElementId(); 610*04fd306cSNickeau try { 611*04fd306cSNickeau $model["$elementId-html"] = $slot->getMarkupFetcher()->getFetchString(); 612*04fd306cSNickeau } catch (ExceptionNotFound|ExceptionNotExists $e) { 613*04fd306cSNickeau // no slot found 614*04fd306cSNickeau } catch (ExceptionCompile $e) { 615*04fd306cSNickeau LogUtility::error("Error while rendering the slot $elementId for the template ($this)", self::CANONICAL, $e); 616*04fd306cSNickeau $model["$elementId-html"] = LogUtility::wrapInRedForHtml("Error: " . $e->getMessage()); 617*04fd306cSNickeau } 618*04fd306cSNickeau } 619*04fd306cSNickeau 620*04fd306cSNickeau /** 621*04fd306cSNickeau * Found in {@link tpl_content()} 622*04fd306cSNickeau * Used to add html such as {@link \action_plugin_combo_routermessage} 623*04fd306cSNickeau * Not sure if this is the right place to add it. 624*04fd306cSNickeau */ 625*04fd306cSNickeau ob_start(); 626*04fd306cSNickeau global $ACT; 627*04fd306cSNickeau \dokuwiki\Extension\Event::createAndTrigger('TPL_ACT_RENDER', $ACT); 628*04fd306cSNickeau $tplActRenderOutput = ob_get_clean(); 629*04fd306cSNickeau if (!empty($tplActRenderOutput)) { 630*04fd306cSNickeau $model["main-content-afterbegin-html"] = $tplActRenderOutput; 631*04fd306cSNickeau $this->hadMessages = true; 632*04fd306cSNickeau } 633*04fd306cSNickeau 634*04fd306cSNickeau } catch (ExceptionNotFound $e) { 635*04fd306cSNickeau // no context path 636*04fd306cSNickeau if (isset($this->mainContent)) { 637*04fd306cSNickeau $model["main-content-html"] = $this->mainContent; 638*04fd306cSNickeau } 639*04fd306cSNickeau } 640*04fd306cSNickeau 641*04fd306cSNickeau 642*04fd306cSNickeau /** 643*04fd306cSNickeau * Head Html 644*04fd306cSNickeau * Snippet, Css and Js from the layout if any 645*04fd306cSNickeau * 646*04fd306cSNickeau * Note that head tag may be added during rendering and must be then called after rendering and toc 647*04fd306cSNickeau * (ie at last then) 648*04fd306cSNickeau */ 649*04fd306cSNickeau $model['head-html'] = $this->getHeadHtml(); 650*04fd306cSNickeau 651*04fd306cSNickeau /** 652*04fd306cSNickeau * Preloaded Css 653*04fd306cSNickeau * (It must come after the head processing as this is where the preloaded script are defined) 654*04fd306cSNickeau * (Not really useful but legacy) 655*04fd306cSNickeau * We add it just before the end of the body tag 656*04fd306cSNickeau */ 657*04fd306cSNickeau try { 658*04fd306cSNickeau $model['preloaded-stylesheet-html'] = $this->getHtmlForPreloadedStyleSheets(); 659*04fd306cSNickeau } catch (ExceptionNotFound $e) { 660*04fd306cSNickeau // no preloaded stylesheet resources 661*04fd306cSNickeau } 662*04fd306cSNickeau 663*04fd306cSNickeau /** 664*04fd306cSNickeau * Powered by 665*04fd306cSNickeau */ 666*04fd306cSNickeau $model['powered-by'] = self::getPoweredBy(); 667*04fd306cSNickeau 668*04fd306cSNickeau /** 669*04fd306cSNickeau * Messages 670*04fd306cSNickeau * (Should come just before the page creation 671*04fd306cSNickeau * due to the $MSG_shown mechanism in {@link html_msgarea()} 672*04fd306cSNickeau * We may also get messages in the head 673*04fd306cSNickeau */ 674*04fd306cSNickeau try { 675*04fd306cSNickeau $model['messages-html'] = $this->getMessages(); 676*04fd306cSNickeau /** 677*04fd306cSNickeau * Because they must be problem and message with the {@link self::getHeadHtml()} 678*04fd306cSNickeau * We process the messages at the end 679*04fd306cSNickeau * It means that the needed script needs to be added manually 680*04fd306cSNickeau */ 681*04fd306cSNickeau $model['head-html'] .= Snippet::getOrCreateFromComponentId("toast", Snippet::EXTENSION_JS)->toXhtml(); 682*04fd306cSNickeau } catch (ExceptionNotFound $e) { 683*04fd306cSNickeau // no messages 684*04fd306cSNickeau } catch (ExceptionBadState $e) { 685*04fd306cSNickeau throw ExceptionRuntimeInternal::withMessageAndError("The toast snippet should have been found", $e); 686*04fd306cSNickeau } 687*04fd306cSNickeau 688*04fd306cSNickeau /** 689*04fd306cSNickeau * Task runner needs the id 690*04fd306cSNickeau */ 691*04fd306cSNickeau if ($this->requestedEnableTaskRunner && isset($this->requestedContextPath)) { 692*04fd306cSNickeau $model['task-runner-html'] = $this->getTaskRunnerImg(); 693*04fd306cSNickeau } 694*04fd306cSNickeau 695*04fd306cSNickeau return $model; 696*04fd306cSNickeau } 697*04fd306cSNickeau 698*04fd306cSNickeau 699*04fd306cSNickeau private 700*04fd306cSNickeau function getRequestedTitleOrDefault(): string 701*04fd306cSNickeau { 702*04fd306cSNickeau 703*04fd306cSNickeau if (isset($this->requestedTitle)) { 704*04fd306cSNickeau return $this->requestedTitle; 705*04fd306cSNickeau } 706*04fd306cSNickeau 707*04fd306cSNickeau try { 708*04fd306cSNickeau $path = $this->getRequestedContextPath(); 709*04fd306cSNickeau $markupPath = MarkupPath::createPageFromPathObject($path); 710*04fd306cSNickeau return PageTitle::createForMarkup($markupPath)->getValueOrDefault(); 711*04fd306cSNickeau } catch (ExceptionNotFound $e) { 712*04fd306cSNickeau // 713*04fd306cSNickeau } 714*04fd306cSNickeau throw new ExceptionBadSyntaxRuntime("A title is mandatory"); 715*04fd306cSNickeau 716*04fd306cSNickeau 717*04fd306cSNickeau } 718*04fd306cSNickeau 719*04fd306cSNickeau 720*04fd306cSNickeau /** 721*04fd306cSNickeau * @throws ExceptionNotFound 722*04fd306cSNickeau */ 723*04fd306cSNickeau private 724*04fd306cSNickeau function getTocOrDefault(): Toc 725*04fd306cSNickeau { 726*04fd306cSNickeau 727*04fd306cSNickeau if (isset($this->toc)) { 728*04fd306cSNickeau /** 729*04fd306cSNickeau * The {@link FetcherPageBundler} 730*04fd306cSNickeau * bundle pages can create a toc for multiples pages 731*04fd306cSNickeau */ 732*04fd306cSNickeau return $this->toc; 733*04fd306cSNickeau } 734*04fd306cSNickeau 735*04fd306cSNickeau $wikiPath = $this->getRequestedContextPath(); 736*04fd306cSNickeau if (FileSystems::isDirectory($wikiPath)) { 737*04fd306cSNickeau LogUtility::error("We have a found an inconsistency. The context path is a directory and does have therefore no toc but the template ($this) has a toc."); 738*04fd306cSNickeau } 739*04fd306cSNickeau $markup = MarkupPath::createPageFromPathObject($wikiPath); 740*04fd306cSNickeau return Toc::createForPage($markup); 741*04fd306cSNickeau 742*04fd306cSNickeau } 743*04fd306cSNickeau 744*04fd306cSNickeau public 745*04fd306cSNickeau function setMainContent(string $mainContent): TemplateForWebPage 746*04fd306cSNickeau { 747*04fd306cSNickeau $this->mainContent = $mainContent; 748*04fd306cSNickeau return $this; 749*04fd306cSNickeau } 750*04fd306cSNickeau 751*04fd306cSNickeau 752*04fd306cSNickeau /** 753*04fd306cSNickeau * @throws ExceptionBadSyntax 754*04fd306cSNickeau */ 755*04fd306cSNickeau public 756*04fd306cSNickeau function renderAsDom(): XmlDocument 757*04fd306cSNickeau { 758*04fd306cSNickeau return XmlDocument::createHtmlDocFromMarkup($this->render()); 759*04fd306cSNickeau } 760*04fd306cSNickeau 761*04fd306cSNickeau /** 762*04fd306cSNickeau * Add the preloaded CSS resources 763*04fd306cSNickeau * at the end 764*04fd306cSNickeau * @throws ExceptionNotFound 765*04fd306cSNickeau */ 766*04fd306cSNickeau private 767*04fd306cSNickeau function getHtmlForPreloadedStyleSheets(): string 768*04fd306cSNickeau { 769*04fd306cSNickeau 770*04fd306cSNickeau // For the preload if any 771*04fd306cSNickeau try { 772*04fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 773*04fd306cSNickeau $preloadedCss = $executionContext->getRuntimeObject(self::PRELOAD_TAG); 774*04fd306cSNickeau } catch (ExceptionNotFound $e) { 775*04fd306cSNickeau throw new ExceptionNotFound("No preloaded resources found"); 776*04fd306cSNickeau } 777*04fd306cSNickeau 778*04fd306cSNickeau // 779*04fd306cSNickeau // Note: Adding this css in an animationFrame 780*04fd306cSNickeau // such as https://github.com/jakearchibald/svgomg/blob/master/src/index.html#L183 781*04fd306cSNickeau // would be difficult to test 782*04fd306cSNickeau 783*04fd306cSNickeau $class = StyleAttribute::addComboStrapSuffix(self::PRELOAD_TAG); 784*04fd306cSNickeau $preloadHtml = "<div class=\"$class\">"; 785*04fd306cSNickeau foreach ($preloadedCss as $link) { 786*04fd306cSNickeau $htmlLink = '<link rel="stylesheet" href="' . $link['href'] . '" '; 787*04fd306cSNickeau if ($link['crossorigin'] != "") { 788*04fd306cSNickeau $htmlLink .= ' crossorigin="' . $link['crossorigin'] . '" '; 789*04fd306cSNickeau } 790*04fd306cSNickeau if (!empty($link['class'])) { 791*04fd306cSNickeau $htmlLink .= ' class="' . $link['class'] . '" '; 792*04fd306cSNickeau } 793*04fd306cSNickeau // No integrity here 794*04fd306cSNickeau $htmlLink .= '>'; 795*04fd306cSNickeau $preloadHtml .= $htmlLink; 796*04fd306cSNickeau } 797*04fd306cSNickeau $preloadHtml .= "</div>"; 798*04fd306cSNickeau return $preloadHtml; 799*04fd306cSNickeau 800*04fd306cSNickeau } 801*04fd306cSNickeau 802*04fd306cSNickeau /** 803*04fd306cSNickeau * Variation of {@link html_msgarea()} 804*04fd306cSNickeau * @throws ExceptionNotFound 805*04fd306cSNickeau */ 806*04fd306cSNickeau public 807*04fd306cSNickeau function getMessages(): string 808*04fd306cSNickeau { 809*04fd306cSNickeau 810*04fd306cSNickeau global $MSG; 811*04fd306cSNickeau 812*04fd306cSNickeau if (!isset($MSG)) { 813*04fd306cSNickeau throw new ExceptionNotFound("No messages"); 814*04fd306cSNickeau } 815*04fd306cSNickeau 816*04fd306cSNickeau // deduplicate and auth 817*04fd306cSNickeau $uniqueMessages = []; 818*04fd306cSNickeau foreach ($MSG as $msg) { 819*04fd306cSNickeau if (!info_msg_allowed($msg)) { 820*04fd306cSNickeau continue; 821*04fd306cSNickeau } 822*04fd306cSNickeau $hash = md5($msg['msg']); 823*04fd306cSNickeau $uniqueMessages[$hash] = $msg; 824*04fd306cSNickeau } 825*04fd306cSNickeau 826*04fd306cSNickeau $messagesByLevel = []; 827*04fd306cSNickeau foreach ($uniqueMessages as $message) { 828*04fd306cSNickeau $level = $message['lvl']; 829*04fd306cSNickeau $messagesByLevel[$level][] = $message; 830*04fd306cSNickeau } 831*04fd306cSNickeau 832*04fd306cSNickeau $toasts = ""; 833*04fd306cSNickeau foreach ($messagesByLevel as $level => $messagesForLevel) { 834*04fd306cSNickeau $level = ucfirst($level); 835*04fd306cSNickeau switch ($level) { 836*04fd306cSNickeau case "Error": 837*04fd306cSNickeau $class = "text-danger"; 838*04fd306cSNickeau $levelName = "Error"; 839*04fd306cSNickeau break; 840*04fd306cSNickeau case "Notify": 841*04fd306cSNickeau $class = "text-warning"; 842*04fd306cSNickeau $levelName = "Warning"; 843*04fd306cSNickeau break; 844*04fd306cSNickeau default: 845*04fd306cSNickeau $levelName = $level; 846*04fd306cSNickeau $class = "text-primary"; 847*04fd306cSNickeau break; 848*04fd306cSNickeau } 849*04fd306cSNickeau $autoHide = "false"; // auto-hidding is really bad ui 850*04fd306cSNickeau $toastMessage = ""; 851*04fd306cSNickeau foreach ($messagesForLevel as $messageForLevel) { 852*04fd306cSNickeau $toastMessage .= "<p>{$messageForLevel['msg']}</p>"; 853*04fd306cSNickeau } 854*04fd306cSNickeau 855*04fd306cSNickeau 856*04fd306cSNickeau $toasts .= <<<EOF 857*04fd306cSNickeau<div role="alert" aria-live="assertive" aria-atomic="true" class="toast fade" data-bs-autohide="$autoHide"> 858*04fd306cSNickeau <div class="toast-header"> 859*04fd306cSNickeau <strong class="me-auto $class">{$levelName}</strong> 860*04fd306cSNickeau <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> 861*04fd306cSNickeau </div> 862*04fd306cSNickeau <div class="toast-body"> 863*04fd306cSNickeau $toastMessage 864*04fd306cSNickeau </div> 865*04fd306cSNickeau</div> 866*04fd306cSNickeauEOF; 867*04fd306cSNickeau } 868*04fd306cSNickeau 869*04fd306cSNickeau unset($GLOBALS['MSG']); 870*04fd306cSNickeau 871*04fd306cSNickeau if ($toasts === "") { 872*04fd306cSNickeau throw new ExceptionNotFound("No messages"); 873*04fd306cSNickeau } 874*04fd306cSNickeau 875*04fd306cSNickeau $this->hadMessages = true; 876*04fd306cSNickeau 877*04fd306cSNickeau // position fixed to not participate into the grid 878*04fd306cSNickeau return <<<EOF 879*04fd306cSNickeau<div class="toast-container position-fixed mb-3 me-3 bottom-0 end-0" id="toastPlacement" style="z-index:1060"> 880*04fd306cSNickeau$toasts 881*04fd306cSNickeau</div> 882*04fd306cSNickeauEOF; 883*04fd306cSNickeau 884*04fd306cSNickeau } 885*04fd306cSNickeau 886*04fd306cSNickeau private 887*04fd306cSNickeau function canBeCached(): bool 888*04fd306cSNickeau { 889*04fd306cSNickeau // no if message 890*04fd306cSNickeau return true; 891*04fd306cSNickeau } 892*04fd306cSNickeau 893*04fd306cSNickeau /** 894*04fd306cSNickeau * Adapted from {@link tpl_indexerWebBug()} 895*04fd306cSNickeau * @return string 896*04fd306cSNickeau */ 897*04fd306cSNickeau private 898*04fd306cSNickeau function getTaskRunnerImg(): string 899*04fd306cSNickeau { 900*04fd306cSNickeau 901*04fd306cSNickeau try { 902*04fd306cSNickeau $htmlUrl = UrlEndpoint::createTaskRunnerUrl() 903*04fd306cSNickeau ->addQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $this->getRequestedContextPath()->getWikiId()) 904*04fd306cSNickeau ->addQueryParameter(time()) 905*04fd306cSNickeau ->toString(); 906*04fd306cSNickeau } catch (ExceptionNotFound $e) { 907*04fd306cSNickeau throw new ExceptionRuntimeInternal("A request path is mandatory when adding a task runner. Disable it if you don't want one in the layout ($this)."); 908*04fd306cSNickeau } 909*04fd306cSNickeau 910*04fd306cSNickeau // no more 1x1 px image because of ad blockers 911*04fd306cSNickeau return TagAttributes::createEmpty() 912*04fd306cSNickeau ->addOutputAttributeValue("id", TemplateForWebPage::TASK_RUNNER_ID) 913*04fd306cSNickeau ->addClassName("d-none") 914*04fd306cSNickeau ->addOutputAttributeValue('width', 2) 915*04fd306cSNickeau ->addOutputAttributeValue('height', 1) 916*04fd306cSNickeau ->addOutputAttributeValue('alt', 'Task Runner') 917*04fd306cSNickeau ->addOutputAttributeValue('src', $htmlUrl) 918*04fd306cSNickeau ->toHtmlEmptyTag("img"); 919*04fd306cSNickeau } 920*04fd306cSNickeau 921*04fd306cSNickeau private 922*04fd306cSNickeau function getRequestedLangOrDefault(): Lang 923*04fd306cSNickeau { 924*04fd306cSNickeau try { 925*04fd306cSNickeau return $this->getRequestedLang(); 926*04fd306cSNickeau } catch (ExceptionNotFound $e) { 927*04fd306cSNickeau return Lang::createFromValue("en"); 928*04fd306cSNickeau } 929*04fd306cSNickeau } 930*04fd306cSNickeau 931*04fd306cSNickeau private 932*04fd306cSNickeau function getTheme(): string 933*04fd306cSNickeau { 934*04fd306cSNickeau return $this->requestedTheme ?? ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getTheme(); 935*04fd306cSNickeau } 936*04fd306cSNickeau 937*04fd306cSNickeau private 938*04fd306cSNickeau function getHeadHtml(): string 939*04fd306cSNickeau { 940*04fd306cSNickeau $snippetManager = PluginUtility::getSnippetManager(); 941*04fd306cSNickeau 942*04fd306cSNickeau if (!$this->isTemplateStringExecutionMode()) { 943*04fd306cSNickeau 944*04fd306cSNickeau /** 945*04fd306cSNickeau * Add the layout js and css first 946*04fd306cSNickeau */ 947*04fd306cSNickeau 948*04fd306cSNickeau try { 949*04fd306cSNickeau $cssPath = $this->getCssPath(); 950*04fd306cSNickeau $content = FileSystems::getContent($cssPath); 951*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet(self::CANONICAL, $content); 952*04fd306cSNickeau } catch (ExceptionNotFound $e) { 953*04fd306cSNickeau // no css found, not a problem 954*04fd306cSNickeau } 955*04fd306cSNickeau try { 956*04fd306cSNickeau $jsPath = $this->getJsPath(); 957*04fd306cSNickeau $snippetManager->attachInternalJavascriptFromPathForRequest(self::CANONICAL, $jsPath); 958*04fd306cSNickeau } catch (ExceptionNotFound $e) { 959*04fd306cSNickeau // not found 960*04fd306cSNickeau } 961*04fd306cSNickeau 962*04fd306cSNickeau 963*04fd306cSNickeau } 964*04fd306cSNickeau 965*04fd306cSNickeau /** 966*04fd306cSNickeau * Dokuwiki Smiley does not have any height 967*04fd306cSNickeau */ 968*04fd306cSNickeau $snippetManager->attachCssInternalStyleSheet("dokuwiki-smiley"); 969*04fd306cSNickeau 970*04fd306cSNickeau /** 971*04fd306cSNickeau * Iframe 972*04fd306cSNickeau */ 973*04fd306cSNickeau if ($this->isIframe) { 974*04fd306cSNickeau global $EVENT_HANDLER; 975*04fd306cSNickeau $EVENT_HANDLER->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'onlyIframeHeadTags'); 976*04fd306cSNickeau } 977*04fd306cSNickeau /** 978*04fd306cSNickeau * Start the meta headers 979*04fd306cSNickeau */ 980*04fd306cSNickeau ob_start(); 981*04fd306cSNickeau try { 982*04fd306cSNickeau tpl_metaheaders(); 983*04fd306cSNickeau $headIcon = $this->getPageIconHeadLinkHtml(); 984*04fd306cSNickeau return $headIcon . ob_get_contents(); 985*04fd306cSNickeau } finally { 986*04fd306cSNickeau ob_end_clean(); 987*04fd306cSNickeau } 988*04fd306cSNickeau 989*04fd306cSNickeau } 990*04fd306cSNickeau 991*04fd306cSNickeau 992*04fd306cSNickeau public 993*04fd306cSNickeau function setRequestedTemplateName(string $templateName): TemplateForWebPage 994*04fd306cSNickeau { 995*04fd306cSNickeau $this->templateName = $templateName; 996*04fd306cSNickeau return $this; 997*04fd306cSNickeau } 998*04fd306cSNickeau 999*04fd306cSNickeau /** 1000*04fd306cSNickeau * Add or not the task runner / web bug call 1001*04fd306cSNickeau * @param bool $b 1002*04fd306cSNickeau * @return TemplateForWebPage 1003*04fd306cSNickeau */ 1004*04fd306cSNickeau public 1005*04fd306cSNickeau function setRequestedEnableTaskRunner(bool $b): TemplateForWebPage 1006*04fd306cSNickeau { 1007*04fd306cSNickeau $this->requestedEnableTaskRunner = $b; 1008*04fd306cSNickeau return $this; 1009*04fd306cSNickeau } 1010*04fd306cSNickeau 1011*04fd306cSNickeau 1012*04fd306cSNickeau /** 1013*04fd306cSNickeau * @param Lang $requestedLang 1014*04fd306cSNickeau * @return TemplateForWebPage 1015*04fd306cSNickeau */ 1016*04fd306cSNickeau public 1017*04fd306cSNickeau function setRequestedLang(Lang $requestedLang): TemplateForWebPage 1018*04fd306cSNickeau { 1019*04fd306cSNickeau $this->requestedLang = $requestedLang; 1020*04fd306cSNickeau return $this; 1021*04fd306cSNickeau } 1022*04fd306cSNickeau 1023*04fd306cSNickeau /** 1024*04fd306cSNickeau * @param string $requestedTitle 1025*04fd306cSNickeau * @return TemplateForWebPage 1026*04fd306cSNickeau */ 1027*04fd306cSNickeau public 1028*04fd306cSNickeau function setRequestedTitle(string $requestedTitle): TemplateForWebPage 1029*04fd306cSNickeau { 1030*04fd306cSNickeau $this->requestedTitle = $requestedTitle; 1031*04fd306cSNickeau return $this; 1032*04fd306cSNickeau } 1033*04fd306cSNickeau 1034*04fd306cSNickeau /** 1035*04fd306cSNickeau * Delete the social head tags 1036*04fd306cSNickeau * (ie the page should not be indexed) 1037*04fd306cSNickeau * This is used for iframe content for instance 1038*04fd306cSNickeau * @param bool $isSocial 1039*04fd306cSNickeau * @return TemplateForWebPage 1040*04fd306cSNickeau */ 1041*04fd306cSNickeau public 1042*04fd306cSNickeau function setIsSocial(bool $isSocial): TemplateForWebPage 1043*04fd306cSNickeau { 1044*04fd306cSNickeau $this->isSocial = $isSocial; 1045*04fd306cSNickeau return $this; 1046*04fd306cSNickeau } 1047*04fd306cSNickeau 1048*04fd306cSNickeau public 1049*04fd306cSNickeau function setRequestedContextPath(WikiPath $contextPath): TemplateForWebPage 1050*04fd306cSNickeau { 1051*04fd306cSNickeau $this->requestedContextPath = $contextPath; 1052*04fd306cSNickeau return $this; 1053*04fd306cSNickeau } 1054*04fd306cSNickeau 1055*04fd306cSNickeau public 1056*04fd306cSNickeau function setToc(Toc $toc): TemplateForWebPage 1057*04fd306cSNickeau { 1058*04fd306cSNickeau $this->toc = $toc; 1059*04fd306cSNickeau return $this; 1060*04fd306cSNickeau } 1061*04fd306cSNickeau 1062*04fd306cSNickeau /** 1063*04fd306cSNickeau * There is two mode of execution, via: 1064*04fd306cSNickeau * * a file template (theme) 1065*04fd306cSNickeau * * or a string template (string) 1066*04fd306cSNickeau * 1067*04fd306cSNickeau * @return bool - true if this a string template executions 1068*04fd306cSNickeau */ 1069*04fd306cSNickeau private 1070*04fd306cSNickeau function isTemplateStringExecutionMode(): bool 1071*04fd306cSNickeau { 1072*04fd306cSNickeau return isset($this->templateString); 1073*04fd306cSNickeau } 1074*04fd306cSNickeau 1075*04fd306cSNickeau private 1076*04fd306cSNickeau function getEngine(): TemplateEngine 1077*04fd306cSNickeau { 1078*04fd306cSNickeau if ($this->isTemplateStringExecutionMode()) { 1079*04fd306cSNickeau return TemplateEngine::createForString(); 1080*04fd306cSNickeau 1081*04fd306cSNickeau } else { 1082*04fd306cSNickeau $theme = $this->getTheme(); 1083*04fd306cSNickeau return TemplateEngine::createForTheme($theme); 1084*04fd306cSNickeau } 1085*04fd306cSNickeau } 1086*04fd306cSNickeau 1087*04fd306cSNickeau private 1088*04fd306cSNickeau function getDefinition(): array 1089*04fd306cSNickeau { 1090*04fd306cSNickeau try { 1091*04fd306cSNickeau if (isset($this->templateDefinition)) { 1092*04fd306cSNickeau return $this->templateDefinition; 1093*04fd306cSNickeau } 1094*04fd306cSNickeau $file = $this->getEngine()->searchTemplateByName("{$this->getTemplateName()}.yml"); 1095*04fd306cSNickeau if (!FileSystems::exists($file)) { 1096*04fd306cSNickeau return []; 1097*04fd306cSNickeau } 1098*04fd306cSNickeau $this->templateDefinition = Yaml::parseFile($file->toAbsoluteId()); 1099*04fd306cSNickeau return $this->templateDefinition; 1100*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1101*04fd306cSNickeau // no template directory, not a theme run 1102*04fd306cSNickeau return []; 1103*04fd306cSNickeau } 1104*04fd306cSNickeau } 1105*04fd306cSNickeau 1106*04fd306cSNickeau private 1107*04fd306cSNickeau function getRailbarLayout(): string 1108*04fd306cSNickeau { 1109*04fd306cSNickeau $definition = $this->getDefinition(); 1110*04fd306cSNickeau if (isset($definition['railbar']['layout'])) { 1111*04fd306cSNickeau return $definition['railbar']['layout']; 1112*04fd306cSNickeau } 1113*04fd306cSNickeau return FetcherRailBar::BOTH_LAYOUT; 1114*04fd306cSNickeau } 1115*04fd306cSNickeau 1116*04fd306cSNickeau /** 1117*04fd306cSNickeau * Keep the only iframe head tag needed 1118*04fd306cSNickeau * @param $event 1119*04fd306cSNickeau * @return void 1120*04fd306cSNickeau */ 1121*04fd306cSNickeau public 1122*04fd306cSNickeau function onlyIframeHeadTags(&$event) 1123*04fd306cSNickeau { 1124*04fd306cSNickeau 1125*04fd306cSNickeau $data = &$event->data; 1126*04fd306cSNickeau foreach ($data as $tag => &$heads) { 1127*04fd306cSNickeau switch ($tag) { 1128*04fd306cSNickeau case "link": 1129*04fd306cSNickeau $deletedRel = ["manifest", "search", "start", "alternate", "canonical"]; 1130*04fd306cSNickeau foreach ($heads as $id => $headAttributes) { 1131*04fd306cSNickeau if (isset($headAttributes['rel'])) { 1132*04fd306cSNickeau $rel = $headAttributes['rel']; 1133*04fd306cSNickeau if (in_array($rel, $deletedRel)) { 1134*04fd306cSNickeau unset($heads[$id]); 1135*04fd306cSNickeau } 1136*04fd306cSNickeau if ($rel === "stylesheet") { 1137*04fd306cSNickeau $href = $headAttributes['href']; 1138*04fd306cSNickeau if (strpos($href, "lib/exe/css.php") !== false) { 1139*04fd306cSNickeau unset($heads[$id]); 1140*04fd306cSNickeau } 1141*04fd306cSNickeau } 1142*04fd306cSNickeau } 1143*04fd306cSNickeau } 1144*04fd306cSNickeau break; 1145*04fd306cSNickeau case "meta": 1146*04fd306cSNickeau $deletedMeta = ["og:url", "og:description", "description", "robots"]; 1147*04fd306cSNickeau foreach ($heads as $id => $headAttributes) { 1148*04fd306cSNickeau if (isset($headAttributes['name']) || isset($headAttributes['property'])) { 1149*04fd306cSNickeau $rel = $headAttributes['name']; 1150*04fd306cSNickeau if ($rel === null) { 1151*04fd306cSNickeau $rel = $headAttributes['property']; 1152*04fd306cSNickeau } 1153*04fd306cSNickeau if (in_array($rel, $deletedMeta)) { 1154*04fd306cSNickeau unset($heads[$id]); 1155*04fd306cSNickeau } 1156*04fd306cSNickeau } 1157*04fd306cSNickeau } 1158*04fd306cSNickeau break; 1159*04fd306cSNickeau case "script": 1160*04fd306cSNickeau foreach ($heads as $id => $headAttributes) { 1161*04fd306cSNickeau if (isset($headAttributes['src'])) { 1162*04fd306cSNickeau $src = $headAttributes['src']; 1163*04fd306cSNickeau if (strpos($src, "lib/exe/js.php") !== false) { 1164*04fd306cSNickeau unset($heads[$id]); 1165*04fd306cSNickeau } 1166*04fd306cSNickeau if (strpos($src, "lib/exe/jquery.php") !== false) { 1167*04fd306cSNickeau unset($heads[$id]); 1168*04fd306cSNickeau } 1169*04fd306cSNickeau } 1170*04fd306cSNickeau } 1171*04fd306cSNickeau break; 1172*04fd306cSNickeau } 1173*04fd306cSNickeau } 1174*04fd306cSNickeau } 1175*04fd306cSNickeau 1176*04fd306cSNickeau} 1177