1<?php
2
3namespace dokuwiki\template\bootstrap3;
4
5/**
6 * DokuWiki Bootstrap3 Template: Template Class
7 *
8 * @link     http://dokuwiki.org/template:bootstrap3
9 * @author   Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
10 * @license  GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 */
12
13class Template
14{
15
16    private $plugins      = [];
17    private $confMetadata = [];
18    private $toolsMenu    = [];
19    private $handlers;
20
21    public $tplDir  = '';
22    public $baseDir = '';
23
24    public function __construct()
25    {
26
27        global $JSINFO;
28        global $INPUT;
29        global $ACT;
30        global $INFO;
31
32        $this->tplDir  = tpl_incdir();
33        $this->baseDir = tpl_basedir();
34
35        $this->initPlugins();
36        $this->initToolsMenu();
37        $this->loadConfMetadata();
38
39        // Get the template info (useful for debug)
40        if (isset($INFO['isadmin']) && $INPUT->str('do') && $INPUT->str('do') == 'check') {
41            msg('Template version ' . $this->getVersion(), 1, '', '', MSG_ADMINS_ONLY);
42        }
43
44        // Populate JSINFO object
45        $JSINFO['bootstrap3'] = [
46            'mode'   => $ACT,
47            'toc'    => [],
48            'config' => [
49                'collapsibleSections'        => (int) $this->getConf('collapsibleSections'),
50                'fixedTopNavbar'             => (int) $this->getConf('fixedTopNavbar'),
51                'showSemanticPopup'          => (int) $this->getConf('showSemanticPopup'),
52                'sidebarOnNavbar'            => (int) $this->getConf('sidebarOnNavbar'),
53                'tagsOnTop'                  => (int) $this->getConf('tagsOnTop'),
54                'tocAffix'                   => (int) $this->getConf('tocAffix'),
55                'tocCollapseOnScroll'        => (int) $this->getConf('tocCollapseOnScroll'),
56                'tocCollapsed'               => (int) $this->getConf('tocCollapsed'),
57                'tocLayout'                  => $this->getConf('tocLayout'),
58                'useAnchorJS'                => (int) $this->getConf('useAnchorJS'),
59                'useAlternativeToolbarIcons' => (int) $this->getConf('useAlternativeToolbarIcons'),
60                'disableSearchSuggest'       => (int) $this->getConf('disableSearchSuggest'),
61            ],
62        ];
63
64        if ($ACT == 'admin') {
65            $JSINFO['bootstrap3']['admin'] = hsc($INPUT->str('page'));
66        }
67
68        if (!defined('MAX_FILE_SIZE') && $pagesize = $this->getConf('domParserMaxPageSize')) {
69            define('MAX_FILE_SIZE', $pagesize);
70        }
71
72        # Start Event Handlers
73        $this->handlers = new EventHandlers($this);
74
75    }
76
77    public function getVersion()
78    {
79        $template_info    = confToHash($this->tplDir . 'template.info.txt');
80        $template_version = 'v' . $template_info['date'];
81
82        if (isset($template_info['build'])) {
83            $template_version .= ' (' . $template_info['build'] . ')';
84        }
85
86        return $template_version;
87    }
88
89    private function initPlugins()
90    {
91        $plugins = ['tplinc', 'tag', 'userhomepage', 'translation', 'pagelist'];
92
93        foreach ($plugins as $plugin) {
94            $this->plugins[$plugin] = plugin_load('helper', $plugin);
95        }
96    }
97
98    public function getPlugin($plugin)
99    {
100        if (plugin_isdisabled($plugin)) {
101            return false;
102        }
103
104        if (!isset($this->plugins[$plugin])) {
105            return false;
106        }
107
108        return $this->plugins[$plugin];
109    }
110
111    /**
112     * Get the singleton instance
113     *
114     * @return Template
115     */
116    public static function getInstance()
117    {
118        static $instance = null;
119
120        if ($instance === null) {
121            $instance = new self;
122        }
123
124        return $instance;
125    }
126
127    /**
128     * Get the content to include from the tplinc plugin
129     *
130     * prefix and postfix are only added when there actually is any content
131     *
132     * @param string $location
133     * @return string
134     */
135    public function includePage($location, $return = false)
136    {
137
138        $content = '';
139
140        if ($plugin = $this->getPlugin('tplinc')) {
141            $content = $plugin->renderIncludes($location);
142        }
143
144        if ($content === '') {
145            $content = tpl_include_page($location, 0, 1, $this->getConf('useACL'));
146        }
147
148        if ($content === '') {
149            return '';
150        }
151
152        $content = $this->normalizeContent($content);
153
154        if ($return) {
155            return $content;
156        }
157
158        echo $content;
159        return '';
160    }
161
162    /**
163     * Get the template configuration metadata
164     *
165     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
166     *
167     * @param   string $key
168     * @return  array|string
169     */
170    public function getConfMetadata($key = null)
171    {
172        if ($key && isset($this->confMetadata[$key])) {
173            return $this->confMetadata[$key];
174        }
175
176        return null;
177    }
178
179    private function loadConfMetadata()
180    {
181        $meta = [];
182        $file = $this->tplDir . 'conf/metadata.php';
183
184        include $file;
185
186        $this->confMetadata = $meta;
187    }
188
189    /**
190     * Simple wrapper for tpl_getConf
191     *
192     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
193     *
194     * @param   string  $key
195     * @param   mixed   $default value
196     * @return  mixed
197     */
198    public function getConf($key, $default = false)
199    {
200        global $ACT, $INFO, $ID, $conf;
201
202        $value = tpl_getConf($key, $default);
203
204        switch ($key) {
205            case 'useAvatar':
206
207                if ($value == 'off') {
208                    return false;
209                }
210
211                return $value;
212
213            case 'bootstrapTheme':
214
215                @list($theme, $bootswatch) = $this->getThemeForNamespace();
216                if ($theme) {
217                    return $theme;
218                }
219
220                return $value;
221
222            case 'bootswatchTheme':
223
224                @list($theme, $bootswatch) = $this->getThemeForNamespace();
225                if ($bootswatch) {
226                    return $bootswatch;
227                }
228
229                return $value;
230
231            case 'showTools':
232            case 'showSearchForm':
233            case 'showPageTools':
234            case 'showEditBtn':
235            case 'showAddNewPage':
236
237                return $value !== 'never' && ($value == 'always' || !empty($_SERVER['REMOTE_USER']));
238
239            case 'showAdminMenu':
240
241                return $value && ($INFO['isadmin'] || $INFO['ismanager']);
242
243            case 'hideLoginLink':
244            case 'showLoginOnFooter':
245
246                return ($value && !isset($_SERVER['REMOTE_USER']));
247
248            case 'showCookieLawBanner':
249
250                return $value && page_findnearest(tpl_getConf('cookieLawBannerPage'), $this->getConf('useACL')) && ($ACT == 'show');
251
252            case 'showSidebar':
253
254                if ($ACT !== 'show') {
255                    return false;
256                }
257
258                if ($this->getConf('showLandingPage')) {
259                    return false;
260                }
261
262                return page_findnearest($conf['sidebar'], $this->getConf('useACL'));
263
264            case 'showRightSidebar':
265
266                if ($ACT !== 'show') {
267                    return false;
268                }
269
270                if ($this->getConf('sidebarPosition') == 'right') {
271                    return false;
272                }
273
274                return page_findnearest(tpl_getConf('rightSidebar'), $this->getConf('useACL'));
275
276            case 'showLandingPage':
277
278                return ($value && (bool) preg_match_all($this->getConf('landingPages'), $ID));
279
280            case 'pageOnPanel':
281
282                if ($this->getConf('showLandingPage')) {
283                    return false;
284                }
285
286                return $value;
287
288            case 'showThemeSwitcher':
289
290                return $value && ($this->getConf('bootstrapTheme') == 'bootswatch');
291
292            case 'tocCollapseSubSections':
293
294                if (!$this->getConf('tocAffix')) {
295                    return false;
296                }
297
298                return $value;
299
300            case 'schemaOrgType':
301
302                if ($semantic = plugin_load('helper', 'semantic')) {
303                    if (method_exists($semantic, 'getSchemaOrgType')) {
304                        return $semantic->getSchemaOrgType();
305                    }
306                }
307
308                return $value;
309
310            case 'tocCollapseOnScroll':
311
312                if ($this->getConf('tocLayout') !== 'default') {
313                    return false;
314                }
315
316                return $value;
317        }
318
319        $metadata = $this->getConfMetadata($key);
320
321        if (isset($metadata[0])) {
322            switch ($metadata[0]) {
323                case 'regex':
324                    return '/' . $value . '/';
325                case 'multicheckbox':
326                    return explode(',', $value);
327            }
328        }
329
330        return $value;
331    }
332
333    /**
334     * Return the Bootswatch.com theme lists defined in metadata.php
335     *
336     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
337     *
338     * @return  array
339     */
340    public function getBootswatchThemeList()
341    {
342        $bootswatch_themes = $this->getConfMetadata('bootswatchTheme');
343        return $bootswatch_themes['_choices'];
344    }
345
346    /**
347     * Get a Gravatar, Libravatar, Office365/EWS URL or local ":user" DokuWiki namespace
348     *
349     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
350     *
351     * @param   string  $username  User ID
352     * @param   string  $email     The email address
353     * @param   string  $size      Size in pixels, defaults to 80px [ 1 - 2048 ]
354     * @param   string  $d         Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
355     * @param   string  $r         Maximum rating (inclusive) [ g | pg | r | x ]
356     *
357     * @return  string
358     */
359    public function getAvatar($username, $email, $size = 80, $d = 'mm', $r = 'g')
360    {
361        global $INFO;
362
363        $avatar_url      = '';
364        $avatar_provider = $this->getConf('useAvatar');
365
366        if (!$avatar_provider) {
367            return false;
368        }
369
370        if ($avatar_provider == 'local') {
371
372            $interwiki = getInterwiki();
373            $user_url  = str_replace('{NAME}', $username, $interwiki['user']);
374            $logo_size = [];
375            $logo      = tpl_getMediaFile(["$user_url.png", "$user_url.jpg", 'images/avatar.png'], false, $logo_size);
376
377            return $logo;
378        }
379
380        if ($avatar_provider == 'activedirectory') {
381            $logo = "data:image/jpeg;base64," . base64_encode($INFO['userinfo']['thumbnailphoto']);
382
383            return $logo;
384        }
385
386        $email = strtolower(trim($email));
387
388        if ($avatar_provider == 'office365') {
389            $office365_url = rtrim($this->getConf('office365URL'), '/');
390            $avatar_url    = $office365_url . '/owa/service.svc/s/GetPersonaPhoto?email=' . $email . '&size=HR' . $size . 'x' . $size;
391        }
392
393        if ($avatar_provider == 'gravatar' || $avatar_provider == 'libavatar') {
394            $gravatar_url  = rtrim($this->getConf('gravatarURL'), '/') . '/';
395            $libavatar_url = rtrim($this->getConf('libavatarURL'), '/') . '/';
396
397            switch ($avatar_provider) {
398                case 'gravatar':
399                    $avatar_url = $gravatar_url;
400                    break;
401                case 'libavatar':
402                    $avatar_url = $libavatar_url;
403                    break;
404            }
405
406            $avatar_url .= md5($email);
407            $avatar_url .= "?s=$size&d=$d&r=$r";
408        }
409
410        if ($avatar_url) {
411            $media_link = ml("$avatar_url&.jpg", ['cache' => 'recache', 'w' => $size, 'h' => $size]);
412            return $media_link;
413        }
414
415        return false;
416    }
417
418    /**
419     * Return template classes
420     *
421     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
422     * @see tpl_classes();
423     *
424     * @return string
425     **/
426    public function getClasses()
427    {
428        global $ACT;
429
430        $page_on_panel    = $this->getConf('pageOnPanel');
431        $bootstrap_theme  = $this->getConf('bootstrapTheme');
432        $bootswatch_theme = $this->getBootswatchTheme();
433
434        $classes   = [];
435        $classes[] = (($bootstrap_theme == 'bootswatch') ? $bootswatch_theme : $bootstrap_theme);
436        $classes[] = trim(tpl_classes());
437
438        if ($page_on_panel) {
439            $classes[] = 'dw-page-on-panel';
440        }
441
442        if (!$this->getConf('tableFullWidth')) {
443            $classes[] = 'dw-table-width';
444        }
445
446        if ($this->isFluidNavbar()) {
447            $classes[] = 'dw-fluid-container';
448        }
449
450        return implode(' ', $classes);
451    }
452
453    /**
454     * Return the current Bootswatch theme
455     *
456     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
457     *
458     * @return  string
459     */
460    public function getBootswatchTheme()
461    {
462        global $INPUT;
463
464        $bootswatch_theme = $this->getConf('bootswatchTheme');
465
466        if ($this->getConf('showThemeSwitcher')) {
467            if (get_doku_pref('bootswatchTheme', null) !== null && get_doku_pref('bootswatchTheme', null) !== '') {
468                $bootswatch_theme = get_doku_pref('bootswatchTheme', null);
469            }
470        }
471        return $bootswatch_theme;
472    }
473
474    /**
475     * Return only the available Bootswatch.com themes
476     *
477     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
478     *
479     * @return  array
480     */
481    public function getAvailableBootswatchThemes()
482    {
483        return array_diff($this->getBootswatchThemeList(), $this->getConf('hideInThemeSwitcher'));
484    }
485
486    /**
487     * Return the active theme
488     *
489     * @return string
490     */
491    public function getTheme()
492    {
493        $bootstrap_theme  = $this->getConf('bootstrapTheme');
494        $bootswatch_theme = $this->getBootswatchTheme();
495        $theme            = (($bootstrap_theme == 'bootswatch') ? $bootswatch_theme : $bootstrap_theme);
496
497        return $theme;
498    }
499
500    /**
501     * Return the active theme
502     *
503     * @return string
504     */
505    public function getThemeFeatures()
506    {
507        $features = [];
508
509        if ($this->isFluidNavbar()) {
510            $features[] = 'fluid-container';
511        }
512
513        if ($this->getConf('fixedTopNavbar')) {
514            $features[] = 'fixed-top-navbar';
515        }
516
517        if ($this->getConf('tocCollapseSubSections')) {
518            $features[] = 'toc-cullapse-sub-sections';
519        }
520
521        return implode(' ', $features);
522    }
523
524    /**
525     * Print some info about the current page
526     *
527     * @author  Andreas Gohr <andi@splitbrain.org>
528     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
529     *
530     * @param   bool $ret return content instead of printing it
531     * @return  bool|string
532     */
533    public function getPageInfo($ret = false)
534    {
535        global $conf;
536        global $lang;
537        global $INFO;
538        global $ID;
539
540        // return if we are not allowed to view the page
541        if (!auth_quickaclcheck($ID)) {
542            return false;
543        }
544
545        // prepare date and path
546        $fn = $INFO['filepath'];
547
548        if (!$conf['fullpath']) {
549            if ($INFO['rev']) {
550                $fn = str_replace(fullpath($conf['olddir']) . '/', '', $fn);
551            } else {
552                $fn = str_replace(fullpath($conf['datadir']) . '/', '', $fn);
553            }
554        }
555
556        $date_format = $this->getConf('pageInfoDateFormat');
557        $page_info   = $this->getConf('pageInfo');
558
559        $fn   = utf8_decodeFN($fn);
560        $date = (($date_format == 'dformat')
561            ? dformat($INFO['lastmod'])
562            : datetime_h($INFO['lastmod']));
563
564        // print it
565        if ($INFO['exists']) {
566            $fn_full = $fn;
567
568            if (!in_array('extension', $page_info)) {
569                $fn = str_replace(['.txt.gz', '.txt'], '', $fn);
570            }
571
572            $out = '<ul class="list-inline">';
573
574            if (in_array('filename', $page_info)) {
575                $out .= '<li>' . iconify('mdi:file-document-outline', ['class' => 'text-muted']) . ' <span title="' . $fn_full . '">' . $fn . '</span></li>';
576            }
577
578            if (in_array('date', $page_info)) {
579                $out .= '<li>' . iconify('mdi:calendar', ['class' => 'text-muted']) . ' ' . $lang['lastmod'] . ' <span title="' . dformat($INFO['lastmod']) . '">' . $date . '</span></li>';
580            }
581
582            if (in_array('editor', $page_info)) {
583                if (isset($INFO['editor'])) {
584                    $user = editorinfo($INFO['editor']);
585
586                    if ($this->getConf('useAvatar')) {
587                        global $auth;
588                        $user_data = $auth->getUserData($INFO['editor']);
589
590                        $avatar_img = $this->getAvatar($INFO['editor'], $user_data['mail'], 16);
591                        $user_img   = '<img src="' . $avatar_img . '" alt="" width="16" height="16" class="img-rounded" /> ';
592                        $user       = str_replace(['iw_user', 'interwiki'], '', $user);
593                        $user       = $user_img . "<bdi>$user<bdi>";
594                    }
595
596                    $out .= '<li class="text-muted">' . $lang['by'] . ' <bdi>' . $user . '</bdi></li>';
597                } else {
598                    $out .= '<li>(' . $lang['external_edit'] . ')</li>';
599                }
600            }
601
602            if ($INFO['locked'] && in_array('locked', $page_info)) {
603                $out .= '<li>' . iconify('mdi:lock', ['class' => 'text-muted']) . ' ' . $lang['lockedby'] . ' ' . editorinfo($INFO['locked']) . '</li>';
604            }
605
606            $out .= '</ul>';
607
608            if ($ret) {
609                return $out;
610            } else {
611                echo $out;
612                return true;
613            }
614        }
615
616        return false;
617    }
618
619    /**
620     * Prints the global message array in Bootstrap style
621     *
622     * @author Andreas Gohr <andi@splitbrain.org>
623     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
624     *
625     * @see html_msgarea()
626     */
627    public function getMessageArea()
628    {
629
630        global $MSG, $MSG_shown;
631
632        /** @var array $MSG */
633        // store if the global $MSG has already been shown and thus HTML output has been started
634        $MSG_shown = true;
635
636        // Check if translation is outdate
637        if ($this->getConf('showTranslation') && $translation = $this->getPlugin('translation')) {
638            global $ID;
639
640            if ($translation->istranslatable($ID)) {
641                $translation->checkage();
642            }
643        }
644
645        if (!isset($MSG)) {
646            return;
647        }
648
649        $shown = [];
650
651        foreach ($MSG as $msg) {
652            $hash = md5($msg['msg']);
653            if (isset($shown[$hash])) {
654                continue;
655            }
656            // skip double messages
657
658            if (info_msg_allowed($msg)) {
659                switch ($msg['lvl']) {
660                    case 'info':
661                        $level = 'info';
662                        $icon  = 'mdi:information';
663                        break;
664
665                    case 'error':
666                        $level = 'danger';
667                        $icon  = 'mdi:alert-octagon';
668                        break;
669
670                    case 'notify':
671                        $level = 'warning';
672                        $icon  = 'mdi:alert';
673                        break;
674
675                    case 'success':
676                        $level = 'success';
677                        $icon  = 'mdi:check-circle';
678                        break;
679                }
680
681                print '<div class="alert alert-' . $level . '">';
682                print iconify($icon, ['class' => 'mr-2']);
683                print $msg['msg'];
684                print '</div>';
685            }
686
687            $shown[$hash] = 1;
688        }
689
690        unset($GLOBALS['MSG']);
691    }
692
693    /**
694     * Get the license (link or image)
695     *
696     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
697     *
698     * @param  string  $type ("link" or "image")
699     * @param  integer $size of image
700     * @param  bool    $return or print
701     * @return string
702     */
703    public function getLicense($type = 'link', $size = 24, $return = false)
704    {
705
706        global $conf, $license, $lang;
707
708        $target = $conf['target']['extern'];
709        $lic    = $license[$conf['license']];
710        $output = '';
711
712        if (!$lic) {
713            return '';
714        }
715
716        if ($type == 'link') {
717            $output .= $lang['license'] . '<br/>';
718        }
719
720        $license_url  = $lic['url'];
721        $license_name = $lic['name'];
722
723        $output .= '<a href="' . $license_url . '" title="' . $license_name . '" target="' . $target . '" itemscope itemtype="http://schema.org/CreativeWork" itemprop="license" rel="license" class="license">';
724
725        if ($type == 'image') {
726            foreach (explode('-', $conf['license']) as $license_img) {
727                if ($license_img == 'publicdomain') {
728                    $license_img = 'pd';
729                }
730
731                $output .= '<img src="' . tpl_basedir() . "images/license/$license_img.png" . '" width="' . $size . '" height="' . $size . '" alt="' . $license_img . '" /> ';
732            }
733        } else {
734            $output .= $lic['name'];
735        }
736
737        $output .= '</a>';
738
739        if ($return) {
740            return $output;
741        }
742
743        echo $output;
744        return '';
745    }
746
747    /**
748     * Add Google Analytics
749     *
750     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
751     *
752     * @return  string
753     */
754    public function getGoogleAnalitycs()
755    {
756        global $INFO;
757        global $ID;
758
759        if (!$this->getConf('useGoogleAnalytics')) {
760            return false;
761        }
762
763        if (!$google_analitycs_id = $this->getConf('googleAnalyticsTrackID')) {
764            return false;
765        }
766
767        if ($this->getConf('googleAnalyticsNoTrackAdmin') && $INFO['isadmin']) {
768            return false;
769        }
770
771        if ($this->getConf('googleAnalyticsNoTrackUsers') && isset($_SERVER['REMOTE_USER'])) {
772            return false;
773        }
774
775        if (tpl_getConf('googleAnalyticsNoTrackPages')) {
776            if (preg_match_all($this->getConf('googleAnalyticsNoTrackPages'), $ID)) {
777                return false;
778            }
779        }
780
781        $out = DOKU_LF;
782        $out .= '// Google Analytics' . DOKU_LF;
783        $out .= 'window.dataLayer = window.dataLayer || [];' . DOKU_LF;
784        $out .= 'function gtag(){dataLayer.push(arguments);}' . DOKU_LF;
785        $out .= 'gtag("js", new Date());' . DOKU_LF;
786
787        if ($this->getConf('googleAnalyticsAnonymizeIP')) {
788            $out .= 'gtag("config", "' . $google_analitycs_id . '", {"anonimize_ip":true});' . DOKU_LF;
789        } else {
790            $out .= 'gtag("config", "' . $google_analitycs_id . '");' . DOKU_LF;
791        }
792
793        if ($this->getConf('googleAnalyticsTrackActions')) {
794            $out .= 'gtag("event", JSINFO.bootstrap3.mode, {"eventCategory":"DokuWiki"});' . DOKU_LF;
795        }
796
797        $out .= '// End Google Analytics' . DOKU_LF;
798
799        return $out;
800    }
801
802    /**
803     * Return the user home-page link
804     *
805     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
806     *
807     * @return  string
808     */
809    public function getUserHomePageLink()
810    {
811        return wl($this->getUserHomePageID());
812    }
813
814    /**
815     * Return the user home-page ID
816     *
817     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
818     *
819     * @return  string
820     */
821    public function getUserHomePageID()
822    {
823        $interwiki = getInterwiki();
824        $page_id   = str_replace('{NAME}', $_SERVER['REMOTE_USER'], $interwiki['user']);
825
826        return cleanID($page_id);
827    }
828
829    /**
830     * Print the breadcrumbs trace with Bootstrap style
831     *
832     * @author Andreas Gohr <andi@splitbrain.org>
833     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
834     *
835     * @return bool
836     */
837    public function getBreadcrumbs()
838    {
839        global $lang;
840        global $conf;
841
842        //check if enabled
843        if (!$conf['breadcrumbs']) {
844            return false;
845        }
846
847        $crumbs = breadcrumbs(); //setup crumb trace
848
849        //render crumbs, highlight the last one
850        print '<ol class="breadcrumb">';
851        print '<li>' . rtrim($lang['breadcrumb'], ':') . '</li>';
852
853        $last = count($crumbs);
854        $i    = 0;
855
856        foreach ($crumbs as $id => $name) {
857            $i++;
858
859            print($i == $last) ? '<li class="active">' : '<li>';
860            tpl_link(wl($id), hsc($name), 'title="' . $id . '"');
861            print '</li>';
862
863            if ($i == $last) {
864                print '</ol>';
865            }
866        }
867
868        return true;
869    }
870
871    /**
872     * Hierarchical breadcrumbs with Bootstrap style
873     *
874     * This code was suggested as replacement for the usual breadcrumbs.
875     * It only makes sense with a deep site structure.
876     *
877     * @author Andreas Gohr <andi@splitbrain.org>
878     * @author Nigel McNie <oracle.shinoda@gmail.com>
879     * @author Sean Coates <sean@caedmon.net>
880     * @author <fredrik@averpil.com>
881     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
882     * @todo   May behave strangely in RTL languages
883     *
884     * @return bool
885     */
886    public function getYouAreHere()
887    {
888        global $conf;
889        global $ID;
890        global $lang;
891
892        // check if enabled
893        if (!$conf['youarehere']) {
894            return false;
895        }
896
897        $parts = explode(':', $ID);
898        $count = count($parts);
899
900        echo '<ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">';
901        echo '<li>' . rtrim($lang['youarehere'], ':') . '</li>';
902
903        // always print the startpage
904        echo '<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
905
906        tpl_link(wl($conf['start']),
907            '<span itemprop="name">' . iconify('mdi:home') . '<span class="sr-only">Home</span></span>',
908            ' itemprop="item"  title="' . $conf['start'] . '"'
909        );
910
911        echo '<meta itemprop="position" content="1" />';
912        echo '</li>';
913
914        $position = 1;
915
916        // print intermediate namespace links
917        $part = '';
918
919        for ($i = 0; $i < $count - 1; $i++) {
920            $part .= $parts[$i] . ':';
921            $page = $part;
922
923            if ($page == $conf['start']) {
924                continue;
925            }
926            // Skip startpage
927
928            $position++;
929
930            // output
931            echo '<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
932
933            $link = html_wikilink($page);
934            $link = str_replace(['<span class="curid">', '</span>'], '', $link);
935            $link = str_replace('<a', '<a itemprop="item" ', $link);
936            $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link);
937            $link = str_replace('<a', '<span itemprop="name"><a', $link);
938            $link = str_replace('</a>', '</a></span>', $link);
939
940            echo $link;
941            echo '<meta itemprop="position" content="' . $position . '" />';
942            echo '</li>';
943        }
944
945        // print current page, skipping start page, skipping for namespace index
946
947        $page = $part . $parts[$i];
948
949        if ($page == $conf['start']) {
950            echo '</ol>';
951            return true;
952        }
953
954        echo '<li class="active" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
955
956        $link = str_replace(['<span class="curid">', '</span>'], '', html_wikilink($page));
957        $link = str_replace('<a ', '<a itemprop="item" ', $link);
958        $link = str_replace('<a', '<span itemprop="name"><a', $link);
959        $link = str_replace('</a>', '</a></span>', $link);
960        $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link);
961
962        echo $link;
963        echo '<meta itemprop="position" content="' . ++$position . '" />';
964        echo '</li>';
965        echo '</ol>';
966
967        return true;
968    }
969
970    /**
971     * Display the page title (and previous namespace page title) on browser titlebar
972     *
973     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
974     * @return string
975     */
976    public function getBrowserPageTitle()
977    {
978        global $conf, $ACT, $ID;
979
980        if ($this->getConf('browserTitleShowNS') && $ACT == 'show') {
981            $ns_page      = '';
982            $ns_parts     = explode(':', $ID);
983            $ns_pages     = [];
984            $ns_titles    = [];
985            $ns_separator = sprintf(' %s ', $this->getConf('browserTitleCharSepNS'));
986
987            if (useHeading('navigation')) {
988                if (count($ns_parts) > 1) {
989                    foreach ($ns_parts as $ns_part) {
990                        $ns_page .= "$ns_part:";
991                        $ns_pages[] = $ns_page;
992                    }
993
994                    $ns_pages = array_unique($ns_pages);
995
996                    foreach ($ns_pages as $ns_page) {
997
998                        $exists = false;
999
1000                        // Igor and later
1001                        if (class_exists('dokuwiki\File\PageResolver')) {
1002                            $resolver = new \dokuwiki\File\PageResolver($ns_page);
1003                            $ns_page = $resolver->resolveId($ns_page);
1004                            $exists = page_exists($ns_page);
1005                        } else {
1006                            // Compatibility with older releases
1007                            resolve_pageid(getNS($ns_page), $ns_page, $exists);
1008                        }
1009
1010                        $ns_page_title_heading = hsc(p_get_first_heading($ns_page));
1011                        $ns_page_title_page    = noNSorNS($ns_page);
1012                        $ns_page_title         = ($exists) ? $ns_page_title_heading : null;
1013
1014                        if ($ns_page_title !== $conf['start']) {
1015                            $ns_titles[] = $ns_page_title;
1016                        }
1017                    }
1018                }
1019
1020                $exists = false;
1021
1022                // Igor and later
1023                if (class_exists('dokuwiki\File\PageResolver')) {
1024                    $resolver = new \dokuwiki\File\PageResolver($ID);
1025                    $id = $resolver->resolveId($ID);
1026                    $exists = page_exists($id);
1027                } else {
1028                    // Compatibility with older releases
1029                    resolve_pageid(getNS($ID), $ID, $exists);
1030                }
1031
1032                if ($exists) {
1033                    $ns_titles[] = tpl_pagetitle($ID, true);
1034                } else {
1035                    $ns_titles[] = noNS($ID);
1036                }
1037
1038                $ns_titles = array_filter(array_unique($ns_titles));
1039            } else {
1040                $ns_titles = $ns_parts;
1041            }
1042
1043            if ($this->getConf('browserTitleOrderNS') == 'normal') {
1044                $ns_titles = array_reverse($ns_titles);
1045            }
1046
1047            $browser_title = implode($ns_separator, $ns_titles);
1048        } else {
1049            $browser_title = tpl_pagetitle($ID, true);
1050        }
1051
1052        return str_replace(
1053            ['@WIKI@', '@TITLE@'],
1054            [strip_tags($conf['title']), $browser_title],
1055            $this->getConf('browserTitle')
1056        );
1057    }
1058
1059    /**
1060     * Return the theme for current namespace
1061     *
1062     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1063     * @return string
1064     */
1065    public function getThemeForNamespace()
1066    {
1067        global $ID;
1068
1069        $themes_filename = DOKU_CONF . 'bootstrap3.themes.conf';
1070
1071        if (!$this->getConf('themeByNamespace')) {
1072            return [];
1073        }
1074
1075        if (!file_exists($themes_filename)) {
1076            return [];
1077        }
1078
1079        $config = confToHash($themes_filename);
1080        krsort($config);
1081
1082        foreach ($config as $page => $theme) {
1083            if (preg_match("/^$page/", "$ID")) {
1084                list($bootstrap, $bootswatch) = explode('/', $theme);
1085
1086                if ($bootstrap && in_array($bootstrap, ['default', 'optional', 'custom'])) {
1087                    return [$bootstrap, $bootswatch];
1088                }
1089
1090                if ($bootstrap == 'bootswatch' && in_array($bootswatch, $this->getBootswatchThemeList())) {
1091                    return [$bootstrap, $bootswatch];
1092                }
1093            }
1094        }
1095
1096        return [];
1097    }
1098
1099    /**
1100     * Make a Bootstrap3 Nav
1101     *
1102     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1103     *
1104     * @param   string   $html
1105     * @param   string   $type (= pills, tabs, navbar)
1106     * @param   boolean  $staked
1107     * @param   string   $optional_class
1108     * @return  string
1109     */
1110    public function toBootstrapNav($html, $type = '', $stacked = false, $optional_class = '')
1111    {
1112        $classes = [];
1113
1114        $classes[] = 'nav';
1115        $classes[] = $optional_class;
1116
1117        switch ($type) {
1118            case 'navbar':
1119            case 'navbar-nav':
1120                $classes[] = 'navbar-nav';
1121                break;
1122            case 'pills':
1123            case 'tabs':
1124                $classes[] = "nav-$type";
1125                break;
1126        }
1127
1128        if ($stacked) {
1129            $classes[] = 'nav-stacked';
1130        }
1131
1132        $class = implode(' ', $classes);
1133
1134        $output = str_replace(
1135            ['<ul class="', '<ul>'],
1136            ["<ul class=\"$class ", "<ul class=\"$class\">"],
1137            $html
1138        );
1139
1140        $output = $this->normalizeList($output);
1141
1142        return $output;
1143    }
1144
1145    /**
1146     * Normalize the DokuWiki list items
1147     *
1148     * @todo    use Simple DOM HTML library
1149     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1150     * @todo    use Simple DOM HTML
1151     * @todo    FIX SimpleNavi curid
1152     *
1153     * @param   string  $html
1154     * @return  string
1155     */
1156    public function normalizeList($list)
1157    {
1158
1159        global $ID;
1160
1161        $list = preg_replace_callback('/data-wiki-id="(.+?)"/', [$this, '_replaceWikiCurrentIdCallback'], $list);
1162
1163        $html = new \simple_html_dom;
1164        $html->load($list, true, false);
1165
1166        # Create data-curid HTML5 attribute and unwrap span.curid for pre-Hogfather release
1167        foreach ($html->find('span.curid') as $elm) {
1168            $elm->firstChild()->setAttribute('data-wiki-curid', 'true');
1169            $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext);
1170        }
1171
1172        # Unwrap div.li element
1173        foreach ($html->find('div.li') as $elm) {
1174            $elm->outertext = str_replace(['<div class="li">', '</div>'], '', $elm->outertext);
1175        }
1176
1177        $list = $html->save();
1178        $html->clear();
1179        unset($html);
1180
1181        $html = new \simple_html_dom;
1182        $html->load($list, true, false);
1183
1184        foreach ($html->find('li') as $elm) {
1185            if ($elm->find('a[data-wiki-curid]')) {
1186                $elm->class .= ' active';
1187            }
1188        }
1189
1190        $list = $html->save();
1191        $html->clear();
1192        unset($html);
1193
1194        # TODO optimize
1195        $list = preg_replace('/<i (.+?)><\/i> <a (.+?)>(.+?)<\/a>/', '<a $2><i $1></i> $3</a>', $list);
1196        $list = preg_replace('/<span (.+?)><\/span> <a (.+?)>(.+?)<\/a>/', '<a $2><span $1></span> $3</a>', $list);
1197
1198        return $list;
1199    }
1200
1201    /**
1202     * Remove data-wiki-id HTML5 attribute
1203     *
1204     * @todo Remove this in future
1205     * @since Hogfather
1206     *
1207     * @param array $matches
1208     *
1209     * @return string
1210     */
1211    private function _replaceWikiCurrentIdCallback($matches)
1212    {
1213
1214        global $ID;
1215
1216        if ($ID == $matches[1]) {
1217            return 'data-wiki-curid="true"';
1218        }
1219
1220        return '';
1221
1222    }
1223
1224    /**
1225     * Return a Bootstrap NavBar and or drop-down menu
1226     *
1227     * @todo    use Simple DOM HTML library
1228     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1229     *
1230     * @return  string
1231     */
1232    public function getNavbar()
1233    {
1234        global $INPUT;
1235
1236        if ($this->getConf('showNavbar') === 'logged' && !$INPUT->server->has('REMOTE_USER')) {
1237            return false;
1238        }
1239
1240        global $ID;
1241        global $conf;
1242
1243        $navbar = $this->toBootstrapNav(tpl_include_page('navbar', 0, 1, $this->getConf('useACL')), 'navbar');
1244
1245        $navbar = str_replace('urlextern', '', $navbar);
1246
1247        $navbar = preg_replace('/<li class="level([0-9]) node"> (.*)/',
1248            '<li class="level$1 node dropdown"><a href="#" class="dropdown-toggle" data-target="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">$2 <span class="caret"></span></a>', $navbar);
1249
1250        $navbar = preg_replace('/<li class="level([0-9]) node active"> (.*)/',
1251            '<li class="level$1 node active dropdown"><a href="#" class="dropdown-toggle" data-target="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">$2 <span class="caret"></span></a>', $navbar);
1252
1253        # FIX for Purplenumbers renderer plugin
1254        # TODO use Simple DOM HTML or improve the regex!
1255        if ($conf['renderer_xhtml'] == 'purplenumbers') {
1256            $navbar = preg_replace('/<li class="level1"> (.*)/',
1257                '<li class="level1 dropdown"><a href="#" class="dropdown-toggle" data-target="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">$1 <span class="caret"></span></a>', $navbar);
1258        }
1259
1260        $navbar = preg_replace('/<ul class="(.*)">\n<li class="level2(.*)">/',
1261            '<ul class="dropdown-menu" role="menu">' . PHP_EOL . '<li class="level2$2">', $navbar);
1262
1263        return $navbar;
1264    }
1265
1266    /**
1267     * Manipulate Sidebar page to add Bootstrap3 styling
1268     *
1269     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1270     *
1271     * @param   string   $sidebar
1272     * @param   boolean  $return
1273     * @return  string
1274     */
1275    public function normalizeSidebar($sidebar, $return = false)
1276    {
1277        $out = $this->toBootstrapNav($sidebar, 'pills', true);
1278        $out = $this->normalizeContent($out);
1279
1280        $html = new \simple_html_dom;
1281        $html->load($out, true, false);
1282
1283        # TODO 'page-header' will be removed in the next release of Bootstrap
1284        foreach ($html->find('h1, h2, h3, h4, h5, h6') as $elm) {
1285
1286            # Skip panel title on sidebar
1287            if (preg_match('/panel-title/', $elm->class)) {
1288                continue;
1289            }
1290
1291            $elm->class .= ' page-header';
1292        }
1293
1294        $out = $html->save();
1295        $html->clear();
1296        unset($html);
1297
1298        if ($return) {
1299            return $out;
1300        }
1301
1302        echo $out;
1303    }
1304
1305    /**
1306     * Return a drop-down page
1307     *
1308     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1309     *
1310     * @param   string  $page name
1311     * @return  string
1312     */
1313    public function getDropDownPage($page)
1314    {
1315
1316        $page = page_findnearest($page, $this->getConf('useACL'));
1317
1318        if (!$page) {
1319            return;
1320        }
1321
1322        $output   = $this->normalizeContent($this->toBootstrapNav(tpl_include_page($page, 0, 1, $this->getConf('useACL')), 'pills', true));
1323        $dropdown = '<ul class="nav navbar-nav dw__dropdown_page">' .
1324        '<li class="dropdown dropdown-large">' .
1325        '<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="">' .
1326        p_get_first_heading($page) .
1327            ' <span class="caret"></span></a>' .
1328            '<ul class="dropdown-menu dropdown-menu-large" role="menu">' .
1329            '<li><div class="container small">' .
1330            $output .
1331            '</div></li></ul></li></ul>';
1332
1333        return $dropdown;
1334    }
1335
1336    /**
1337     * Include left or right sidebar
1338     *
1339     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1340     *
1341     * @param   string  $type left or right sidebar
1342     * @return  boolean
1343     */
1344    public function includeSidebar($type)
1345    {
1346        global $conf;
1347
1348        $left_sidebar       = $conf['sidebar'];
1349        $right_sidebar      = $this->getConf('rightSidebar');
1350        $left_sidebar_grid  = $this->getConf('leftSidebarGrid');
1351        $right_sidebar_grid = $this->getConf('rightSidebarGrid');
1352
1353        if (!$this->getConf('showSidebar')) {
1354            return false;
1355        }
1356
1357        switch ($type) {
1358            case 'left':
1359
1360                if ($this->getConf('sidebarPosition') == 'left') {
1361                    $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter');
1362                }
1363
1364                return true;
1365
1366            case 'right':
1367
1368                if ($this->getConf('sidebarPosition') == 'right') {
1369                    $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter');
1370                }
1371
1372                if ($this->getConf('showRightSidebar')
1373                    && $this->getConf('sidebarPosition') == 'left') {
1374                    $this->sidebarWrapper($right_sidebar, 'dokuwiki__rightaside', $right_sidebar_grid, 'rightsidebarheader', 'rightsidebarfooter');
1375                }
1376
1377                return true;
1378        }
1379
1380        return false;
1381    }
1382
1383    /**
1384     * Wrapper for left or right sidebar
1385     *
1386     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1387     *
1388     * @param  string  $sidebar_page
1389     * @param  string  $sidebar_id
1390     * @param  string  $sidebar_header
1391     * @param  string  $sidebar_footer
1392     */
1393    private function sidebarWrapper($sidebar_page, $sidebar_id, $sidebar_class, $sidebar_header, $sidebar_footer)
1394    {
1395        global $lang;
1396        global $TPL;
1397
1398        @require $this->tplDir . 'tpl/sidebar.php';
1399    }
1400
1401    /**
1402     * Add Bootstrap classes in a DokuWiki content
1403     *
1404     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1405     *
1406     * @param   string  $content from tpl_content() or from tpl_include_page()
1407     * @return  string  with Bootstrap styles
1408     */
1409    public function normalizeContent($content)
1410    {
1411        global $ACT;
1412        global $INPUT;
1413        global $INFO;
1414
1415        # FIX :-\ smile
1416        $content = str_replace(['alt=":-\"', "alt=':-\'"], 'alt=":-&#92;"', $content);
1417
1418        # Workaround for ToDo Plugin
1419        $content = str_replace('checked="checked"', ' checked="checked"', $content);
1420
1421        # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB)
1422        if (strlen($content) > MAX_FILE_SIZE) {
1423            return $content;
1424        }
1425
1426        # Import HTML string
1427        $html = new \simple_html_dom;
1428        $html->load($content, true, false);
1429
1430        # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB)
1431        if (!$html) {
1432            return $content;
1433        }
1434
1435        # Move Current Page ID to <a> element and create data-curid HTML5 attribute (pre-Hogfather release)
1436        foreach ($html->find('.curid') as $elm) {
1437            foreach ($elm->find('a') as $link) {
1438                $link->class .= ' curid';
1439                $link->attr[' data-curid'] = 'true'; # FIX attribute
1440            }
1441        }
1442
1443        # Unwrap span.curid elements
1444        foreach ($html->find('span.curid') as $elm) {
1445            $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext);
1446        }
1447
1448        # Footnotes
1449        foreach ($html->find('.footnotes') as $elm) {
1450            $elm->outertext = '<hr/>' . $elm->outertext;
1451        }
1452
1453        # Accessibility (a11y)
1454        foreach ($html->find('.a11y') as $elm) {
1455            if (!preg_match('/picker/', $elm->class)) {
1456                $elm->class .= ' sr-only';
1457            }
1458        }
1459
1460        # Fix list overlap in media images
1461        foreach ($html->find('ul, ol') as $elm) {
1462            if (!preg_match('/(nav|dropdown-menu)/', $elm->class)) {
1463                $elm->class .= ' fix-media-list-overlap';
1464            }
1465        }
1466
1467        # Buttons
1468        foreach ($html->find('.button') as $elm) {
1469            if ($elm->tag !== 'form') {
1470                $elm->class .= ' btn';
1471            }
1472        }
1473
1474        foreach ($html->find('[type=button], [type=submit], [type=reset]') as $elm) {
1475            $elm->class .= ' btn btn-default';
1476        }
1477
1478        # Tabs
1479        foreach ($html->find('.tabs') as $elm) {
1480            $elm->class = 'nav nav-tabs';
1481        }
1482
1483        # Tabs (active)
1484        foreach ($html->find('.nav-tabs strong') as $elm) {
1485            $elm->outertext = '<a href="#">' . $elm->innertext . "</a>";
1486            $parent         = $elm->parent()->class .= ' active';
1487        }
1488
1489        # Page Heading (h1-h2)
1490        # TODO this class will be removed in Bootstrap >= 4.0 version
1491        foreach ($html->find('h1,h2,h3') as $elm) {
1492            $elm->class .= ' page-header pb-3 mb-4 mt-5'; # TODO replace page-header with border-bottom in BS4
1493        }
1494
1495        # Media Images
1496        foreach ($html->find('img[class^=media]') as $elm) {
1497            $elm->class .= ' img-responsive';
1498        }
1499
1500        # Checkbox
1501        foreach ($html->find('input[type=checkbox]') as $elm) {
1502            $elm->class .= ' checkbox-inline';
1503        }
1504
1505        # Radio button
1506        foreach ($html->find('input[type=radio]') as $elm) {
1507            $elm->class .= ' radio-inline';
1508        }
1509
1510        # Label
1511        foreach ($html->find('label') as $elm) {
1512            $elm->class .= ' control-label';
1513        }
1514
1515        # Form controls
1516        foreach ($html->find('input, select, textarea') as $elm) {
1517            if (!in_array($elm->type, ['submit', 'reset', 'button', 'hidden', 'image', 'checkbox', 'radio', 'color'])) {
1518                $elm->class .= ' form-control';
1519            }
1520        }
1521
1522        # Forms
1523        # TODO main form
1524        foreach ($html->find('form') as $elm) {
1525            if (!preg_match('/form-horizontal/', $elm->class)) {
1526                $elm->class .= ' form-inline';
1527            }
1528        }
1529
1530        # Alerts
1531        foreach ($html->find('div.info, div.error, div.success, div.notify') as $elm) {
1532            switch ($elm->class) {
1533                case 'info':
1534                    $elm->class     = 'alert alert-info';
1535                    $elm->innertext = iconify('mdi:information') . ' ' . $elm->innertext;
1536                    break;
1537
1538                case 'error':
1539                    $elm->class     = 'alert alert-danger';
1540                    $elm->innertext = iconify('mdi:alert-octagon') . ' ' . $elm->innertext;
1541                    break;
1542
1543                case 'success':
1544                    $elm->class     = 'alert alert-success';
1545                    $elm->innertext = iconify('mdi:check-circle') . ' ' . $elm->innertext;
1546                    break;
1547
1548                case 'notify':
1549                case 'msg notify':
1550                    $elm->class     = 'alert alert-warning';
1551                    $elm->innertext = iconify('mdi:alert') . ' ' . $elm->innertext;
1552                    break;
1553            }
1554        }
1555
1556        # Tables
1557
1558        $table_classes = 'table';
1559
1560        foreach ($this->getConf('tableStyle') as $class) {
1561            if ($class == 'responsive') {
1562                foreach ($html->find('div.table') as $elm) {
1563                    $elm->class = str_replace($elm->class, 'table', '');
1564                    $elm->class .= ' table-responsive';
1565                }
1566            } else {
1567                $table_classes .= " table-$class";
1568            }
1569        }
1570
1571        foreach ($html->find('table.inline,table.import_failures') as $elm) {
1572            $elm->class .= " $table_classes";
1573        }
1574
1575        foreach ($html->find('div.table') as $elm) {
1576            $elm->class = trim(str_replace('table', '', $elm->class));
1577        }
1578
1579        # Tag and Pagelist (table)
1580
1581        if ($this->getPlugin('tag') || $this->getPlugin('pagelist')) {
1582            foreach ($html->find('table.ul') as $elm) {
1583                $elm->class .= " $table_classes";
1584            }
1585        }
1586
1587        $content = $html->save();
1588
1589        $html->clear();
1590        unset($html);
1591
1592        # ----- Actions -----
1593
1594        # Search
1595
1596        if ($ACT == 'search') {
1597            # Import HTML string
1598            $html = new \simple_html_dom;
1599            $html->load($content, true, false);
1600
1601            foreach ($html->find('fieldset.search-form button[type="submit"]') as $elm) {
1602                $elm->class .= ' btn-primary';
1603                $elm->innertext = iconify('mdi:magnify', ['class' => 'mr-2']) . $elm->innertext;
1604            }
1605
1606            $content = $html->save();
1607
1608            $html->clear();
1609            unset($html);
1610        }
1611
1612        # Index / Sitemap
1613
1614        if ($ACT == 'index') {
1615            # Import HTML string
1616            $html = new \simple_html_dom;
1617            $html->load($content, true, false);
1618
1619            foreach ($html->find('.idx_dir') as $idx => $elm) {
1620                $parent = $elm->parent()->parent();
1621
1622                if (preg_match('/open/', $parent->class)) {
1623                    $elm->innertext = iconify('mdi:folder-open', ['class' => 'text-primary mr-2']) . $elm->innertext;
1624                }
1625
1626                if (preg_match('/closed/', $parent->class)) {
1627                    $elm->innertext = iconify('mdi:folder', ['class' => 'text-primary mr-2']) . $elm->innertext;
1628                }
1629            }
1630
1631            foreach ($html->find('.idx .wikilink1') as $elm) {
1632                $elm->innertext = iconify('mdi:file-document-outline', ['class' => 'text-muted mr-2']) . $elm->innertext;
1633            }
1634
1635            $content = $html->save();
1636
1637            $html->clear();
1638            unset($html);
1639        }
1640
1641        # Admin Pages
1642
1643        if ($ACT == 'admin') {
1644            # Import HTML string
1645            $html = new \simple_html_dom;
1646            $html->load($content, true, false);
1647
1648            // Set specific icon in Admin Page
1649            if ($INPUT->str('page')) {
1650                if ($admin_pagetitle = $html->find('h1.page-header', 0)) {
1651                    $admin_pagetitle->class .= ' ' . hsc($INPUT->str('page'));
1652                }
1653            }
1654
1655            # ACL
1656
1657            if ($INPUT->str('page') == 'acl') {
1658                foreach ($html->find('[name*=cmd[update]]') as $elm) {
1659                    $elm->class .= ' btn-success';
1660                    if ($elm->tag == 'button') {
1661                        $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1662                    }
1663                }
1664            }
1665
1666            # Popularity
1667
1668            if ($INPUT->str('page') == 'popularity') {
1669                foreach ($html->find('[type=submit]') as $elm) {
1670                    $elm->class .= ' btn-primary';
1671
1672                    if ($elm->tag == 'button') {
1673                        $elm->innertext = iconify('mdi:arrow-right') . ' ' . $elm->innertext;
1674                    }
1675                }
1676            }
1677
1678            # Revert Manager
1679
1680            if ($INPUT->str('page') == 'revert') {
1681                foreach ($html->find('[type=submit]') as $idx => $elm) {
1682                    if ($idx == 0) {
1683                        $elm->class .= ' btn-primary';
1684                        if ($elm->tag == 'button') {
1685                            $elm->innertext = iconify('mdi:magnify') . ' ' . $elm->innertext;
1686                        }
1687                    }
1688
1689                    if ($idx == 1) {
1690                        $elm->class .= ' btn-success';
1691                        if ($elm->tag == 'button') {
1692                            $elm->innertext = iconify('mdi:refresh') . ' ' . $elm->innertext;
1693                        }
1694                    }
1695                }
1696            }
1697
1698            # Config
1699
1700            if ($INPUT->str('page') == 'config') {
1701                foreach ($html->find('[type=submit]') as $elm) {
1702                    $elm->class .= ' btn-success';
1703                    if ($elm->tag == 'button') {
1704                        $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1705                    }
1706                }
1707
1708                foreach ($html->find('#config__manager') as $cm_elm) {
1709                    $save_button = '';
1710
1711                    foreach ($cm_elm->find('p') as $elm) {
1712                        $save_button    = '<div class="pull-right">' . $elm->outertext . '</div>';
1713                        $elm->outertext = '</div>' . $elm->outertext;
1714                    }
1715
1716                    foreach ($cm_elm->find('fieldset') as $elm) {
1717                        $elm->innertext .= $save_button;
1718                    }
1719                }
1720            }
1721
1722            # User Manager
1723
1724            if ($INPUT->str('page') == 'usermanager') {
1725                foreach ($html->find('.notes') as $elm) {
1726                    $elm->class = str_replace('notes', '', $elm->class);
1727                }
1728
1729                foreach ($html->find('h2') as $idx => $elm) {
1730                    switch ($idx) {
1731                        case 0:
1732                            $elm->innertext = iconify('mdi:account-multiple') . ' ' . $elm->innertext;
1733                            break;
1734                        case 1:
1735                            $elm->innertext = iconify('mdi:account-plus') . ' ' . $elm->innertext;
1736                            break;
1737                        case 2:
1738                            $elm->innertext = iconify('mdi:account-edit') . ' ' . $elm->innertext;
1739                            break;
1740                    }
1741                }
1742
1743                foreach ($html->find('.import_users h2') as $elm) {
1744                    $elm->innertext = iconify('mdi:account-multiple-plus') . ' ' . $elm->innertext;
1745                }
1746
1747                foreach ($html->find('button[name*=fn[delete]]') as $elm) {
1748                    $elm->class .= ' btn btn-danger';
1749                    $elm->innertext = iconify('mdi:account-minus') . ' ' . $elm->innertext;
1750                }
1751
1752                foreach ($html->find('button[name*=fn[add]]') as $elm) {
1753                    $elm->class .= ' btn btn-success';
1754                    $elm->innertext = iconify('mdi:plus') . ' ' . $elm->innertext;
1755                }
1756
1757                foreach ($html->find('button[name*=fn[modify]]') as $elm) {
1758                    $elm->class .= ' btn btn-success';
1759                    $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1760                }
1761
1762                foreach ($html->find('button[name*=fn[import]]') as $elm) {
1763                    $elm->class .= ' btn btn-primary';
1764                    $elm->innertext = iconify('mdi:upload') . ' ' . $elm->innertext;
1765                }
1766
1767                foreach ($html->find('button[name*=fn[export]]') as $elm) {
1768                    $elm->class .= ' btn btn-primary';
1769                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1770                }
1771
1772                foreach ($html->find('button[name*=fn[start]]') as $elm) {
1773                    $elm->class .= ' btn btn-default';
1774                    $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext;
1775                }
1776
1777                foreach ($html->find('button[name*=fn[prev]]') as $elm) {
1778                    $elm->class .= ' btn btn-default';
1779                    $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext;
1780                }
1781
1782                foreach ($html->find('button[name*=fn[next]]') as $elm) {
1783                    $elm->class .= ' btn btn-default';
1784                    $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext;
1785                }
1786
1787                foreach ($html->find('button[name*=fn[last]]') as $elm) {
1788                    $elm->class .= ' btn btn-default';
1789                    $elm->innertext = iconify('mdi:chevron-double-right') . ' ' . $elm->innertext;
1790                }
1791            }
1792
1793            # Extension Manager
1794
1795            if ($INPUT->str('page') == 'extension') {
1796                foreach ($html->find('.actions') as $elm) {
1797                    $elm->class .= ' pl-4 btn-group btn-group-xs';
1798                }
1799
1800                foreach ($html->find('.actions .uninstall') as $elm) {
1801                    $elm->class .= ' btn-danger';
1802                    $elm->innertext = iconify('mdi:delete') . ' ' . $elm->innertext;
1803                }
1804
1805                foreach ($html->find('.actions .enable') as $elm) {
1806                    $elm->class .= ' btn-success';
1807                    $elm->innertext = iconify('mdi:check') . ' ' . $elm->innertext;
1808                }
1809
1810                foreach ($html->find('.actions .disable') as $elm) {
1811                    $elm->class .= ' btn-warning';
1812                    $elm->innertext = iconify('mdi:block-helper') . ' ' . $elm->innertext;
1813                }
1814
1815                foreach ($html->find('.actions .install, .actions .update, .actions .reinstall') as $elm) {
1816                    $elm->class .= ' btn-primary';
1817                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1818                }
1819
1820                foreach ($html->find('form.install [type=submit]') as $elm) {
1821                    $elm->class .= ' btn btn-success';
1822                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1823                }
1824
1825                foreach ($html->find('form.search [type=submit]') as $elm) {
1826                    $elm->class .= ' btn btn-primary';
1827                    $elm->innertext = iconify('mdi:cloud-search') . ' ' . $elm->innertext;
1828                }
1829
1830                foreach ($html->find('.permerror') as $elm) {
1831                    $elm->class .= ' pull-left';
1832                }
1833            }
1834
1835            # Admin page
1836            if ($INPUT->str('page') == null) {
1837                foreach ($html->find('ul.admin_tasks, ul.admin_plugins') as $admin_task) {
1838                    $admin_task->class .= ' list-group';
1839
1840                    foreach ($admin_task->find('a') as $item) {
1841                        $item->class .= ' list-group-item';
1842                        $item->style = 'max-height: 50px'; # TODO remove
1843                    }
1844
1845                    foreach ($admin_task->find('.icon') as $item) {
1846                        if ($item->innertext) {
1847                            continue;
1848                        }
1849
1850                        $item->innertext = iconify('mdi:puzzle', ['class' => 'text-success']);
1851                    }
1852                }
1853
1854                foreach ($html->find('h2') as $elm) {
1855                    $elm->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $elm->innertext;
1856                }
1857
1858                foreach ($html->find('ul.admin_plugins') as $admin_plugins) {
1859                    $admin_plugins->class .= ' col-sm-4';
1860                    foreach ($admin_plugins->find('li') as $idx => $item) {
1861                        if ($idx > 0 && $idx % 5 == 0) {
1862                            $item->outertext = '</ul><ul class="' . $admin_plugins->class . '">' . $item->outertext;
1863                        }
1864                    }
1865                }
1866
1867                # DokuWiki logo
1868                if ($admin_version = $html->getElementById('admin__version')) {
1869                    $admin_version->innertext = '<div class="dokuwiki__version"><img src="' . DOKU_BASE . 'lib/tpl/dokuwiki/images/logo.png" class="p-2" alt="" width="32" height="32" /> ' . $admin_version->innertext . '</div>';
1870
1871                    $template_version = $this->getVersion();
1872
1873                    $admin_version->innertext .= '<div class="template__version"><img src="' . tpl_basedir() . 'images/bootstrap.png" class="p-2" height="32" width="32" alt="" />Template ' . $template_version . '</div>';
1874                }
1875            }
1876
1877            $content = $html->save();
1878
1879            $html->clear();
1880            unset($html);
1881
1882            # Configuration Manager Template Sections
1883            if ($INPUT->str('page') == 'config') {
1884                # Import HTML string
1885                $html = new \simple_html_dom;
1886                $html->load($content, true, false);
1887
1888                foreach ($html->find('fieldset[id^="plugin__"]') as $elm) {
1889
1890                    /** @var array $matches */
1891                    preg_match('/plugin_+(\w+[^_])_+plugin_settings_name/', $elm->id, $matches);
1892
1893                    $plugin_name = $matches[1];
1894
1895                    if ($extension = plugin_load('helper', 'extension_extension')) {
1896                        if ($extension->setExtension($plugin_name)) {
1897                            foreach ($elm->find('legend') as $legend) {
1898                                $legend->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $legend->innertext . ' <br/><h6>' . $extension->getDescription() . ' <a class="urlextern" href="' . $extension->getURL() . '" target="_blank">Docs</a></h6>';
1899                            }
1900                        }
1901                    } else {
1902                        foreach ($elm->find('legend') as $legend) {
1903                            $legend->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $legend->innertext;
1904                        }
1905                    }
1906                }
1907
1908                $dokuwiki_configs = [
1909                    '#_basic'          => 'mdi:settings',
1910                    '#_display'        => 'mdi:monitor',
1911                    '#_authentication' => 'mdi:shield-account',
1912                    '#_anti_spam'      => 'mdi:block-helper',
1913                    '#_editing'        => 'mdi:pencil',
1914                    '#_links'          => 'mdi:link-variant',
1915                    '#_media'          => 'mdi:folder-image',
1916                    '#_notifications'  => 'mdi:email',
1917                    '#_syndication'    => 'mdi:rss',
1918                    '#_advanced'       => 'mdi:palette-advanced',
1919                    '#_network'        => 'mdi:network',
1920                ];
1921
1922                foreach ($dokuwiki_configs as $selector => $icon) {
1923                    foreach ($html->find("$selector legend") as $elm) {
1924                        $elm->innertext = iconify($icon) . ' ' . $elm->innertext;
1925                    }
1926                }
1927
1928                $content = $html->save();
1929
1930                $html->clear();
1931                unset($html);
1932
1933                $admin_sections = [
1934                    // Section => [ Insert Before, Icon ]
1935                    'theme'            => ['bootstrapTheme', 'mdi:palette'],
1936                    'sidebar'          => ['sidebarPosition', 'mdi:page-layout-sidebar-left'],
1937                    'navbar'           => ['inverseNavbar', 'mdi:page-layout-header'],
1938                    'semantic'         => ['semantic', 'mdi:share-variant'],
1939                    'layout'           => ['fluidContainer', 'mdi:monitor'],
1940                    'toc'              => ['tocAffix', 'mdi:view-list'],
1941                    'discussion'       => ['showDiscussion', 'mdi:comment-text-multiple'],
1942                    'avatar'           => ['useAvatar', 'mdi:account'],
1943                    'cookie_law'       => ['showCookieLawBanner', 'mdi:scale-balance'],
1944                    'google_analytics' => ['useGoogleAnalytics', 'mdi:google'],
1945                    'browser_title'    => ['browserTitle', 'mdi:format-title'],
1946                    'page'             => ['showPageInfo', 'mdi:file'],
1947                ];
1948
1949                foreach ($admin_sections as $section => $items) {
1950                    $search = $items[0];
1951                    $icon   = $items[1];
1952
1953                    $content = preg_replace(
1954                        '/<span class="outkey">(tpl»bootstrap3»' . $search . ')<\/span>/',
1955                        '<h3 id="bootstrap3__' . $section . '" class="mt-5">' . iconify($icon) . ' ' . tpl_getLang("config_$section") . '</h3></td><td></td></tr><tr><td class="label"><span class="outkey">$1</span>',
1956                        $content
1957                    );
1958                }
1959            }
1960        }
1961
1962        # Difference and Draft
1963
1964        if ($ACT == 'diff' || $ACT == 'draft') {
1965            # Import HTML string
1966            $html = new \simple_html_dom;
1967            $html->load($content, true, false);
1968
1969            foreach ($html->find('.diff-lineheader') as $elm) {
1970                $elm->style = 'opacity: 0.5';
1971                $elm->class .= ' text-center px-3';
1972
1973                if ($elm->innertext == '+') {
1974                    $elm->class .= ' bg-success';
1975                }
1976                if ($elm->innertext == '-') {
1977                    $elm->class .= ' bg-danger';
1978                }
1979            }
1980
1981            foreach ($html->find('.diff_sidebyside .diff-deletedline, .diff_sidebyside .diff-addedline') as $elm) {
1982                $elm->class .= ' w-50';
1983            }
1984
1985            foreach ($html->find('.diff-deletedline') as $elm) {
1986                $elm->class .= ' bg-danger';
1987            }
1988
1989            foreach ($html->find('.diff-addedline') as $elm) {
1990                $elm->class .= ' bg-success';
1991            }
1992
1993            foreach ($html->find('.diffprevrev') as $elm) {
1994                $elm->class .= ' btn btn-default';
1995                $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext;
1996            }
1997
1998            foreach ($html->find('.diffnextrev') as $elm) {
1999                $elm->class .= ' btn btn-default';
2000                $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext;
2001            }
2002
2003            foreach ($html->find('.diffbothprevrev') as $elm) {
2004                $elm->class .= ' btn btn-default';
2005                $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext;
2006            }
2007
2008            foreach ($html->find('.minor') as $elm) {
2009                $elm->class .= ' text-muted';
2010            }
2011
2012            $content = $html->save();
2013
2014            $html->clear();
2015            unset($html);
2016        }
2017
2018        # Add icons for Extensions, Actions, etc.
2019
2020        $svg_icon      = null;
2021        $iconify_icon  = null;
2022        $iconify_attrs = ['class' => 'mr-2'];
2023
2024        if (!$INFO['exists'] && $ACT == 'show') {
2025            $iconify_icon           = 'mdi:alert';
2026            $iconify_attrs['style'] = 'color:orange';
2027        }
2028
2029        $menu_class = "\\dokuwiki\\Menu\\Item\\$ACT";
2030
2031        if (class_exists($menu_class, false)) {
2032            $menu_item = new $menu_class;
2033            $svg_icon  = $menu_item->getSvg();
2034        }
2035
2036        switch ($ACT) {
2037            case 'admin':
2038
2039                if (($plugin = plugin_load('admin', $INPUT->str('page'))) !== null) {
2040                    if (method_exists($plugin, 'getMenuIcon')) {
2041                        $svg_icon = $plugin->getMenuIcon();
2042
2043                        if (!file_exists($svg_icon)) {
2044                            $iconify_icon = 'mdi:puzzle';
2045                            $svg_icon     = null;
2046                        }
2047                    } else {
2048                        $iconify_icon = 'mdi:puzzle';
2049                        $svg_icon     = null;
2050                    }
2051                }
2052
2053                break;
2054
2055            case 'resendpwd':
2056                $iconify_icon = 'mdi:lock-reset';
2057                break;
2058
2059            case 'denied':
2060                $iconify_icon           = 'mdi:block-helper';
2061                $iconify_attrs['style'] = 'color:red';
2062                break;
2063
2064            case 'search':
2065                $iconify_icon = 'mdi:search-web';
2066                break;
2067
2068            case 'preview':
2069                $iconify_icon = 'mdi:file-eye';
2070                break;
2071
2072            case 'diff':
2073                $iconify_icon = 'mdi:file-compare';
2074                break;
2075
2076            case 'showtag':
2077                $iconify_icon = 'mdi:tag-multiple';
2078                break;
2079
2080            case 'draft':
2081                $iconify_icon = 'mdi:android-studio';
2082                break;
2083
2084        }
2085
2086        if ($svg_icon) {
2087            $svg_attrs = ['class' => 'iconify mr-2'];
2088
2089            if ($ACT == 'admin' && $INPUT->str('page') == 'extension') {
2090                $svg_attrs['style'] = 'fill: green;';
2091            }
2092
2093            $svg = SVG::icon($svg_icon, null, '1em', $svg_attrs);
2094
2095            # Import HTML string
2096            $html = new \simple_html_dom;
2097            $html->load($content, true, false);
2098
2099            foreach ($html->find('h1') as $elm) {
2100                $elm->innertext = $svg . ' ' . $elm->innertext;
2101                break;
2102            }
2103
2104            $content = $html->save();
2105
2106            $html->clear();
2107            unset($html);
2108        }
2109
2110        if ($iconify_icon) {
2111            # Import HTML string
2112            $html = new \simple_html_dom;
2113            $html->load($content, true, false);
2114
2115            foreach ($html->find('h1') as $elm) {
2116                $elm->innertext = iconify($iconify_icon, $iconify_attrs) . $elm->innertext;
2117                break;
2118            }
2119
2120            $content = $html->save();
2121
2122            $html->clear();
2123            unset($html);
2124        }
2125
2126        return $content;
2127    }
2128
2129    /**
2130     * Detect the fluid navbar flag
2131     *
2132     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2133     * @return boolean
2134     */
2135    public function isFluidNavbar()
2136    {
2137        $fluid_container  = $this->getConf('fluidContainer');
2138        $fixed_top_nabvar = $this->getConf('fixedTopNavbar');
2139
2140        return ($fluid_container || ($fluid_container && !$fixed_top_nabvar) || (!$fluid_container && !$fixed_top_nabvar));
2141    }
2142
2143    /**
2144     * Calculate automatically the grid size for main container
2145     *
2146     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2147     *
2148     * @return  string
2149     */
2150    public function getContainerGrid()
2151    {
2152        global $ID;
2153
2154        $result = '';
2155
2156        $grids = [
2157            'sm' => ['left' => 0, 'right' => 0],
2158            'md' => ['left' => 0, 'right' => 0],
2159        ];
2160
2161        $show_right_sidebar = $this->getConf('showRightSidebar');
2162        $show_left_sidebar  = $this->getConf('showSidebar');
2163        $fluid_container    = $this->getConf('fluidContainer');
2164
2165        if ($this->getConf('showLandingPage') && (bool) preg_match($this->getConf('landingPages'), $ID)) {
2166            $show_left_sidebar = false;
2167        }
2168
2169        if ($show_left_sidebar) {
2170            foreach (explode(' ', $this->getConf('leftSidebarGrid')) as $grid) {
2171                list($col, $media, $size) = explode('-', $grid);
2172                $grids[$media]['left']    = (int) $size;
2173            }
2174        }
2175
2176        if ($show_right_sidebar) {
2177            foreach (explode(' ', $this->getConf('rightSidebarGrid')) as $grid) {
2178                list($col, $media, $size) = explode('-', $grid);
2179                $grids[$media]['right']   = (int) $size;
2180            }
2181        }
2182
2183        foreach ($grids as $media => $position) {
2184            $left  = $position['left'];
2185            $right = $position['right'];
2186            $result .= sprintf('col-%s-%s ', $media, (12 - $left - $right));
2187        }
2188
2189        return $result;
2190    }
2191
2192    /**
2193     * Places the TOC where the function is called
2194     *
2195     * If you use this you most probably want to call tpl_content with
2196     * a false argument
2197     *
2198     * @author Andreas Gohr <andi@splitbrain.org>
2199     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2200     *
2201     * @param bool $return Should the TOC be returned instead to be printed?
2202     * @return string
2203     */
2204    public function getTOC($return = false)
2205    {
2206        global $TOC;
2207        global $ACT;
2208        global $ID;
2209        global $REV;
2210        global $INFO;
2211        global $conf;
2212        global $INPUT;
2213
2214        $toc = [];
2215
2216        if (is_array($TOC)) {
2217            // if a TOC was prepared in global scope, always use it
2218            $toc = $TOC;
2219        } elseif (($ACT == 'show' || substr($ACT, 0, 6) == 'export') && !$REV && $INFO['exists']) {
2220            // get TOC from metadata, render if neccessary
2221            $meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
2222            if (isset($meta['internal']['toc'])) {
2223                $tocok = $meta['internal']['toc'];
2224            } else {
2225                $tocok = true;
2226            }
2227            $toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
2228            if (!$tocok || !is_array($toc) || !$conf['tocminheads'] || count($toc) < $conf['tocminheads']) {
2229                $toc = [];
2230            }
2231        } elseif ($ACT == 'admin') {
2232            // try to load admin plugin TOC
2233            /** @var $plugin DokuWiki_Admin_Plugin */
2234            if ($plugin = plugin_getRequestAdminPlugin()) {
2235                $toc = $plugin->getTOC();
2236                $TOC = $toc; // avoid later rebuild
2237            }
2238        }
2239
2240        $toc_check     = end($toc);
2241        $toc_undefined = null;
2242
2243        if (isset($toc_check['link']) && !preg_match('/bootstrap/', $toc_check['link'])) {
2244            $toc_undefined = array_pop($toc);
2245        }
2246
2247        \dokuwiki\Extension\Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false);
2248
2249        if ($ACT == 'admin' && $INPUT->str('page') == 'config') {
2250            $bootstrap3_sections = [
2251                'theme', 'sidebar', 'navbar', 'semantic', 'layout', 'toc',
2252                'discussion', 'avatar', 'cookie_law', 'google_analytics',
2253                'browser_title', 'page',
2254            ];
2255
2256            foreach ($bootstrap3_sections as $id) {
2257                $toc[] = [
2258                    'link'  => "#bootstrap3__$id",
2259                    'title' => tpl_getLang("config_$id"),
2260                    'type'  => 'ul',
2261                    'level' => 3,
2262                ];
2263            }
2264        }
2265
2266        if ($toc_undefined) {
2267            $toc[] = $toc_undefined;
2268        }
2269
2270        $html = $this->renderTOC($toc);
2271
2272        if ($return) {
2273            return $html;
2274        }
2275
2276        echo $html;
2277        return '';
2278    }
2279
2280    /**
2281     * Return the TOC rendered to XHTML with Bootstrap3 style
2282     *
2283     * @author Andreas Gohr <andi@splitbrain.org>
2284     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2285     *
2286     * @param array $toc
2287     * @return string html
2288     */
2289    private function renderTOC($toc)
2290    {
2291        if (!count($toc)) {
2292            return '';
2293        }
2294
2295        global $lang;
2296
2297        $json_toc = [];
2298
2299        foreach ($toc as $item) {
2300            $json_toc[] = [
2301                'link'  => (isset($item['link']) ? $item['link'] : '#' . $item['hid']),
2302                'title' => $item['title'],
2303                'level' => $item['level'],
2304            ];
2305        }
2306
2307        $out = '';
2308        $out .= '<script>JSINFO.bootstrap3.toc = ' . json_encode($json_toc) . ';</script>' . DOKU_LF;
2309
2310        if ($this->getConf('tocLayout') !== 'navbar') {
2311            $out .= '<!-- TOC START -->' . DOKU_LF;
2312            $out .= '<div class="dw-toc hidden-print">' . DOKU_LF;
2313            $out .= '<nav id="dw__toc" role="navigation" class="toc-panel panel panel-default small">' . DOKU_LF;
2314            $out .= '<h6 data-toggle="collapse" data-target="#dw__toc .toc-body" title="' . $lang['toc'] . '" class="panel-heading toc-title">' . iconify('mdi:view-list') . ' ';
2315            $out .= '<span>' . $lang['toc'] . '</span>';
2316            $out .= ' <i class="caret"></i></h6>' . DOKU_LF;
2317            $out .= '<div class="panel-body  toc-body collapse ' . (!$this->getConf('tocCollapsed') ? 'in' : '') . '">' . DOKU_LF;
2318            $out .= $this->normalizeList(html_buildlist($toc, 'nav toc', 'html_list_toc', 'html_li_default', true)) . DOKU_LF;
2319            $out .= '</div>' . DOKU_LF;
2320            $out .= '</nav>' . DOKU_LF;
2321            $out .= '</div>' . DOKU_LF;
2322            $out .= '<!-- TOC END -->' . DOKU_LF;
2323        }
2324
2325        return $out;
2326    }
2327
2328    private function initToolsMenu()
2329    {
2330        global $ACT;
2331
2332        $tools_menus = [
2333            'user' => ['icon' => 'mdi:account', 'object' => new \dokuwiki\Menu\UserMenu],
2334            'site' => ['icon' => 'mdi:toolbox', 'object' => new \dokuwiki\Menu\SiteMenu],
2335            'page' => ['icon' => 'mdi:file-document-outline', 'object' => new \dokuwiki\template\bootstrap3\Menu\PageMenu],
2336        ];
2337
2338        if (defined('DOKU_MEDIADETAIL')) {
2339            $tools_menus['page'] = ['icon' => 'mdi:image', 'object' => new \dokuwiki\template\bootstrap3\Menu\DetailMenu];
2340        }
2341
2342        foreach ($tools_menus as $tool => $data) {
2343            foreach ($data['object']->getItems() as $item) {
2344                $attr   = buildAttributes($item->getLinkAttributes());
2345                $active = 'action';
2346
2347                if ($ACT == $item->getType() || ($ACT == 'revisions' && $item->getType() == 'revs') || ($ACT == 'diff' && $item->getType() == 'revs')) {
2348                    $active .= ' active';
2349                }
2350
2351                if ($item->getType() == 'shareon') {
2352                    $active .= ' dropdown';
2353                }
2354
2355                $html = '<li class="' . $active . '">';
2356                $html .= "<a $attr>";
2357                $html .= \inlineSVG($item->getSvg());
2358                $html .= '<span>' . hsc($item->getLabel()) . '</span>';
2359                $html .= "</a>";
2360
2361                if ($item->getType() == 'shareon') {
2362                    $html .= $item->getDropDownMenu();
2363                }
2364
2365                $html .= '</li>';
2366
2367                $tools_menus[$tool]['menu'][$item->getType()]['object'] = $item;
2368                $tools_menus[$tool]['menu'][$item->getType()]['html']   = $html;
2369            }
2370        }
2371
2372        $this->toolsMenu = $tools_menus;
2373    }
2374
2375    public function getToolsMenu()
2376    {
2377        return $this->toolsMenu;
2378    }
2379
2380    public function getToolMenu($tool)
2381    {
2382        return $this->toolsMenu[$tool];
2383    }
2384
2385    public function getToolMenuItem($tool, $item)
2386    {
2387        if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) {
2388            return $this->toolsMenu[$tool]['menu'][$item]['object'];
2389        }
2390        return null;
2391    }
2392
2393    public function getToolMenuItemLink($tool, $item)
2394    {
2395        if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) {
2396            return $this->toolsMenu[$tool]['menu'][$item]['html'];
2397        }
2398        return null;
2399    }
2400
2401    public function getNavbarHeight()
2402    {
2403        switch ($this->getBootswatchTheme()) {
2404            case 'simplex':
2405            case 'superhero':
2406                return 40;
2407
2408            case 'yeti':
2409                return 45;
2410
2411            case 'cerulean':
2412            case 'cosmo':
2413            case 'custom':
2414            case 'cyborg':
2415            case 'lumen':
2416            case 'slate':
2417            case 'spacelab':
2418            case 'solar':
2419            case 'united':
2420                return 50;
2421
2422            case 'darkly':
2423            case 'flatly':
2424            case 'journal':
2425            case 'sandstone':
2426                return 60;
2427
2428            case 'paper':
2429                return 64;
2430
2431            case 'readable':
2432                return 65;
2433
2434            default:
2435                return 50;
2436        }
2437    }
2438}
2439