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 .= 'window.dataLayer = window.dataLayer || [];' . DOKU_LF; 784 $out .= 'function gtag(){dataLayer.push(arguments);}' . DOKU_LF; 785 $out .= 'gtag("js", new Date());' . DOKU_LF; 786 787 if ($this->getConf('googleAnalyticsAnonymizeIP')) { 788 $out .= 'gtag("config", "' . $google_analitycs_id . '", {"anonimize_ip":true});' . DOKU_LF; 789 } else { 790 $out .= 'gtag("config", "' . $google_analitycs_id . '");' . DOKU_LF; 791 } 792 793 if ($this->getConf('googleAnalyticsTrackActions')) { 794 $out .= 'gtag("event", JSINFO.bootstrap3.mode, {"eventCategory":"DokuWiki"});' . 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 947 $page = $part . $parts[$i]; 948 949 if ($page == $conf['start']) { 950 echo '</ol>'; 951 return true; 952 } 953 954 echo '<li class="active" itemprop="itemListElement" itemscope itemtype="http://schema.org/ListItem">'; 955 956 $link = str_replace(['<span class="curid">', '</span>'], '', html_wikilink($page)); 957 $link = str_replace('<a ', '<a itemprop="item" ', $link); 958 $link = str_replace('<a', '<span itemprop="name"><a', $link); 959 $link = str_replace('</a>', '</a></span>', $link); 960 $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link); 961 962 echo $link; 963 echo '<meta itemprop="position" content="' . ++$position . '" />'; 964 echo '</li>'; 965 echo '</ol>'; 966 967 return true; 968 } 969 970 /** 971 * Display the page title (and previous namespace page title) on browser titlebar 972 * 973 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 974 * @return string 975 */ 976 public function getBrowserPageTitle() 977 { 978 global $conf, $ACT, $ID; 979 980 if ($this->getConf('browserTitleShowNS') && $ACT == 'show') { 981 $ns_page = ''; 982 $ns_parts = explode(':', $ID); 983 $ns_pages = []; 984 $ns_titles = []; 985 $ns_separator = sprintf(' %s ', $this->getConf('browserTitleCharSepNS')); 986 987 if (useHeading('navigation')) { 988 if (count($ns_parts) > 1) { 989 foreach ($ns_parts as $ns_part) { 990 $ns_page .= "$ns_part:"; 991 $ns_pages[] = $ns_page; 992 } 993 994 $ns_pages = array_unique($ns_pages); 995 996 foreach ($ns_pages as $ns_page) { 997 998 $exists = false; 999 1000 // Igor and later 1001 if (class_exists('dokuwiki\File\PageResolver')) { 1002 $resolver = new \dokuwiki\File\PageResolver($ns_page); 1003 $ns_page = $resolver->resolveId($ns_page); 1004 $exists = page_exists($ns_page); 1005 } else { 1006 // Compatibility with older releases 1007 resolve_pageid(getNS($ns_page), $ns_page, $exists); 1008 } 1009 1010 $ns_page_title_heading = hsc(p_get_first_heading($ns_page)); 1011 $ns_page_title_page = noNSorNS($ns_page); 1012 $ns_page_title = ($exists) ? $ns_page_title_heading : null; 1013 1014 if ($ns_page_title !== $conf['start']) { 1015 $ns_titles[] = $ns_page_title; 1016 } 1017 } 1018 } 1019 1020 $exists = false; 1021 1022 // Igor and later 1023 if (class_exists('dokuwiki\File\PageResolver')) { 1024 $resolver = new \dokuwiki\File\PageResolver($ID); 1025 $id = $resolver->resolveId($ID); 1026 $exists = page_exists($id); 1027 } else { 1028 // Compatibility with older releases 1029 resolve_pageid(getNS($ID), $ID, $exists); 1030 } 1031 1032 if ($exists) { 1033 $ns_titles[] = tpl_pagetitle($ID, true); 1034 } else { 1035 $ns_titles[] = noNS($ID); 1036 } 1037 1038 $ns_titles = array_filter(array_unique($ns_titles)); 1039 } else { 1040 $ns_titles = $ns_parts; 1041 } 1042 1043 if ($this->getConf('browserTitleOrderNS') == 'normal') { 1044 $ns_titles = array_reverse($ns_titles); 1045 } 1046 1047 $browser_title = implode($ns_separator, $ns_titles); 1048 } else { 1049 $browser_title = tpl_pagetitle($ID, true); 1050 } 1051 1052 return str_replace( 1053 ['@WIKI@', '@TITLE@'], 1054 [strip_tags($conf['title']), $browser_title], 1055 $this->getConf('browserTitle') 1056 ); 1057 } 1058 1059 /** 1060 * Return the theme for current namespace 1061 * 1062 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1063 * @return string 1064 */ 1065 public function getThemeForNamespace() 1066 { 1067 global $ID; 1068 1069 $themes_filename = DOKU_CONF . 'bootstrap3.themes.conf'; 1070 1071 if (!$this->getConf('themeByNamespace')) { 1072 return []; 1073 } 1074 1075 if (!file_exists($themes_filename)) { 1076 return []; 1077 } 1078 1079 $config = confToHash($themes_filename); 1080 krsort($config); 1081 1082 foreach ($config as $page => $theme) { 1083 if (preg_match("/^$page/", "$ID")) { 1084 list($bootstrap, $bootswatch) = explode('/', $theme); 1085 1086 if ($bootstrap && in_array($bootstrap, ['default', 'optional', 'custom'])) { 1087 return [$bootstrap, $bootswatch]; 1088 } 1089 1090 if ($bootstrap == 'bootswatch' && in_array($bootswatch, $this->getBootswatchThemeList())) { 1091 return [$bootstrap, $bootswatch]; 1092 } 1093 } 1094 } 1095 1096 return []; 1097 } 1098 1099 /** 1100 * Make a Bootstrap3 Nav 1101 * 1102 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1103 * 1104 * @param string $html 1105 * @param string $type (= pills, tabs, navbar) 1106 * @param boolean $staked 1107 * @param string $optional_class 1108 * @return string 1109 */ 1110 public function toBootstrapNav($html, $type = '', $stacked = false, $optional_class = '') 1111 { 1112 $classes = []; 1113 1114 $classes[] = 'nav'; 1115 $classes[] = $optional_class; 1116 1117 switch ($type) { 1118 case 'navbar': 1119 case 'navbar-nav': 1120 $classes[] = 'navbar-nav'; 1121 break; 1122 case 'pills': 1123 case 'tabs': 1124 $classes[] = "nav-$type"; 1125 break; 1126 } 1127 1128 if ($stacked) { 1129 $classes[] = 'nav-stacked'; 1130 } 1131 1132 $class = implode(' ', $classes); 1133 1134 $output = str_replace( 1135 ['<ul class="', '<ul>'], 1136 ["<ul class=\"$class ", "<ul class=\"$class\">"], 1137 $html 1138 ); 1139 1140 $output = $this->normalizeList($output); 1141 1142 return $output; 1143 } 1144 1145 /** 1146 * Normalize the DokuWiki list items 1147 * 1148 * @todo use Simple DOM HTML library 1149 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1150 * @todo use Simple DOM HTML 1151 * @todo FIX SimpleNavi curid 1152 * 1153 * @param string $html 1154 * @return string 1155 */ 1156 public function normalizeList($list) 1157 { 1158 1159 global $ID; 1160 1161 $list = preg_replace_callback('/data-wiki-id="(.+?)"/', [$this, '_replaceWikiCurrentIdCallback'], $list); 1162 1163 $html = new \simple_html_dom; 1164 $html->load($list, true, false); 1165 1166 # Create data-curid HTML5 attribute and unwrap span.curid for pre-Hogfather release 1167 foreach ($html->find('span.curid') as $elm) { 1168 $elm->firstChild()->setAttribute('data-wiki-curid', 'true'); 1169 $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext); 1170 } 1171 1172 # Unwrap div.li element 1173 foreach ($html->find('div.li') as $elm) { 1174 $elm->outertext = str_replace(['<div class="li">', '</div>'], '', $elm->outertext); 1175 } 1176 1177 $list = $html->save(); 1178 $html->clear(); 1179 unset($html); 1180 1181 $html = new \simple_html_dom; 1182 $html->load($list, true, false); 1183 1184 foreach ($html->find('li') as $elm) { 1185 if ($elm->find('a[data-wiki-curid]')) { 1186 $elm->class .= ' active'; 1187 } 1188 } 1189 1190 $list = $html->save(); 1191 $html->clear(); 1192 unset($html); 1193 1194 # TODO optimize 1195 $list = preg_replace('/<i (.+?)><\/i> <a (.+?)>(.+?)<\/a>/', '<a $2><i $1></i> $3</a>', $list); 1196 $list = preg_replace('/<span (.+?)><\/span> <a (.+?)>(.+?)<\/a>/', '<a $2><span $1></span> $3</a>', $list); 1197 1198 return $list; 1199 } 1200 1201 /** 1202 * Remove data-wiki-id HTML5 attribute 1203 * 1204 * @todo Remove this in future 1205 * @since Hogfather 1206 * 1207 * @param array $matches 1208 * 1209 * @return string 1210 */ 1211 private function _replaceWikiCurrentIdCallback($matches) 1212 { 1213 1214 global $ID; 1215 1216 if ($ID == $matches[1]) { 1217 return 'data-wiki-curid="true"'; 1218 } 1219 1220 return ''; 1221 1222 } 1223 1224 /** 1225 * Return a Bootstrap NavBar and or drop-down menu 1226 * 1227 * @todo use Simple DOM HTML library 1228 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1229 * 1230 * @return string 1231 */ 1232 public function getNavbar() 1233 { 1234 global $INPUT; 1235 1236 if ($this->getConf('showNavbar') === 'logged' && !$INPUT->server->has('REMOTE_USER')) { 1237 return false; 1238 } 1239 1240 global $ID; 1241 global $conf; 1242 1243 $navbar = $this->toBootstrapNav(tpl_include_page('navbar', 0, 1, $this->getConf('useACL')), 'navbar'); 1244 1245 $navbar = str_replace('urlextern', '', $navbar); 1246 1247 $navbar = preg_replace('/<li class="level([0-9]) node"> (.*)/', 1248 '<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); 1249 1250 $navbar = preg_replace('/<li class="level([0-9]) node active"> (.*)/', 1251 '<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); 1252 1253 # FIX for Purplenumbers renderer plugin 1254 # TODO use Simple DOM HTML or improve the regex! 1255 if ($conf['renderer_xhtml'] == 'purplenumbers') { 1256 $navbar = preg_replace('/<li class="level1"> (.*)/', 1257 '<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); 1258 } 1259 1260 $navbar = preg_replace('/<ul class="(.*)">\n<li class="level2(.*)">/', 1261 '<ul class="dropdown-menu" role="menu">' . PHP_EOL . '<li class="level2$2">', $navbar); 1262 1263 return $navbar; 1264 } 1265 1266 /** 1267 * Manipulate Sidebar page to add Bootstrap3 styling 1268 * 1269 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1270 * 1271 * @param string $sidebar 1272 * @param boolean $return 1273 * @return string 1274 */ 1275 public function normalizeSidebar($sidebar, $return = false) 1276 { 1277 $out = $this->toBootstrapNav($sidebar, 'pills', true); 1278 $out = $this->normalizeContent($out); 1279 1280 $html = new \simple_html_dom; 1281 $html->load($out, true, false); 1282 1283 # TODO 'page-header' will be removed in the next release of Bootstrap 1284 foreach ($html->find('h1, h2, h3, h4, h5, h6') as $elm) { 1285 1286 # Skip panel title on sidebar 1287 if (preg_match('/panel-title/', $elm->class)) { 1288 continue; 1289 } 1290 1291 $elm->class .= ' page-header'; 1292 } 1293 1294 $out = $html->save(); 1295 $html->clear(); 1296 unset($html); 1297 1298 if ($return) { 1299 return $out; 1300 } 1301 1302 echo $out; 1303 } 1304 1305 /** 1306 * Return a drop-down page 1307 * 1308 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1309 * 1310 * @param string $page name 1311 * @return string 1312 */ 1313 public function getDropDownPage($page) 1314 { 1315 1316 $page = page_findnearest($page, $this->getConf('useACL')); 1317 1318 if (!$page) { 1319 return; 1320 } 1321 1322 $output = $this->normalizeContent($this->toBootstrapNav(tpl_include_page($page, 0, 1, $this->getConf('useACL')), 'pills', true)); 1323 $dropdown = '<ul class="nav navbar-nav dw__dropdown_page">' . 1324 '<li class="dropdown dropdown-large">' . 1325 '<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="">' . 1326 p_get_first_heading($page) . 1327 ' <span class="caret"></span></a>' . 1328 '<ul class="dropdown-menu dropdown-menu-large" role="menu">' . 1329 '<li><div class="container small">' . 1330 $output . 1331 '</div></li></ul></li></ul>'; 1332 1333 return $dropdown; 1334 } 1335 1336 /** 1337 * Include left or right sidebar 1338 * 1339 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1340 * 1341 * @param string $type left or right sidebar 1342 * @return boolean 1343 */ 1344 public function includeSidebar($type) 1345 { 1346 global $conf; 1347 1348 $left_sidebar = $conf['sidebar']; 1349 $right_sidebar = $this->getConf('rightSidebar'); 1350 $left_sidebar_grid = $this->getConf('leftSidebarGrid'); 1351 $right_sidebar_grid = $this->getConf('rightSidebarGrid'); 1352 1353 if (!$this->getConf('showSidebar')) { 1354 return false; 1355 } 1356 1357 switch ($type) { 1358 case 'left': 1359 1360 if ($this->getConf('sidebarPosition') == 'left') { 1361 $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter'); 1362 } 1363 1364 return true; 1365 1366 case 'right': 1367 1368 if ($this->getConf('sidebarPosition') == 'right') { 1369 $this->sidebarWrapper($left_sidebar, 'dokuwiki__aside', $left_sidebar_grid, 'sidebarheader', 'sidebarfooter'); 1370 } 1371 1372 if ($this->getConf('showRightSidebar') 1373 && $this->getConf('sidebarPosition') == 'left') { 1374 $this->sidebarWrapper($right_sidebar, 'dokuwiki__rightaside', $right_sidebar_grid, 'rightsidebarheader', 'rightsidebarfooter'); 1375 } 1376 1377 return true; 1378 } 1379 1380 return false; 1381 } 1382 1383 /** 1384 * Wrapper for left or right sidebar 1385 * 1386 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1387 * 1388 * @param string $sidebar_page 1389 * @param string $sidebar_id 1390 * @param string $sidebar_header 1391 * @param string $sidebar_footer 1392 */ 1393 private function sidebarWrapper($sidebar_page, $sidebar_id, $sidebar_class, $sidebar_header, $sidebar_footer) 1394 { 1395 global $lang; 1396 global $TPL; 1397 1398 @require $this->tplDir . 'tpl/sidebar.php'; 1399 } 1400 1401 /** 1402 * Add Bootstrap classes in a DokuWiki content 1403 * 1404 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 1405 * 1406 * @param string $content from tpl_content() or from tpl_include_page() 1407 * @return string with Bootstrap styles 1408 */ 1409 public function normalizeContent($content) 1410 { 1411 global $ACT; 1412 global $INPUT; 1413 global $INFO; 1414 1415 # FIX :-\ smile 1416 $content = str_replace(['alt=":-\"', "alt=':-\'"], 'alt=":-\"', $content); 1417 1418 # Workaround for ToDo Plugin 1419 $content = str_replace('checked="checked"', ' checked="checked"', $content); 1420 1421 # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB) 1422 if (strlen($content) > MAX_FILE_SIZE) { 1423 return $content; 1424 } 1425 1426 # Import HTML string 1427 $html = new \simple_html_dom; 1428 $html->load($content, true, false); 1429 1430 # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB) 1431 if (!$html) { 1432 return $content; 1433 } 1434 1435 # Move Current Page ID to <a> element and create data-curid HTML5 attribute (pre-Hogfather release) 1436 foreach ($html->find('.curid') as $elm) { 1437 foreach ($elm->find('a') as $link) { 1438 $link->class .= ' curid'; 1439 $link->attr[' data-curid'] = 'true'; # FIX attribute 1440 } 1441 } 1442 1443 # Unwrap span.curid elements 1444 foreach ($html->find('span.curid') as $elm) { 1445 $elm->outertext = str_replace(['<span class="curid">', '</span>'], '', $elm->outertext); 1446 } 1447 1448 # Footnotes 1449 foreach ($html->find('.footnotes') as $elm) { 1450 $elm->outertext = '<hr/>' . $elm->outertext; 1451 } 1452 1453 # Accessibility (a11y) 1454 foreach ($html->find('.a11y') as $elm) { 1455 if (!preg_match('/picker/', $elm->class)) { 1456 $elm->class .= ' sr-only'; 1457 } 1458 } 1459 1460 # Fix list overlap in media images 1461 foreach ($html->find('ul, ol') as $elm) { 1462 if (!preg_match('/(nav|dropdown-menu)/', $elm->class)) { 1463 $elm->class .= ' fix-media-list-overlap'; 1464 } 1465 } 1466 1467 # Buttons 1468 foreach ($html->find('.button') as $elm) { 1469 if ($elm->tag !== 'form') { 1470 $elm->class .= ' btn'; 1471 } 1472 } 1473 1474 foreach ($html->find('[type=button], [type=submit], [type=reset]') as $elm) { 1475 $elm->class .= ' btn btn-default'; 1476 } 1477 1478 # Tabs 1479 foreach ($html->find('.tabs') as $elm) { 1480 $elm->class = 'nav nav-tabs'; 1481 } 1482 1483 # Tabs (active) 1484 foreach ($html->find('.nav-tabs strong') as $elm) { 1485 $elm->outertext = '<a href="#">' . $elm->innertext . "</a>"; 1486 $parent = $elm->parent()->class .= ' active'; 1487 } 1488 1489 # Page Heading (h1-h2) 1490 # TODO this class will be removed in Bootstrap >= 4.0 version 1491 foreach ($html->find('h1,h2,h3') as $elm) { 1492 $elm->class .= ' page-header pb-3 mb-4 mt-5'; # TODO replace page-header with border-bottom in BS4 1493 } 1494 1495 # Media Images 1496 foreach ($html->find('img[class^=media]') as $elm) { 1497 $elm->class .= ' img-responsive'; 1498 } 1499 1500 # Checkbox 1501 foreach ($html->find('input[type=checkbox]') as $elm) { 1502 $elm->class .= ' checkbox-inline'; 1503 } 1504 1505 # Radio button 1506 foreach ($html->find('input[type=radio]') as $elm) { 1507 $elm->class .= ' radio-inline'; 1508 } 1509 1510 # Label 1511 foreach ($html->find('label') as $elm) { 1512 $elm->class .= ' control-label'; 1513 } 1514 1515 # Form controls 1516 foreach ($html->find('input, select, textarea') as $elm) { 1517 if (!in_array($elm->type, ['submit', 'reset', 'button', 'hidden', 'image', 'checkbox', 'radio', 'color'])) { 1518 $elm->class .= ' form-control'; 1519 } 1520 } 1521 1522 # Forms 1523 # TODO main form 1524 foreach ($html->find('form') as $elm) { 1525 if (!preg_match('/form-horizontal/', $elm->class)) { 1526 $elm->class .= ' form-inline'; 1527 } 1528 } 1529 1530 # Alerts 1531 foreach ($html->find('div.info, div.error, div.success, div.notify') as $elm) { 1532 switch ($elm->class) { 1533 case 'info': 1534 $elm->class = 'alert alert-info'; 1535 $elm->innertext = iconify('mdi:information') . ' ' . $elm->innertext; 1536 break; 1537 1538 case 'error': 1539 $elm->class = 'alert alert-danger'; 1540 $elm->innertext = iconify('mdi:alert-octagon') . ' ' . $elm->innertext; 1541 break; 1542 1543 case 'success': 1544 $elm->class = 'alert alert-success'; 1545 $elm->innertext = iconify('mdi:check-circle') . ' ' . $elm->innertext; 1546 break; 1547 1548 case 'notify': 1549 case 'msg notify': 1550 $elm->class = 'alert alert-warning'; 1551 $elm->innertext = iconify('mdi:alert') . ' ' . $elm->innertext; 1552 break; 1553 } 1554 } 1555 1556 # Tables 1557 1558 $table_classes = 'table'; 1559 1560 foreach ($this->getConf('tableStyle') as $class) { 1561 if ($class == 'responsive') { 1562 foreach ($html->find('div.table') as $elm) { 1563 $elm->class = str_replace($elm->class, 'table', ''); 1564 $elm->class .= ' table-responsive'; 1565 } 1566 } else { 1567 $table_classes .= " table-$class"; 1568 } 1569 } 1570 1571 foreach ($html->find('table.inline,table.import_failures') as $elm) { 1572 $elm->class .= " $table_classes"; 1573 } 1574 1575 foreach ($html->find('div.table') as $elm) { 1576 $elm->class = trim(str_replace('table', '', $elm->class)); 1577 } 1578 1579 # Tag and Pagelist (table) 1580 1581 if ($this->getPlugin('tag') || $this->getPlugin('pagelist')) { 1582 foreach ($html->find('table.ul') as $elm) { 1583 $elm->class .= " $table_classes"; 1584 } 1585 } 1586 1587 $content = $html->save(); 1588 1589 $html->clear(); 1590 unset($html); 1591 1592 # ----- Actions ----- 1593 1594 # Search 1595 1596 if ($ACT == 'search') { 1597 # Import HTML string 1598 $html = new \simple_html_dom; 1599 $html->load($content, true, false); 1600 1601 foreach ($html->find('fieldset.search-form button[type="submit"]') as $elm) { 1602 $elm->class .= ' btn-primary'; 1603 $elm->innertext = iconify('mdi:magnify', ['class' => 'mr-2']) . $elm->innertext; 1604 } 1605 1606 $content = $html->save(); 1607 1608 $html->clear(); 1609 unset($html); 1610 } 1611 1612 # Index / Sitemap 1613 1614 if ($ACT == 'index') { 1615 # Import HTML string 1616 $html = new \simple_html_dom; 1617 $html->load($content, true, false); 1618 1619 foreach ($html->find('.idx_dir') as $idx => $elm) { 1620 $parent = $elm->parent()->parent(); 1621 1622 if (preg_match('/open/', $parent->class)) { 1623 $elm->innertext = iconify('mdi:folder-open', ['class' => 'text-primary mr-2']) . $elm->innertext; 1624 } 1625 1626 if (preg_match('/closed/', $parent->class)) { 1627 $elm->innertext = iconify('mdi:folder', ['class' => 'text-primary mr-2']) . $elm->innertext; 1628 } 1629 } 1630 1631 foreach ($html->find('.idx .wikilink1') as $elm) { 1632 $elm->innertext = iconify('mdi:file-document-outline', ['class' => 'text-muted mr-2']) . $elm->innertext; 1633 } 1634 1635 $content = $html->save(); 1636 1637 $html->clear(); 1638 unset($html); 1639 } 1640 1641 # Admin Pages 1642 1643 if ($ACT == 'admin') { 1644 # Import HTML string 1645 $html = new \simple_html_dom; 1646 $html->load($content, true, false); 1647 1648 // Set specific icon in Admin Page 1649 if ($INPUT->str('page')) { 1650 if ($admin_pagetitle = $html->find('h1.page-header', 0)) { 1651 $admin_pagetitle->class .= ' ' . hsc($INPUT->str('page')); 1652 } 1653 } 1654 1655 # ACL 1656 1657 if ($INPUT->str('page') == 'acl') { 1658 foreach ($html->find('[name*=cmd[update]]') as $elm) { 1659 $elm->class .= ' btn-success'; 1660 if ($elm->tag == 'button') { 1661 $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext; 1662 } 1663 } 1664 } 1665 1666 # Popularity 1667 1668 if ($INPUT->str('page') == 'popularity') { 1669 foreach ($html->find('[type=submit]') as $elm) { 1670 $elm->class .= ' btn-primary'; 1671 1672 if ($elm->tag == 'button') { 1673 $elm->innertext = iconify('mdi:arrow-right') . ' ' . $elm->innertext; 1674 } 1675 } 1676 } 1677 1678 # Revert Manager 1679 1680 if ($INPUT->str('page') == 'revert') { 1681 foreach ($html->find('[type=submit]') as $idx => $elm) { 1682 if ($idx == 0) { 1683 $elm->class .= ' btn-primary'; 1684 if ($elm->tag == 'button') { 1685 $elm->innertext = iconify('mdi:magnify') . ' ' . $elm->innertext; 1686 } 1687 } 1688 1689 if ($idx == 1) { 1690 $elm->class .= ' btn-success'; 1691 if ($elm->tag == 'button') { 1692 $elm->innertext = iconify('mdi:refresh') . ' ' . $elm->innertext; 1693 } 1694 } 1695 } 1696 } 1697 1698 # Config 1699 1700 if ($INPUT->str('page') == 'config') { 1701 foreach ($html->find('[type=submit]') as $elm) { 1702 $elm->class .= ' btn-success'; 1703 if ($elm->tag == 'button') { 1704 $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext; 1705 } 1706 } 1707 1708 foreach ($html->find('#config__manager') as $cm_elm) { 1709 $save_button = ''; 1710 1711 foreach ($cm_elm->find('p') as $elm) { 1712 $save_button = '<div class="pull-right">' . $elm->outertext . '</div>'; 1713 $elm->outertext = '</div>' . $elm->outertext; 1714 } 1715 1716 foreach ($cm_elm->find('fieldset') as $elm) { 1717 $elm->innertext .= $save_button; 1718 } 1719 } 1720 } 1721 1722 # User Manager 1723 1724 if ($INPUT->str('page') == 'usermanager') { 1725 foreach ($html->find('.notes') as $elm) { 1726 $elm->class = str_replace('notes', '', $elm->class); 1727 } 1728 1729 foreach ($html->find('h2') as $idx => $elm) { 1730 switch ($idx) { 1731 case 0: 1732 $elm->innertext = iconify('mdi:account-multiple') . ' ' . $elm->innertext; 1733 break; 1734 case 1: 1735 $elm->innertext = iconify('mdi:account-plus') . ' ' . $elm->innertext; 1736 break; 1737 case 2: 1738 $elm->innertext = iconify('mdi:account-edit') . ' ' . $elm->innertext; 1739 break; 1740 } 1741 } 1742 1743 foreach ($html->find('.import_users h2') as $elm) { 1744 $elm->innertext = iconify('mdi:account-multiple-plus') . ' ' . $elm->innertext; 1745 } 1746 1747 foreach ($html->find('button[name*=fn[delete]]') as $elm) { 1748 $elm->class .= ' btn btn-danger'; 1749 $elm->innertext = iconify('mdi:account-minus') . ' ' . $elm->innertext; 1750 } 1751 1752 foreach ($html->find('button[name*=fn[add]]') as $elm) { 1753 $elm->class .= ' btn btn-success'; 1754 $elm->innertext = iconify('mdi:plus') . ' ' . $elm->innertext; 1755 } 1756 1757 foreach ($html->find('button[name*=fn[modify]]') as $elm) { 1758 $elm->class .= ' btn btn-success'; 1759 $elm->innertext = iconify('mdi:content-save') . ' ' . $elm->innertext; 1760 } 1761 1762 foreach ($html->find('button[name*=fn[import]]') as $elm) { 1763 $elm->class .= ' btn btn-primary'; 1764 $elm->innertext = iconify('mdi:upload') . ' ' . $elm->innertext; 1765 } 1766 1767 foreach ($html->find('button[name*=fn[export]]') as $elm) { 1768 $elm->class .= ' btn btn-primary'; 1769 $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext; 1770 } 1771 1772 foreach ($html->find('button[name*=fn[start]]') as $elm) { 1773 $elm->class .= ' btn btn-default'; 1774 $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext; 1775 } 1776 1777 foreach ($html->find('button[name*=fn[prev]]') as $elm) { 1778 $elm->class .= ' btn btn-default'; 1779 $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext; 1780 } 1781 1782 foreach ($html->find('button[name*=fn[next]]') as $elm) { 1783 $elm->class .= ' btn btn-default'; 1784 $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext; 1785 } 1786 1787 foreach ($html->find('button[name*=fn[last]]') as $elm) { 1788 $elm->class .= ' btn btn-default'; 1789 $elm->innertext = iconify('mdi:chevron-double-right') . ' ' . $elm->innertext; 1790 } 1791 } 1792 1793 # Extension Manager 1794 1795 if ($INPUT->str('page') == 'extension') { 1796 foreach ($html->find('.actions') as $elm) { 1797 $elm->class .= ' pl-4 btn-group btn-group-xs'; 1798 } 1799 1800 foreach ($html->find('.actions .uninstall') as $elm) { 1801 $elm->class .= ' btn-danger'; 1802 $elm->innertext = iconify('mdi:delete') . ' ' . $elm->innertext; 1803 } 1804 1805 foreach ($html->find('.actions .enable') as $elm) { 1806 $elm->class .= ' btn-success'; 1807 $elm->innertext = iconify('mdi:check') . ' ' . $elm->innertext; 1808 } 1809 1810 foreach ($html->find('.actions .disable') as $elm) { 1811 $elm->class .= ' btn-warning'; 1812 $elm->innertext = iconify('mdi:block-helper') . ' ' . $elm->innertext; 1813 } 1814 1815 foreach ($html->find('.actions .install, .actions .update, .actions .reinstall') as $elm) { 1816 $elm->class .= ' btn-primary'; 1817 $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext; 1818 } 1819 1820 foreach ($html->find('form.install [type=submit]') as $elm) { 1821 $elm->class .= ' btn btn-success'; 1822 $elm->innertext = iconify('mdi:download') . ' ' . $elm->innertext; 1823 } 1824 1825 foreach ($html->find('form.search [type=submit]') as $elm) { 1826 $elm->class .= ' btn btn-primary'; 1827 $elm->innertext = iconify('mdi:cloud-search') . ' ' . $elm->innertext; 1828 } 1829 1830 foreach ($html->find('.permerror') as $elm) { 1831 $elm->class .= ' pull-left'; 1832 } 1833 } 1834 1835 # Admin page 1836 if ($INPUT->str('page') == null) { 1837 foreach ($html->find('ul.admin_tasks, ul.admin_plugins') as $admin_task) { 1838 $admin_task->class .= ' list-group'; 1839 1840 foreach ($admin_task->find('a') as $item) { 1841 $item->class .= ' list-group-item'; 1842 $item->style = 'max-height: 50px'; # TODO remove 1843 } 1844 1845 foreach ($admin_task->find('.icon') as $item) { 1846 if ($item->innertext) { 1847 continue; 1848 } 1849 1850 $item->innertext = iconify('mdi:puzzle', ['class' => 'text-success']); 1851 } 1852 } 1853 1854 foreach ($html->find('h2') as $elm) { 1855 $elm->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $elm->innertext; 1856 } 1857 1858 foreach ($html->find('ul.admin_plugins') as $admin_plugins) { 1859 $admin_plugins->class .= ' col-sm-4'; 1860 foreach ($admin_plugins->find('li') as $idx => $item) { 1861 if ($idx > 0 && $idx % 5 == 0) { 1862 $item->outertext = '</ul><ul class="' . $admin_plugins->class . '">' . $item->outertext; 1863 } 1864 } 1865 } 1866 1867 # DokuWiki logo 1868 if ($admin_version = $html->getElementById('admin__version')) { 1869 $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>'; 1870 1871 $template_version = $this->getVersion(); 1872 1873 $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>'; 1874 } 1875 } 1876 1877 $content = $html->save(); 1878 1879 $html->clear(); 1880 unset($html); 1881 1882 # Configuration Manager Template Sections 1883 if ($INPUT->str('page') == 'config') { 1884 # Import HTML string 1885 $html = new \simple_html_dom; 1886 $html->load($content, true, false); 1887 1888 foreach ($html->find('fieldset[id^="plugin__"]') as $elm) { 1889 1890 /** @var array $matches */ 1891 preg_match('/plugin_+(\w+[^_])_+plugin_settings_name/', $elm->id, $matches); 1892 1893 $plugin_name = $matches[1]; 1894 1895 if ($extension = plugin_load('helper', 'extension_extension')) { 1896 if ($extension->setExtension($plugin_name)) { 1897 foreach ($elm->find('legend') as $legend) { 1898 $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>'; 1899 } 1900 } 1901 } else { 1902 foreach ($elm->find('legend') as $legend) { 1903 $legend->innertext = iconify('mdi:puzzle', ['class' => 'text-success']) . ' ' . $legend->innertext; 1904 } 1905 } 1906 } 1907 1908 $dokuwiki_configs = [ 1909 '#_basic' => 'mdi:settings', 1910 '#_display' => 'mdi:monitor', 1911 '#_authentication' => 'mdi:shield-account', 1912 '#_anti_spam' => 'mdi:block-helper', 1913 '#_editing' => 'mdi:pencil', 1914 '#_links' => 'mdi:link-variant', 1915 '#_media' => 'mdi:folder-image', 1916 '#_notifications' => 'mdi:email', 1917 '#_syndication' => 'mdi:rss', 1918 '#_advanced' => 'mdi:palette-advanced', 1919 '#_network' => 'mdi:network', 1920 ]; 1921 1922 foreach ($dokuwiki_configs as $selector => $icon) { 1923 foreach ($html->find("$selector legend") as $elm) { 1924 $elm->innertext = iconify($icon) . ' ' . $elm->innertext; 1925 } 1926 } 1927 1928 $content = $html->save(); 1929 1930 $html->clear(); 1931 unset($html); 1932 1933 $admin_sections = [ 1934 // Section => [ Insert Before, Icon ] 1935 'theme' => ['bootstrapTheme', 'mdi:palette'], 1936 'sidebar' => ['sidebarPosition', 'mdi:page-layout-sidebar-left'], 1937 'navbar' => ['inverseNavbar', 'mdi:page-layout-header'], 1938 'semantic' => ['semantic', 'mdi:share-variant'], 1939 'layout' => ['fluidContainer', 'mdi:monitor'], 1940 'toc' => ['tocAffix', 'mdi:view-list'], 1941 'discussion' => ['showDiscussion', 'mdi:comment-text-multiple'], 1942 'avatar' => ['useAvatar', 'mdi:account'], 1943 'cookie_law' => ['showCookieLawBanner', 'mdi:scale-balance'], 1944 'google_analytics' => ['useGoogleAnalytics', 'mdi:google'], 1945 'browser_title' => ['browserTitle', 'mdi:format-title'], 1946 'page' => ['showPageInfo', 'mdi:file'], 1947 ]; 1948 1949 foreach ($admin_sections as $section => $items) { 1950 $search = $items[0]; 1951 $icon = $items[1]; 1952 1953 $content = preg_replace( 1954 '/<span class="outkey">(tpl»bootstrap3»' . $search . ')<\/span>/', 1955 '<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>', 1956 $content 1957 ); 1958 } 1959 } 1960 } 1961 1962 # Difference and Draft 1963 1964 if ($ACT == 'diff' || $ACT == 'draft') { 1965 # Import HTML string 1966 $html = new \simple_html_dom; 1967 $html->load($content, true, false); 1968 1969 foreach ($html->find('.diff-lineheader') as $elm) { 1970 $elm->style = 'opacity: 0.5'; 1971 $elm->class .= ' text-center px-3'; 1972 1973 if ($elm->innertext == '+') { 1974 $elm->class .= ' bg-success'; 1975 } 1976 if ($elm->innertext == '-') { 1977 $elm->class .= ' bg-danger'; 1978 } 1979 } 1980 1981 foreach ($html->find('.diff_sidebyside .diff-deletedline, .diff_sidebyside .diff-addedline') as $elm) { 1982 $elm->class .= ' w-50'; 1983 } 1984 1985 foreach ($html->find('.diff-deletedline') as $elm) { 1986 $elm->class .= ' bg-danger'; 1987 } 1988 1989 foreach ($html->find('.diff-addedline') as $elm) { 1990 $elm->class .= ' bg-success'; 1991 } 1992 1993 foreach ($html->find('.diffprevrev') as $elm) { 1994 $elm->class .= ' btn btn-default'; 1995 $elm->innertext = iconify('mdi:chevron-left') . ' ' . $elm->innertext; 1996 } 1997 1998 foreach ($html->find('.diffnextrev') as $elm) { 1999 $elm->class .= ' btn btn-default'; 2000 $elm->innertext = iconify('mdi:chevron-right') . ' ' . $elm->innertext; 2001 } 2002 2003 foreach ($html->find('.diffbothprevrev') as $elm) { 2004 $elm->class .= ' btn btn-default'; 2005 $elm->innertext = iconify('mdi:chevron-double-left') . ' ' . $elm->innertext; 2006 } 2007 2008 foreach ($html->find('.minor') as $elm) { 2009 $elm->class .= ' text-muted'; 2010 } 2011 2012 $content = $html->save(); 2013 2014 $html->clear(); 2015 unset($html); 2016 } 2017 2018 # Add icons for Extensions, Actions, etc. 2019 2020 $svg_icon = null; 2021 $iconify_icon = null; 2022 $iconify_attrs = ['class' => 'mr-2']; 2023 2024 if (!$INFO['exists'] && $ACT == 'show') { 2025 $iconify_icon = 'mdi:alert'; 2026 $iconify_attrs['style'] = 'color:orange'; 2027 } 2028 2029 $menu_class = "\\dokuwiki\\Menu\\Item\\$ACT"; 2030 2031 if (class_exists($menu_class, false)) { 2032 $menu_item = new $menu_class; 2033 $svg_icon = $menu_item->getSvg(); 2034 } 2035 2036 switch ($ACT) { 2037 case 'admin': 2038 2039 if (($plugin = plugin_load('admin', $INPUT->str('page'))) !== null) { 2040 if (method_exists($plugin, 'getMenuIcon')) { 2041 $svg_icon = $plugin->getMenuIcon(); 2042 2043 if (!file_exists($svg_icon)) { 2044 $iconify_icon = 'mdi:puzzle'; 2045 $svg_icon = null; 2046 } 2047 } else { 2048 $iconify_icon = 'mdi:puzzle'; 2049 $svg_icon = null; 2050 } 2051 } 2052 2053 break; 2054 2055 case 'resendpwd': 2056 $iconify_icon = 'mdi:lock-reset'; 2057 break; 2058 2059 case 'denied': 2060 $iconify_icon = 'mdi:block-helper'; 2061 $iconify_attrs['style'] = 'color:red'; 2062 break; 2063 2064 case 'search': 2065 $iconify_icon = 'mdi:search-web'; 2066 break; 2067 2068 case 'preview': 2069 $iconify_icon = 'mdi:file-eye'; 2070 break; 2071 2072 case 'diff': 2073 $iconify_icon = 'mdi:file-compare'; 2074 break; 2075 2076 case 'showtag': 2077 $iconify_icon = 'mdi:tag-multiple'; 2078 break; 2079 2080 case 'draft': 2081 $iconify_icon = 'mdi:android-studio'; 2082 break; 2083 2084 } 2085 2086 if ($svg_icon) { 2087 $svg_attrs = ['class' => 'iconify mr-2']; 2088 2089 if ($ACT == 'admin' && $INPUT->str('page') == 'extension') { 2090 $svg_attrs['style'] = 'fill: green;'; 2091 } 2092 2093 $svg = SVG::icon($svg_icon, null, '1em', $svg_attrs); 2094 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 = $svg . ' ' . $elm->innertext; 2101 break; 2102 } 2103 2104 $content = $html->save(); 2105 2106 $html->clear(); 2107 unset($html); 2108 } 2109 2110 if ($iconify_icon) { 2111 # Import HTML string 2112 $html = new \simple_html_dom; 2113 $html->load($content, true, false); 2114 2115 foreach ($html->find('h1') as $elm) { 2116 $elm->innertext = iconify($iconify_icon, $iconify_attrs) . $elm->innertext; 2117 break; 2118 } 2119 2120 $content = $html->save(); 2121 2122 $html->clear(); 2123 unset($html); 2124 } 2125 2126 return $content; 2127 } 2128 2129 /** 2130 * Detect the fluid navbar flag 2131 * 2132 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 2133 * @return boolean 2134 */ 2135 public function isFluidNavbar() 2136 { 2137 $fluid_container = $this->getConf('fluidContainer'); 2138 $fixed_top_nabvar = $this->getConf('fixedTopNavbar'); 2139 2140 return ($fluid_container || ($fluid_container && !$fixed_top_nabvar) || (!$fluid_container && !$fixed_top_nabvar)); 2141 } 2142 2143 /** 2144 * Calculate automatically the grid size for main container 2145 * 2146 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 2147 * 2148 * @return string 2149 */ 2150 public function getContainerGrid() 2151 { 2152 global $ID; 2153 2154 $result = ''; 2155 2156 $grids = [ 2157 'sm' => ['left' => 0, 'right' => 0], 2158 'md' => ['left' => 0, 'right' => 0], 2159 ]; 2160 2161 $show_right_sidebar = $this->getConf('showRightSidebar'); 2162 $show_left_sidebar = $this->getConf('showSidebar'); 2163 $fluid_container = $this->getConf('fluidContainer'); 2164 2165 if ($this->getConf('showLandingPage') && (bool) preg_match($this->getConf('landingPages'), $ID)) { 2166 $show_left_sidebar = false; 2167 } 2168 2169 if ($show_left_sidebar) { 2170 foreach (explode(' ', $this->getConf('leftSidebarGrid')) as $grid) { 2171 list($col, $media, $size) = explode('-', $grid); 2172 $grids[$media]['left'] = (int) $size; 2173 } 2174 } 2175 2176 if ($show_right_sidebar) { 2177 foreach (explode(' ', $this->getConf('rightSidebarGrid')) as $grid) { 2178 list($col, $media, $size) = explode('-', $grid); 2179 $grids[$media]['right'] = (int) $size; 2180 } 2181 } 2182 2183 foreach ($grids as $media => $position) { 2184 $left = $position['left']; 2185 $right = $position['right']; 2186 $result .= sprintf('col-%s-%s ', $media, (12 - $left - $right)); 2187 } 2188 2189 return $result; 2190 } 2191 2192 /** 2193 * Places the TOC where the function is called 2194 * 2195 * If you use this you most probably want to call tpl_content with 2196 * a false argument 2197 * 2198 * @author Andreas Gohr <andi@splitbrain.org> 2199 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 2200 * 2201 * @param bool $return Should the TOC be returned instead to be printed? 2202 * @return string 2203 */ 2204 public function getTOC($return = false) 2205 { 2206 global $TOC; 2207 global $ACT; 2208 global $ID; 2209 global $REV; 2210 global $INFO; 2211 global $conf; 2212 global $INPUT; 2213 2214 $toc = []; 2215 2216 if (is_array($TOC)) { 2217 // if a TOC was prepared in global scope, always use it 2218 $toc = $TOC; 2219 } elseif (($ACT == 'show' || substr($ACT, 0, 6) == 'export') && !$REV && $INFO['exists']) { 2220 // get TOC from metadata, render if neccessary 2221 $meta = p_get_metadata($ID, '', METADATA_RENDER_USING_CACHE); 2222 if (isset($meta['internal']['toc'])) { 2223 $tocok = $meta['internal']['toc']; 2224 } else { 2225 $tocok = true; 2226 } 2227 $toc = isset($meta['description']['tableofcontents']) ? $meta['description']['tableofcontents'] : null; 2228 if (!$tocok || !is_array($toc) || !$conf['tocminheads'] || count($toc) < $conf['tocminheads']) { 2229 $toc = []; 2230 } 2231 } elseif ($ACT == 'admin') { 2232 // try to load admin plugin TOC 2233 /** @var $plugin DokuWiki_Admin_Plugin */ 2234 if ($plugin = plugin_getRequestAdminPlugin()) { 2235 $toc = $plugin->getTOC(); 2236 $TOC = $toc; // avoid later rebuild 2237 } 2238 } 2239 2240 $toc_check = end($toc); 2241 $toc_undefined = null; 2242 2243 if (isset($toc_check['link']) && !preg_match('/bootstrap/', $toc_check['link'])) { 2244 $toc_undefined = array_pop($toc); 2245 } 2246 2247 \dokuwiki\Extension\Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false); 2248 2249 if ($ACT == 'admin' && $INPUT->str('page') == 'config') { 2250 $bootstrap3_sections = [ 2251 'theme', 'sidebar', 'navbar', 'semantic', 'layout', 'toc', 2252 'discussion', 'avatar', 'cookie_law', 'google_analytics', 2253 'browser_title', 'page', 2254 ]; 2255 2256 foreach ($bootstrap3_sections as $id) { 2257 $toc[] = [ 2258 'link' => "#bootstrap3__$id", 2259 'title' => tpl_getLang("config_$id"), 2260 'type' => 'ul', 2261 'level' => 3, 2262 ]; 2263 } 2264 } 2265 2266 if ($toc_undefined) { 2267 $toc[] = $toc_undefined; 2268 } 2269 2270 $html = $this->renderTOC($toc); 2271 2272 if ($return) { 2273 return $html; 2274 } 2275 2276 echo $html; 2277 return ''; 2278 } 2279 2280 /** 2281 * Return the TOC rendered to XHTML with Bootstrap3 style 2282 * 2283 * @author Andreas Gohr <andi@splitbrain.org> 2284 * @author Giuseppe Di Terlizzi <giuseppe.diterlizzi@gmail.com> 2285 * 2286 * @param array $toc 2287 * @return string html 2288 */ 2289 private function renderTOC($toc) 2290 { 2291 if (!count($toc)) { 2292 return ''; 2293 } 2294 2295 global $lang; 2296 2297 $json_toc = []; 2298 2299 foreach ($toc as $item) { 2300 $json_toc[] = [ 2301 'link' => (isset($item['link']) ? $item['link'] : '#' . $item['hid']), 2302 'title' => $item['title'], 2303 'level' => $item['level'], 2304 ]; 2305 } 2306 2307 $out = ''; 2308 $out .= '<script>JSINFO.bootstrap3.toc = ' . json_encode($json_toc) . ';</script>' . DOKU_LF; 2309 2310 if ($this->getConf('tocLayout') !== 'navbar') { 2311 $out .= '<!-- TOC START -->' . DOKU_LF; 2312 $out .= '<div class="dw-toc hidden-print">' . DOKU_LF; 2313 $out .= '<nav id="dw__toc" role="navigation" class="toc-panel panel panel-default small">' . DOKU_LF; 2314 $out .= '<h6 data-toggle="collapse" data-target="#dw__toc .toc-body" title="' . $lang['toc'] . '" class="panel-heading toc-title">' . iconify('mdi:view-list') . ' '; 2315 $out .= '<span>' . $lang['toc'] . '</span>'; 2316 $out .= ' <i class="caret"></i></h6>' . DOKU_LF; 2317 $out .= '<div class="panel-body toc-body collapse ' . (!$this->getConf('tocCollapsed') ? 'in' : '') . '">' . DOKU_LF; 2318 $out .= $this->normalizeList(html_buildlist($toc, 'nav toc', 'html_list_toc', 'html_li_default', true)) . DOKU_LF; 2319 $out .= '</div>' . DOKU_LF; 2320 $out .= '</nav>' . DOKU_LF; 2321 $out .= '</div>' . DOKU_LF; 2322 $out .= '<!-- TOC END -->' . DOKU_LF; 2323 } 2324 2325 return $out; 2326 } 2327 2328 private function initToolsMenu() 2329 { 2330 global $ACT; 2331 2332 $tools_menus = [ 2333 'user' => ['icon' => 'mdi:account', 'object' => new \dokuwiki\Menu\UserMenu], 2334 'site' => ['icon' => 'mdi:toolbox', 'object' => new \dokuwiki\Menu\SiteMenu], 2335 'page' => ['icon' => 'mdi:file-document-outline', 'object' => new \dokuwiki\template\bootstrap3\Menu\PageMenu], 2336 ]; 2337 2338 if (defined('DOKU_MEDIADETAIL')) { 2339 $tools_menus['page'] = ['icon' => 'mdi:image', 'object' => new \dokuwiki\template\bootstrap3\Menu\DetailMenu]; 2340 } 2341 2342 foreach ($tools_menus as $tool => $data) { 2343 foreach ($data['object']->getItems() as $item) { 2344 $attr = buildAttributes($item->getLinkAttributes()); 2345 $active = 'action'; 2346 2347 if ($ACT == $item->getType() || ($ACT == 'revisions' && $item->getType() == 'revs') || ($ACT == 'diff' && $item->getType() == 'revs')) { 2348 $active .= ' active'; 2349 } 2350 2351 if ($item->getType() == 'shareon') { 2352 $active .= ' dropdown'; 2353 } 2354 2355 $html = '<li class="' . $active . '">'; 2356 $html .= "<a $attr>"; 2357 $html .= \inlineSVG($item->getSvg()); 2358 $html .= '<span>' . hsc($item->getLabel()) . '</span>'; 2359 $html .= "</a>"; 2360 2361 if ($item->getType() == 'shareon') { 2362 $html .= $item->getDropDownMenu(); 2363 } 2364 2365 $html .= '</li>'; 2366 2367 $tools_menus[$tool]['menu'][$item->getType()]['object'] = $item; 2368 $tools_menus[$tool]['menu'][$item->getType()]['html'] = $html; 2369 } 2370 } 2371 2372 $this->toolsMenu = $tools_menus; 2373 } 2374 2375 public function getToolsMenu() 2376 { 2377 return $this->toolsMenu; 2378 } 2379 2380 public function getToolMenu($tool) 2381 { 2382 return $this->toolsMenu[$tool]; 2383 } 2384 2385 public function getToolMenuItem($tool, $item) 2386 { 2387 if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) { 2388 return $this->toolsMenu[$tool]['menu'][$item]['object']; 2389 } 2390 return null; 2391 } 2392 2393 public function getToolMenuItemLink($tool, $item) 2394 { 2395 if (isset($this->toolsMenu[$tool]) && isset($this->toolsMenu[$tool]['menu'][$item])) { 2396 return $this->toolsMenu[$tool]['menu'][$item]['html']; 2397 } 2398 return null; 2399 } 2400 2401 public function getNavbarHeight() 2402 { 2403 switch ($this->getBootswatchTheme()) { 2404 case 'simplex': 2405 case 'superhero': 2406 return 40; 2407 2408 case 'yeti': 2409 return 45; 2410 2411 case 'cerulean': 2412 case 'cosmo': 2413 case 'custom': 2414 case 'cyborg': 2415 case 'lumen': 2416 case 'slate': 2417 case 'spacelab': 2418 case 'solar': 2419 case 'united': 2420 return 50; 2421 2422 case 'darkly': 2423 case 'flatly': 2424 case 'journal': 2425 case 'sandstone': 2426 return 60; 2427 2428 case 'paper': 2429 return 64; 2430 2431 case 'readable': 2432 return 65; 2433 2434 default: 2435 return 50; 2436 } 2437 } 2438} 2439