1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15use Doku_Event;
16use dokuwiki\Extension\Event;
17use dokuwiki\Menu\PageMenu;
18use dokuwiki\Menu\SiteMenu;
19use dokuwiki\Menu\UserMenu;
20use dokuwiki\plugin\config\core\Configuration;
21use dokuwiki\plugin\config\core\Writer;
22use Exception;
23
24
25/**
26 * Class TplUtility
27 * @package ComboStrap
28 * Utility class
29 */
30class TplUtility
31{
32
33    /**
34     * Constant for the function {@link msg()}
35     * -1 = error, 0 = info, 1 = success, 2 = notify
36     */
37    const LVL_MSG_ERROR = -1;
38    const LVL_MSG_INFO = 0;
39    const LVL_MSG_SUCCESS = 1;
40    const LVL_MSG_WARNING = 2;
41    const LVL_MSG_DEBUG = 3;
42    const TEMPLATE_NAME = 'strap';
43
44
45    const CONF_HEADER_SLOT_PAGE_NAME = "headerSlotPageName";
46
47    const CONF_FOOTER_SLOT_PAGE_NAME = "footerSlotPageName";
48
49
50    /**
51     * @deprecated for  {@link TplUtility::CONF_BOOTSTRAP_VERSION_STYLESHEET}
52     */
53    const CONF_BOOTSTRAP_VERSION = "bootstrapVersion";
54    /**
55     * @deprecated for  {@link TplUtility::CONF_BOOTSTRAP_VERSION_STYLESHEET}
56     */
57    const CONF_BOOTSTRAP_STYLESHEET = "bootstrapStylesheet";
58
59    /**
60     * Stylesheet and Boostrap should have the same version
61     * This conf is a mix between the version and the stylesheet
62     *
63     * majorVersion.0.0 - stylesheetname
64     */
65    const CONF_BOOTSTRAP_VERSION_STYLESHEET = "bootstrapVersionStylesheet";
66    /**
67     * The separator in {@link TplUtility::CONF_BOOTSTRAP_VERSION_STYLESHEET}
68     */
69    const BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR = " - ";
70    const DEFAULT_BOOTSTRAP_VERSION_STYLESHEET = "5.0.1" . self::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . "bootstrap";
71
72    /**
73     * Jquery UI
74     */
75    const CONF_JQUERY_DOKU = 'jQueryDoku';
76    const CONF_REM_SIZE = "remSize";
77    const CONF_GRID_COLUMNS = "gridColumns";
78    const CONF_USE_CDN = "useCDN";
79
80    const CONF_PRELOAD_CSS = "preloadCss"; // preload all css ?
81    const BS_4_BOOTSTRAP_VERSION_STYLESHEET = "4.5.0 - bootstrap";
82
83    const CONF_SIDEKICK_OLD = "sidekickbar";
84    const CONF_SIDEKICK_SLOT_PAGE_NAME = "sidekickSlotPageName";
85    const CONF_SLOT_HEADER_PAGE_NAME_VALUE = "slot_header";
86
87    /**
88     * @deprecated see {@link TplUtility::CONF_HEADER_SLOT_PAGE_NAME}
89     */
90    const CONF_HEADER_OLD = "headerbar";
91    /**
92     * @deprecated
93     */
94    const CONF_HEADER_OLD_VALUE = TplUtility::CONF_HEADER_OLD;
95    /**
96     * @deprecated see {@link TplUtility::CONF_FOOTER_SLOT_PAGE_NAME}
97     */
98    const CONF_FOOTER_OLD = "footerbar";
99
100    /**
101     * Disable the javascript of Dokuwiki
102     * if public
103     * https://combostrap.com/frontend/optimization
104     */
105    const CONF_DISABLE_BACKEND_JAVASCRIPT = "disableBackendJavascript";
106
107    /**
108     * A parameter switch to allows the update
109     * of conf in test
110     */
111    const COMBO_TEST_UPDATE = "combo_update_conf";
112
113    /**
114     * Do we show the rail bar for anonymous user
115     */
116    const CONF_PRIVATE_RAIL_BAR = "privateRailbar";
117
118    /**
119     * When do we toggle from offcanvas to fixed railbar
120     */
121    const CONF_BREAKPOINT_RAIL_BAR = "breakpointRailbar";
122
123    /**
124     * Breakpoint naming
125     */
126    const BREAKPOINT_EXTRA_SMALL_NAME = "extra-small";
127    const BREAKPOINT_SMALL_NAME = "small";
128    const BREAKPOINT_MEDIUM_NAME = "medium";
129    const BREAKPOINT_LARGE_NAME = "large";
130    const BREAKPOINT_EXTRA_LARGE_NAME = "extra-large";
131    const BREAKPOINT_EXTRA_EXTRA_LARGE_NAME = "extra-extra-large";
132    const BREAKPOINT_NEVER_NAME = "never";
133
134    /**
135     * @var array|null
136     */
137    private static $TEMPLATE_INFO = null;
138
139
140    /**
141     * Print the breadcrumbs trace with Bootstrap class
142     *
143     * @param string $sep Separator between entries
144     * @return bool
145     * @author Nicolas GERARD
146     *
147     *
148     */
149    static function renderTrailBreadcrumb($sep = '�')
150    {
151
152        global $conf;
153        global $lang;
154
155        //check if enabled
156        if (!$conf['breadcrumbs']) return false;
157
158        $crumbs = breadcrumbs(); //setup crumb trace
159
160        echo '<nav id="breadcrumb" aria-label="breadcrumb" class="my-3 d-print-none">' . PHP_EOL;
161
162        $i = 0;
163        // Try to get the template custom breadcrumb
164        // $breadCrumb = tpl_getLang('breadcrumb');
165        // if ($breadCrumb == '') {
166        //    // If not present for the language, get the default one
167        //    $breadCrumb = $lang['breadcrumb'];
168        // }
169
170        // echo '<span id="breadCrumbTitle" ">' . $breadCrumb . ':   </span>' . PHP_EOL;
171        echo '<ol class="breadcrumb py-1 px-2" style="background-color:unset">' . PHP_EOL;
172        print '<li class="pr-2" style="display:flex;font-weight: 200">' . $lang['breadcrumb'] . '</li>';
173
174        foreach ($crumbs as $id => $name) {
175            $i++;
176
177            if ($i == 0) {
178                print '<li class="breadcrumb-item active">';
179            } else {
180                print '<li class="breadcrumb-item">';
181            }
182            if ($name == "start") {
183                $name = "Home";
184            }
185            tpl_link(wl($id), hsc($name), 'title="' . $name . '"');
186
187            print '</li>' . PHP_EOL;
188
189        }
190        echo '</ol>' . PHP_EOL;
191        echo '</nav>' . PHP_EOL;
192        return true;
193    }
194
195    /**
196     * @param string $text add a comment into the HTML page
197     */
198    private static function addAsHtmlComment($text)
199    {
200        print_r('<!-- TplUtility Comment: ' . hsc($text) . '-->');
201    }
202
203    private static function getApexDomainUrl()
204    {
205        return self::getTemplateInfo()["url"];
206    }
207
208    private static function getTemplateInfo()
209    {
210        if (self::$TEMPLATE_INFO == null) {
211            self::$TEMPLATE_INFO = confToHash(__DIR__ . '/../template.info.txt');
212        }
213        return self::$TEMPLATE_INFO;
214    }
215
216    public static function getFullQualifyVersion()
217    {
218        return "v" . self::getTemplateInfo()['version'] . " (" . self::getTemplateInfo()['date'] . ")";
219    }
220
221
222    private static function getStrapUrl()
223    {
224        return self::getTemplateInfo()["strap"];
225    }
226
227
228    public static function registerHeaderHandler()
229    {
230        /**
231         * In test, we may test for 4 and for 5
232         * on the same test, making two request
233         * This two requests will register the event two times
234         * To avoid that we use a global variable
235         */
236        global $COMBO_STRAP_METAHEADER_HOOK_ALREADY_REGISTERED;
237        if ($COMBO_STRAP_METAHEADER_HOOK_ALREADY_REGISTERED !== true) {
238            global $EVENT_HANDLER;
239            $method = array('\Combostrap\TplUtility', 'handleBootstrapMetaHeaders');
240            /**
241             * A call to a method is via an array and the hook declare a string
242             * @noinspection PhpParamsInspection
243             */
244            $EVENT_HANDLER->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', null, $method);
245            $COMBO_STRAP_METAHEADER_HOOK_ALREADY_REGISTERED = true;
246        }
247
248    }
249
250    /**
251     * Add the preloaded CSS resources
252     * at the end
253     */
254    public static function addPreloadedResources()
255    {
256        // For the preload if any
257        global $preloadedCss;
258        //
259        // Note: Adding this css in an animationFrame
260        // such as https://github.com/jakearchibald/svgomg/blob/master/src/index.html#L183
261        // would be difficult to test
262        if (isset($preloadedCss)) {
263            foreach ($preloadedCss as $link) {
264                $htmlLink = '<link rel="stylesheet" href="' . $link['href'] . '" ';
265                if ($link['crossorigin'] != "") {
266                    $htmlLink .= ' crossorigin="' . $link['crossorigin'] . '" ';
267                }
268                if (!empty($link['class'])) {
269                    $htmlLink .= ' class="' . $link['class'] . '" ';
270                }
271                // No integrity here
272                $htmlLink .= '>';
273                ptln($htmlLink);
274            }
275            /**
276             * Reset
277             * Needed in test when we start two requests
278             */
279            $preloadedCss = [];
280        }
281
282    }
283
284    /**
285     * @param $linkData - an array of link style sheet data
286     * @return array - the array with the preload attributes
287     */
288    private static function toPreloadCss($linkData)
289    {
290        /**
291         * Save the stylesheet to load it at the end
292         */
293        global $preloadedCss;
294        $preloadedCss[] = $linkData;
295
296        /**
297         * Modify the actual tag data
298         * Change the loading mechanism to preload
299         */
300        $linkData['rel'] = 'preload';
301        $linkData['as'] = 'style';
302        return $linkData;
303    }
304
305    public static function getBootStrapVersion()
306    {
307        $bootstrapStyleSheetVersion = tpl_getConf(TplUtility::CONF_BOOTSTRAP_VERSION_STYLESHEET, TplUtility::DEFAULT_BOOTSTRAP_VERSION_STYLESHEET);
308        $bootstrapStyleSheetArray = explode(self::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR, $bootstrapStyleSheetVersion);
309        return $bootstrapStyleSheetArray[0];
310    }
311
312    public static function getStyleSheetConf()
313    {
314        $bootstrapStyleSheetVersion = tpl_getConf(TplUtility::CONF_BOOTSTRAP_VERSION_STYLESHEET, TplUtility::DEFAULT_BOOTSTRAP_VERSION_STYLESHEET);
315        $bootstrapStyleSheetArray = explode(self::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR, $bootstrapStyleSheetVersion);
316        return $bootstrapStyleSheetArray[1];
317    }
318
319    /**
320     * Return the XHMTL for the bar or null if not found
321     *
322     * An adaptation from {@link tpl_include_page()}
323     * to make the cache namespace
324     *
325     * @param $barName
326     * @return string|null
327     *
328     *
329     * Note: Even if there is no sidebar
330     * the rendering may output
331     * debug information in the form of
332     * an HTML comment
333     */
334    public static function renderSlot($barName)
335    {
336
337        if (class_exists("ComboStrap\Page")) {
338            $page = new Page($barName);
339            return $page->toXhtml();
340        } else {
341            TplUtility::msg("The combo plugin is not installed, sidebars automatic bursting will not work", self::LVL_MSG_INFO, "sidebars");
342            return tpl_include_page($barName, 0, 1);
343        }
344
345    }
346
347    private static function getBootStrapMajorVersion()
348    {
349        return self::getBootStrapVersion()[0];
350    }
351
352
353    public static function getSideKickSlotPageName()
354    {
355
356        return TplUtility::migrateSlotConfAndGetValue(
357            TplUtility::CONF_SIDEKICK_SLOT_PAGE_NAME,
358            "slot_sidekick",
359            TplUtility::CONF_SIDEKICK_OLD,
360            "sidekickbar",
361            "sidekick_slot"
362        );
363    }
364
365    public static function getHeaderSlotPageName()
366    {
367
368        return TplUtility::migrateSlotConfAndGetValue(
369            TplUtility::CONF_HEADER_SLOT_PAGE_NAME,
370            TplUtility::CONF_SLOT_HEADER_PAGE_NAME_VALUE,
371            TplUtility::CONF_HEADER_OLD,
372            TplUtility::CONF_HEADER_OLD_VALUE,
373            "header_slot"
374        );
375
376    }
377
378    public static function getFooterSlotPageName()
379    {
380        return self::migrateSlotConfAndGetValue(
381            TplUtility::CONF_FOOTER_SLOT_PAGE_NAME,
382            "slot_footer",
383            TplUtility::CONF_FOOTER_OLD,
384            "footerbar",
385            "footer_slot"
386        );
387    }
388
389    /**
390     * @param string $key the key configuration
391     * @param string $value the value
392     * @return bool
393     */
394    public static function updateConfiguration($key, $value)
395    {
396
397        /**
398         * Hack to avoid updating during {@link \TestRequest}
399         * when not asked
400         * Because the test request environment is wiped out only on the class level,
401         * the class / test function needs to specifically say that it's open
402         * to the modification of the configuration
403         */
404        global $_REQUEST;
405        if (defined('DOKU_UNITTEST') && !isset($_REQUEST[self::COMBO_TEST_UPDATE])) {
406
407            /**
408             * This hack resolves two problems
409             *
410             * First one
411             * this is a test request
412             * the local.php file has a the `DOKU_TMP_DATA`
413             * constant in the file and updating the file
414             * with this method will then update the value of savedir to DOKU_TMP_DATA
415             * we get then the error
416             * The datadir ('pages') at DOKU_TMP_DATA/pages is not found
417             *
418             *
419             * Second one
420             * if in a php test unit, we send a php request two times
421             * the headers have been already send and the
422             * {@link msg()} function will send them
423             * causing the {@link TplUtility::outputBufferShouldBeEmpty() output buffer check} to fail
424             */
425            global $MSG_shown;
426            if (isset($MSG_shown) || headers_sent()) {
427                return false;
428            } else {
429                return true;
430            }
431
432        }
433
434
435        $configuration = new Configuration();
436        $settings = $configuration->getSettings();
437
438        $key = "tpl____strap____" . $key;
439        if (isset($settings[$key])) {
440            $setting = &$settings[$key];
441            $setting->update($value);
442            /**
443             * We cannot update the setting
444             * via the configuration object
445             * We are taking another pass
446             */
447
448            $writer = new Writer();
449            if (!$writer->isLocked()) {
450                try {
451                    $writer->save($settings);
452                    return true;
453                } catch (Exception $e) {
454                    TplUtility::msg("An error occurred while trying to save automatically the configuration ($key) to the value ($value). Error: " . $e->getMessage());
455                    return false;
456                }
457            } else {
458                TplUtility::msg("The configuration file was locked. The upgrade configuration ($key) value could not be not changed to ($value)");
459                return false;
460            }
461
462        } else {
463
464            /**
465             * When we run test,
466             * strap is not always the active template
467             * and therefore the configurations are not loaded
468             */
469            global $conf;
470            if ($conf['template'] == TplUtility::TEMPLATE_NAME) {
471                TplUtility::msg("The configuration ($key) is unknown and was therefore not change to ($value)");
472            }
473        }
474
475        return false;
476
477
478    }
479
480    /**
481     * Helper to migrate from bar to slot
482     * @return mixed|string
483     */
484    public static function migrateSlotConfAndGetValue($newConf, $newDefaultValue, $oldConf, $oldDefaultValue, $canonical)
485    {
486
487        $name = tpl_getConf($newConf, null);
488        if ($name == null) {
489            $name = tpl_getConf($oldConf, null);
490        }
491        if ($name == null) {
492
493            $foundOldName = false;
494            if (page_exists($oldConf)) {
495                $foundOldName = true;
496            }
497
498            if (!$foundOldName) {
499                global $conf;
500                $startPageName = $conf["start"];
501                $startPagePath = wikiFN($startPageName);
502                $directory = dirname($startPagePath);
503
504
505                $childrenDirectories = glob($directory . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR);
506                foreach ($childrenDirectories as $childrenDirectory) {
507                    $directoryName = pathinfo($childrenDirectory)['filename'];
508                    $dokuFilePath = $directoryName . ":" . $oldDefaultValue;
509                    if (page_exists($dokuFilePath)) {
510                        $foundOldName = true;
511                        break;
512                    }
513                }
514            }
515
516            if ($foundOldName) {
517                $name = $oldDefaultValue;
518            } else {
519                $name = $newDefaultValue;
520            }
521            $updated = TplUtility::updateConfiguration($newConf, $name);
522            if ($updated) {
523                TplUtility::msg("The <a href=\"https://combostrap.com/$canonical\">$newConf</a> configuration was set with the value <mark>$name</mark>", self::LVL_MSG_INFO, $canonical);
524            }
525        }
526        return $name;
527    }
528
529    /**
530     * Output buffer checks
531     *
532     * It should be null before printing otherwise
533     * you may get a text before the HTML header
534     * and it mess up the whole page
535     */
536    public static function outputBufferShouldBeEmpty()
537    {
538        $length = ob_get_length();
539        if ($length > 0) {
540            $ob = ob_get_contents();
541            ob_clean();
542            /**
543             * If you got this problem check that this is not a character before a  `<?php` declaration
544             */
545            TplUtility::msg("A plugin has send text before the creation of the page. Because it will mess the rendering, we have deleted it. The content was: (" . $ob . ")", TplUtility::LVL_MSG_ERROR, "strap");
546        }
547    }
548
549
550    /**
551     * @return string
552     * Railbar items can add snippet in the head
553     * And should then be could before the HTML output
554     *
555     * In Google Material Design, they call it a
556     * navigational drawer
557     * https://material.io/components/navigation-drawer
558     */
559    public static function getRailBar()
560    {
561
562        if (tpl_getConf(TplUtility::CONF_PRIVATE_RAIL_BAR) === 1 && empty($_SERVER['REMOTE_USER'])) {
563            return "";
564        }
565        $breakpoint = tpl_getConf(TplUtility::CONF_BREAKPOINT_RAIL_BAR, TplUtility::BREAKPOINT_LARGE_NAME);
566
567        $bootstrapBreakpoint = "";
568        switch($breakpoint){
569            case TplUtility::BREAKPOINT_EXTRA_SMALL_NAME:
570                $bootstrapBreakpoint = "xs";
571                break;
572            case TplUtility::BREAKPOINT_SMALL_NAME:
573                $bootstrapBreakpoint = "sm";
574                break;
575            case TplUtility::BREAKPOINT_MEDIUM_NAME:
576                $bootstrapBreakpoint = "md";
577                break;
578            case TplUtility::BREAKPOINT_LARGE_NAME:
579                $bootstrapBreakpoint = "lg";
580                break;
581            case TplUtility::BREAKPOINT_EXTRA_LARGE_NAME:
582                $bootstrapBreakpoint = "xl";
583                break;
584            case TplUtility::BREAKPOINT_EXTRA_EXTRA_LARGE_NAME:
585                $bootstrapBreakpoint = "xxl";
586                break;
587        }
588
589        $classOffCanvas="";
590        $classFixed="";
591        if(!empty($bootstrapBreakpoint)){
592            $classOffCanvas="class=\"d-$bootstrapBreakpoint-none\"";
593            $classFixed="class=\"d-none d-$bootstrapBreakpoint-flex\"";
594        }
595
596        $railBarListItems = TplUtility::getRailBarListItems();
597        $railBarOffCanvas = <<<EOF
598<div id="railbar-offcanvas-wrapper" $classOffCanvas>
599    <button id="railbar-offcanvas-open" class="btn" type="button" data-bs-toggle="offcanvas"
600            data-bs-target="#railbar-offcanvas" aria-controls="railbar-offcanvas">
601    </button>
602
603    <div id="railbar-offcanvas" class="offcanvas offcanvas-end" tabindex="-1"
604         aria-labelledby="offcanvas-label"
605         style="visibility: hidden;" aria-hidden="true">
606         <h5 class="d-none" id="offcanvas-label">Railbar</h5>
607        <!-- Pseudo relative element  https://stackoverflow.com/questions/6040005/relatively-position-an-element-without-it-taking-up-space-in-document-flow -->
608        <div style="position: relative; width: 0; height: 0">
609            <button id="railbar-offcanvas-close" class="btn" type="button" data-bs-dismiss="offcanvas"
610                    aria-label="Close">
611            </button>
612        </div>
613        <div id="railbar-offcanvas-body" class="offcanvas-body" style="align-items: center;display: flex;">
614            $railBarListItems
615        </div>
616    </div>
617</div>
618EOF;
619
620        if($breakpoint!=TplUtility::BREAKPOINT_NEVER_NAME) {
621            $zIndexRailbar = 1000; // A navigation bar (below the drop down because we use it in the search box for auto-completion)
622            $railBarFixed = <<<EOF
623<div id="railbar-fixed" style="z-index: $zIndexRailbar;" $classFixed>
624    <div class="tools">
625        $railBarListItems
626    </div>
627</div>
628EOF;
629            return <<<EOF
630$railBarOffCanvas
631$railBarFixed
632EOF;
633        } else {
634
635            return $railBarOffCanvas;
636
637        }
638
639
640    }
641
642    /**
643     *
644     * https://material.io/components/navigation-rail|Navigation rail
645     * @return string - the ul part of the railbar
646     */
647    public
648    static function getRailBarListItems()
649    {
650        $liUserTools = (new UserMenu())->getListItems('action');
651        $liPageTools = (new PageMenu())->getListItems();
652        $liSiteTools = (new SiteMenu())->getListItems('action');
653        // FYI: The below code outputs all menu in mobile (in another HTML layout)
654        // echo (new \dokuwiki\Menu\MobileMenu())->getDropdown($lang['tools']);
655        return <<<EOF
656<ul class="railbar">
657    <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>User</span><svg style="height:19px"></svg></a></li>
658    $liUserTools
659    <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>Page</span><svg style="height:19px"></svg></a></li>
660    $liPageTools
661    <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>Website</span><svg style="height:19px"></svg></a></li>
662    $liSiteTools
663</ul>
664EOF;
665
666    }
667
668    /**
669     * Hierarchical breadcrumbs
670     *
671     * This will return the Hierarchical breadcrumbs.
672     *
673     * Config:
674     *    - $conf['youarehere'] must be true
675     *    - add $lang['youarehere'] if $printPrefix is true
676     *
677     * @param bool $printPrefix print or not the $lang['youarehere']
678     * @return string
679     */
680    function renderHierarchicalBreadcrumb($printPrefix = false)
681    {
682
683        global $conf;
684        global $lang;
685
686        // check if enabled
687        if (!$conf['youarehere']) return;
688
689        // print intermediate namespace links
690        $htmlOutput = '<ol class="breadcrumb">' . PHP_EOL;
691
692        // Print the home page
693        $htmlOutput .= '<li>' . PHP_EOL;
694        if ($printPrefix) {
695            $htmlOutput .= $lang['youarehere'] . ' ';
696        }
697        $page = $conf['start'];
698        $htmlOutput .= tpl_link(wl($page), '<span class="glyphicon glyphicon-home" aria-hidden="true"></span>', 'title="' . tpl_pagetitle($page, true) . '"', $return = true);
699        $htmlOutput .= '</li>' . PHP_EOL;
700
701        // Print the parts if there is more than one
702        global $ID;
703        $idParts = explode(':', $ID);
704        if (count($idParts) > 1) {
705
706            // Print the parts without the last one ($count -1)
707            $page = "";
708            for ($i = 0; $i < count($idParts) - 1; $i++) {
709
710                $page .= $idParts[$i] . ':';
711
712                // Skip home page of the namespace
713                // if ($page == $conf['start']) continue;
714
715                // The last part is the active one
716//            if ($i == $count) {
717//                $htmlOutput .= '<li class="active">';
718//            } else {
719//                $htmlOutput .= '<li>';
720//            }
721
722                $htmlOutput .= '<li>';
723                // html_wikilink because the page has the form pagename: and not pagename:pagename
724                $htmlOutput .= html_wikilink($page);
725                $htmlOutput .= '</li>' . PHP_EOL;
726
727            }
728        }
729
730        // Skipping Wiki Global Root Home Page
731//    resolve_pageid('', $page, $exists);
732//    if(isset($page) && $page == $idPart.$idParts[$i]) {
733//        echo '</ol>'.PHP_EOL;
734//        return true;
735//    }
736//    // skipping for namespace index
737//    $page = $idPart.$idParts[$i];
738//    if($page == $conf['start']) {
739//        echo '</ol>'.PHP_EOL;
740//        return true;
741//    }
742
743        // print current page
744//    print '<li>';
745//    tpl_link(wl($page), tpl_pagetitle($page,true), 'title="' . $page . '"');
746        $htmlOutput .= '</li>' . PHP_EOL;
747        // close the breadcrumb
748        $htmlOutput .= '</ol>' . PHP_EOL;
749        return $htmlOutput;
750
751    }
752
753
754    /*
755     * Function return the page name from an id
756     * @author Nicolas GERARD
757     *
758     * @param string $sep Separator between entries
759     * @return bool
760     */
761
762    function getPageTitle($id)
763    {
764
765        // page names
766        $name = noNSorNS($id);
767        if (useHeading('navigation')) {
768            // get page title
769            $title = p_get_first_heading($id, METADATA_RENDER_USING_SIMPLE_CACHE);
770            if ($title) {
771                $name = $title;
772            }
773        }
774        return $name;
775
776    }
777
778
779
780    /**
781     * This is a fork of tpl_actionlink where I have added the class parameters
782     *
783     * Like the action buttons but links
784     *
785     * @param string $type action command
786     * @param string $pre prefix of link
787     * @param string $suf suffix of link
788     * @param string $inner innerHML of link
789     * @param bool $return if true it returns html, otherwise prints
790     * @param string $class the class to be added
791     * @return bool|string html or false if no data, true if printed
792     * @see    tpl_get_action
793     *
794     * @author Adrian Lang <mail@adrianlang.de>
795     */
796    function renderActionLink($type, $class = '', $pre = '', $suf = '', $inner = '', $return = false)
797    {
798        global $lang;
799        $data = tpl_get_action($type);
800        if ($data === false) {
801            return false;
802        } elseif (!is_array($data)) {
803            $out = sprintf($data, 'link');
804        } else {
805            /**
806             * @var string $accesskey
807             * @var string $id
808             * @var string $method
809             * @var bool $nofollow
810             * @var array $params
811             * @var string $replacement
812             */
813            extract($data);
814            if (strpos($id, '#') === 0) {
815                $linktarget = $id;
816            } else {
817                $linktarget = wl($id, $params);
818            }
819            $caption = $lang['btn_' . $type];
820            if (strpos($caption, '%s')) {
821                $caption = sprintf($caption, $replacement);
822            }
823            $akey = $addTitle = '';
824            if ($accesskey) {
825                $akey = 'accesskey="' . $accesskey . '" ';
826                $addTitle = ' [' . strtoupper($accesskey) . ']';
827            }
828            $rel = $nofollow ? 'rel="nofollow" ' : '';
829            $out = $pre . tpl_link(
830                    $linktarget, (($inner) ? $inner : $caption),
831                    'class="nav-link action ' . $type . ' ' . $class . '" ' .
832                    $akey . $rel .
833                    'title="' . hsc($caption) . $addTitle . '"', true
834                ) . $suf;
835        }
836        if ($return) return $out;
837        echo $out;
838        return true;
839    }
840
841
842    /**
843     * @return array
844     * Return the headers needed by this template
845     *
846     * @throws Exception
847     */
848    static function getBootstrapMetaHeaders()
849    {
850
851        // The version
852        $bootstrapVersion = TplUtility::getBootStrapVersion();
853        if ($bootstrapVersion === false) {
854            /**
855             * Strap may be called for test
856             * by combo
857             * In this case, the conf may not be reloaded
858             */
859            self::reloadConf();
860            $bootstrapVersion = TplUtility::getBootStrapVersion();
861            if ($bootstrapVersion === false) {
862                throw new Exception("Bootstrap version should not be false");
863            }
864        }
865        $scriptsMeta = self::buildBootstrapMetas($bootstrapVersion);
866
867        // if cdn
868        $useCdn = tpl_getConf(self::CONF_USE_CDN);
869
870
871        // Build the returned Js script array
872        $jsScripts = array();
873        foreach ($scriptsMeta as $key => $script) {
874            $path_parts = pathinfo($script["file"]);
875            $extension = $path_parts['extension'];
876            if ($extension === "js") {
877                $src = DOKU_BASE . "lib/tpl/strap/bootstrap/$bootstrapVersion/" . $script["file"];
878                if ($useCdn) {
879                    if (isset($script["url"])) {
880                        $src = $script["url"];
881                    }
882                }
883                $jsScripts[$key] =
884                    array(
885                        'src' => $src,
886                        'defer' => null
887                    );
888                if (isset($script['integrity'])) {
889                    $jsScripts[$key]['integrity'] = $script['integrity'];
890                    $jsScripts[$key]['crossorigin'] = 'anonymous';
891                }
892            }
893        }
894
895        $css = array();
896        $cssScript = $scriptsMeta['css'];
897        $href = DOKU_BASE . "lib/tpl/strap/bootstrap/$bootstrapVersion/" . $cssScript["file"];
898        if ($useCdn) {
899            if (isset($script["url"])) {
900                $href = $script["url"];
901            }
902        }
903        $css['css'] =
904            array(
905                'href' => $href,
906                'rel' => "stylesheet"
907            );
908        if (isset($script['integrity'])) {
909            $css['css']['integrity'] = $script['integrity'];
910            $css['css']['crossorigin'] = 'anonymous';
911        }
912
913
914        return array(
915            'script' => $jsScripts,
916            'link' => $css
917        );
918
919
920    }
921
922    /**
923     * @return array - A list of all available stylesheets
924     * This function is used to build the configuration as a list of files
925     */
926    static function getStylesheetsForMetadataConfiguration()
927    {
928        $cssVersionsMetas = self::getStyleSheetsFromJsonFileAsArray();
929        $listVersionStylesheetMeta = array();
930        foreach ($cssVersionsMetas as $bootstrapVersion => $cssVersionMeta) {
931            foreach ($cssVersionMeta as $fileName => $values) {
932                $listVersionStylesheetMeta[] = $bootstrapVersion . TplUtility::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . $fileName;
933            }
934        }
935        return $listVersionStylesheetMeta;
936    }
937
938    /**
939     *
940     * @param $version - return only the selected version if set
941     * @return array - an array of the meta JSON custom files
942     */
943    static function getStyleSheetsFromJsonFileAsArray($version = null)
944    {
945
946        $jsonAsArray = true;
947        $stylesheetsFile = __DIR__ . '/../bootstrap/bootstrapStylesheet.json';
948        $styleSheets = json_decode(file_get_contents($stylesheetsFile), $jsonAsArray);
949        if ($styleSheets == null) {
950            self::msg("Unable to read the file {$stylesheetsFile} as json");
951        }
952
953
954        $localStyleSheetsFile = __DIR__ . '/../bootstrap/bootstrapLocal.json';
955        if (file_exists($localStyleSheetsFile)) {
956            $localStyleSheets = json_decode(file_get_contents($localStyleSheetsFile), $jsonAsArray);
957            if ($localStyleSheets == null) {
958                self::msg("Unable to read the file {$localStyleSheets} as json");
959            }
960            foreach ($styleSheets as $bootstrapVersion => &$stylesheetsFiles) {
961                if (isset($localStyleSheets[$bootstrapVersion])) {
962                    $stylesheetsFiles = array_merge($stylesheetsFiles, $localStyleSheets[$bootstrapVersion]);
963                }
964            }
965        }
966
967        if (isset($version)) {
968            if (!isset($styleSheets[$version])) {
969                self::msg("The bootstrap version ($version) could not be found in the custom CSS file ($stylesheetsFile, or $localStyleSheetsFile)");
970            } else {
971                $styleSheets = $styleSheets[$version];
972            }
973        }
974
975        /**
976         * Select Rtl or Ltr
977         * Stylesheet name may have another level
978         * with direction property of the language
979         *
980         * Bootstrap needs another stylesheet
981         * See https://getbootstrap.com/docs/5.0/getting-started/rtl/
982         */
983        global $lang;
984        $direction = $lang["direction"];
985        if (empty($direction)) {
986            $direction = "ltr";
987        }
988        $directedStyleSheets = [];
989        foreach ($styleSheets as $name => $styleSheetDefinition) {
990            if (isset($styleSheetDefinition[$direction])) {
991                $directedStyleSheets[$name] = $styleSheetDefinition[$direction];
992            } else {
993                $directedStyleSheets[$name] = $styleSheetDefinition;
994            }
995        }
996
997        return $directedStyleSheets;
998    }
999
1000    /**
1001     *
1002     * Build from all Bootstrap JSON meta files only one array
1003     * @param $version
1004     * @return array
1005     *
1006     */
1007    static function buildBootstrapMetas($version)
1008    {
1009
1010        $jsonAsArray = true;
1011        $bootstrapJsonFile = __DIR__ . '/../bootstrap/bootstrapJavascript.json';
1012        $bootstrapMetas = json_decode(file_get_contents($bootstrapJsonFile), $jsonAsArray);
1013        // Decodage problem
1014        if ($bootstrapMetas == null) {
1015            self::msg("Unable to read the file {$bootstrapJsonFile} as json");
1016            return array();
1017        }
1018        if (!isset($bootstrapMetas[$version])) {
1019            self::msg("The bootstrap version ($version) could not be found in the file $bootstrapJsonFile");
1020            return array();
1021        }
1022        $bootstrapMetas = $bootstrapMetas[$version];
1023
1024
1025        // Css
1026        $bootstrapCssFile = TplUtility::getStyleSheetConf();
1027        $bootstrapCustomMetas = self::getStyleSheetsFromJsonFileAsArray($version);
1028
1029        if (!isset($bootstrapCustomMetas[$bootstrapCssFile])) {
1030            self::msg("The bootstrap custom file ($bootstrapCssFile) could not be found in the custom CSS files for the version ($version)");
1031        } else {
1032            $bootstrapMetas['css'] = $bootstrapCustomMetas[$bootstrapCssFile];
1033        }
1034
1035
1036        return $bootstrapMetas;
1037    }
1038
1039    /**
1040     * @param Doku_Event $event
1041     * @param $param
1042     * Function that handle the META HEADER event
1043     *   * It will add the Bootstrap Js and CSS
1044     *   * Make all script and resources defer
1045     * @throws Exception
1046     */
1047    static function handleBootstrapMetaHeaders(Doku_Event &$event, $param)
1048    {
1049
1050        $debug = tpl_getConf('debug');
1051        if ($debug) {
1052            self::addAsHtmlComment('Request: ' . json_encode($_REQUEST));
1053        }
1054
1055
1056        $newHeaderTypes = array();
1057        $bootstrapHeaders = self::getBootstrapMetaHeaders();
1058        $eventHeaderTypes = $event->data;
1059        foreach ($eventHeaderTypes as $headerType => $headerData) {
1060            switch ($headerType) {
1061
1062                case "link":
1063                    // index, rss, manifest, search, alternate, stylesheet
1064                    // delete edit
1065                    $bootstrapCss = $bootstrapHeaders[$headerType]['css'];
1066                    $headerData[] = $bootstrapCss;
1067
1068                    // preload all CSS is an heresy as it creates a FOUC (Flash of non-styled element)
1069                    // but we known it now and this is
1070                    $cssPreloadConf = tpl_getConf(self::CONF_PRELOAD_CSS);
1071                    $newLinkData = array();
1072                    foreach ($headerData as $linkData) {
1073                        switch ($linkData['rel']) {
1074                            case 'edit':
1075                                break;
1076                            case 'stylesheet':
1077                                /**
1078                                 * Preloading default to the configuration
1079                                 */
1080                                $preload = $cssPreloadConf;
1081                                /**
1082                                 * Preload can be set at the array level with the critical attribute
1083                                 * If the preload attribute is present
1084                                 * We get that for instance for css animation style sheet
1085                                 * that are not needed for rendering
1086                                 */
1087                                if (isset($linkData["critical"])) {
1088                                    $critical = $linkData["critical"];
1089                                    $preload = !(filter_var($critical, FILTER_VALIDATE_BOOLEAN));
1090                                    unset($linkData["critical"]);
1091                                }
1092                                if ($preload) {
1093                                    $newLinkData[] = TplUtility::toPreloadCss($linkData);
1094                                } else {
1095                                    $newLinkData[] = $linkData;
1096                                }
1097                                break;
1098                            default:
1099                                $newLinkData[] = $linkData;
1100                                break;
1101                        }
1102                    }
1103
1104                    $newHeaderTypes[$headerType] = $newLinkData;
1105                    break;
1106
1107                case "script":
1108
1109                    /**
1110                     * Do we delete the dokuwiki javascript ?
1111                     */
1112                    $scriptToDeletes = [];
1113                    if (empty($_SERVER['REMOTE_USER']) && tpl_getConf(TplUtility::CONF_DISABLE_BACKEND_JAVASCRIPT, 0)) {
1114                        $scriptToDeletes = [
1115                            //'JSINFO', Don't delete Jsinfo !! It contains metadata information (that is used to get context)
1116                            'js.php'
1117                        ];
1118                        if (TplUtility::getBootStrapMajorVersion() == "5") {
1119                            // bs 5 does not depends on jquery
1120                            $scriptToDeletes[] = "jquery.php";
1121                        }
1122                    }
1123
1124                    /**
1125                     * The new script array
1126                     */
1127                    $newScriptData = array();
1128                    // A variable to hold the Jquery scripts
1129                    // jquery-migrate, jquery, jquery-ui ou jquery.php
1130                    // see https://www.dokuwiki.org/config:jquerycdn
1131                    $jqueryDokuScripts = array();
1132                    foreach ($headerData as $scriptData) {
1133
1134                        foreach ($scriptToDeletes as $scriptToDelete) {
1135                            if (isset($scriptData["_data"]) && !empty($scriptData["_data"])) {
1136                                $haystack = $scriptData["_data"];
1137                            } else {
1138                                $haystack = $scriptData["src"];
1139                            }
1140                            if (preg_match("/$scriptToDelete/i", $haystack)) {
1141                                continue 2;
1142                            }
1143                        }
1144
1145                        $critical = false;
1146                        if (isset($scriptData["critical"])) {
1147                            $critical = $scriptData["critical"];
1148                            unset($scriptData["critical"]);
1149                        }
1150
1151                        // defer is only for external resource
1152                        // if this is not, this is illegal
1153                        if (isset($scriptData["src"])) {
1154                            if (!$critical) {
1155                                $scriptData['defer'] = null;
1156                            }
1157                        }
1158
1159                        if (isset($scriptData["type"])) {
1160                            $type = strtolower($scriptData["type"]);
1161                            if ($type == "text/javascript") {
1162                                unset($scriptData["type"]);
1163                            }
1164                        }
1165
1166                        // The charset attribute on the script element is obsolete.
1167                        if (isset($scriptData["charset"])) {
1168                            unset($scriptData["charset"]);
1169                        }
1170
1171                        // Jquery ?
1172                        $jqueryFound = false;
1173                        // script may also be just an online script without the src attribute
1174                        if (array_key_exists('src', $scriptData)) {
1175                            $jqueryFound = strpos($scriptData['src'], 'jquery');
1176                        }
1177                        if ($jqueryFound === false) {
1178                            $newScriptData[] = $scriptData;
1179                        } else {
1180                            $jqueryDokuScripts[] = $scriptData;
1181                        }
1182
1183                    }
1184
1185                    // Add Jquery at the beginning
1186                    $boostrapMajorVersion = TplUtility::getBootStrapMajorVersion();
1187                    if ($boostrapMajorVersion == "4") {
1188                        if (
1189                            empty($_SERVER['REMOTE_USER'])
1190                            && tpl_getConf(self::CONF_JQUERY_DOKU) == 0
1191                        ) {
1192                            // We take the Jquery of Bootstrap
1193                            $newScriptData = array_merge($bootstrapHeaders[$headerType], $newScriptData);
1194                        } else {
1195                            // Logged in
1196                            // We take the Jqueries of doku and we add Bootstrap
1197                            $newScriptData = array_merge($jqueryDokuScripts, $newScriptData); // js
1198                            // We had popper of Bootstrap
1199                            $newScriptData[] = $bootstrapHeaders[$headerType]['popper'];
1200                            // We had the js of Bootstrap
1201                            $newScriptData[] = $bootstrapHeaders[$headerType]['js'];
1202                        }
1203                    } else {
1204
1205                        // There is no JQuery in 5
1206                        // We had the js of Bootstrap and popper
1207                        // Add Jquery before the js.php
1208                        $newScriptData = array_merge($jqueryDokuScripts, $newScriptData); // js
1209                        // Then add at the top of the top (first of the first) bootstrap
1210                        // Why ? Because Jquery should be last to be able to see the missing icon
1211                        // https://stackoverflow.com/questions/17367736/jquery-ui-dialog-missing-close-icon
1212                        $bootstrap[] = $bootstrapHeaders[$headerType]['popper'];
1213                        $bootstrap[] = $bootstrapHeaders[$headerType]['js'];
1214                        $newScriptData = array_merge($bootstrap, $newScriptData);
1215
1216                    }
1217
1218
1219                    $newHeaderTypes[$headerType] = $newScriptData;
1220                    break;
1221                case "meta":
1222                    $newHeaderData = array();
1223                    foreach ($headerData as $metaData) {
1224                        // Content should never be null
1225                        // Name may change
1226                        // https://www.w3.org/TR/html4/struct/global.html#edef-META
1227                        if (!key_exists("content", $metaData)) {
1228                            $message = "Strap - The head meta (" . print_r($metaData, true) . ") does not have a content property";
1229                            msg($message, -1, "", "", MSG_ADMINS_ONLY);
1230                            if (defined('DOKU_UNITTEST')
1231                            ) {
1232                                throw new \RuntimeException($message);
1233                            }
1234                        } else {
1235                            $content = $metaData["content"];
1236                            if (empty($content)) {
1237                                $messageEmpty = "Strap - the below head meta has an empty content property (" . print_r($metaData, true) . ")";
1238                                msg($messageEmpty, -1, "", "", MSG_ADMINS_ONLY);
1239                                if (defined('DOKU_UNITTEST')
1240                                ) {
1241                                    throw new \RuntimeException($messageEmpty);
1242                                }
1243                            } else {
1244                                $newHeaderData[] = $metaData;
1245                            }
1246                        }
1247                    }
1248                    $newHeaderTypes[$headerType] = $newHeaderData;
1249                    break;
1250                case "noscript": // https://github.com/ComboStrap/dokuwiki-plugin-gtm/blob/master/action.php#L32
1251                case "style":
1252                    $newHeaderTypes[$headerType] = $headerData;
1253                    break;
1254                default:
1255                    $message = "Strap - The header type ($headerType) is unknown and was not controlled.";
1256                    $newHeaderTypes[$headerType] = $headerData;
1257                    msg($message, -1, "", "", MSG_ADMINS_ONLY);
1258                    if (defined('DOKU_UNITTEST')
1259                    ) {
1260                        throw new \RuntimeException($message);
1261                    }
1262            }
1263        }
1264
1265        if ($debug) {
1266            self::addAsHtmlComment('Script Header : ' . json_encode($newHeaderTypes['script']));
1267        }
1268        $event->data = $newHeaderTypes;
1269
1270
1271    }
1272
1273    /**
1274     * Returns the icon link as created by https://realfavicongenerator.net/
1275     *
1276     *
1277     *
1278     * @return string
1279     */
1280    static function renderFaviconMetaLinks()
1281    {
1282
1283        $return = '';
1284
1285        // FavIcon.ico
1286        $possibleLocation = array(':wiki:favicon.ico', ':favicon.ico', 'images/favicon.ico');
1287        $return .= '<link rel="shortcut icon" href="' . tpl_getMediaFile($possibleLocation, true) . '" />' . NL;
1288
1289        // Icon Png
1290        $possibleLocation = array(':wiki:favicon-32x32.png', ':favicon-32x32.png', 'images/favicon-32x32.png');
1291        $return .= '<link rel="icon" type="image/png" sizes="32x32" href="' . tpl_getMediaFile($possibleLocation,  true) . '"/>';
1292
1293        $possibleLocation = array(':wiki:favicon-16x16.png', ':favicon-16x16.png', 'images/favicon-16x16.png');
1294        $return .= '<link rel="icon" type="image/png" sizes="16x16" href="' . tpl_getMediaFile($possibleLocation,  true) . '"/>';
1295
1296        // Apple touch icon
1297        $possibleLocation = array(':wiki:apple-touch-icon.png', ':apple-touch-icon.png', 'images/apple-touch-icon.png');
1298        $return .= '<link rel="apple-touch-icon" href="' . tpl_getMediaFile($possibleLocation,  true) . '" />' . NL;
1299
1300        return $return;
1301
1302    }
1303
1304    static function renderPageTitle()
1305    {
1306
1307        global $conf;
1308        global $ID;
1309        $title = tpl_pagetitle($ID, true) . ' |' . $conf["title"];
1310        // trigger event here
1311        Event::createAndTrigger('TPL_TITLE_OUTPUT', $title, '\ComboStrap\TplUtility::callBackPageTitle', true);
1312        return true;
1313
1314    }
1315
1316    /**
1317     * Print the title that we get back from the event TPL_TITLE_OUTPUT
1318     * triggered by the function {@link tpl_strap_title()}
1319     * @param $title
1320     */
1321    static function callBackPageTitle($title)
1322    {
1323        echo $title;
1324    }
1325
1326    /**
1327     *
1328     * Set a template conf value
1329     *
1330     * To set a template configuration, you need to first load them
1331     * and there is no set function in template.php
1332     *
1333     * @param $confName - the configuration name
1334     * @param $confValue - the configuration value
1335     */
1336    static function setConf($confName, $confValue)
1337    {
1338
1339        /**
1340         * Env variable
1341         */
1342        global $conf;
1343        $template = $conf['template'];
1344
1345        if ($template != "strap") {
1346            throw new \RuntimeException("This is not the strap template, in test, active it in setup");
1347        }
1348
1349        /**
1350         *  Make sure to load the configuration first by calling getConf
1351         */
1352        $actualValue = tpl_getConf($confName);
1353        if ($actualValue === false) {
1354
1355            self::reloadConf();
1356
1357            // Check that the conf was loaded
1358            if (tpl_getConf($confName) === false) {
1359                throw new \RuntimeException("The configuration (" . $confName . ") returns no value or has no default");
1360            }
1361        }
1362
1363        $conf['tpl'][$template][$confName] = $confValue;
1364
1365    }
1366
1367    /**
1368     * When running multiple test, the function {@link tpl_getConf()}
1369     * does not reload the configuration twice
1370     */
1371    static function reloadConf()
1372    {
1373        /**
1374         * Env variable
1375         */
1376        global $conf;
1377        $template = $conf['template'];
1378
1379        $tconf = tpl_loadConfig();
1380        if ($tconf !== false) {
1381            foreach ($tconf as $key => $value) {
1382                if (isset($conf['tpl'][$template][$key])) continue;
1383                $conf['tpl'][$template][$key] = $value;
1384            }
1385        }
1386    }
1387
1388
1389    /**
1390     * Send a message to a manager and log it
1391     * Fail if in test
1392     * @param string $message
1393     * @param int $level - the level see LVL constant
1394     * @param string $canonical - the canonical
1395     */
1396    static function msg($message, $level = self::LVL_MSG_ERROR, $canonical = "strap")
1397    {
1398        $strapUrl = self::getStrapUrl();
1399        $prefix = "<a href=\"$strapUrl\">Strap</a>";
1400        $prefix = '<a href="https://combostrap.com/' . $canonical . '">' . ucfirst($canonical) . '</a>';
1401
1402        $htmlMsg = $prefix . " - " . $message;
1403        if ($level != self::LVL_MSG_DEBUG) {
1404            msg($htmlMsg, $level, '', '', MSG_MANAGERS_ONLY);
1405        }
1406        /**
1407         * Print to a log file
1408         * Note: {@link dbg()} dbg print to the web page
1409         */
1410        $prefix = 'strap';
1411        if ($canonical != null) {
1412            $prefix .= ' - ' . $canonical;
1413        }
1414        $msg = $prefix . ' - ' . $message;
1415        dbglog($msg);
1416        if (defined('DOKU_UNITTEST') && ($level == self::LVL_MSG_WARNING || $level == self::LVL_MSG_ERROR)) {
1417            throw new \RuntimeException($msg);
1418        }
1419    }
1420
1421    /**
1422     * @param bool $prependTOC
1423     * @return false|string - Adapted from {@link tpl_content()} to return the HTML
1424     */
1425    static function tpl_content(bool $prependTOC = true)
1426    {
1427        global $ACT;
1428        global $INFO;
1429        $INFO['prependTOC'] = $prependTOC;
1430
1431        ob_start();
1432        Event::createAndTrigger('TPL_ACT_RENDER', $ACT, 'tpl_content_core');
1433        $html_output = ob_get_clean();
1434
1435        /**
1436         * The action null does nothing.
1437         * See {@link Event::trigger()}
1438         */
1439        Event::createAndTrigger('TPL_CONTENT_DISPLAY', $html_output, null);
1440
1441        return $html_output;
1442    }
1443
1444    static function getPageHeader()
1445    {
1446
1447        $navBarPageName = TplUtility::getHeaderSlotPageName();
1448        if ($id = page_findnearest($navBarPageName)) {
1449
1450            $header = tpl_include_page($id, 0, false);
1451
1452        } else {
1453
1454            $domain = self::getApexDomainUrl();
1455            $header = '<div class="container p-3" style="text-align: center;position:relative;z-index:100">Welcome to the <a href="' . $domain . '/">Strap template</a>.<br/>
1456            If you don\'t known the <a href="https://combostrap.com/">ComboStrap</a>, it\'s recommended to follow the <a href="' . $domain . '/getting_started">Getting Started Guide</a>.<br/>
1457            Otherwise, to create a menu bar in the header, create a page with the id (' . html_wikilink(':' . $navBarPageName) . ') and the <a href="' . $domain . '/menubar">menubar component</a>.
1458            </div>';
1459
1460        }
1461        // No header on print
1462        return "<div class=\"d-print-none\">$header</div>";
1463
1464    }
1465
1466    static function getFooter()
1467    {
1468        $domain = self::getApexDomainUrl();
1469
1470        $footerPageName = TplUtility::getFooterSlotPageName();
1471        if ($id = page_findnearest($footerPageName)) {
1472            $footer = tpl_include_page($id, 0, false);
1473        } else {
1474            $footer = '<div class="container p-3" style="text-align: center">Welcome to the <a href="' . $domain . '/strap">Strap template</a>. To get started, create a page with the id ' . html_wikilink(':' . $footerPageName) . ' to create a footer.</div>';
1475        }
1476
1477        // No footer on print
1478        return "<div class=\"d-print-none\">$footer</div>";
1479    }
1480
1481    static function getPoweredBy()
1482    {
1483
1484        $domain = self::getApexDomainUrl();
1485        $version = self::getFullQualifyVersion();
1486        $poweredBy = "<div class=\"mx-auto\" style=\"width: 300px;text-align: center;margin-bottom: 1rem\">";
1487        $poweredBy .= "  <small><i>Powered by <a href=\"$domain\" title=\"ComboStrap " . $version . "\" style=\"color:#495057\">ComboStrap</a></i></small>";
1488        $poweredBy .= '</div>';
1489        return $poweredBy;
1490    }
1491
1492
1493}
1494
1495
1496