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 .= "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
784(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
785m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
786})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');" . DOKU_LF;
787
788        $out .= 'ga("create", "' . $google_analitycs_id . '", "auto");' . DOKU_LF;
789        $out .= 'ga("send", "pageview");' . DOKU_LF;
790
791        if ($this->getConf('googleAnalyticsAnonymizeIP')) {
792            $out .= 'ga("set", "anonymizeIp", true);' . DOKU_LF;
793        }
794
795        if ($this->getConf('googleAnalyticsTrackActions')) {
796            $out .= 'ga("send", "event", "DokuWiki", JSINFO.bootstrap3.mode);' . DOKU_LF;
797        }
798
799        $out .= '// End Google Analytics' . DOKU_LF;
800
801        return $out;
802    }
803
804    /**
805     * Return the user home-page link
806     *
807     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
808     *
809     * @return  string
810     */
811    public function getUserHomePageLink()
812    {
813        return wl($this->getUserHomePageID());
814    }
815
816    /**
817     * Return the user home-page ID
818     *
819     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
820     *
821     * @return  string
822     */
823    public function getUserHomePageID()
824    {
825        $interwiki = getInterwiki();
826        $page_id   = str_replace('{NAME}', $_SERVER['REMOTE_USER'], $interwiki['user']);
827
828        return cleanID($page_id);
829    }
830
831    /**
832     * Print the breadcrumbs trace with Bootstrap style
833     *
834     * @author Andreas Gohr <andi@splitbrain.org>
835     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
836     *
837     * @return bool
838     */
839    public function getBreadcrumbs()
840    {
841        global $lang;
842        global $conf;
843
844        //check if enabled
845        if (!$conf['breadcrumbs']) {
846            return false;
847        }
848
849        $crumbs = breadcrumbs(); //setup crumb trace
850
851        //render crumbs, highlight the last one
852        print '<ol class="breadcrumb">';
853        print '<li>' . rtrim($lang['breadcrumb'], ':') . '</li>';
854
855        $last = count($crumbs);
856        $i    = 0;
857
858        foreach ($crumbs as $id => $name) {
859            $i++;
860
861            print($i == $last) ? '<li class="active">' : '<li>';
862            tpl_link(wl($id), hsc($name), 'title="' . $id . '"');
863            print '</li>';
864
865            if ($i == $last) {
866                print '</ol>';
867            }
868        }
869
870        return true;
871    }
872
873    /**
874     * Hierarchical breadcrumbs with Bootstrap style
875     *
876     * This code was suggested as replacement for the usual breadcrumbs.
877     * It only makes sense with a deep site structure.
878     *
879     * @author Andreas Gohr <andi@splitbrain.org>
880     * @author Nigel McNie <oracle.shinoda@gmail.com>
881     * @author Sean Coates <sean@caedmon.net>
882     * @author <fredrik@averpil.com>
883     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
884     * @todo   May behave strangely in RTL languages
885     *
886     * @return bool
887     */
888    public function getYouAreHere()
889    {
890        global $conf;
891        global $ID;
892        global $lang;
893
894        // check if enabled
895        if (!$conf['youarehere']) {
896            return false;
897        }
898
899        $parts = explode(':', $ID);
900        $count = count($parts);
901
902        echo '<ol class="breadcrumb" itemscope itemtype="http://schema.org/BreadcrumbList">';
903        echo '<li>' . rtrim($lang['youarehere'], ':') . '</li>';
904
905        // always print the startpage
906        echo '<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
907
908        tpl_link(wl($conf['start']),
909            '<span itemprop="name">' . iconify('mdi:home') . '<span class="sr-only">Home</span></span>',
910            ' itemprop="item"  title="' . $conf['start'] . '"'
911        );
912
913        echo '<meta itemprop="position" content="1" />';
914        echo '</li>';
915
916        $position = 1;
917
918        // print intermediate namespace links
919        $part = '';
920
921        for ($i = 0; $i < $count - 1; $i++) {
922            $part .= $parts[$i] . ':';
923            $page = $part;
924
925            if ($page == $conf['start']) {
926                continue;
927            }
928            // Skip startpage
929
930            $position++;
931
932            // output
933            echo '<li itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
934
935            $link = html_wikilink($page);
936            $link = str_replace(['<span class="curid">', '</span>'], '', $link);
937            $link = str_replace('<a', '<a itemprop="item" ', $link);
938            $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link);
939            $link = str_replace('<a', '<span itemprop="name"><a', $link);
940            $link = str_replace('</a>', '</a></span>', $link);
941
942            echo $link;
943            echo '<meta itemprop="position" content="' . $position . '" />';
944            echo '</li>';
945        }
946
947        // print current page, skipping start page, skipping for namespace index
948        $exists = false;
949        resolve_pageid('', $page, $exists);
950
951        if (isset($page) && $page == $part . $parts[$i]) {
952            echo '</ol>';
953            return true;
954        }
955
956        $page = $part . $parts[$i];
957
958        if ($page == $conf['start']) {
959            echo '</ol>';
960            return true;
961        }
962
963        echo '<li class="active" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">';
964
965        $link = str_replace(['<span class="curid">', '</span>'], '', html_wikilink($page));
966        $link = str_replace('<a ', '<a itemprop="item" ', $link);
967        $link = str_replace('<a', '<span itemprop="name"><a', $link);
968        $link = str_replace('</a>', '</a></span>', $link);
969        $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link);
970
971        echo $link;
972        echo '<meta itemprop="position" content="' . ++$position . '" />';
973        echo '</li>';
974        echo '</ol>';
975
976        return true;
977    }
978
979    /**
980     * Display the page title (and previous namespace page title) on browser titlebar
981     *
982     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
983     * @return string
984     */
985    public function getBrowserPageTitle()
986    {
987        global $conf, $ACT, $ID;
988
989        if ($this->getConf('browserTitleShowNS') && $ACT == 'show') {
990            $ns_page      = '';
991            $ns_parts     = explode(':', $ID);
992            $ns_pages     = [];
993            $ns_titles    = [];
994            $ns_separator = sprintf(' %s ', $this->getConf('browserTitleCharSepNS'));
995
996            if (useHeading('navigation')) {
997                if (count($ns_parts) > 1) {
998                    foreach ($ns_parts as $ns_part) {
999                        $ns_page .= "$ns_part:";
1000                        $ns_pages[] = $ns_page;
1001                    }
1002
1003                    $ns_pages = array_unique($ns_pages);
1004
1005                    foreach ($ns_pages as $ns_page) {
1006                        $exists = false;
1007                        resolve_pageid(getNS($ns_page), $ns_page, $exists);
1008
1009                        $ns_page_title_heading = hsc(p_get_first_heading($ns_page));
1010                        $ns_page_title_page    = noNSorNS($ns_page);
1011                        $ns_page_title         = ($exists) ? $ns_page_title_heading : null;
1012
1013                        if ($ns_page_title !== $conf['start']) {
1014                            $ns_titles[] = $ns_page_title;
1015                        }
1016                    }
1017                }
1018
1019                resolve_pageid(getNS($ID), $ID, $exists);
1020
1021                if ($exists) {
1022                    $ns_titles[] = tpl_pagetitle($ID, true);
1023                } else {
1024                    $ns_titles[] = noNS($ID);
1025                }
1026
1027                $ns_titles = array_filter(array_unique($ns_titles));
1028            } else {
1029                $ns_titles = $ns_parts;
1030            }
1031
1032            if ($this->getConf('browserTitleOrderNS') == 'normal') {
1033                $ns_titles = array_reverse($ns_titles);
1034            }
1035
1036            $browser_title = implode($ns_separator, $ns_titles);
1037        } else {
1038            $browser_title = tpl_pagetitle($ID, true);
1039        }
1040
1041        return str_replace(
1042            ['@WIKI@', '@TITLE@'],
1043            [strip_tags($conf['title']), $browser_title],
1044            $this->getConf('browserTitle')
1045        );
1046    }
1047
1048    /**
1049     * Return the theme for current namespace
1050     *
1051     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1052     * @return string
1053     */
1054    public function getThemeForNamespace()
1055    {
1056        global $ID;
1057
1058        $themes_filename = DOKU_CONF . 'bootstrap3.themes.conf';
1059
1060        if (!$this->getConf('themeByNamespace')) {
1061            return [];
1062        }
1063
1064        if (!file_exists($themes_filename)) {
1065            return [];
1066        }
1067
1068        $config = confToHash($themes_filename);
1069        krsort($config);
1070
1071        foreach ($config as $page => $theme) {
1072            if (preg_match("/^$page/", "$ID")) {
1073                list($bootstrap, $bootswatch) = explode('/', $theme);
1074
1075                if ($bootstrap && in_array($bootstrap, ['default', 'optional', 'custom'])) {
1076                    return [$bootstrap, $bootswatch];
1077                }
1078
1079                if ($bootstrap == 'bootswatch' && in_array($bootswatch, $this->getBootswatchThemeList())) {
1080                    return [$bootstrap, $bootswatch];
1081                }
1082            }
1083        }
1084
1085        return [];
1086    }
1087
1088    /**
1089     * Make a Bootstrap3 Nav
1090     *
1091     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1092     *
1093     * @param   string   $html
1094     * @param   string   $type (= pills, tabs, navbar)
1095     * @param   boolean  $staked
1096     * @param   string   $optional_class
1097     * @return  string
1098     */
1099    public function toBootstrapNav($html, $type = '', $stacked = false, $optional_class = '')
1100    {
1101        $classes = [];
1102
1103        $classes[] = 'nav';
1104        $classes[] = $optional_class;
1105
1106        switch ($type) {
1107            case 'navbar':
1108            case 'navbar-nav':
1109                $classes[] = 'navbar-nav';
1110                break;
1111            case 'pills':
1112            case 'tabs':
1113                $classes[] = "nav-$type";
1114                break;
1115        }
1116
1117        if ($stacked) {
1118            $classes[] = 'nav-stacked';
1119        }
1120
1121        $class = implode(' ', $classes);
1122
1123        $output = str_replace(
1124            ['<ul class="', '<ul>'],
1125            ["<ul class=\"$class ", "<ul class=\"$class\">"],
1126            $html
1127        );
1128
1129        $output = $this->normalizeList($output);
1130
1131        return $output;
1132    }
1133
1134    /**
1135     * Normalize the DokuWiki list items
1136     *
1137     * @todo    use Simple DOM HTML library
1138     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1139     * @todo    use Simple DOM HTML
1140     * @todo    FIX SimpleNavi curid
1141     *
1142     * @param   string  $html
1143     * @return  string
1144     */
1145    public function normalizeList($list)
1146    {
1147
1148        global $ID;
1149
1150        $list = preg_replace_callback('/data-wiki-id="(.+?)"/', [$this, '_replaceWikiCurrentIdCallback'], $list);
1151
1152        $html = new \simple_html_dom;
1153        $html->load($list, true, false);
1154
1155        # Create data-curid HTML5 attribute and unwrap span.curid for pre-Hogfather release
1156        foreach ($html->find('span.curid') as $elm) {
1157            $elm->firstChild()->setAttribute('data-wiki-curid', 'true');
1158            $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext);
1159        }
1160
1161        # Unwrap div.li element
1162        foreach ($html->find('div.li') as $elm) {
1163            $elm->outertext = str_replace(['<div class="li">', '</div>'], '', $elm->outertext);
1164        }
1165
1166        $list = $html->save();
1167        $html->clear();
1168        unset($html);
1169
1170        $html = new \simple_html_dom;
1171        $html->load($list, true, false);
1172
1173        foreach ($html->find('li') as $elm) {
1174            if ($elm->find('a[data-wiki-curid]')) {
1175                $elm->class .= ' active';
1176            }
1177        }
1178
1179        $list = $html->save();
1180        $html->clear();
1181        unset($html);
1182
1183        # TODO optimize
1184        $list = preg_replace('/<i (.+?)><\/i> <a (.+?)>(.+?)<\/a>/', '<a $2><i $1></i> $3</a>', $list);
1185        $list = preg_replace('/<span (.+?)><\/span> <a (.+?)>(.+?)<\/a>/', '<a $2><span $1></span> $3</a>', $list);
1186
1187        return $list;
1188    }
1189
1190    /**
1191     * Remove data-wiki-id HTML5 attribute
1192     *
1193     * @todo Remove this in future
1194     * @since Hogfather
1195     *
1196     * @param array $matches
1197     *
1198     * @return string
1199     */
1200    private function _replaceWikiCurrentIdCallback($matches)
1201    {
1202
1203        global $ID;
1204
1205        if ($ID == $matches[1]) {
1206            return 'data-wiki-curid="true"';
1207        }
1208
1209        return '';
1210
1211    }
1212
1213    /**
1214     * Return a Bootstrap NavBar and or drop-down menu
1215     *
1216     * @todo    use Simple DOM HTML library
1217     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1218     *
1219     * @return  string
1220     */
1221    public function getNavbar()
1222    {
1223        if ($this->getConf('showNavbar') === 'logged' && !$_SERVER['REMOTE_USER']) {
1224            return false;
1225        }
1226
1227        global $ID;
1228        global $conf;
1229
1230        $navbar = $this->toBootstrapNav(tpl_include_page('navbar', 0, 1, $this->getConf('useACL')), 'navbar');
1231
1232        $navbar = str_replace('urlextern', '', $navbar);
1233
1234        $navbar = preg_replace('/<li class="level([0-9]) node"> (.*)/',
1235            '<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);
1236
1237        $navbar = preg_replace('/<li class="level([0-9]) node active"> (.*)/',
1238            '<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);
1239
1240        # FIX for Purplenumbers renderer plugin
1241        # TODO use Simple DOM HTML or improve the regex!
1242        if ($conf['renderer_xhtml'] == 'purplenumbers') {
1243            $navbar = preg_replace('/<li class="level1"> (.*)/',
1244                '<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);
1245        }
1246
1247        $navbar = preg_replace('/<ul class="(.*)">\n<li class="level2(.*)">/',
1248            '<ul class="dropdown-menu" role="menu">' . PHP_EOL . '<li class="level2$2">', $navbar);
1249
1250        return $navbar;
1251    }
1252
1253    /**
1254     * Manipulate Sidebar page to add Bootstrap3 styling
1255     *
1256     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1257     *
1258     * @param   string   $sidebar
1259     * @param   boolean  $return
1260     * @return  string
1261     */
1262    public function normalizeSidebar($sidebar, $return = false)
1263    {
1264        $out = $this->toBootstrapNav($sidebar, 'pills', true);
1265        $out = $this->normalizeContent($out);
1266
1267        $html = new \simple_html_dom;
1268        $html->load($out, true, false);
1269
1270        # TODO 'page-header' will be removed in the next release of Bootstrap
1271        foreach ($html->find('h1, h2, h3, h4, h5, h6') as $elm) {
1272
1273            # Skip panel title on sidebar
1274            if (preg_match('/panel-title/', $elm->class)) {
1275                continue;
1276            }
1277
1278            $elm->class .= ' page-header';
1279        }
1280
1281        $out = $html->save();
1282        $html->clear();
1283        unset($html);
1284
1285        if ($return) {
1286            return $out;
1287        }
1288
1289        echo $out;
1290    }
1291
1292    /**
1293     * Return a drop-down page
1294     *
1295     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1296     *
1297     * @param   string  $page name
1298     * @return  string
1299     */
1300    public function getDropDownPage($page)
1301    {
1302
1303        $page = page_findnearest($page, $this->getConf('useACL'));
1304
1305        if (!$page) {
1306            return;
1307        }
1308
1309        $output   = $this->normalizeContent($this->toBootstrapNav(tpl_include_page($page, 0, 1, $this->getConf('useACL')), 'pills', true));
1310        $dropdown = '<ul class="nav navbar-nav dw__dropdown_page">' .
1311        '<li class="dropdown dropdown-large">' .
1312        '<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="">' .
1313        p_get_first_heading($page) .
1314            ' <span class="caret"></span></a>' .
1315            '<ul class="dropdown-menu dropdown-menu-large" role="menu">' .
1316            '<li><div class="container small">' .
1317            $output .
1318            '</div></li></ul></li></ul>';
1319
1320        return $dropdown;
1321    }
1322
1323    /**
1324     * Include left or right sidebar
1325     *
1326     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1327     *
1328     * @param   string  $type left or right sidebar
1329     * @return  boolean
1330     */
1331    public function includeSidebar($type)
1332    {
1333        global $conf;
1334
1335        $left_sidebar       = $conf['sidebar'];
1336        $right_sidebar      = $this->getConf('rightSidebar');
1337        $left_sidebar_grid  = $this->getConf('leftSidebarGrid');
1338        $right_sidebar_grid = $this->getConf('rightSidebarGrid');
1339
1340        if (!$this->getConf('showSidebar')) {
1341            return false;
1342        }
1343
1344        switch ($type) {
1345            case 'left':
1346
1347                if ($this->getConf('sidebarPosition') == 'left') {
1348                    $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter');
1349                }
1350
1351                return true;
1352
1353            case 'right':
1354
1355                if ($this->getConf('sidebarPosition') == 'right') {
1356                    $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter');
1357                }
1358
1359                if ($this->getConf('showRightSidebar')
1360                    && $this->getConf('sidebarPosition') == 'left') {
1361                    $this->sidebarWrapper($right_sidebar, 'dokuwiki__rightaside', $right_sidebar_grid, 'rightsidebarheader', 'rightsidebarfooter');
1362                }
1363
1364                return true;
1365        }
1366
1367        return false;
1368    }
1369
1370    /**
1371     * Wrapper for left or right sidebar
1372     *
1373     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1374     *
1375     * @param  string  $sidebar_page
1376     * @param  string  $sidebar_id
1377     * @param  string  $sidebar_header
1378     * @param  string  $sidebar_footer
1379     */
1380    private function sidebarWrapper($sidebar_page, $sidebar_id, $sidebar_class, $sidebar_header, $sidebar_footer)
1381    {
1382        global $lang;
1383        global $TPL;
1384
1385        @require $this->tplDir . 'tpl/sidebar.php';
1386    }
1387
1388    /**
1389     * Add Bootstrap classes in a DokuWiki content
1390     *
1391     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
1392     *
1393     * @param   string  $content from tpl_content() or from tpl_include_page()
1394     * @return  string  with Bootstrap styles
1395     */
1396    public function normalizeContent($content)
1397    {
1398        global $ACT;
1399        global $INPUT;
1400        global $INFO;
1401
1402        # FIX :-\ smile
1403        $content = str_replace(['alt=":-\"', "alt=':-\'"], 'alt=":-&#92;"', $content);
1404
1405        # Workaround for ToDo Plugin
1406        $content = str_replace('checked="checked"', ' checked="checked"', $content);
1407
1408        # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB)
1409        if (strlen($content) > MAX_FILE_SIZE) {
1410            return $content;
1411        }
1412
1413        # Import HTML string
1414        $html = new \simple_html_dom;
1415        $html->load($content, true, false);
1416
1417        # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB)
1418        if (!$html) {
1419            return $content;
1420        }
1421
1422        # Move Current Page ID to <a> element and create data-curid HTML5 attribute (pre-Hogfather release)
1423        foreach ($html->find('.curid') as $elm) {
1424            foreach ($elm->find('a') as $link) {
1425                $link->class .= ' curid';
1426                $link->attr[' data-curid'] = 'true'; # FIX attribute
1427            }
1428        }
1429
1430        # Unwrap span.curid elements
1431        foreach ($html->find('span.curid') as $elm) {
1432            $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext);
1433        }
1434
1435        # Footnotes
1436        foreach ($html->find('.footnotes') as $elm) {
1437            $elm->outertext = '<hr/>' . $elm->outertext;
1438        }
1439
1440        # Accessibility (a11y)
1441        foreach ($html->find('.a11y') as $elm) {
1442            if (!preg_match('/picker/', $elm->class)) {
1443                $elm->class .= ' sr-only';
1444            }
1445        }
1446
1447        # Fix list overlap in media images
1448        foreach ($html->find('ul, ol') as $elm) {
1449            if (!preg_match('/(nav|dropdown-menu)/', $elm->class)) {
1450                $elm->class .= ' fix-media-list-overlap';
1451            }
1452        }
1453
1454        # Buttons
1455        foreach ($html->find('.button') as $elm) {
1456            if ($elm->tag !== 'form') {
1457                $elm->class .= ' btn';
1458            }
1459        }
1460
1461        foreach ($html->find('[type=button], [type=submit], [type=reset]') as $elm) {
1462            $elm->class .= ' btn btn-default';
1463        }
1464
1465        # Tabs
1466        foreach ($html->find('.tabs') as $elm) {
1467            $elm->class = 'nav nav-tabs';
1468        }
1469
1470        # Tabs (active)
1471        foreach ($html->find('.nav-tabs strong') as $elm) {
1472            $elm->outertext = '<a href="#">' . $elm->innertext . "</a>";
1473            $parent         = $elm->parent()->class .= ' active';
1474        }
1475
1476        # Page Heading (h1-h2)
1477        # TODO this class will be removed in Bootstrap >= 4.0 version
1478        foreach ($html->find('h1,h2,h3') as $elm) {
1479            $elm->class .= ' page-header pb-3 mb-4 mt-0'; # TODO replace page-header with border-bottom in BS4
1480        }
1481
1482        # Media Images
1483        foreach ($html->find('img[class^=media]') as $elm) {
1484            $elm->class .= ' img-responsive';
1485        }
1486
1487        # Checkbox
1488        foreach ($html->find('input[type=checkbox]') as $elm) {
1489            $elm->class .= ' checkbox-inline';
1490        }
1491
1492        # Radio button
1493        foreach ($html->find('input[type=radio]') as $elm) {
1494            $elm->class .= ' radio-inline';
1495        }
1496
1497        # Label
1498        foreach ($html->find('label') as $elm) {
1499            $elm->class .= ' control-label';
1500        }
1501
1502        # Form controls
1503        foreach ($html->find('input, select, textarea') as $elm) {
1504            if (!in_array($elm->type, ['submit', 'reset', 'button', 'hidden', 'image', 'checkbox', 'radio'])) {
1505                $elm->class .= ' form-control';
1506            }
1507        }
1508
1509        # Forms
1510        # TODO main form
1511        foreach ($html->find('form') as $elm) {
1512            if (!preg_match('/form-horizontal/', $elm->class)) {
1513                $elm->class .= ' form-inline';
1514            }
1515        }
1516
1517        # Alerts
1518        foreach ($html->find('div.info, div.error, div.success, div.notify') as $elm) {
1519            switch ($elm->class) {
1520                case 'info':
1521                    $elm->class     = 'alert alert-info';
1522                    $elm->innertext = iconify('mdi:information') . ' ' . $elm->innertext;
1523                    break;
1524
1525                case 'error':
1526                    $elm->class     = 'alert alert-danger';
1527                    $elm->innertext = iconify('mdi:alert-octagon') . ' ' . $elm->innertext;
1528                    break;
1529
1530                case 'success':
1531                    $elm->class     = 'alert alert-success';
1532                    $elm->innertext = iconify('mdi:check-circle') . ' ' . $elm->innertext;
1533                    break;
1534
1535                case 'notify':
1536                case 'msg notify':
1537                    $elm->class     = 'alert alert-warning';
1538                    $elm->innertext = iconify('mdi:alert') . ' ' . $elm->innertext;
1539                    break;
1540            }
1541        }
1542
1543        # Tables
1544
1545        $table_classes = 'table';
1546
1547        foreach ($this->getConf('tableStyle') as $class) {
1548            if ($class == 'responsive') {
1549                foreach ($html->find('div.table') as $elm) {
1550                    $elm->class = 'table-responsive';
1551                }
1552            } else {
1553                $table_classes .= " table-$class";
1554            }
1555        }
1556
1557        foreach ($html->find('table.inline,table.import_failures') as $elm) {
1558            $elm->class .= " $table_classes";
1559        }
1560
1561        foreach ($html->find('div.table') as $elm) {
1562            $elm->class = trim(str_replace('table', '', $elm->class));
1563        }
1564
1565        # Tag and Pagelist (table)
1566
1567        if ($this->getPlugin('tag') || $this->getPlugin('pagelist')) {
1568            foreach ($html->find('table.ul') as $elm) {
1569                $elm->class .= " $table_classes";
1570            }
1571        }
1572
1573        $content = $html->save();
1574
1575        $html->clear();
1576        unset($html);
1577
1578        # ----- Actions -----
1579
1580        # Search
1581
1582        if ($ACT == 'search') {
1583            # Import HTML string
1584            $html = new \simple_html_dom;
1585            $html->load($content, true, false);
1586
1587            foreach ($html->find('fieldset.search-form button[type="submit"]') as $elm) {
1588                $elm->class .= ' btn-primary';
1589                $elm->innertext = iconify('mdi:magnify', ['class' => 'mr-2']) . $elm->innertext;
1590            }
1591
1592            $content = $html->save();
1593
1594            $html->clear();
1595            unset($html);
1596        }
1597
1598        # Index / Sitemap
1599
1600        if ($ACT == 'index') {
1601            # Import HTML string
1602            $html = new \simple_html_dom;
1603            $html->load($content, true, false);
1604
1605            foreach ($html->find('.idx_dir') as $idx => $elm) {
1606                $parent = $elm->parent()->parent();
1607
1608                if (preg_match('/open/', $parent->class)) {
1609                    $elm->innertext = iconify('mdi:folder-open', ['class' => 'text-primary mr-2']) . $elm->innertext;
1610                }
1611
1612                if (preg_match('/closed/', $parent->class)) {
1613                    $elm->innertext = iconify('mdi:folder', ['class' => 'text-primary mr-2']) . $elm->innertext;
1614                }
1615            }
1616
1617            foreach ($html->find('.idx .wikilink1') as $elm) {
1618                $elm->innertext = iconify('mdi:file-document-outline', ['class' => 'text-muted mr-2']) . $elm->innertext;
1619            }
1620
1621            $content = $html->save();
1622
1623            $html->clear();
1624            unset($html);
1625        }
1626
1627        # Admin Pages
1628
1629        if ($ACT == 'admin') {
1630            # Import HTML string
1631            $html = new \simple_html_dom;
1632            $html->load($content, true, false);
1633
1634            // Set specific icon in Admin Page
1635            if ($INPUT->str('page')) {
1636                if ($admin_pagetitle = $html->find('h1.page-header', 0)) {
1637                    $admin_pagetitle->class .= ' ' . hsc($INPUT->str('page'));
1638                }
1639            }
1640
1641            # ACL
1642
1643            if ($INPUT->str('page') == 'acl') {
1644                foreach ($html->find('[name*=cmd[update]]') as $elm) {
1645                    $elm->class .= ' btn-success';
1646                    if ($elm->tag == 'button') {
1647                        $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1648                    }
1649                }
1650            }
1651
1652            # Popularity
1653
1654            if ($INPUT->str('page') == 'popularity') {
1655                foreach ($html->find('[type=submit]') as $elm) {
1656                    $elm->class .= ' btn-primary';
1657
1658                    if ($elm->tag == 'button') {
1659                        $elm->innertext = iconify('mdi:arrow-right') . ' ' . $elm->innertext;
1660                    }
1661                }
1662            }
1663
1664            # Revert Manager
1665
1666            if ($INPUT->str('page') == 'revert') {
1667                foreach ($html->find('[type=submit]') as $idx => $elm) {
1668                    if ($idx == 0) {
1669                        $elm->class .= ' btn-primary';
1670                        if ($elm->tag == 'button') {
1671                            $elm->innertext = iconify('mdi:magnify') . ' ' . $elm->innertext;
1672                        }
1673                    }
1674
1675                    if ($idx == 1) {
1676                        $elm->class .= ' btn-success';
1677                        if ($elm->tag == 'button') {
1678                            $elm->innertext = iconify('mdi:refresh') . ' ' . $elm->innertext;
1679                        }
1680                    }
1681                }
1682            }
1683
1684            # Config
1685
1686            if ($INPUT->str('page') == 'config') {
1687                foreach ($html->find('[type=submit]') as $elm) {
1688                    $elm->class .= ' btn-success';
1689                    if ($elm->tag == 'button') {
1690                        $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1691                    }
1692                }
1693
1694                foreach ($html->find('#config__manager') as $cm_elm) {
1695                    $save_button = '';
1696
1697                    foreach ($cm_elm->find('p') as $elm) {
1698                        $save_button    = '<div class="pull-right">' . $elm->outertext . '</div>';
1699                        $elm->outertext = '</div>' . $elm->outertext;
1700                    }
1701
1702                    foreach ($cm_elm->find('fieldset') as $elm) {
1703                        $elm->innertext .= $save_button;
1704                    }
1705                }
1706            }
1707
1708            # User Manager
1709
1710            if ($INPUT->str('page') == 'usermanager') {
1711                foreach ($html->find('.notes') as $elm) {
1712                    $elm->class = str_replace('notes', '', $elm->class);
1713                }
1714
1715                foreach ($html->find('h2') as $idx => $elm) {
1716                    switch ($idx) {
1717                        case 0:
1718                            $elm->innertext = iconify('mdi:account-multiple') . ' ' . $elm->innertext;
1719                            break;
1720                        case 1:
1721                            $elm->innertext = iconify('mdi:account-plus') . ' ' . $elm->innertext;
1722                            break;
1723                        case 2:
1724                            $elm->innertext = iconify('mdi:account-edit') . ' ' . $elm->innertext;
1725                            break;
1726                    }
1727                }
1728
1729                foreach ($html->find('.import_users h2') as $elm) {
1730                    $elm->innertext = iconify('mdi:account-multiple-plus') . ' ' . $elm->innertext;
1731                }
1732
1733                foreach ($html->find('button[name*=fn[delete]]') as $elm) {
1734                    $elm->class .= ' btn btn-danger';
1735                    $elm->innertext = iconify('mdi:account-minus') . ' ' . $elm->innertext;
1736                }
1737
1738                foreach ($html->find('button[name*=fn[add]]') as $elm) {
1739                    $elm->class .= ' btn btn-success';
1740                    $elm->innertext = iconify('mdi:plus') . ' ' . $elm->innertext;
1741                }
1742
1743                foreach ($html->find('button[name*=fn[modify]]') as $elm) {
1744                    $elm->class .= ' btn btn-success';
1745                    $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext;
1746                }
1747
1748                foreach ($html->find('button[name*=fn[import]]') as $elm) {
1749                    $elm->class .= ' btn btn-primary';
1750                    $elm->innertext = iconify('mdi:upload') . ' ' . $elm->innertext;
1751                }
1752
1753                foreach ($html->find('button[name*=fn[export]]') as $elm) {
1754                    $elm->class .= ' btn btn-primary';
1755                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1756                }
1757
1758                foreach ($html->find('button[name*=fn[start]]') as $elm) {
1759                    $elm->class .= ' btn btn-default';
1760                    $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext;
1761                }
1762
1763                foreach ($html->find('button[name*=fn[prev]]') as $elm) {
1764                    $elm->class .= ' btn btn-default';
1765                    $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext;
1766                }
1767
1768                foreach ($html->find('button[name*=fn[next]]') as $elm) {
1769                    $elm->class .= ' btn btn-default';
1770                    $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext;
1771                }
1772
1773                foreach ($html->find('button[name*=fn[last]]') as $elm) {
1774                    $elm->class .= ' btn btn-default';
1775                    $elm->innertext = iconify('mdi:chevron-double-right') . ' ' . $elm->innertext;
1776                }
1777            }
1778
1779            # Extension Manager
1780
1781            if ($INPUT->str('page') == 'extension') {
1782                foreach ($html->find('.actions') as $elm) {
1783                    $elm->class .= ' pl-4 btn-group btn-group-xs';
1784                }
1785
1786                foreach ($html->find('.actions .uninstall') as $elm) {
1787                    $elm->class .= ' btn-danger';
1788                    $elm->innertext = iconify('mdi:delete') . ' ' . $elm->innertext;
1789                }
1790
1791                foreach ($html->find('.actions .enable') as $elm) {
1792                    $elm->class .= ' btn-success';
1793                    $elm->innertext = iconify('mdi:check') . ' ' . $elm->innertext;
1794                }
1795
1796                foreach ($html->find('.actions .disable') as $elm) {
1797                    $elm->class .= ' btn-warning';
1798                    $elm->innertext = iconify('mdi:block-helper') . ' ' . $elm->innertext;
1799                }
1800
1801                foreach ($html->find('.actions .install, .actions .update, .actions .reinstall') as $elm) {
1802                    $elm->class .= ' btn-primary';
1803                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1804                }
1805
1806                foreach ($html->find('form.install [type=submit]') as $elm) {
1807                    $elm->class .= ' btn btn-success';
1808                    $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext;
1809                }
1810
1811                foreach ($html->find('form.search [type=submit]') as $elm) {
1812                    $elm->class .= ' btn btn-primary';
1813                    $elm->innertext = iconify('mdi:cloud-search') . ' ' . $elm->innertext;
1814                }
1815
1816                foreach ($html->find('.permerror') as $elm) {
1817                    $elm->class .= ' pull-left';
1818                }
1819            }
1820
1821            # Admin page
1822            if ($INPUT->str('page') == null) {
1823                foreach ($html->find('ul.admin_tasks, ul.admin_plugins') as $admin_task) {
1824                    $admin_task->class .= ' list-group';
1825
1826                    foreach ($admin_task->find('a') as $item) {
1827                        $item->class .= ' list-group-item';
1828                        $item->style = 'max-height: 50px'; # TODO remove
1829                    }
1830
1831                    foreach ($admin_task->find('.icon') as $item) {
1832                        if ($item->innertext) {
1833                            continue;
1834                        }
1835
1836                        $item->innertext = iconify('mdi:puzzle', ['class' => 'text-success']);
1837                    }
1838                }
1839
1840                foreach ($html->find('h2') as $elm) {
1841                    $elm->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $elm->innertext;
1842                }
1843
1844                foreach ($html->find('ul.admin_plugins') as $admin_plugins) {
1845                    $admin_plugins->class .= ' col-sm-4';
1846                    foreach ($admin_plugins->find('li') as $idx => $item) {
1847                        if ($idx > 0 && $idx % 5 == 0) {
1848                            $item->outertext = '</ul><ul class="' . $admin_plugins->class . '">' . $item->outertext;
1849                        }
1850                    }
1851                }
1852
1853                # DokuWiki logo
1854                if ($admin_version = $html->getElementById('admin__version')) {
1855                    $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>';
1856
1857                    $template_version = $this->getVersion();
1858
1859                    $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>';
1860                }
1861            }
1862
1863            $content = $html->save();
1864
1865            $html->clear();
1866            unset($html);
1867
1868            # Configuration Manager Template Sections
1869            if ($INPUT->str('page') == 'config') {
1870                # Import HTML string
1871                $html = new \simple_html_dom;
1872                $html->load($content, true, false);
1873
1874                foreach ($html->find('fieldset[id^="plugin__"]') as $elm) {
1875
1876                    /** @var array $matches */
1877                    preg_match('/plugin_+(\w+[^_])_+plugin_settings_name/', $elm->id, $matches);
1878
1879                    $plugin_name = $matches[1];
1880
1881                    if ($extension = plugin_load('helper', 'extension_extension')) {
1882                        if ($extension->setExtension($plugin_name)) {
1883                            foreach ($elm->find('legend') as $legend) {
1884                                $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>';
1885                            }
1886                        }
1887                    } else {
1888                        foreach ($elm->find('legend') as $legend) {
1889                            $legend->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $legend->innertext;
1890                        }
1891                    }
1892                }
1893
1894                $dokuwiki_configs = [
1895                    '#_basic'          => 'mdi:settings',
1896                    '#_display'        => 'mdi:monitor',
1897                    '#_authentication' => 'mdi:shield-account',
1898                    '#_anti_spam'      => 'mdi:block-helper',
1899                    '#_editing'        => 'mdi:pencil',
1900                    '#_links'          => 'mdi:link-variant',
1901                    '#_media'          => 'mdi:folder-image',
1902                    '#_notifications'  => 'mdi:email',
1903                    '#_syndication'    => 'mdi:rss',
1904                    '#_advanced'       => 'mdi:palette-advanced',
1905                    '#_network'        => 'mdi:network',
1906                ];
1907
1908                foreach ($dokuwiki_configs as $selector => $icon) {
1909                    foreach ($html->find("$selector legend") as $elm) {
1910                        $elm->innertext = iconify($icon) . ' ' . $elm->innertext;
1911                    }
1912                }
1913
1914                $content = $html->save();
1915
1916                $html->clear();
1917                unset($html);
1918
1919                $admin_sections = [
1920                    // Section => [ Insert Before, Icon ]
1921                    'theme'            => ['bootstrapTheme', 'mdi:palette'],
1922                    'sidebar'          => ['sidebarPosition', 'mdi:page-layout-sidebar-left'],
1923                    'navbar'           => ['inverseNavbar', 'mdi:page-layout-header'],
1924                    'semantic'         => ['semantic', 'mdi:share-variant'],
1925                    'layout'           => ['fluidContainer', 'mdi:monitor'],
1926                    'toc'              => ['tocAffix', 'mdi:view-list'],
1927                    'discussion'       => ['showDiscussion', 'mdi:comment-text-multiple'],
1928                    'avatar'           => ['useAvatar', 'mdi:account'],
1929                    'cookie_law'       => ['showCookieLawBanner', 'mdi:scale-balance'],
1930                    'google_analytics' => ['useGoogleAnalytics', 'mdi:google'],
1931                    'browser_title'    => ['browserTitle', 'mdi:format-title'],
1932                    'page'             => ['showPageInfo', 'mdi:file'],
1933                ];
1934
1935                foreach ($admin_sections as $section => $items) {
1936                    $search = $items[0];
1937                    $icon   = $items[1];
1938
1939                    $content = preg_replace(
1940                        '/<span class="outkey">(tpl»bootstrap3»' . $search . ')<\/span>/',
1941                        '<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>',
1942                        $content
1943                    );
1944                }
1945            }
1946        }
1947
1948        # Difference and Draft
1949
1950        if ($ACT == 'diff' || $ACT == 'draft') {
1951            # Import HTML string
1952            $html = new \simple_html_dom;
1953            $html->load($content, true, false);
1954
1955            foreach ($html->find('.diff-lineheader') as $elm) {
1956                $elm->style = 'opacity: 0.5';
1957                $elm->class .= ' text-center px-3';
1958
1959                if ($elm->innertext == '+') {
1960                    $elm->class .= ' bg-success';
1961                }
1962                if ($elm->innertext == '-') {
1963                    $elm->class .= ' bg-danger';
1964                }
1965            }
1966
1967            foreach ($html->find('.diff_sidebyside .diff-deletedline, .diff_sidebyside .diff-addedline') as $elm) {
1968                $elm->class .= ' w-50';
1969            }
1970
1971            foreach ($html->find('.diff-deletedline') as $elm) {
1972                $elm->class .= ' bg-danger';
1973            }
1974
1975            foreach ($html->find('.diff-addedline') as $elm) {
1976                $elm->class .= ' bg-success';
1977            }
1978
1979            foreach ($html->find('.diffprevrev') as $elm) {
1980                $elm->class .= ' btn btn-default';
1981                $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext;
1982            }
1983
1984            foreach ($html->find('.diffnextrev') as $elm) {
1985                $elm->class .= ' btn btn-default';
1986                $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext;
1987            }
1988
1989            foreach ($html->find('.diffbothprevrev') as $elm) {
1990                $elm->class .= ' btn btn-default';
1991                $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext;
1992            }
1993
1994            foreach ($html->find('.minor') as $elm) {
1995                $elm->class .= ' text-muted';
1996            }
1997
1998            $content = $html->save();
1999
2000            $html->clear();
2001            unset($html);
2002        }
2003
2004        # Add icons for Extensions, Actions, etc.
2005
2006        $svg_icon      = null;
2007        $iconify_icon  = null;
2008        $iconify_attrs = ['class' => 'mr-2'];
2009
2010        if (!$INFO['exists'] && $ACT == 'show') {
2011            $iconify_icon           = 'mdi:alert';
2012            $iconify_attrs['style'] = 'color:orange';
2013        }
2014
2015        $menu_class = "\\dokuwiki\\Menu\\Item\\$ACT";
2016
2017        if (class_exists($menu_class, false)) {
2018            $menu_item = new $menu_class;
2019            $svg_icon  = $menu_item->getSvg();
2020        }
2021
2022        switch ($ACT) {
2023            case 'admin':
2024
2025                if (($plugin = plugin_load('admin', $INPUT->str('page'))) !== null) {
2026                    if (method_exists($plugin, 'getMenuIcon')) {
2027                        $svg_icon = $plugin->getMenuIcon();
2028
2029                        if (!file_exists($svg_icon)) {
2030                            $iconify_icon = 'mdi:puzzle';
2031                            $svg_icon     = null;
2032                        }
2033                    } else {
2034                        $iconify_icon = 'mdi:puzzle';
2035                        $svg_icon     = null;
2036                    }
2037                }
2038
2039                break;
2040
2041            case 'resendpwd':
2042                $iconify_icon = 'mdi:lock-reset';
2043                break;
2044
2045            case 'denied':
2046                $iconify_icon           = 'mdi:block-helper';
2047                $iconify_attrs['style'] = 'color:red';
2048                break;
2049
2050            case 'search':
2051                $iconify_icon = 'mdi:search-web';
2052                break;
2053
2054            case 'preview':
2055                $iconify_icon = 'mdi:file-eye';
2056                break;
2057
2058            case 'diff':
2059                $iconify_icon = 'mdi:file-compare';
2060                break;
2061
2062            case 'showtag':
2063                $iconify_icon = 'mdi:tag-multiple';
2064                break;
2065
2066            case 'draft':
2067                $iconify_icon = 'mdi:android-studio';
2068                break;
2069
2070        }
2071
2072        if ($svg_icon) {
2073            $svg_attrs = ['class' => 'iconify mr-2'];
2074
2075            if ($ACT == 'admin' && $INPUT->str('page') == 'extension') {
2076                $svg_attrs['style'] = 'fill: green;';
2077            }
2078
2079            $svg = SVG::icon($svg_icon, null, '1em', $svg_attrs);
2080
2081            # Import HTML string
2082            $html = new \simple_html_dom;
2083            $html->load($content, true, false);
2084
2085            foreach ($html->find('h1') as $elm) {
2086                $elm->innertext = $svg . ' ' . $elm->innertext;
2087                break;
2088            }
2089
2090            $content = $html->save();
2091
2092            $html->clear();
2093            unset($html);
2094        }
2095
2096        if ($iconify_icon) {
2097            # Import HTML string
2098            $html = new \simple_html_dom;
2099            $html->load($content, true, false);
2100
2101            foreach ($html->find('h1') as $elm) {
2102                $elm->innertext = iconify($iconify_icon, $iconify_attrs) . $elm->innertext;
2103                break;
2104            }
2105
2106            $content = $html->save();
2107
2108            $html->clear();
2109            unset($html);
2110        }
2111
2112        return $content;
2113    }
2114
2115    /**
2116     * Detect the fluid navbar flag
2117     *
2118     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2119     * @return boolean
2120     */
2121    public function isFluidNavbar()
2122    {
2123        $fluid_container  = $this->getConf('fluidContainer');
2124        $fixed_top_nabvar = $this->getConf('fixedTopNavbar');
2125
2126        return ($fluid_container || ($fluid_container && !$fixed_top_nabvar) || (!$fluid_container && !$fixed_top_nabvar));
2127    }
2128
2129    /**
2130     * Calculate automatically the grid size for main container
2131     *
2132     * @author  Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2133     *
2134     * @return  string
2135     */
2136    public function getContainerGrid()
2137    {
2138        global $ID;
2139
2140        $result = '';
2141
2142        $grids = [
2143            'sm' => ['left' => 0, 'right' => 0],
2144            'md' => ['left' => 0, 'right' => 0],
2145        ];
2146
2147        $show_right_sidebar = $this->getConf('showRightSidebar');
2148        $show_left_sidebar  = $this->getConf('showSidebar');
2149        $fluid_container    = $this->getConf('fluidContainer');
2150
2151        if ($this->getConf('showLandingPage') && (bool) preg_match($this->getConf('landingPages'), $ID)) {
2152            $show_left_sidebar = false;
2153        }
2154
2155        if ($show_left_sidebar) {
2156            foreach (explode(' ', $this->getConf('leftSidebarGrid')) as $grid) {
2157                list($col, $media, $size) = explode('-', $grid);
2158                $grids[$media]['left']    = (int) $size;
2159            }
2160        }
2161
2162        if ($show_right_sidebar) {
2163            foreach (explode(' ', $this->getConf('rightSidebarGrid')) as $grid) {
2164                list($col, $media, $size) = explode('-', $grid);
2165                $grids[$media]['right']   = (int) $size;
2166            }
2167        }
2168
2169        foreach ($grids as $media => $position) {
2170            $left  = $position['left'];
2171            $right = $position['right'];
2172            $result .= sprintf('col-%s-%s ', $media, (12 - $left - $right));
2173        }
2174
2175        return $result;
2176    }
2177
2178    /**
2179     * Places the TOC where the function is called
2180     *
2181     * If you use this you most probably want to call tpl_content with
2182     * a false argument
2183     *
2184     * @author Andreas Gohr <andi@splitbrain.org>
2185     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2186     *
2187     * @param bool $return Should the TOC be returned instead to be printed?
2188     * @return string
2189     */
2190    public function getTOC($return = false)
2191    {
2192        global $TOC;
2193        global $ACT;
2194        global $ID;
2195        global $REV;
2196        global $INFO;
2197        global $conf;
2198        global $INPUT;
2199
2200        $toc = [];
2201
2202        if (is_array($TOC)) {
2203            // if a TOC was prepared in global scope, always use it
2204            $toc = $TOC;
2205        } elseif (($ACT == 'show' || substr($ACT, 0, 6) == 'export') && !$REV && $INFO['exists']) {
2206            // get TOC from metadata, render if neccessary
2207            $meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE);
2208            if (isset($meta['internal']['toc'])) {
2209                $tocok = $meta['internal']['toc'];
2210            } else {
2211                $tocok = true;
2212            }
2213            $toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null;
2214            if (!$tocok || !is_array($toc) || !$conf['tocminheads'] || count($toc) < $conf['tocminheads']) {
2215                $toc = [];
2216            }
2217        } elseif ($ACT == 'admin') {
2218            // try to load admin plugin TOC
2219            /** @var $plugin DokuWiki_Admin_Plugin */
2220            if ($plugin = plugin_getRequestAdminPlugin()) {
2221                $toc = $plugin->getTOC();
2222                $TOC = $toc; // avoid later rebuild
2223            }
2224        }
2225
2226        $toc_check     = end($toc);
2227        $toc_undefined = null;
2228
2229        if (isset($toc_check['link']) && !preg_match('/bootstrap/', $toc_check['link'])) {
2230            $toc_undefined = array_pop($toc);
2231        }
2232
2233        \dokuwiki\Extension\Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false);
2234
2235        if ($ACT == 'admin' && $INPUT->str('page') == 'config') {
2236            $bootstrap3_sections = [
2237                'theme', 'sidebar', 'navbar', 'semantic', 'layout', 'toc',
2238                'discussion', 'avatar', 'cookie_law', 'google_analytics',
2239                'browser_title', 'page',
2240            ];
2241
2242            foreach ($bootstrap3_sections as $id) {
2243                $toc[] = [
2244                    'link'  => "#bootstrap3__$id",
2245                    'title' => tpl_getLang("config_$id"),
2246                    'type'  => 'ul',
2247                    'level' => 3,
2248                ];
2249            }
2250        }
2251
2252        if ($toc_undefined) {
2253            $toc[] = $toc_undefined;
2254        }
2255
2256        $html = $this->renderTOC($toc);
2257
2258        if ($return) {
2259            return $html;
2260        }
2261
2262        echo $html;
2263        return '';
2264    }
2265
2266    /**
2267     * Return the TOC rendered to XHTML with Bootstrap3 style
2268     *
2269     * @author Andreas Gohr <andi@splitbrain.org>
2270     * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com>
2271     *
2272     * @param array $toc
2273     * @return string html
2274     */
2275    private function renderTOC($toc)
2276    {
2277        if (!count($toc)) {
2278            return '';
2279        }
2280
2281        global $lang;
2282
2283        $json_toc = [];
2284
2285        foreach ($toc as $item) {
2286            $json_toc[] = [
2287                'link'  => (isset($item['link']) ? $item['link'] : '#' . $item['hid']),
2288                'title' => $item['title'],
2289                'level' => $item['level'],
2290            ];
2291        }
2292
2293        $out = '';
2294        $out .= '<script>JSINFO.bootstrap3.toc = ' . json_encode($json_toc) . ';</script>' . DOKU_LF;
2295
2296        if ($this->getConf('tocLayout') !== 'navbar') {
2297            $out .= '<!-- TOC START -->' . DOKU_LF;
2298            $out .= '<div class="dw-toc hidden-print">' . DOKU_LF;
2299            $out .= '<nav id="dw__toc" role="navigation" class="toc-panel panel panel-default small">' . DOKU_LF;
2300            $out .= '<h6 data-toggle="collapse" data-target="#dw__toc .toc-body" title="' . $lang['toc'] . '" class="panel-heading toc-title">' . iconify('mdi:view-list') . ' ';
2301            $out .= '<span>' . $lang['toc'] . '</span>';
2302            $out .= ' <i class="caret"></i></h6>' . DOKU_LF;
2303            $out .= '<div class="panel-body  toc-body collapse ' . (!$this->getConf('tocCollapsed') ? 'in' : '') . '">' . DOKU_LF;
2304            $out .= $this->normalizeList(html_buildlist($toc, 'nav toc', 'html_list_toc', 'html_li_default', true)) . DOKU_LF;
2305            $out .= '</div>' . DOKU_LF;
2306            $out .= '</nav>' . DOKU_LF;
2307            $out .= '</div>' . DOKU_LF;
2308            $out .= '<!-- TOC END -->' . DOKU_LF;
2309        }
2310
2311        return $out;
2312    }
2313
2314    private function initToolsMenu()
2315    {
2316        global $ACT;
2317
2318        $tools_menus = [
2319            'user' => ['icon' => 'mdi:account', 'object' => new \dokuwiki\Menu\UserMenu],
2320            'site' => ['icon' => 'mdi:toolbox', 'object' => new \dokuwiki\Menu\SiteMenu],
2321            'page' => ['icon' => 'mdi:file-document-outline', 'object' => new \dokuwiki\template\bootstrap3\Menu\PageMenu],
2322        ];
2323
2324        if (defined('DOKU_MEDIADETAIL')) {
2325            $tools_menus['page'] = ['icon' => 'mdi:image', 'object' => new \dokuwiki\template\bootstrap3\Menu\DetailMenu];
2326        }
2327
2328        foreach ($tools_menus as $tool => $data) {
2329            foreach ($data['object']->getItems() as $item) {
2330                $attr   = buildAttributes($item->getLinkAttributes());
2331                $active = 'action';
2332
2333                if ($ACT == $item->getType() || ($ACT == 'revisions' && $item->getType() == 'revs') || ($ACT == 'diff' && $item->getType() == 'revs')) {
2334                    $active .= ' active';
2335                }
2336
2337                if ($item->getType() == 'shareon') {
2338                    $active .= ' dropdown';
2339                }
2340
2341                $html = '<li class="' . $active . '">';
2342                $html .= "<a $attr>";
2343                $html .= \inlineSVG($item->getSvg());
2344                $html .= '<span>' . hsc($item->getLabel()) . '</span>';
2345                $html .= "</a>";
2346
2347                if ($item->getType() == 'shareon') {
2348                    $html .= $item->getDropDownMenu();
2349                }
2350
2351                $html .= '</li>';
2352
2353                $tools_menus[$tool]['menu'][$item->getType()]['object'] = $item;
2354                $tools_menus[$tool]['menu'][$item->getType()]['html']   = $html;
2355            }
2356        }
2357
2358        $this->toolsMenu = $tools_menus;
2359    }
2360
2361    public function getToolsMenu()
2362    {
2363        return $this->toolsMenu;
2364    }
2365
2366    public function getToolMenu($tool)
2367    {
2368        return $this->toolsMenu[$tool];
2369    }
2370
2371    public function getToolMenuItem($tool, $item)
2372    {
2373        if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) {
2374            return $this->toolsMenu[$tool]['menu'][$item]['object'];
2375        }
2376        return null;
2377    }
2378
2379    public function getToolMenuItemLink($tool, $item)
2380    {
2381        if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) {
2382            return $this->toolsMenu[$tool]['menu'][$item]['html'];
2383        }
2384        return null;
2385    }
2386
2387    public function getNavbarHeight()
2388    {
2389        switch ($this->getBootswatchTheme()) {
2390            case 'simplex':
2391            case 'superhero':
2392                return 40;
2393
2394            case 'yeti':
2395                return 45;
2396
2397            case 'cerulean':
2398            case 'cosmo':
2399            case 'custom':
2400            case 'cyborg':
2401            case 'lumen':
2402            case 'slate':
2403            case 'spacelab':
2404            case 'solar':
2405            case 'united':
2406                return 50;
2407
2408            case 'darkly':
2409            case 'flatly':
2410            case 'journal':
2411            case 'sandstone':
2412                return 60;
2413
2414            case 'paper':
2415                return 64;
2416
2417            case 'readable':
2418                return 65;
2419
2420            default:
2421                return 50;
2422        }
2423    }
2424}
2425