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