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