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