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