1 <?php
2 
3 namespace 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 
13 class 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),
785 m=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