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