* @license GPL 2 (http://www.gnu.org/licenses/gpl.html) */ class Template { private $plugins = []; private $confMetadata = []; private $toolsMenu = []; private $handlers; public $tplDir = ''; public $baseDir = ''; public function __construct() { global $JSINFO; global $INPUT; global $ACT; global $INFO; $this->tplDir = tpl_incdir(); $this->baseDir = tpl_basedir(); $this->initPlugins(); $this->initToolsMenu(); $this->loadConfMetadata(); // Get the template info (useful for debug) if (isset($INFO['isadmin']) && $INPUT->str('do') && $INPUT->str('do') == 'check') { msg('Template version ' . $this->getVersion(), 1, '', '', MSG_ADMINS_ONLY); } // Populate JSINFO object $JSINFO['bootstrap3'] = [ 'mode' => $ACT, 'toc' => [], 'config' => [ 'collapsibleSections' => (int) $this->getConf('collapsibleSections'), 'fixedTopNavbar' => (int) $this->getConf('fixedTopNavbar'), 'showSemanticPopup' => (int) $this->getConf('showSemanticPopup'), 'sidebarOnNavbar' => (int) $this->getConf('sidebarOnNavbar'), 'tagsOnTop' => (int) $this->getConf('tagsOnTop'), 'tocAffix' => (int) $this->getConf('tocAffix'), 'tocCollapseOnScroll' => (int) $this->getConf('tocCollapseOnScroll'), 'tocCollapsed' => (int) $this->getConf('tocCollapsed'), 'tocLayout' => $this->getConf('tocLayout'), 'useAnchorJS' => (int) $this->getConf('useAnchorJS'), 'useAlternativeToolbarIcons' => (int) $this->getConf('useAlternativeToolbarIcons'), 'disableSearchSuggest' => (int) $this->getConf('disableSearchSuggest'), ], ]; if ($ACT == 'admin') { $JSINFO['bootstrap3']['admin'] = hsc($INPUT->str('page')); } if (!defined('MAX_FILE_SIZE') && $pagesize = $this->getConf('domParserMaxPageSize')) { define('MAX_FILE_SIZE', $pagesize); } # Start Event Handlers $this->handlers = new EventHandlers($this); } public function getVersion() { $template_info = confToHash($this->tplDir . 'template.info.txt'); $template_version = 'v' . $template_info['date']; if (isset($template_info['build'])) { $template_version .= ' (' . $template_info['build'] . ')'; } return $template_version; } private function initPlugins() { $plugins = ['tplinc', 'tag', 'userhomepage', 'translation', 'pagelist']; foreach ($plugins as $plugin) { $this->plugins[$plugin] = plugin_load('helper', $plugin); } } public function getPlugin($plugin) { if (plugin_isdisabled($plugin)) { return false; } if (!isset($this->plugins[$plugin])) { return false; } return $this->plugins[$plugin]; } /** * Get the singleton instance * * @return Template */ public static function getInstance() { static $instance = null; if ($instance === null) { $instance = new self; } return $instance; } /** * Get the content to include from the tplinc plugin * * prefix and postfix are only added when there actually is any content * * @param string $location * @return string */ public function includePage($location, $return = false) { $content = ''; if ($plugin = $this->getPlugin('tplinc')) { $content = $plugin->renderIncludes($location); } if ($content === '') { $content = tpl_include_page($location, 0, 1, $this->getConf('useACL')); } if ($content === '') { return ''; } $content = $this->normalizeContent($content); if ($return) { return $content; } echo $content; return ''; } /** * Get the template configuration metadata * * @author Giuseppe Di Terlizzi * * @param string $key * @return array|string */ public function getConfMetadata($key = null) { if ($key && isset($this->confMetadata[$key])) { return $this->confMetadata[$key]; } return null; } private function loadConfMetadata() { $meta = []; $file = $this->tplDir . 'conf/metadata.php'; include $file; $this->confMetadata = $meta; } /** * Simple wrapper for tpl_getConf * * @author Giuseppe Di Terlizzi * * @param string $key * @param mixed $default value * @return mixed */ public function getConf($key, $default = false) { global $ACT, $INFO, $ID, $conf; $value = tpl_getConf($key, $default); switch ($key) { case 'useAvatar': if ($value == 'off') { return false; } return $value; case 'bootstrapTheme': @list($theme, $bootswatch) = $this->getThemeForNamespace(); if ($theme) { return $theme; } return $value; case 'bootswatchTheme': @list($theme, $bootswatch) = $this->getThemeForNamespace(); if ($bootswatch) { return $bootswatch; } return $value; case 'showTools': case 'showSearchForm': case 'showPageTools': case 'showEditBtn': case 'showAddNewPage': return $value !== 'never' && ($value == 'always' || !empty($_SERVER['REMOTE_USER'])); case 'showAdminMenu': return $value && ($INFO['isadmin'] || $INFO['ismanager']); case 'hideLoginLink': case 'showLoginOnFooter': return ($value && !isset($_SERVER['REMOTE_USER'])); case 'showCookieLawBanner': return $value && page_findnearest(tpl_getConf('cookieLawBannerPage'), $this->getConf('useACL')) && ($ACT == 'show'); case 'showSidebar': if ($ACT !== 'show') { return false; } if ($this->getConf('showLandingPage')) { return false; } return page_findnearest($conf['sidebar'], $this->getConf('useACL')); case 'showRightSidebar': if ($ACT !== 'show') { return false; } if ($this->getConf('sidebarPosition') == 'right') { return false; } return page_findnearest(tpl_getConf('rightSidebar'), $this->getConf('useACL')); case 'showLandingPage': return ($value && (bool) preg_match_all($this->getConf('landingPages'), $ID)); case 'pageOnPanel': if ($this->getConf('showLandingPage')) { return false; } return $value; case 'showThemeSwitcher': return $value && ($this->getConf('bootstrapTheme') == 'bootswatch'); case 'tocCollapseSubSections': if (!$this->getConf('tocAffix')) { return false; } return $value; case 'schemaOrgType': if ($semantic = plugin_load('helper', 'semantic')) { if (method_exists($semantic, 'getSchemaOrgType')) { return $semantic->getSchemaOrgType(); } } return $value; case 'tocCollapseOnScroll': if ($this->getConf('tocLayout') !== 'default') { return false; } return $value; } $metadata = $this->getConfMetadata($key); if (isset($metadata[0])) { switch ($metadata[0]) { case 'regex': return '/' . $value . '/'; case 'multicheckbox': return explode(',', $value); } } return $value; } /** * Return the Bootswatch.com theme lists defined in metadata.php * * @author Giuseppe Di Terlizzi * * @return array */ public function getBootswatchThemeList() { $bootswatch_themes = $this->getConfMetadata('bootswatchTheme'); return $bootswatch_themes['_choices']; } /** * Get a Gravatar, Libravatar, Office365/EWS URL or local ":user" DokuWiki namespace * * @author Giuseppe Di Terlizzi * * @param string $username User ID * @param string $email The email address * @param string $size Size in pixels, defaults to 80px [ 1 - 2048 ] * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ] * @param string $r Maximum rating (inclusive) [ g | pg | r | x ] * * @return string */ public function getAvatar($username, $email, $size = 80, $d = 'mm', $r = 'g') { global $INFO; $avatar_url = ''; $avatar_provider = $this->getConf('useAvatar'); if (!$avatar_provider) { return false; } if ($avatar_provider == 'local') { $interwiki = getInterwiki(); $user_url = str_replace('{NAME}', $username, $interwiki['user']); $logo_size = []; $logo = tpl_getMediaFile(["$user_url.png", "$user_url.jpg", 'images/avatar.png'], false, $logo_size); return $logo; } if ($avatar_provider == 'activedirectory') { $logo = "data:image/jpeg;base64," . base64_encode($INFO['userinfo']['thumbnailphoto']); return $logo; } $email = strtolower(trim($email)); if ($avatar_provider == 'office365') { $office365_url = rtrim($this->getConf('office365URL'), '/'); $avatar_url = $office365_url . '/owa/service.svc/s/GetPersonaPhoto?email=' . $email . '&size=HR' . $size . 'x' . $size; } if ($avatar_provider == 'gravatar' || $avatar_provider == 'libavatar') { $gravatar_url = rtrim($this->getConf('gravatarURL'), '/') . '/'; $libavatar_url = rtrim($this->getConf('libavatarURL'), '/') . '/'; switch ($avatar_provider) { case 'gravatar': $avatar_url = $gravatar_url; break; case 'libavatar': $avatar_url = $libavatar_url; break; } $avatar_url .= md5($email); $avatar_url .= "?s=$size&d=$d&r=$r"; } if ($avatar_url) { $media_link = ml("$avatar_url&.jpg", ['cache' => 'recache', 'w' => $size, 'h' => $size]); return $media_link; } return false; } /** * Return template classes * * @author Giuseppe Di Terlizzi * @see tpl_classes(); * * @return string **/ public function getClasses() { global $ACT; $page_on_panel = $this->getConf('pageOnPanel'); $bootstrap_theme = $this->getConf('bootstrapTheme'); $bootswatch_theme = $this->getBootswatchTheme(); $classes = []; $classes[] = (($bootstrap_theme == 'bootswatch') ? $bootswatch_theme : $bootstrap_theme); $classes[] = trim(tpl_classes()); if ($page_on_panel) { $classes[] = 'dw-page-on-panel'; } if (!$this->getConf('tableFullWidth')) { $classes[] = 'dw-table-width'; } if ($this->isFluidNavbar()) { $classes[] = 'dw-fluid-container'; } return implode(' ', $classes); } /** * Return the current Bootswatch theme * * @author Giuseppe Di Terlizzi * * @return string */ public function getBootswatchTheme() { global $INPUT; $bootswatch_theme = $this->getConf('bootswatchTheme'); if ($this->getConf('showThemeSwitcher')) { if (get_doku_pref('bootswatchTheme', null) !== null && get_doku_pref('bootswatchTheme', null) !== '') { $bootswatch_theme = get_doku_pref('bootswatchTheme', null); } } return $bootswatch_theme; } /** * Return only the available Bootswatch.com themes * * @author Giuseppe Di Terlizzi * * @return array */ public function getAvailableBootswatchThemes() { return array_diff($this->getBootswatchThemeList(), $this->getConf('hideInThemeSwitcher')); } /** * Return the active theme * * @return string */ public function getTheme() { $bootstrap_theme = $this->getConf('bootstrapTheme'); $bootswatch_theme = $this->getBootswatchTheme(); $theme = (($bootstrap_theme == 'bootswatch') ? $bootswatch_theme : $bootstrap_theme); return $theme; } /** * Return the active theme * * @return string */ public function getThemeFeatures() { $features = []; if ($this->isFluidNavbar()) { $features[] = 'fluid-container'; } if ($this->getConf('fixedTopNavbar')) { $features[] = 'fixed-top-navbar'; } if ($this->getConf('tocCollapseSubSections')) { $features[] = 'toc-cullapse-sub-sections'; } return implode(' ', $features); } /** * Print some info about the current page * * @author Andreas Gohr * @author Giuseppe Di Terlizzi * * @param bool $ret return content instead of printing it * @return bool|string */ public function getPageInfo($ret = false) { global $conf; global $lang; global $INFO; global $ID; // return if we are not allowed to view the page if (!auth_quickaclcheck($ID)) { return false; } // prepare date and path $fn = $INFO['filepath']; if (!$conf['fullpath']) { if ($INFO['rev']) { $fn = str_replace(fullpath($conf['olddir']) . '/', '', $fn); } else { $fn = str_replace(fullpath($conf['datadir']) . '/', '', $fn); } } $date_format = $this->getConf('pageInfoDateFormat'); $page_info = $this->getConf('pageInfo'); $fn = utf8_decodeFN($fn); $date = (($date_format == 'dformat') ? dformat($INFO['lastmod']) : datetime_h($INFO['lastmod'])); // print it if ($INFO['exists']) { $fn_full = $fn; if (!in_array('extension', $page_info)) { $fn = str_replace(['.txt.gz', '.txt'], '', $fn); } $out = '
    '; if (in_array('filename', $page_info)) { $out .= '
  • ' . iconify('mdi:file-document-outline', ['class' => 'text-muted']) . ' ' . $fn . '
  • '; } if (in_array('date', $page_info)) { $out .= '
  • ' . iconify('mdi:calendar', ['class' => 'text-muted']) . ' ' . $lang['lastmod'] . ' ' . $date . '
  • '; } if (in_array('editor', $page_info)) { if (isset($INFO['editor'])) { $user = editorinfo($INFO['editor']); if ($this->getConf('useAvatar')) { global $auth; $user_data = $auth->getUserData($INFO['editor']); $avatar_img = $this->getAvatar($INFO['editor'], $user_data['mail'], 16); $user_img = ' '; $user = str_replace(['iw_user', 'interwiki'], '', $user); $user = $user_img . "$user"; } $out .= '
  • ' . $lang['by'] . ' ' . $user . '
  • '; } else { $out .= '
  • (' . $lang['external_edit'] . ')
  • '; } } if ($INFO['locked'] && in_array('locked', $page_info)) { $out .= '
  • ' . iconify('mdi:lock', ['class' => 'text-muted']) . ' ' . $lang['lockedby'] . ' ' . editorinfo($INFO['locked']) . '
  • '; } $out .= '
'; if ($ret) { return $out; } else { echo $out; return true; } } return false; } /** * Prints the global message array in Bootstrap style * * @author Andreas Gohr * @author Giuseppe Di Terlizzi * * @see html_msgarea() */ public function getMessageArea() { global $MSG, $MSG_shown; /** @var array $MSG */ // store if the global $MSG has already been shown and thus HTML output has been started $MSG_shown = true; // Check if translation is outdate if ($this->getConf('showTranslation') && $translation = $this->getPlugin('translation')) { global $ID; if ($translation->istranslatable($ID)) { $translation->checkage(); } } if (!isset($MSG)) { return; } $shown = []; foreach ($MSG as $msg) { $hash = md5($msg['msg']); if (isset($shown[$hash])) { continue; } // skip double messages if (info_msg_allowed($msg)) { switch ($msg['lvl']) { case 'info': $level = 'info'; $icon = 'mdi:information'; break; case 'error': $level = 'danger'; $icon = 'mdi:alert-octagon'; break; case 'notify': $level = 'warning'; $icon = 'mdi:alert'; break; case 'success': $level = 'success'; $icon = 'mdi:check-circle'; break; } print '
'; print iconify($icon, ['class' => 'mr-2']); print $msg['msg']; print '
'; } $shown[$hash] = 1; } unset($GLOBALS['MSG']); } /** * Get the license (link or image) * * @author Giuseppe Di Terlizzi * * @param string $type ("link" or "image") * @param integer $size of image * @param bool $return or print * @return string */ public function getLicense($type = 'link', $size = 24, $return = false) { global $conf, $license, $lang; $target = $conf['target']['extern']; $lic = $license[$conf['license']]; $output = ''; if (!$lic) { return ''; } if ($type == 'link') { $output .= $lang['license'] . '
'; } $license_url = $lic['url']; $license_name = $lic['name']; $output .= ''; if ($type == 'image') { foreach (explode('-', $conf['license']) as $license_img) { if ($license_img == 'publicdomain') { $license_img = 'pd'; } $output .= '' . $license_img . ' '; } } else { $output .= $lic['name']; } $output .= ''; if ($return) { return $output; } echo $output; return ''; } /** * Add Google Analytics * * @author Giuseppe Di Terlizzi * * @return string */ public function getGoogleAnalitycs() { global $INFO; global $ID; if (!$this->getConf('useGoogleAnalytics')) { return false; } if (!$google_analitycs_id = $this->getConf('googleAnalyticsTrackID')) { return false; } if ($this->getConf('googleAnalyticsNoTrackAdmin') && $INFO['isadmin']) { return false; } if ($this->getConf('googleAnalyticsNoTrackUsers') && isset($_SERVER['REMOTE_USER'])) { return false; } if (tpl_getConf('googleAnalyticsNoTrackPages')) { if (preg_match_all($this->getConf('googleAnalyticsNoTrackPages'), $ID)) { return false; } } $out = DOKU_LF; $out .= '// Google Analytics' . DOKU_LF; $out .= "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');" . DOKU_LF; $out .= 'ga("create", "' . $google_analitycs_id . '", "auto");' . DOKU_LF; $out .= 'ga("send", "pageview");' . DOKU_LF; if ($this->getConf('googleAnalyticsAnonymizeIP')) { $out .= 'ga("set", "anonymizeIp", true);' . DOKU_LF; } if ($this->getConf('googleAnalyticsTrackActions')) { $out .= 'ga("send", "event", "DokuWiki", JSINFO.bootstrap3.mode);' . DOKU_LF; } $out .= '// End Google Analytics' . DOKU_LF; return $out; } /** * Return the user home-page link * * @author Giuseppe Di Terlizzi * * @return string */ public function getUserHomePageLink() { return wl($this->getUserHomePageID()); } /** * Return the user home-page ID * * @author Giuseppe Di Terlizzi * * @return string */ public function getUserHomePageID() { $interwiki = getInterwiki(); $page_id = str_replace('{NAME}', $_SERVER['REMOTE_USER'], $interwiki['user']); return cleanID($page_id); } /** * Print the breadcrumbs trace with Bootstrap style * * @author Andreas Gohr * @author Giuseppe Di Terlizzi * * @return bool */ public function getBreadcrumbs() { global $lang; global $conf; //check if enabled if (!$conf['breadcrumbs']) { return false; } $crumbs = breadcrumbs(); //setup crumb trace //render crumbs, highlight the last one print ''; } } return true; } /** * Hierarchical breadcrumbs with Bootstrap style * * This code was suggested as replacement for the usual breadcrumbs. * It only makes sense with a deep site structure. * * @author Andreas Gohr * @author Nigel McNie * @author Sean Coates * @author * @author Giuseppe Di Terlizzi * @todo May behave strangely in RTL languages * * @return bool */ public function getYouAreHere() { global $conf; global $ID; global $lang; // check if enabled if (!$conf['youarehere']) { return false; } $parts = explode(':', $ID); $count = count($parts); echo ''; return true; } $page = $part . $parts[$i]; if ($page == $conf['start']) { echo ''; return true; } echo '
  • '; $link = str_replace(['', ''], '', html_wikilink($page)); $link = str_replace('', '', $link); $link = preg_replace('/data-wiki-id="(.+?)"/', '', $link); echo $link; echo ''; echo '
  • '; echo ''; return true; } /** * Display the page title (and previous namespace page title) on browser titlebar * * @author Giuseppe Di Terlizzi * @return string */ public function getBrowserPageTitle() { global $conf, $ACT, $ID; if ($this->getConf('browserTitleShowNS') && $ACT == 'show') { $ns_page = ''; $ns_parts = explode(':', $ID); $ns_pages = []; $ns_titles = []; $ns_separator = sprintf(' %s ', $this->getConf('browserTitleCharSepNS')); if (useHeading('navigation')) { if (count($ns_parts) > 1) { foreach ($ns_parts as $ns_part) { $ns_page .= "$ns_part:"; $ns_pages[] = $ns_page; } $ns_pages = array_unique($ns_pages); foreach ($ns_pages as $ns_page) { $exists = false; resolve_pageid(getNS($ns_page), $ns_page, $exists); $ns_page_title_heading = hsc(p_get_first_heading($ns_page)); $ns_page_title_page = noNSorNS($ns_page); $ns_page_title = ($exists) ? $ns_page_title_heading : null; if ($ns_page_title !== $conf['start']) { $ns_titles[] = $ns_page_title; } } } resolve_pageid(getNS($ID), $ID, $exists); if ($exists) { $ns_titles[] = tpl_pagetitle($ID, true); } else { $ns_titles[] = noNS($ID); } $ns_titles = array_filter(array_unique($ns_titles)); } else { $ns_titles = $ns_parts; } if ($this->getConf('browserTitleOrderNS') == 'normal') { $ns_titles = array_reverse($ns_titles); } $browser_title = implode($ns_separator, $ns_titles); } else { $browser_title = tpl_pagetitle($ID, true); } return str_replace( ['@WIKI@', '@TITLE@'], [strip_tags($conf['title']), $browser_title], $this->getConf('browserTitle') ); } /** * Return the theme for current namespace * * @author Giuseppe Di Terlizzi * @return string */ public function getThemeForNamespace() { global $ID; $themes_filename = DOKU_CONF . 'bootstrap3.themes.conf'; if (!$this->getConf('themeByNamespace')) { return []; } if (!file_exists($themes_filename)) { return []; } $config = confToHash($themes_filename); krsort($config); foreach ($config as $page => $theme) { if (preg_match("/^$page/", "$ID")) { list($bootstrap, $bootswatch) = explode('/', $theme); if ($bootstrap && in_array($bootstrap, ['default', 'optional', 'custom'])) { return [$bootstrap, $bootswatch]; } if ($bootstrap == 'bootswatch' && in_array($bootswatch, $this->getBootswatchThemeList())) { return [$bootstrap, $bootswatch]; } } } return []; } /** * Make a Bootstrap3 Nav * * @author Giuseppe Di Terlizzi * * @param string $html * @param string $type (= pills, tabs, navbar) * @param boolean $staked * @param string $optional_class * @return string */ public function toBootstrapNav($html, $type = '', $stacked = false, $optional_class = '') { $classes = []; $classes[] = 'nav'; $classes[] = $optional_class; switch ($type) { case 'navbar': case 'navbar-nav': $classes[] = 'navbar-nav'; break; case 'pills': case 'tabs': $classes[] = "nav-$type"; break; } if ($stacked) { $classes[] = 'nav-stacked'; } $class = implode(' ', $classes); $output = str_replace( ['
      "], $html ); $output = $this->normalizeList($output); return $output; } /** * Normalize the DokuWiki list items * * @todo use Simple DOM HTML library * @author Giuseppe Di Terlizzi * @todo use Simple DOM HTML * @todo FIX SimpleNavi curid * * @param string $html * @return string */ public function normalizeList($list) { global $ID; $list = preg_replace_callback('/data-wiki-id="(.+?)"/', [$this, '_replaceWikiCurrentIdCallback'], $list); $html = new \simple_html_dom; $html->load($list, true, false); # Create data-curid HTML5 attribute and unwrap span.curid for pre-Hogfather release foreach ($html->find('span.curid') as $elm) { $elm->firstChild()->setAttribute('data-wiki-curid', 'true'); $elm->outertext = str_replace(['', ''], '', $elm->outertext); } # Unwrap div.li element foreach ($html->find('div.li') as $elm) { $elm->outertext = str_replace(['
      ', '
      '], '', $elm->outertext); } $list = $html->save(); $html->clear(); unset($html); $html = new \simple_html_dom; $html->load($list, true, false); foreach ($html->find('li') as $elm) { if ($elm->find('a[data-wiki-curid]')) { $elm->class .= ' active'; } } $list = $html->save(); $html->clear(); unset($html); # TODO optimize $list = preg_replace('/<\/i> (.+?)<\/a>/', ' $3', $list); $list = preg_replace('/<\/span> (.+?)<\/a>/', ' $3', $list); return $list; } /** * Remove data-wiki-id HTML5 attribute * * @todo Remove this in future * @since Hogfather * * @param array $matches * * @return string */ private function _replaceWikiCurrentIdCallback($matches) { global $ID; if ($ID == $matches[1]) { return 'data-wiki-curid="true"'; } return ''; } /** * Return a Bootstrap NavBar and or drop-down menu * * @todo use Simple DOM HTML library * @author Giuseppe Di Terlizzi * * @return string */ public function getNavbar() { if ($this->getConf('showNavbar') === 'logged' && !$_SERVER['REMOTE_USER']) { return false; } global $ID; global $conf; $navbar = $this->toBootstrapNav(tpl_include_page('navbar', 0, 1, $this->getConf('useACL')), 'navbar'); $navbar = str_replace('urlextern', '', $navbar); $navbar = preg_replace('/
    • (.*)/', '
    • (.*)/', '
    • (.*)/', '