xref: /plugin/combo/ComboStrap/TemplateForWebPage.php (revision e3d00612903f9ea95a6a4629e52559c09f2478f9)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeaunamespace ComboStrap;
404fd306cSNickeau
504fd306cSNickeau
604fd306cSNickeauuse ComboStrap\Meta\Field\PageTemplateName;
704fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
804fd306cSNickeauuse ComboStrap\Web\UrlEndpoint;
904fd306cSNickeauuse ComboStrap\Xml\XmlDocument;
1004fd306cSNickeauuse ComboStrap\Xml\XmlElement;
1104fd306cSNickeauuse Symfony\Component\Yaml\Yaml;
1204fd306cSNickeau
1304fd306cSNickeau/**
1404fd306cSNickeau * A page template is the object
1504fd306cSNickeau * that generates a HTML page
1604fd306cSNickeau * (ie the templating engine)
1704fd306cSNickeau *
1804fd306cSNickeau * It's used by Fetcher that creates pages such
1904fd306cSNickeau * as {@link FetcherPage}, {@link FetcherMarkupWebcode} or {@link FetcherPageBundler}
2004fd306cSNickeau */
2104fd306cSNickeauclass TemplateForWebPage
2204fd306cSNickeau{
2304fd306cSNickeau
2404fd306cSNickeau
2504fd306cSNickeau    /**
2604fd306cSNickeau     * An internal configuration
2704fd306cSNickeau     * to tell if the page is social
2804fd306cSNickeau     * (ie seo, search engine, friendly)
2904fd306cSNickeau     */
3004fd306cSNickeau    const CONF_INTERNAL_IS_SOCIAL = "web-page-is-social";
3104fd306cSNickeau
3204fd306cSNickeau    /**
3304fd306cSNickeau     * DocType is required by bootstrap and chrome
3404fd306cSNickeau     * https://developer.chrome.com/docs/lighthouse/best-practices/doctype/
3504fd306cSNickeau     * https://getbootstrap.com/docs/5.0/getting-started/introduction/#html5-doctype
3604fd306cSNickeau     * <!doctype html>
37*e3d00612Sgerardnico     *
38*e3d00612Sgerardnico     * The eol `\n` is needed for lightouse
3904fd306cSNickeau     */
40*e3d00612Sgerardnico    const DOCTYPE = "<!doctype html>\n";
4104fd306cSNickeau
4204fd306cSNickeau    private array $templateDefinition;
4304fd306cSNickeau    const CANONICAL = "template";
4404fd306cSNickeau
4504fd306cSNickeau
4604fd306cSNickeau    public const UTF_8_CHARSET_VALUE = "utf-8";
4704fd306cSNickeau    public const VIEWPORT_RESPONSIVE_VALUE = "width=device-width, initial-scale=1";
4804fd306cSNickeau    public const TASK_RUNNER_ID = "task-runner";
4904fd306cSNickeau    public const APPLE_TOUCH_ICON_REL_VALUE = "apple-touch-icon";
5004fd306cSNickeau
5104fd306cSNickeau    public const PRELOAD_TAG = "preload";
5204fd306cSNickeau
5304fd306cSNickeau    private string $templateName;
5404fd306cSNickeau
5504fd306cSNickeau
5604fd306cSNickeau    private string $requestedTitle;
5704fd306cSNickeau
5804fd306cSNickeau
5904fd306cSNickeau    private bool $requestedEnableTaskRunner = true;
6004fd306cSNickeau    private WikiPath $requestedContextPath;
6104fd306cSNickeau    private Lang $requestedLang;
6204fd306cSNickeau    private Toc $toc;
6304fd306cSNickeau    private bool $isSocial;
6404fd306cSNickeau    private string $mainContent;
6504fd306cSNickeau    private string $templateString;
6604fd306cSNickeau    private array $model;
6704fd306cSNickeau    private bool $hadMessages = false;
6804fd306cSNickeau    private string $requestedTheme;
6904fd306cSNickeau    private bool $isIframe = false;
7004fd306cSNickeau    private array $slots;
7104fd306cSNickeau
7204fd306cSNickeau
7304fd306cSNickeau    public static function create(): TemplateForWebPage
7404fd306cSNickeau    {
7504fd306cSNickeau        return new TemplateForWebPage();
7604fd306cSNickeau    }
7704fd306cSNickeau
7804fd306cSNickeau    public static function config(): TemplateForWebPage
7904fd306cSNickeau    {
8004fd306cSNickeau        return new TemplateForWebPage();
8104fd306cSNickeau    }
8204fd306cSNickeau
8304fd306cSNickeau    public static function getPoweredBy(): string
8404fd306cSNickeau    {
8504fd306cSNickeau        $domain = PluginUtility::$URL_APEX;
8604fd306cSNickeau        $version = PluginUtility::$INFO_PLUGIN['version'] . " (" . PluginUtility::$INFO_PLUGIN['date'] . ")";
8704fd306cSNickeau        $poweredBy = "<div class=\"mx-auto\" style=\"width: 300px;text-align: center;margin-bottom: 1rem\">";
8804fd306cSNickeau        $poweredBy .= "  <small><i>Powered by <a href=\"$domain\" title=\"ComboStrap " . $version . "\" style=\"color:#495057\">ComboStrap</a></i></small>";
8904fd306cSNickeau        $poweredBy .= '</div>';
9004fd306cSNickeau        return $poweredBy;
9104fd306cSNickeau    }
9204fd306cSNickeau
9304fd306cSNickeau
9404fd306cSNickeau    /**
9504fd306cSNickeau     * @throws ExceptionNotFound
9604fd306cSNickeau     */
9704fd306cSNickeau    public function getHtmlTemplatePath(): LocalPath
9804fd306cSNickeau    {
9904fd306cSNickeau        return $this->getEngine()->searchTemplateByName($this->templateName . "." . TemplateEngine::EXTENSION_HBS);
10004fd306cSNickeau    }
10104fd306cSNickeau
10204fd306cSNickeau    public function setTemplateString(string $templateString): TemplateForWebPage
10304fd306cSNickeau    {
10404fd306cSNickeau        $this->templateString = $templateString;
10504fd306cSNickeau        return $this;
10604fd306cSNickeau    }
10704fd306cSNickeau
10804fd306cSNickeau    public function setModel(array $model): TemplateForWebPage
10904fd306cSNickeau    {
11004fd306cSNickeau        $this->model = $model;
11104fd306cSNickeau        return $this;
11204fd306cSNickeau    }
11304fd306cSNickeau
11404fd306cSNickeau    /**
11504fd306cSNickeau     * @return WikiPath from where the markup slot should be searched
11604fd306cSNickeau     * @throws ExceptionNotFound
11704fd306cSNickeau     */
11804fd306cSNickeau    public function getRequestedContextPath(): WikiPath
11904fd306cSNickeau    {
12004fd306cSNickeau        if (!isset($this->requestedContextPath)) {
12104fd306cSNickeau            throw new ExceptionNotFound("A requested context path was not found");
12204fd306cSNickeau        }
12304fd306cSNickeau        return $this->requestedContextPath;
12404fd306cSNickeau    }
12504fd306cSNickeau
12604fd306cSNickeau    /**
12704fd306cSNickeau     *
12804fd306cSNickeau     * @return string - the page as html string (not dom because that's not how works dokuwiki)
12904fd306cSNickeau     *
13004fd306cSNickeau     */
13104fd306cSNickeau    public function render(): string
13204fd306cSNickeau    {
13304fd306cSNickeau
13404fd306cSNickeau        $executionContext = (ExecutionContext::getActualOrCreateFromEnv())
13504fd306cSNickeau            ->setExecutingPageTemplate($this);
13604fd306cSNickeau        try {
13704fd306cSNickeau
13804fd306cSNickeau
13904fd306cSNickeau            $pageTemplateEngine = $this->getEngine();
14004fd306cSNickeau            if ($this->isTemplateStringExecutionMode()) {
14104fd306cSNickeau                $template = $this->templateString;
14204fd306cSNickeau            } else {
14304fd306cSNickeau                $pageTemplateEngine = $this->getEngine();
14404fd306cSNickeau                $template = $this->getTemplateName();
14504fd306cSNickeau                if (!$pageTemplateEngine->templateExists($template)) {
14604fd306cSNickeau                    $defaultTemplate = PageTemplateName::HOLY_TEMPLATE_VALUE;
14704fd306cSNickeau                    LogUtility::warning("The template ($template) was not found, the default template ($defaultTemplate) was used instead.");
14804fd306cSNickeau                    $template = $defaultTemplate;
14904fd306cSNickeau                    $this->setRequestedTemplateName($template);
15004fd306cSNickeau                }
15104fd306cSNickeau            }
15204fd306cSNickeau
15304fd306cSNickeau            /**
15404fd306cSNickeau             * Get model should came after template validation
15504fd306cSNickeau             * as the template definition is named dependent
15604fd306cSNickeau             * (Create a builder, nom de dieu)
15704fd306cSNickeau             */
15804fd306cSNickeau            $model = $this->getModel();
15904fd306cSNickeau
16004fd306cSNickeau
16104fd306cSNickeau            return self::DOCTYPE . $pageTemplateEngine->renderWebPage($template, $model);
16204fd306cSNickeau
16304fd306cSNickeau
16404fd306cSNickeau        } finally {
16504fd306cSNickeau            $executionContext
16604fd306cSNickeau                ->closeExecutingPageTemplate();
16704fd306cSNickeau        }
16804fd306cSNickeau
16904fd306cSNickeau    }
17004fd306cSNickeau
17104fd306cSNickeau    /**
17204fd306cSNickeau     * @return string[]
17304fd306cSNickeau     */
17404fd306cSNickeau    public function getElementIds(): array
17504fd306cSNickeau    {
17604fd306cSNickeau        $definition = $this->getDefinition();
17704fd306cSNickeau        $elements = $definition['elements'];
17804fd306cSNickeau        if ($elements == null) {
17904fd306cSNickeau            return [];
18004fd306cSNickeau        }
18104fd306cSNickeau        return $elements;
18204fd306cSNickeau
18304fd306cSNickeau    }
18404fd306cSNickeau
18504fd306cSNickeau
18604fd306cSNickeau    /**
18704fd306cSNickeau     * @throws ExceptionNotFound
18804fd306cSNickeau     */
18904fd306cSNickeau    private function getRequestedLang(): Lang
19004fd306cSNickeau    {
19104fd306cSNickeau        if (!isset($this->requestedLang)) {
19204fd306cSNickeau            throw new ExceptionNotFound("No requested lang");
19304fd306cSNickeau        }
19404fd306cSNickeau        return $this->requestedLang;
19504fd306cSNickeau    }
19604fd306cSNickeau
19704fd306cSNickeau
19804fd306cSNickeau    public function getTemplateName(): string
19904fd306cSNickeau    {
20004fd306cSNickeau        if (isset($this->templateName)) {
20104fd306cSNickeau            return $this->templateName;
20204fd306cSNickeau        }
20304fd306cSNickeau        try {
20404fd306cSNickeau            $requestedPath = $this->getRequestedContextPath();
20504fd306cSNickeau            return PageTemplateName::createFromPage(MarkupPath::createPageFromPathObject($requestedPath))
20604fd306cSNickeau                ->getValueOrDefault();
20704fd306cSNickeau        } catch (ExceptionNotFound $e) {
20804fd306cSNickeau            // no requested path
20904fd306cSNickeau        }
21004fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
21104fd306cSNickeau            ->getConfig()
21204fd306cSNickeau            ->getDefaultLayoutName();
21304fd306cSNickeau    }
21404fd306cSNickeau
21504fd306cSNickeau
21604fd306cSNickeau    public function __toString()
21704fd306cSNickeau    {
21804fd306cSNickeau        return $this->templateName;
21904fd306cSNickeau    }
22004fd306cSNickeau
22104fd306cSNickeau    /**
22204fd306cSNickeau     * @throws ExceptionNotFound
22304fd306cSNickeau     */
22404fd306cSNickeau    public function getCssPath(): LocalPath
22504fd306cSNickeau    {
22604fd306cSNickeau        return $this->getEngine()->searchTemplateByName("$this->templateName.css");
22704fd306cSNickeau    }
22804fd306cSNickeau
22904fd306cSNickeau    /**
23004fd306cSNickeau     * @throws ExceptionNotFound
23104fd306cSNickeau     */
23204fd306cSNickeau    public function getJsPath(): LocalPath
23304fd306cSNickeau    {
23404fd306cSNickeau        $jsPath = $this->getEngine()->searchTemplateByName("$this->templateName.js");
23504fd306cSNickeau        if (!FileSystems::exists($jsPath)) {
23604fd306cSNickeau            throw new ExceptionNotFound("No js file");
23704fd306cSNickeau        }
23804fd306cSNickeau        return $jsPath;
23904fd306cSNickeau    }
24004fd306cSNickeau
24104fd306cSNickeau    public function hasMessages(): bool
24204fd306cSNickeau    {
24304fd306cSNickeau        return $this->hadMessages;
24404fd306cSNickeau    }
24504fd306cSNickeau
24604fd306cSNickeau    public function setRequestedTheme(string $themeName): TemplateForWebPage
24704fd306cSNickeau    {
24804fd306cSNickeau        $this->requestedTheme = $themeName;
24904fd306cSNickeau        return $this;
25004fd306cSNickeau    }
25104fd306cSNickeau
25204fd306cSNickeau    public function hasElement(string $elementId): bool
25304fd306cSNickeau    {
25404fd306cSNickeau        return in_array($elementId, $this->getElementIds());
25504fd306cSNickeau    }
25604fd306cSNickeau
25704fd306cSNickeau    public function isSocial(): bool
25804fd306cSNickeau    {
25904fd306cSNickeau        if (isset($this->isSocial)) {
26004fd306cSNickeau            return $this->isSocial;
26104fd306cSNickeau        }
26204fd306cSNickeau        try {
26304fd306cSNickeau            $path = $this->getRequestedContextPath();
26404fd306cSNickeau            if (!FileSystems::exists($path)) {
26504fd306cSNickeau                return false;
26604fd306cSNickeau            }
26704fd306cSNickeau            $markup = MarkupPath::createPageFromPathObject($path);
26804fd306cSNickeau            if ($markup->isSlot()) {
26904fd306cSNickeau                // slot are not social
27004fd306cSNickeau                return false;
27104fd306cSNickeau            }
27204fd306cSNickeau        } catch (ExceptionNotFound $e) {
27304fd306cSNickeau            // not a path run
27404fd306cSNickeau            return false;
27504fd306cSNickeau        }
27604fd306cSNickeau        if ($this->isIframe) {
27704fd306cSNickeau            return false;
27804fd306cSNickeau        }
27904fd306cSNickeau        return ExecutionContext::getActualOrCreateFromEnv()
28004fd306cSNickeau            ->getConfig()
28104fd306cSNickeau            ->getBooleanValue(self::CONF_INTERNAL_IS_SOCIAL, true);
28204fd306cSNickeau
28304fd306cSNickeau    }
28404fd306cSNickeau
28504fd306cSNickeau    public function setIsIframe(bool $isIframe): TemplateForWebPage
28604fd306cSNickeau    {
28704fd306cSNickeau        $this->isIframe = $isIframe;
28804fd306cSNickeau        return $this;
28904fd306cSNickeau    }
29004fd306cSNickeau
29104fd306cSNickeau    /**
29204fd306cSNickeau     * @return TemplateSlot[]
29304fd306cSNickeau     */
29404fd306cSNickeau    public function getSlots(): array
29504fd306cSNickeau    {
29604fd306cSNickeau        if (isset($this->slots)) {
29704fd306cSNickeau            return $this->slots;
29804fd306cSNickeau        }
29904fd306cSNickeau        $this->slots = [];
30004fd306cSNickeau        foreach ($this->getElementIds() as $elementId) {
30104fd306cSNickeau            if ($elementId === TemplateSlot::MAIN_TOC_ID) {
30204fd306cSNickeau                /**
30304fd306cSNickeau                 * Main toc element is not a slot
30404fd306cSNickeau                 */
30504fd306cSNickeau                continue;
30604fd306cSNickeau            }
30704fd306cSNickeau
30804fd306cSNickeau            try {
30904fd306cSNickeau                $this->slots[] = TemplateSlot::createFromElementId($elementId, $this->getRequestedContextPath());
31004fd306cSNickeau            } catch (ExceptionNotFound $e) {
31104fd306cSNickeau                LogUtility::internalError("This template is not for a markup path, it cannot have slots then.");
31204fd306cSNickeau            }
31304fd306cSNickeau        }
31404fd306cSNickeau        return $this->slots;
31504fd306cSNickeau    }
31604fd306cSNickeau
31704fd306cSNickeau
31804fd306cSNickeau    /**
31904fd306cSNickeau     * Character set
32004fd306cSNickeau     * Note: avoid using {@link Html::encode() character entities} in your HTML,
32104fd306cSNickeau     * provided their encoding matches that of the document (generally UTF-8)
32204fd306cSNickeau     */
32304fd306cSNickeau    private function checkCharSetMeta(XmlElement $head)
32404fd306cSNickeau    {
32504fd306cSNickeau        $charsetValue = TemplateForWebPage::UTF_8_CHARSET_VALUE;
32604fd306cSNickeau        try {
32704fd306cSNickeau            $metaCharset = $head->querySelector("meta[charset]");
32804fd306cSNickeau            $charsetActualValue = $metaCharset->getAttribute("charset");
32904fd306cSNickeau            if ($charsetActualValue !== $charsetValue) {
33004fd306cSNickeau                LogUtility::warning("The actual charset ($charsetActualValue) should be $charsetValue");
33104fd306cSNickeau            }
33204fd306cSNickeau        } catch (ExceptionBadSyntax|ExceptionNotFound $e) {
33304fd306cSNickeau            try {
33404fd306cSNickeau                $metaCharset = $head->getDocument()
33504fd306cSNickeau                    ->createElement("meta")
33604fd306cSNickeau                    ->setAttribute("charset", $charsetValue);
33704fd306cSNickeau                $head->appendChild($metaCharset);
33804fd306cSNickeau            } catch (\DOMException $e) {
33904fd306cSNickeau                throw new ExceptionRuntimeInternal("Bad local name meta, should not occur", self::CANONICAL, 1, $e);
34004fd306cSNickeau            }
34104fd306cSNickeau        }
34204fd306cSNickeau    }
34304fd306cSNickeau
34404fd306cSNickeau    /**
34504fd306cSNickeau     * @param XmlElement $head
34604fd306cSNickeau     * @return void
34704fd306cSNickeau     * Adapted from {@link TplUtility::renderFaviconMetaLinks()}
34804fd306cSNickeau     */
34904fd306cSNickeau    private function getPageIconHeadLinkHtml(): string
35004fd306cSNickeau    {
35104fd306cSNickeau        $html = $this->getShortcutFavIconHtmlLink();
35204fd306cSNickeau        $html .= $this->getIconHtmlLink();
35304fd306cSNickeau        $html .= $this->getAppleTouchIconHtmlLink();
35404fd306cSNickeau        return $html;
35504fd306cSNickeau    }
35604fd306cSNickeau
35704fd306cSNickeau    /**
35804fd306cSNickeau     * Add a favIcon.ico
35904fd306cSNickeau     *
36004fd306cSNickeau     */
36104fd306cSNickeau    private function getShortcutFavIconHtmlLink(): string
36204fd306cSNickeau    {
36304fd306cSNickeau
36404fd306cSNickeau        $internalFavIcon = WikiPath::createComboResource('images:favicon.ico');
36504fd306cSNickeau        $iconPaths = array(
36604fd306cSNickeau            WikiPath::createMediaPathFromId(':favicon.ico'),
36704fd306cSNickeau            WikiPath::createMediaPathFromId(':wiki:favicon.ico'),
36804fd306cSNickeau            $internalFavIcon
36904fd306cSNickeau        );
37004fd306cSNickeau        try {
37104fd306cSNickeau            /**
37204fd306cSNickeau             * @var WikiPath $icoWikiPath - we give wiki paths, we get wiki path
37304fd306cSNickeau             */
37404fd306cSNickeau            $icoWikiPath = FileSystems::getFirstExistingPath($iconPaths);
37504fd306cSNickeau        } catch (ExceptionNotFound $e) {
37604fd306cSNickeau            LogUtility::internalError("The internal fav icon ($internalFavIcon) should be at minimal found", self::CANONICAL);
37704fd306cSNickeau            return "";
37804fd306cSNickeau        }
37904fd306cSNickeau
38004fd306cSNickeau        return TagAttributes::createEmpty()
38104fd306cSNickeau            ->addOutputAttributeValue("rel", "shortcut icon")
38204fd306cSNickeau            ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($icoWikiPath)->getFetchUrl()->toAbsoluteUrl()->toString())
38304fd306cSNickeau            ->toHtmlEmptyTag("link");
38404fd306cSNickeau
38504fd306cSNickeau    }
38604fd306cSNickeau
38704fd306cSNickeau    /**
38804fd306cSNickeau     * Add Icon Png (16x16 and 32x32)
38904fd306cSNickeau     * @return string
39004fd306cSNickeau     */
39104fd306cSNickeau    private function getIconHtmlLink(): string
39204fd306cSNickeau    {
39304fd306cSNickeau
39404fd306cSNickeau        $html = "";
39504fd306cSNickeau        $sizeValues = ["32x32", "16x16"];
39604fd306cSNickeau        foreach ($sizeValues as $sizeValue) {
39704fd306cSNickeau
39804fd306cSNickeau            $internalIcon = WikiPath::createComboResource(":images:favicon-$sizeValue.png");
39904fd306cSNickeau            $iconPaths = array(
40004fd306cSNickeau                WikiPath::createMediaPathFromId(":favicon-$sizeValue.png"),
40104fd306cSNickeau                WikiPath::createMediaPathFromId(":wiki:favicon-$sizeValue.png"),
40204fd306cSNickeau                $internalIcon
40304fd306cSNickeau            );
40404fd306cSNickeau            try {
40504fd306cSNickeau                /**
40604fd306cSNickeau                 * @var WikiPath $iconPath - to say to the linter that this is a wiki path
40704fd306cSNickeau                 */
40804fd306cSNickeau                $iconPath = FileSystems::getFirstExistingPath($iconPaths);
40904fd306cSNickeau            } catch (ExceptionNotFound $e) {
41004fd306cSNickeau                LogUtility::internalError("The internal icon ($internalIcon) should be at minimal found", self::CANONICAL);
41104fd306cSNickeau                continue;
41204fd306cSNickeau            }
41304fd306cSNickeau            $html .= TagAttributes::createEmpty()
41404fd306cSNickeau                ->addOutputAttributeValue("rel", "icon")
41504fd306cSNickeau                ->addOutputAttributeValue("sizes", $sizeValue)
41604fd306cSNickeau                ->addOutputAttributeValue("type", Mime::PNG)
41704fd306cSNickeau                ->addOutputAttributeValue("href", FetcherRawLocalPath::createFromPath($iconPath)->getFetchUrl()->toAbsoluteUrl()->toString())
41804fd306cSNickeau                ->toHtmlEmptyTag("link");
41904fd306cSNickeau        }
42004fd306cSNickeau        return $html;
42104fd306cSNickeau    }
42204fd306cSNickeau
42304fd306cSNickeau    /**
42404fd306cSNickeau     * Add Apple touch icon
42504fd306cSNickeau     *
42604fd306cSNickeau     * @return string
42704fd306cSNickeau     */
42804fd306cSNickeau    private function getAppleTouchIconHtmlLink(): string
42904fd306cSNickeau    {
43004fd306cSNickeau
43104fd306cSNickeau        $internalIcon = WikiPath::createComboResource(":images:apple-touch-icon.png");
43204fd306cSNickeau        $iconPaths = array(
43304fd306cSNickeau            WikiPath::createMediaPathFromId(":apple-touch-icon.png"),
43404fd306cSNickeau            WikiPath::createMediaPathFromId(":wiki:apple-touch-icon.png"),
43504fd306cSNickeau            $internalIcon
43604fd306cSNickeau        );
43704fd306cSNickeau        try {
43804fd306cSNickeau            /**
43904fd306cSNickeau             * @var WikiPath $iconPath - to say to the linter that this is a wiki path
44004fd306cSNickeau             */
44104fd306cSNickeau            $iconPath = FileSystems::getFirstExistingPath($iconPaths);
44204fd306cSNickeau        } catch (ExceptionNotFound $e) {
44304fd306cSNickeau            LogUtility::internalError("The internal apple icon ($internalIcon) should be at minimal found", self::CANONICAL);
44404fd306cSNickeau            return "";
44504fd306cSNickeau        }
44604fd306cSNickeau        try {
44704fd306cSNickeau            $fetcherLocalPath = FetcherRaster::createImageRasterFetchFromPath($iconPath);
44804fd306cSNickeau            $sizesValue = "{$fetcherLocalPath->getIntrinsicWidth()}x{$fetcherLocalPath->getIntrinsicHeight()}";
44904fd306cSNickeau
45004fd306cSNickeau            return TagAttributes::createEmpty()
45104fd306cSNickeau                ->addOutputAttributeValue("rel", self::APPLE_TOUCH_ICON_REL_VALUE)
45204fd306cSNickeau                ->addOutputAttributeValue("sizes", $sizesValue)
45304fd306cSNickeau                ->addOutputAttributeValue("type", Mime::PNG)
45404fd306cSNickeau                ->addOutputAttributeValue("href", $fetcherLocalPath->getFetchUrl()->toAbsoluteUrl()->toString())
45504fd306cSNickeau                ->toHtmlEmptyTag("link");
45604fd306cSNickeau        } catch (\Exception $e) {
45704fd306cSNickeau            LogUtility::internalError("The file ($iconPath) should be found and the local name should be good. Error: {$e->getMessage()}");
45804fd306cSNickeau            return "";
45904fd306cSNickeau        }
46004fd306cSNickeau    }
46104fd306cSNickeau
46204fd306cSNickeau    public
46304fd306cSNickeau    function getModel(): array
46404fd306cSNickeau    {
46504fd306cSNickeau
46604fd306cSNickeau        $executionConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig();
46704fd306cSNickeau
46804fd306cSNickeau        /**
46904fd306cSNickeau         * Mandatory HTML attributes
47004fd306cSNickeau         */
47104fd306cSNickeau        $model =
47204fd306cSNickeau            [
47304fd306cSNickeau                PageTitle::PROPERTY_NAME => $this->getRequestedTitleOrDefault(),
47404fd306cSNickeau                Lang::PROPERTY_NAME => $this->getRequestedLangOrDefault()->getValueOrDefault(),
47504fd306cSNickeau                // The direction is not yet calculated from the page, we let the browser determine it from the lang
47604fd306cSNickeau                // dokuwiki has a direction config also ...
47704fd306cSNickeau                // "dir" => $this->getRequestedLangOrDefault()->getDirection()
47804fd306cSNickeau            ];
47904fd306cSNickeau
48004fd306cSNickeau        if (isset($this->model)) {
48104fd306cSNickeau            return array_merge($model, $this->model);
48204fd306cSNickeau        }
48304fd306cSNickeau
48404fd306cSNickeau        /**
48504fd306cSNickeau         * The width of the layout
48604fd306cSNickeau         */
48704fd306cSNickeau        $container = $executionConfig->getValue(ContainerTag::DEFAULT_LAYOUT_CONTAINER_CONF, ContainerTag::DEFAULT_LAYOUT_CONTAINER_DEFAULT_VALUE);
48804fd306cSNickeau        $containerClass = ContainerTag::getClassName($container);
48904fd306cSNickeau        $model["layout-container-class"] = $containerClass;
49004fd306cSNickeau
49104fd306cSNickeau
49204fd306cSNickeau        /**
49304fd306cSNickeau         * The rem
49404fd306cSNickeau         */
49504fd306cSNickeau        try {
49604fd306cSNickeau            $model["rem-size"] = $executionConfig->getRemFontSize();
49704fd306cSNickeau        } catch (ExceptionNotFound $e) {
49804fd306cSNickeau            // ok none
49904fd306cSNickeau        }
50004fd306cSNickeau
50104fd306cSNickeau
50204fd306cSNickeau        /**
50304fd306cSNickeau         * Body class
50404fd306cSNickeau         * {@link tpl_classes} will add the dokuwiki class.
50504fd306cSNickeau         * See https://www.dokuwiki.org/devel:templates#dokuwiki_class
50604fd306cSNickeau         * dokuwiki__top ID is needed for the "Back to top" utility
50704fd306cSNickeau         * used also by some plugins
50804fd306cSNickeau         * dokwuiki as class is also needed as it's used by the linkwizard
50904fd306cSNickeau         * to locate where to add the node (ie .appendTo('.dokuwiki:first'))
51004fd306cSNickeau         */
51104fd306cSNickeau        $bodyDokuwikiClass = tpl_classes();
51204fd306cSNickeau        try {
51304fd306cSNickeau            $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("{$this->getTheme()}-{$this->getTemplateName()}");
51404fd306cSNickeau        } catch (\Exception $e) {
51504fd306cSNickeau            $bodyTemplateIdentifierClass = StyleAttribute::addComboStrapSuffix("template-string");
51604fd306cSNickeau        }
51704fd306cSNickeau        // position relative is for the toast and messages that are in the corner
51804fd306cSNickeau        $model['body-classes'] = "$bodyDokuwikiClass position-relative $bodyTemplateIdentifierClass";
51904fd306cSNickeau
52004fd306cSNickeau        /**
52104fd306cSNickeau         * Data coupled to a page
52204fd306cSNickeau         */
52304fd306cSNickeau        try {
52404fd306cSNickeau
52504fd306cSNickeau            $contextPath = $this->getRequestedContextPath();
52604fd306cSNickeau            $markupPath = MarkupPath::createPageFromPathObject($contextPath);
52704fd306cSNickeau            /**
52804fd306cSNickeau             * Meta
52904fd306cSNickeau             */
53004fd306cSNickeau            $metadata = $markupPath->getMetadataForRendering();
53104fd306cSNickeau            $model = array_merge($metadata, $model);
53204fd306cSNickeau
53304fd306cSNickeau
53404fd306cSNickeau            /**
53504fd306cSNickeau             * Railbar
53604fd306cSNickeau             * You can define the layout type by page
53704fd306cSNickeau             * This is not a handelbars helper because it needs some css snippet.
53804fd306cSNickeau             */
53904fd306cSNickeau            $railBarLayout = $this->getRailbarLayout();
54004fd306cSNickeau            try {
54104fd306cSNickeau                $model["railbar-html"] = FetcherRailBar::createRailBar()
54204fd306cSNickeau                    ->setRequestedLayout($railBarLayout)
54304fd306cSNickeau                    ->setRequestedPath($contextPath)
54404fd306cSNickeau                    ->getFetchString();
54504fd306cSNickeau            } catch (ExceptionBadArgument $e) {
54604fd306cSNickeau                LogUtility::error("Error while creating the railbar layout");
54704fd306cSNickeau            }
54804fd306cSNickeau
54904fd306cSNickeau            /**
55004fd306cSNickeau             * Css Variables Colors
55104fd306cSNickeau             * Added for now in `head-partial.hbs`
55204fd306cSNickeau             */
55304fd306cSNickeau            try {
55404fd306cSNickeau                $primaryColor = $executionConfig->getPrimaryColor();
55504fd306cSNickeau                $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = $primaryColor->toCssValue();
55604fd306cSNickeau                $model[BrandingColors::PRIMARY_COLOR_TEXT_ATTRIBUTE] = ColorSystem::toTextColor($primaryColor);
55704fd306cSNickeau                $model[BrandingColors::PRIMARY_COLOR_TEXT_HOVER_ATTRIBUTE] = ColorSystem::toTextHoverColor($primaryColor);
55804fd306cSNickeau            } catch (ExceptionNotFound $e) {
55904fd306cSNickeau                // not found
56004fd306cSNickeau                $model[BrandingColors::PRIMARY_COLOR_TEMPLATE_ATTRIBUTE] = null;
56104fd306cSNickeau            }
56204fd306cSNickeau            try {
56304fd306cSNickeau                $secondaryColor = $executionConfig->getSecondaryColor();
56404fd306cSNickeau                $model[BrandingColors::SECONDARY_COLOR_TEMPLATE_ATTRIBUTE] = $secondaryColor->toCssValue();
56504fd306cSNickeau            } catch (ExceptionNotFound $e) {
56604fd306cSNickeau                // not found
56704fd306cSNickeau            }
56804fd306cSNickeau
56904fd306cSNickeau
57004fd306cSNickeau            /**
57104fd306cSNickeau             * Main
57204fd306cSNickeau             */
57304fd306cSNickeau            if (isset($this->mainContent)) {
57404fd306cSNickeau                $model["main-content-html"] = $this->mainContent;
57504fd306cSNickeau            } else {
57604fd306cSNickeau                try {
57704fd306cSNickeau                    if (!$markupPath->isSlot()) {
57804fd306cSNickeau                        $requestedContextPathForMain = $this->getRequestedContextPath();
57904fd306cSNickeau                    } else {
58004fd306cSNickeau                        try {
58104fd306cSNickeau                            $markupContextPath = SlotSystem::getContextPath();
58204fd306cSNickeau                            SlotSystem::sendContextPathMessage($markupContextPath);
58304fd306cSNickeau                            $requestedContextPathForMain = $markupContextPath->toWikiPath();
58404fd306cSNickeau                        } catch (ExceptionNotFound|ExceptionCast $e) {
58504fd306cSNickeau                            $requestedContextPathForMain = $this->getRequestedContextPath();
58604fd306cSNickeau                        }
58704fd306cSNickeau                    }
58804fd306cSNickeau                    $model["main-content-html"] = FetcherMarkup::confRoot()
58904fd306cSNickeau                        ->setRequestedMimeToXhtml()
59004fd306cSNickeau                        ->setRequestedContextPath($requestedContextPathForMain)
59104fd306cSNickeau                        ->setRequestedExecutingPath($this->getRequestedContextPath())
59204fd306cSNickeau                        ->build()
59304fd306cSNickeau                        ->getFetchString();
59404fd306cSNickeau                } catch (ExceptionCompile|ExceptionNotExists|ExceptionNotExists $e) {
59504fd306cSNickeau                    LogUtility::error("Error while rendering the page content.", self::CANONICAL, $e);
59604fd306cSNickeau                    $model["main-content-html"] = "An error has occured. " . $e->getMessage();
59704fd306cSNickeau                }
59804fd306cSNickeau            }
59904fd306cSNickeau
60004fd306cSNickeau            /**
60104fd306cSNickeau             * Toc (after main execution please)
60204fd306cSNickeau             */
60304fd306cSNickeau            $model['toc-class'] = Toc::getClass();
60404fd306cSNickeau            $model['toc-html'] = $this->getTocOrDefault()->toXhtml();
60504fd306cSNickeau
60604fd306cSNickeau            /**
60704fd306cSNickeau             * Slots
60804fd306cSNickeau             */
60904fd306cSNickeau            foreach ($this->getSlots() as $slot) {
61004fd306cSNickeau
61104fd306cSNickeau                $elementId = $slot->getElementId();
61204fd306cSNickeau                try {
61304fd306cSNickeau                    $model["$elementId-html"] = $slot->getMarkupFetcher()->getFetchString();
61404fd306cSNickeau                } catch (ExceptionNotFound|ExceptionNotExists $e) {
61504fd306cSNickeau                    // no slot found
61604fd306cSNickeau                } catch (ExceptionCompile $e) {
61704fd306cSNickeau                    LogUtility::error("Error while rendering the slot $elementId for the template ($this)", self::CANONICAL, $e);
61804fd306cSNickeau                    $model["$elementId-html"] = LogUtility::wrapInRedForHtml("Error: " . $e->getMessage());
61904fd306cSNickeau                }
62004fd306cSNickeau            }
62104fd306cSNickeau
62204fd306cSNickeau            /**
62304fd306cSNickeau             * Found in {@link tpl_content()}
62404fd306cSNickeau             * Used to add html such as {@link \action_plugin_combo_routermessage}
62504fd306cSNickeau             * Not sure if this is the right place to add it.
62604fd306cSNickeau             */
62704fd306cSNickeau            ob_start();
62804fd306cSNickeau            global $ACT;
62904fd306cSNickeau            \dokuwiki\Extension\Event::createAndTrigger('TPL_ACT_RENDER', $ACT);
63004fd306cSNickeau            $tplActRenderOutput = ob_get_clean();
63104fd306cSNickeau            if (!empty($tplActRenderOutput)) {
63204fd306cSNickeau                $model["main-content-afterbegin-html"] = $tplActRenderOutput;
63304fd306cSNickeau                $this->hadMessages = true;
63404fd306cSNickeau            }
63504fd306cSNickeau
63604fd306cSNickeau        } catch (ExceptionNotFound $e) {
63704fd306cSNickeau            // no context path
63804fd306cSNickeau            if (isset($this->mainContent)) {
63904fd306cSNickeau                $model["main-content-html"] = $this->mainContent;
64004fd306cSNickeau            }
64104fd306cSNickeau        }
64204fd306cSNickeau
64304fd306cSNickeau
64404fd306cSNickeau        /**
64504fd306cSNickeau         * Head Html
64604fd306cSNickeau         * Snippet, Css and Js from the layout if any
64704fd306cSNickeau         *
64804fd306cSNickeau         * Note that head tag may be added during rendering and must be then called after rendering and toc
64904fd306cSNickeau         * (ie at last then)
65004fd306cSNickeau         */
65104fd306cSNickeau        $model['head-html'] = $this->getHeadHtml();
65204fd306cSNickeau
65304fd306cSNickeau        /**
65404fd306cSNickeau         * Preloaded Css
65504fd306cSNickeau         * (It must come after the head processing as this is where the preloaded script are defined)
65604fd306cSNickeau         * (Not really useful but legacy)
65704fd306cSNickeau         * We add it just before the end of the body tag
65804fd306cSNickeau         */
65904fd306cSNickeau        try {
66004fd306cSNickeau            $model['preloaded-stylesheet-html'] = $this->getHtmlForPreloadedStyleSheets();
66104fd306cSNickeau        } catch (ExceptionNotFound $e) {
66204fd306cSNickeau            // no preloaded stylesheet resources
66304fd306cSNickeau        }
66404fd306cSNickeau
66504fd306cSNickeau        /**
66604fd306cSNickeau         * Powered by
66704fd306cSNickeau         */
66804fd306cSNickeau        $model['powered-by'] = self::getPoweredBy();
66904fd306cSNickeau
67004fd306cSNickeau        /**
67104fd306cSNickeau         * Messages
67204fd306cSNickeau         * (Should come just before the page creation
67304fd306cSNickeau         * due to the $MSG_shown mechanism in {@link html_msgarea()}
67404fd306cSNickeau         * We may also get messages in the head
67504fd306cSNickeau         */
67604fd306cSNickeau        try {
67704fd306cSNickeau            $model['messages-html'] = $this->getMessages();
67804fd306cSNickeau            /**
67904fd306cSNickeau             * Because they must be problem and message with the {@link self::getHeadHtml()}
68004fd306cSNickeau             * We process the messages at the end
68104fd306cSNickeau             * It means that the needed script needs to be added manually
68204fd306cSNickeau             */
68304fd306cSNickeau            $model['head-html'] .= Snippet::getOrCreateFromComponentId("toast", Snippet::EXTENSION_JS)->toXhtml();
68404fd306cSNickeau        } catch (ExceptionNotFound $e) {
68504fd306cSNickeau            // no messages
68604fd306cSNickeau        } catch (ExceptionBadState $e) {
68704fd306cSNickeau            throw ExceptionRuntimeInternal::withMessageAndError("The toast snippet should have been found", $e);
68804fd306cSNickeau        }
68904fd306cSNickeau
69004fd306cSNickeau        /**
69104fd306cSNickeau         * Task runner needs the id
69204fd306cSNickeau         */
69304fd306cSNickeau        if ($this->requestedEnableTaskRunner && isset($this->requestedContextPath)) {
69404fd306cSNickeau            $model['task-runner-html'] = $this->getTaskRunnerImg();
69504fd306cSNickeau        }
69604fd306cSNickeau
69704fd306cSNickeau        return $model;
69804fd306cSNickeau    }
69904fd306cSNickeau
70004fd306cSNickeau
70104fd306cSNickeau    private
70204fd306cSNickeau    function getRequestedTitleOrDefault(): string
70304fd306cSNickeau    {
70404fd306cSNickeau
70504fd306cSNickeau        if (isset($this->requestedTitle)) {
70604fd306cSNickeau            return $this->requestedTitle;
70704fd306cSNickeau        }
70804fd306cSNickeau
70904fd306cSNickeau        try {
71004fd306cSNickeau            $path = $this->getRequestedContextPath();
71104fd306cSNickeau            $markupPath = MarkupPath::createPageFromPathObject($path);
71204fd306cSNickeau            return PageTitle::createForMarkup($markupPath)->getValueOrDefault();
71304fd306cSNickeau        } catch (ExceptionNotFound $e) {
71404fd306cSNickeau            //
71504fd306cSNickeau        }
71604fd306cSNickeau        throw new ExceptionBadSyntaxRuntime("A title is mandatory");
71704fd306cSNickeau
71804fd306cSNickeau
71904fd306cSNickeau    }
72004fd306cSNickeau
72104fd306cSNickeau
72204fd306cSNickeau    /**
72304fd306cSNickeau     * @throws ExceptionNotFound
72404fd306cSNickeau     */
72504fd306cSNickeau    private
72604fd306cSNickeau    function getTocOrDefault(): Toc
72704fd306cSNickeau    {
72804fd306cSNickeau
72904fd306cSNickeau        if (isset($this->toc)) {
73004fd306cSNickeau            /**
73104fd306cSNickeau             * The {@link FetcherPageBundler}
73204fd306cSNickeau             * bundle pages can create a toc for multiples pages
73304fd306cSNickeau             */
73404fd306cSNickeau            return $this->toc;
73504fd306cSNickeau        }
73604fd306cSNickeau
73704fd306cSNickeau        $wikiPath = $this->getRequestedContextPath();
73804fd306cSNickeau        if (FileSystems::isDirectory($wikiPath)) {
73904fd306cSNickeau            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.");
74004fd306cSNickeau        }
74104fd306cSNickeau        $markup = MarkupPath::createPageFromPathObject($wikiPath);
74204fd306cSNickeau        return Toc::createForPage($markup);
74304fd306cSNickeau
74404fd306cSNickeau    }
74504fd306cSNickeau
74604fd306cSNickeau    public
74704fd306cSNickeau    function setMainContent(string $mainContent): TemplateForWebPage
74804fd306cSNickeau    {
74904fd306cSNickeau        $this->mainContent = $mainContent;
75004fd306cSNickeau        return $this;
75104fd306cSNickeau    }
75204fd306cSNickeau
75304fd306cSNickeau
75404fd306cSNickeau    /**
75504fd306cSNickeau     * @throws ExceptionBadSyntax
75604fd306cSNickeau     */
75704fd306cSNickeau    public
75804fd306cSNickeau    function renderAsDom(): XmlDocument
75904fd306cSNickeau    {
76004fd306cSNickeau        return XmlDocument::createHtmlDocFromMarkup($this->render());
76104fd306cSNickeau    }
76204fd306cSNickeau
76304fd306cSNickeau    /**
76404fd306cSNickeau     * Add the preloaded CSS resources
76504fd306cSNickeau     * at the end
76604fd306cSNickeau     * @throws ExceptionNotFound
76704fd306cSNickeau     */
76804fd306cSNickeau    private
76904fd306cSNickeau    function getHtmlForPreloadedStyleSheets(): string
77004fd306cSNickeau    {
77104fd306cSNickeau
77204fd306cSNickeau        // For the preload if any
77304fd306cSNickeau        try {
77404fd306cSNickeau            $executionContext = ExecutionContext::getActualOrCreateFromEnv();
77504fd306cSNickeau            $preloadedCss = $executionContext->getRuntimeObject(self::PRELOAD_TAG);
77604fd306cSNickeau        } catch (ExceptionNotFound $e) {
77704fd306cSNickeau            throw new ExceptionNotFound("No preloaded resources found");
77804fd306cSNickeau        }
77904fd306cSNickeau
78004fd306cSNickeau        //
78104fd306cSNickeau        // Note: Adding this css in an animationFrame
78204fd306cSNickeau        // such as https://github.com/jakearchibald/svgomg/blob/master/src/index.html#L183
78304fd306cSNickeau        // would be difficult to test
78404fd306cSNickeau
78504fd306cSNickeau        $class = StyleAttribute::addComboStrapSuffix(self::PRELOAD_TAG);
78604fd306cSNickeau        $preloadHtml = "<div class=\"$class\">";
78704fd306cSNickeau        foreach ($preloadedCss as $link) {
78804fd306cSNickeau            $htmlLink = '<link rel="stylesheet" href="' . $link['href'] . '" ';
78904fd306cSNickeau            if ($link['crossorigin'] != "") {
79004fd306cSNickeau                $htmlLink .= ' crossorigin="' . $link['crossorigin'] . '" ';
79104fd306cSNickeau            }
79204fd306cSNickeau            if (!empty($link['class'])) {
79304fd306cSNickeau                $htmlLink .= ' class="' . $link['class'] . '" ';
79404fd306cSNickeau            }
79504fd306cSNickeau            // No integrity here
79604fd306cSNickeau            $htmlLink .= '>';
79704fd306cSNickeau            $preloadHtml .= $htmlLink;
79804fd306cSNickeau        }
79904fd306cSNickeau        $preloadHtml .= "</div>";
80004fd306cSNickeau        return $preloadHtml;
80104fd306cSNickeau
80204fd306cSNickeau    }
80304fd306cSNickeau
80404fd306cSNickeau    /**
80504fd306cSNickeau     * Variation of {@link html_msgarea()}
80604fd306cSNickeau     * @throws ExceptionNotFound
80704fd306cSNickeau     */
80804fd306cSNickeau    public
80904fd306cSNickeau    function getMessages(): string
81004fd306cSNickeau    {
81104fd306cSNickeau
81204fd306cSNickeau        global $MSG;
81304fd306cSNickeau
81404fd306cSNickeau        if (!isset($MSG)) {
81504fd306cSNickeau            throw new ExceptionNotFound("No messages");
81604fd306cSNickeau        }
81704fd306cSNickeau
81804fd306cSNickeau        // deduplicate and auth
81904fd306cSNickeau        $uniqueMessages = [];
82004fd306cSNickeau        foreach ($MSG as $msg) {
82104fd306cSNickeau            if (!info_msg_allowed($msg)) {
82204fd306cSNickeau                continue;
82304fd306cSNickeau            }
82404fd306cSNickeau            $hash = md5($msg['msg']);
82504fd306cSNickeau            $uniqueMessages[$hash] = $msg;
82604fd306cSNickeau        }
82704fd306cSNickeau
82804fd306cSNickeau        $messagesByLevel = [];
82904fd306cSNickeau        foreach ($uniqueMessages as $message) {
83004fd306cSNickeau            $level = $message['lvl'];
83104fd306cSNickeau            $messagesByLevel[$level][] = $message;
83204fd306cSNickeau        }
83304fd306cSNickeau
83404fd306cSNickeau        $toasts = "";
83504fd306cSNickeau        foreach ($messagesByLevel as $level => $messagesForLevel) {
83604fd306cSNickeau            $level = ucfirst($level);
83704fd306cSNickeau            switch ($level) {
83804fd306cSNickeau                case "Error":
83904fd306cSNickeau                    $class = "text-danger";
84004fd306cSNickeau                    $levelName = "Error";
84104fd306cSNickeau                    break;
84204fd306cSNickeau                case "Notify":
84304fd306cSNickeau                    $class = "text-warning";
84404fd306cSNickeau                    $levelName = "Warning";
84504fd306cSNickeau                    break;
84604fd306cSNickeau                default:
84704fd306cSNickeau                    $levelName = $level;
84804fd306cSNickeau                    $class = "text-primary";
84904fd306cSNickeau                    break;
85004fd306cSNickeau            }
85104fd306cSNickeau            $autoHide = "false"; // auto-hidding is really bad ui
85204fd306cSNickeau            $toastMessage = "";
85304fd306cSNickeau            foreach ($messagesForLevel as $messageForLevel) {
85404fd306cSNickeau                $toastMessage .= "<p>{$messageForLevel['msg']}</p>";
85504fd306cSNickeau            }
85604fd306cSNickeau
85704fd306cSNickeau
85804fd306cSNickeau            $toasts .= <<<EOF
85904fd306cSNickeau<div role="alert" aria-live="assertive" aria-atomic="true" class="toast fade" data-bs-autohide="$autoHide">
86004fd306cSNickeau  <div class="toast-header">
86104fd306cSNickeau    <strong class="me-auto $class">{$levelName}</strong>
86204fd306cSNickeau    <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
86304fd306cSNickeau  </div>
86404fd306cSNickeau  <div class="toast-body">
86504fd306cSNickeau        $toastMessage
86604fd306cSNickeau  </div>
86704fd306cSNickeau</div>
86804fd306cSNickeauEOF;
86904fd306cSNickeau        }
87004fd306cSNickeau
87104fd306cSNickeau        unset($GLOBALS['MSG']);
87204fd306cSNickeau
87304fd306cSNickeau        if ($toasts === "") {
87404fd306cSNickeau            throw new ExceptionNotFound("No messages");
87504fd306cSNickeau        }
87604fd306cSNickeau
87704fd306cSNickeau        $this->hadMessages = true;
87804fd306cSNickeau
87904fd306cSNickeau        // position fixed to not participate into the grid
88004fd306cSNickeau        return <<<EOF
88104fd306cSNickeau<div class="toast-container position-fixed mb-3 me-3 bottom-0 end-0" id="toastPlacement" style="z-index:1060">
88204fd306cSNickeau$toasts
88304fd306cSNickeau</div>
88404fd306cSNickeauEOF;
88504fd306cSNickeau
88604fd306cSNickeau    }
88704fd306cSNickeau
88804fd306cSNickeau    private
88904fd306cSNickeau    function canBeCached(): bool
89004fd306cSNickeau    {
89104fd306cSNickeau        // no if message
89204fd306cSNickeau        return true;
89304fd306cSNickeau    }
89404fd306cSNickeau
89504fd306cSNickeau    /**
89604fd306cSNickeau     * Adapted from {@link tpl_indexerWebBug()}
89704fd306cSNickeau     * @return string
89804fd306cSNickeau     */
89904fd306cSNickeau    private
90004fd306cSNickeau    function getTaskRunnerImg(): string
90104fd306cSNickeau    {
90204fd306cSNickeau
90304fd306cSNickeau        try {
90404fd306cSNickeau            $htmlUrl = UrlEndpoint::createTaskRunnerUrl()
90504fd306cSNickeau                ->addQueryParameter(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE, $this->getRequestedContextPath()->getWikiId())
90604fd306cSNickeau                ->addQueryParameter(time())
90704fd306cSNickeau                ->toString();
90804fd306cSNickeau        } catch (ExceptionNotFound $e) {
90904fd306cSNickeau            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).");
91004fd306cSNickeau        }
91104fd306cSNickeau
91204fd306cSNickeau        // no more 1x1 px image because of ad blockers
91304fd306cSNickeau        return TagAttributes::createEmpty()
91404fd306cSNickeau            ->addOutputAttributeValue("id", TemplateForWebPage::TASK_RUNNER_ID)
91504fd306cSNickeau            ->addClassName("d-none")
91604fd306cSNickeau            ->addOutputAttributeValue('width', 2)
91704fd306cSNickeau            ->addOutputAttributeValue('height', 1)
91804fd306cSNickeau            ->addOutputAttributeValue('alt', 'Task Runner')
91904fd306cSNickeau            ->addOutputAttributeValue('src', $htmlUrl)
92004fd306cSNickeau            ->toHtmlEmptyTag("img");
92104fd306cSNickeau    }
92204fd306cSNickeau
92304fd306cSNickeau    private
92404fd306cSNickeau    function getRequestedLangOrDefault(): Lang
92504fd306cSNickeau    {
92604fd306cSNickeau        try {
92704fd306cSNickeau            return $this->getRequestedLang();
92804fd306cSNickeau        } catch (ExceptionNotFound $e) {
92904fd306cSNickeau            return Lang::createFromValue("en");
93004fd306cSNickeau        }
93104fd306cSNickeau    }
93204fd306cSNickeau
93304fd306cSNickeau    private
93404fd306cSNickeau    function getTheme(): string
93504fd306cSNickeau    {
93604fd306cSNickeau        return $this->requestedTheme ?? ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getTheme();
93704fd306cSNickeau    }
93804fd306cSNickeau
93904fd306cSNickeau    private
94004fd306cSNickeau    function getHeadHtml(): string
94104fd306cSNickeau    {
94204fd306cSNickeau        $snippetManager = PluginUtility::getSnippetManager();
94304fd306cSNickeau
94404fd306cSNickeau        if (!$this->isTemplateStringExecutionMode()) {
94504fd306cSNickeau
94604fd306cSNickeau            /**
94704fd306cSNickeau             * Add the layout js and css first
94804fd306cSNickeau             */
94904fd306cSNickeau
95004fd306cSNickeau            try {
95104fd306cSNickeau                $cssPath = $this->getCssPath();
95204fd306cSNickeau                $content = FileSystems::getContent($cssPath);
95304fd306cSNickeau                $snippetManager->attachCssInternalStylesheet(self::CANONICAL, $content);
95404fd306cSNickeau            } catch (ExceptionNotFound $e) {
95504fd306cSNickeau                // no css found, not a problem
95604fd306cSNickeau            }
95704fd306cSNickeau            try {
95804fd306cSNickeau                $jsPath = $this->getJsPath();
95904fd306cSNickeau                $snippetManager->attachInternalJavascriptFromPathForRequest(self::CANONICAL, $jsPath);
96004fd306cSNickeau            } catch (ExceptionNotFound $e) {
96104fd306cSNickeau                // not found
96204fd306cSNickeau            }
96304fd306cSNickeau
96404fd306cSNickeau
96504fd306cSNickeau        }
96604fd306cSNickeau
96704fd306cSNickeau        /**
96804fd306cSNickeau         * Dokuwiki Smiley does not have any height
96904fd306cSNickeau         */
97004fd306cSNickeau        $snippetManager->attachCssInternalStyleSheet("dokuwiki-smiley");
97104fd306cSNickeau
97204fd306cSNickeau        /**
97304fd306cSNickeau         * Iframe
97404fd306cSNickeau         */
97504fd306cSNickeau        if ($this->isIframe) {
97604fd306cSNickeau            global $EVENT_HANDLER;
97704fd306cSNickeau            $EVENT_HANDLER->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'onlyIframeHeadTags');
97804fd306cSNickeau        }
97904fd306cSNickeau        /**
98004fd306cSNickeau         * Start the meta headers
98104fd306cSNickeau         */
98204fd306cSNickeau        ob_start();
98304fd306cSNickeau        try {
98404fd306cSNickeau            tpl_metaheaders();
98504fd306cSNickeau            $headIcon = $this->getPageIconHeadLinkHtml();
98604fd306cSNickeau            return $headIcon . ob_get_contents();
98704fd306cSNickeau        } finally {
98804fd306cSNickeau            ob_end_clean();
98904fd306cSNickeau        }
99004fd306cSNickeau
99104fd306cSNickeau    }
99204fd306cSNickeau
99304fd306cSNickeau
99404fd306cSNickeau    public
99504fd306cSNickeau    function setRequestedTemplateName(string $templateName): TemplateForWebPage
99604fd306cSNickeau    {
99704fd306cSNickeau        $this->templateName = $templateName;
99804fd306cSNickeau        return $this;
99904fd306cSNickeau    }
100004fd306cSNickeau
100104fd306cSNickeau    /**
100204fd306cSNickeau     * Add or not the task runner / web bug call
100304fd306cSNickeau     * @param bool $b
100404fd306cSNickeau     * @return TemplateForWebPage
100504fd306cSNickeau     */
100604fd306cSNickeau    public
100704fd306cSNickeau    function setRequestedEnableTaskRunner(bool $b): TemplateForWebPage
100804fd306cSNickeau    {
100904fd306cSNickeau        $this->requestedEnableTaskRunner = $b;
101004fd306cSNickeau        return $this;
101104fd306cSNickeau    }
101204fd306cSNickeau
101304fd306cSNickeau
101404fd306cSNickeau    /**
101504fd306cSNickeau     * @param Lang $requestedLang
101604fd306cSNickeau     * @return TemplateForWebPage
101704fd306cSNickeau     */
101804fd306cSNickeau    public
101904fd306cSNickeau    function setRequestedLang(Lang $requestedLang): TemplateForWebPage
102004fd306cSNickeau    {
102104fd306cSNickeau        $this->requestedLang = $requestedLang;
102204fd306cSNickeau        return $this;
102304fd306cSNickeau    }
102404fd306cSNickeau
102504fd306cSNickeau    /**
102604fd306cSNickeau     * @param string $requestedTitle
102704fd306cSNickeau     * @return TemplateForWebPage
102804fd306cSNickeau     */
102904fd306cSNickeau    public
103004fd306cSNickeau    function setRequestedTitle(string $requestedTitle): TemplateForWebPage
103104fd306cSNickeau    {
103204fd306cSNickeau        $this->requestedTitle = $requestedTitle;
103304fd306cSNickeau        return $this;
103404fd306cSNickeau    }
103504fd306cSNickeau
103604fd306cSNickeau    /**
103704fd306cSNickeau     * Delete the social head tags
103804fd306cSNickeau     * (ie the page should not be indexed)
103904fd306cSNickeau     * This is used for iframe content for instance
104004fd306cSNickeau     * @param bool $isSocial
104104fd306cSNickeau     * @return TemplateForWebPage
104204fd306cSNickeau     */
104304fd306cSNickeau    public
104404fd306cSNickeau    function setIsSocial(bool $isSocial): TemplateForWebPage
104504fd306cSNickeau    {
104604fd306cSNickeau        $this->isSocial = $isSocial;
104704fd306cSNickeau        return $this;
104804fd306cSNickeau    }
104904fd306cSNickeau
105004fd306cSNickeau    public
105104fd306cSNickeau    function setRequestedContextPath(WikiPath $contextPath): TemplateForWebPage
105204fd306cSNickeau    {
105304fd306cSNickeau        $this->requestedContextPath = $contextPath;
105404fd306cSNickeau        return $this;
105504fd306cSNickeau    }
105604fd306cSNickeau
105704fd306cSNickeau    public
105804fd306cSNickeau    function setToc(Toc $toc): TemplateForWebPage
105904fd306cSNickeau    {
106004fd306cSNickeau        $this->toc = $toc;
106104fd306cSNickeau        return $this;
106204fd306cSNickeau    }
106304fd306cSNickeau
106404fd306cSNickeau    /**
106504fd306cSNickeau     * There is two mode of execution, via:
106604fd306cSNickeau     * * a file template (theme)
106704fd306cSNickeau     * * or a string template (string)
106804fd306cSNickeau     *
106904fd306cSNickeau     * @return bool - true if this a string template executions
107004fd306cSNickeau     */
107104fd306cSNickeau    private
107204fd306cSNickeau    function isTemplateStringExecutionMode(): bool
107304fd306cSNickeau    {
107404fd306cSNickeau        return isset($this->templateString);
107504fd306cSNickeau    }
107604fd306cSNickeau
107704fd306cSNickeau    private
107804fd306cSNickeau    function getEngine(): TemplateEngine
107904fd306cSNickeau    {
108004fd306cSNickeau        if ($this->isTemplateStringExecutionMode()) {
108104fd306cSNickeau            return TemplateEngine::createForString();
108204fd306cSNickeau
108304fd306cSNickeau        } else {
108404fd306cSNickeau            $theme = $this->getTheme();
108504fd306cSNickeau            return TemplateEngine::createForTheme($theme);
108604fd306cSNickeau        }
108704fd306cSNickeau    }
108804fd306cSNickeau
108904fd306cSNickeau    private
109004fd306cSNickeau    function getDefinition(): array
109104fd306cSNickeau    {
109204fd306cSNickeau        try {
109304fd306cSNickeau            if (isset($this->templateDefinition)) {
109404fd306cSNickeau                return $this->templateDefinition;
109504fd306cSNickeau            }
109604fd306cSNickeau            $file = $this->getEngine()->searchTemplateByName("{$this->getTemplateName()}.yml");
109704fd306cSNickeau            if (!FileSystems::exists($file)) {
109804fd306cSNickeau                return [];
109904fd306cSNickeau            }
110004fd306cSNickeau            $this->templateDefinition = Yaml::parseFile($file->toAbsoluteId());
110104fd306cSNickeau            return $this->templateDefinition;
110204fd306cSNickeau        } catch (ExceptionNotFound $e) {
110304fd306cSNickeau            // no template directory, not a theme run
110404fd306cSNickeau            return [];
110504fd306cSNickeau        }
110604fd306cSNickeau    }
110704fd306cSNickeau
110804fd306cSNickeau    private
110904fd306cSNickeau    function getRailbarLayout(): string
111004fd306cSNickeau    {
111104fd306cSNickeau        $definition = $this->getDefinition();
111204fd306cSNickeau        if (isset($definition['railbar']['layout'])) {
111304fd306cSNickeau            return $definition['railbar']['layout'];
111404fd306cSNickeau        }
111504fd306cSNickeau        return FetcherRailBar::BOTH_LAYOUT;
111604fd306cSNickeau    }
111704fd306cSNickeau
111804fd306cSNickeau    /**
111904fd306cSNickeau     * Keep the only iframe head tag needed
112004fd306cSNickeau     * @param $event
112104fd306cSNickeau     * @return void
112204fd306cSNickeau     */
112304fd306cSNickeau    public
112404fd306cSNickeau    function onlyIframeHeadTags(&$event)
112504fd306cSNickeau    {
112604fd306cSNickeau
112704fd306cSNickeau        $data = &$event->data;
112804fd306cSNickeau        foreach ($data as $tag => &$heads) {
112904fd306cSNickeau            switch ($tag) {
113004fd306cSNickeau                case "link":
113104fd306cSNickeau                    $deletedRel = ["manifest", "search", "start", "alternate", "canonical"];
113204fd306cSNickeau                    foreach ($heads as $id => $headAttributes) {
113304fd306cSNickeau                        if (isset($headAttributes['rel'])) {
113404fd306cSNickeau                            $rel = $headAttributes['rel'];
113504fd306cSNickeau                            if (in_array($rel, $deletedRel)) {
113604fd306cSNickeau                                unset($heads[$id]);
113704fd306cSNickeau                            }
113804fd306cSNickeau                            if ($rel === "stylesheet") {
113904fd306cSNickeau                                $href = $headAttributes['href'];
114004fd306cSNickeau                                if (strpos($href, "lib/exe/css.php") !== false) {
114104fd306cSNickeau                                    unset($heads[$id]);
114204fd306cSNickeau                                }
114304fd306cSNickeau                            }
114404fd306cSNickeau                        }
114504fd306cSNickeau                    }
114604fd306cSNickeau                    break;
114704fd306cSNickeau                case "meta":
114804fd306cSNickeau                    $deletedMeta = ["og:url", "og:description", "description", "robots"];
114904fd306cSNickeau                    foreach ($heads as $id => $headAttributes) {
115004fd306cSNickeau                        if (isset($headAttributes['name']) || isset($headAttributes['property'])) {
115104fd306cSNickeau                            $rel = $headAttributes['name'];
115204fd306cSNickeau                            if ($rel === null) {
115304fd306cSNickeau                                $rel = $headAttributes['property'];
115404fd306cSNickeau                            }
115504fd306cSNickeau                            if (in_array($rel, $deletedMeta)) {
115604fd306cSNickeau                                unset($heads[$id]);
115704fd306cSNickeau                            }
115804fd306cSNickeau                        }
115904fd306cSNickeau                    }
116004fd306cSNickeau                    break;
116104fd306cSNickeau                case "script":
116204fd306cSNickeau                    foreach ($heads as $id => $headAttributes) {
116304fd306cSNickeau                        if (isset($headAttributes['src'])) {
116404fd306cSNickeau                            $src = $headAttributes['src'];
116504fd306cSNickeau                            if (strpos($src, "lib/exe/js.php") !== false) {
116604fd306cSNickeau                                unset($heads[$id]);
116704fd306cSNickeau                            }
116804fd306cSNickeau                            if (strpos($src, "lib/exe/jquery.php") !== false) {
116904fd306cSNickeau                                unset($heads[$id]);
117004fd306cSNickeau                            }
117104fd306cSNickeau                        }
117204fd306cSNickeau                    }
117304fd306cSNickeau                    break;
117404fd306cSNickeau            }
117504fd306cSNickeau        }
117604fd306cSNickeau    }
117704fd306cSNickeau
117804fd306cSNickeau}
1179