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