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