You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

2425 lines
76 KiB

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