*/ use dokuwiki\Extension\ActionPlugin; use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; use dokuwiki\plugin\indexmenu\Search; use dokuwiki\Ui\Index; /** * Class action_plugin_indexmenu */ class action_plugin_indexmenu extends ActionPlugin { /** * plugin should use this method to register its handlers with the dokuwiki's event controller * * @param EventHandler $controller DokuWiki's event controller object. */ public function register(EventHandler $controller) { if ($this->getConf('only_admins')) { $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'removeSyntaxIfNotAdmin'); } if ($this->getConf('page_index') != '') { $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'loadOwnIndexPage'); } $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extendJSINFO'); $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'purgeCache'); if ($this->getConf('show_sort')) { $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'showSortNumberAtTopOfPage'); } $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCalls'); $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addStylesForSkins'); } /** * Check if user has permission to insert indexmenu * * @param Event $event * * @author Samuele Tognini */ public function removeSyntaxIfNotAdmin(Event $event) { global $INFO; if (!$INFO['ismanager']) { $event->data[0][1] = preg_replace("/{{indexmenu(|_n)>.+?}}/", "", $event->data[0][1]); } } /** * Add additional info to $JSINFO * * @param Event $event * * @author Gerrit Uitslag * @author Samuele Tognini */ public function extendJSINFO(Event $event) { global $INFO, $JSINFO; $JSINFO['isadmin'] = (int)$INFO['isadmin']; $JSINFO['isauth'] = isset($INFO['userinfo']) ? (int) $INFO['userinfo'] : 0; } /** * Check for pages changes and eventually purge cache. * * @param Event $event * * @author Samuele Tognini */ public function purgeCache(Event $event) { global $ID; global $conf; global $INPUT; global $INFO; /** @var cache_parser $cache */ $cache = &$event->data; if (!isset($cache->page)) return; //purge only xhtml cache if ($cache->mode != "xhtml") return; //Check if it is an indexmenu page if (!p_get_metadata($ID, 'indexmenu hasindexmenu')) return; $aclcache = $this->getConf('aclcache'); if ($conf['useacl']) { $newkey = false; if ($aclcache == 'user') { //Cache per user if ($INPUT->server->str('REMOTE_USER')) { $newkey = $INPUT->server->str('REMOTE_USER'); } } elseif ($aclcache == 'groups') { //Cache per groups if (isset($INFO['userinfo']['grps'])) { $newkey = implode('#', $INFO['userinfo']['grps']); } } if ($newkey) { $cache->key .= "#" . $newkey; $cache->cache = getCacheName($cache->key, $cache->ext); } } //Check if a page is more recent than purgefile. if (@filemtime($cache->cache) < @filemtime($conf['cachedir'] . '/purgefile')) { $event->preventDefault(); $event->stopPropagation(); $event->result = false; } } /** * Render a defined page as index. * * @param Event $event * * @author Samuele Tognini */ public function loadOwnIndexPage(Event $event) { if ('index' != $event->data) return; if (!file_exists(wikiFN($this->getConf('page_index')))) return; global $lang; echo '

' . $lang['btn_index'] . "

\n"; echo p_wiki_xhtml($this->getConf('page_index')); $event->preventDefault(); $event->stopPropagation(); } /** * Display the indexmenu sort number. * * @param Event $event * * @author Samuele Tognini */ public function showSortNumberAtTopOfPage(Event $event) { global $ID, $ACT, $INFO; if ($INFO['isadmin'] && $ACT == 'show') { if ($n = p_get_metadata($ID, 'indexmenu_n')) { echo '
'; echo $this->getLang('showsort') . $n; echo '
'; } } } /** * Handles ajax requests for indexmenu * * @param Event $event */ public function ajaxCalls(Event $event) { if ($event->data !== 'indexmenu') { return; } //no other ajax call handlers needed $event->stopPropagation(); $event->preventDefault(); global $INPUT; switch ($INPUT->str('req')) { case 'local': //list themes $this->getlocalThemes(); break; case 'toc': //print toc preview if ($INPUT->has('id')) { echo $this->printToc($INPUT->str('id')); } break; case 'index': //for dTree //retrieval of data of the extra nodes for the indexmenu (if ajax loading set with max#m(#n) if ($INPUT->has('idx')) { echo $this->printIndex($INPUT->str('idx')); } break; case 'fancytree': //data for new index build with Fancytree $this->getDataFancyTree(); break; } } /** * Handles ajax requests for FancyTree * * @return void */ private function getDataFancyTree() { global $INPUT; $ns = $INPUT->str('ns', ''); $ns = rtrim($ns, ':'); //key of directory has extra : on the end $level = -1; //opened levels. -1=all levels open $max = 1; //levels to load by lazyloading. Before the default was 0. CHANGED to 1. $skipFileCombined = []; $skipNsCombined = []; if ($INPUT->int('max') > 0) { $max = $INPUT->int('max'); // max#n#m, if init: #n, otherwise #m $level = $max; } if ($INPUT->int('level', -10) >= -1) { $level = $INPUT->int('level'); } $isInit = $INPUT->bool('init'); $currentPage = $INPUT->str('currentpage'); if ($isInit) { $subnss = $INPUT->arr('subnss'); // if 'navbar' is enabled add current ns to list if ($INPUT->bool('navbar')) { $currentNs = getNS($currentPage); if ($currentNs !== false) { $subnss[] = [$currentNs, 1]; } } // alternative, via javascript.. https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html#loadKeyPath } else { //not set via javascript at the moment.. ajax opens per level, so subnss has no use here $subnss = $INPUT->str('subnss'); if ($subnss !== '') { $subnss = [[cleanID($subnss), 1]]; } } $skipf = $INPUT->str('skipfile'); $skipFileCombined[] = $this->getConf('skip_file'); if (!empty($skipf)) { $index = 0; //prefix is '=' or '+' if ($skipf[0] == '+') { $index = 1; } $skipFileCombined[$index] = substr($skipf, 1); } $skipn = $INPUT->str('skipns'); $skipNsCombined[] = $this->getConf('skip_index'); if (!empty($skipn)) { $index = 0; //prefix is '=' or '+' if ($skipn[0] == '+') { $index = 1; } $skipNsCombined[$index] = substr($skipn, 1); } $opts = [ //only set for init, lazy requests equal to max 'level' => $level, //nons only needed for init as it has no nested nodes 'nons' => $INPUT->bool('nons'), 'nopg' => $INPUT->bool('nopg'), //init with complex array, empty if lazy loading 'subnss' => $subnss, 'max' => $max, 'skipnscombined' => $skipNsCombined, 'skipfilecombined' => $skipFileCombined, 'headpage' => $this->getConf('headpage'), 'hide_headpage' => $this->getConf('hide_headpage'), ]; $sort = [ 'sort' => $INPUT->str('sort'), 'msort' => $INPUT->str('msort'), 'rsort' => $INPUT->bool('rsort'), 'nsort' => $INPUT->bool('nsort'), 'hsort' => $INPUT->bool('hsort') ]; $opts['tempNew'] = true; //TODO temporary for recognizing treenew in the search function $search = new Search($sort); $data = $search->search($ns, $opts); $fancytreeData = $search->buildFancytreeData($data, $isInit, $currentPage, $opts['nopg']); //add eventually debug info if ($isInit) { //for lazy loading are other items than children not supported. // $fancytreeData['opts'] = $opts; // $fancytreeData['sort'] = $sort; // $fancytreeData['debug'] = $data; } else { //returns only children, therefore, add debug info to first child // $fancytreeData[0]['opts'] = $opts; // $fancytreeData[0]['sort'] = $sort; // $fancytreeData[0]['debug'] = $data; } header('Content-Type: application/json'); echo json_encode($fancytreeData); } /** * Print a list of local themes * * @author Samuele Tognini * @author Gerrit Uitslag */ private function getlocalThemes() { header('Content-Type: application/json'); $themebase = 'lib/plugins/indexmenu/images'; $handle = @opendir(DOKU_INC . $themebase); $themes = []; while (false !== ($file = readdir($handle))) { if ( is_dir(DOKU_INC . $themebase . '/' . $file) && $file != "." && $file != ".." && $file != "repository" && $file != "tmp" && $file != ".svn" ) { $themes[] = $file; } } closedir($handle); sort($themes); echo json_encode([ 'themebase' => $themebase, 'themes' => $themes ]); } /** * Print a toc preview * * @param string $id * @return string * * @author Samuele Tognini * @author Andreas Gohr */ private function printToc($id) { $id = cleanID($id); if (auth_quickaclcheck($id) < AUTH_READ) return ''; $meta = p_get_metadata($id); $toc = $meta['description']['tableofcontents'] ?? []; if (count($toc) > 1) { //display ToC of two or more headings $out = $this->renderToc($toc); } else { //display page abstract $out = $this->renderAbstract($id, $meta); } return $out; } /** * Return the TOC rendered to XHTML * * @param $toc * @return string * * @author Andreas Gohr * @author Gerrit Uitslag */ private function renderToc($toc) { global $lang; $out = '
'; $out .= $lang['toc']; $out .= '
'; $out .= '
'; $out .= html_buildlist($toc, 'toc', [$this, 'formatIndexmenuListTocItem'], null, true); $out .= '
'; return $out; } /** * Return the page abstract rendered to XHTML * * @param $id * @param array $meta by reference * @return string */ private function renderAbstract($id, $meta) { $out = ''; if ($meta['description']['abstract']) { $out .= '
'; $out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info); $out .= '
'; } return $out; } /** * Callback for html_buildlist * * @param $item * @return string */ public function formatIndexmenuListTocItem($item) { global $INPUT; $id = cleanID($INPUT->str('id')); if (isset($item['hid'])) { $link = '#' . $item['hid']; } else { $link = $item['link']; } //prefix anchers with page id if ($link[0] == '#') { $link = wl($id, $link, false, ''); } return '' . hsc($item['title']) . ''; } /** * Print index nodes * * @param $ns * @return string * * @author Rene Hadler * @author Samuele Tognini * @author Andreas Gohr */ private function printIndex($ns) { global $conf, $INPUT; $idxm = new syntax_plugin_indexmenu_indexmenu(); $ns = $idxm->parseNs(rawurldecode($ns)); $level = -1; $max = 0; $data = []; $skipfilecombined = []; $skipnscombined = []; if ($INPUT->int('max') > 0) { $max = $INPUT->int('max'); $level = $max; } $nss = $INPUT->str('nss', '', true); $sort['sort'] = $INPUT->str('sort', '', true); $sort['msort'] = $INPUT->str('msort', '', true); $sort['rsort'] = $INPUT->bool('rsort', false, true); $sort['nsort'] = $INPUT->bool('nsort', false, true); $sort['hsort'] = $INPUT->bool('hsort', false, true); $search = new Search($sort); $fsdir = "/" . utf8_encodeFN(str_replace(':', '/', $ns)); $skipf = utf8_decodeFN($INPUT->str('skipfile')); $skipfilecombined[] = $this->getConf('skip_file'); if (!empty($skipf)) { $index = 0; if ($skipf[0] == '+') { $index = 1; } $skipfilecombined[$index] = substr($skipf, 1); } $skipn = utf8_decodeFN($INPUT->str('skipns')); $skipnscombined[] = $this->getConf('skip_index'); if (!empty($skipn)) { $index = 0; if ($skipn[0] == '+') { $index = 1; } $skipnscombined[$index] = substr($skipn, 1); } $opts = [ 'level' => $level, 'nons' => $INPUT->bool('nons', false, true), 'nss' => [[$nss, 1]], 'max' => $max, 'js' => false, 'nopg' => $INPUT->bool('nopg', false, true), 'skipnscombined' => $skipnscombined, 'skipfilecombined' => $skipfilecombined, 'headpage' => $idxm->getConf('headpage'), 'hide_headpage' => $idxm->getConf('hide_headpage') ]; if ($sort['sort'] || $sort['msort'] || $sort['rsort'] || $sort['hsort']) { $search->customSearch($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir); } else { search($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir); } $out = ''; if ($INPUT->int('nojs') === 1) { $idx = new Index(); $out_tmp = html_buildlist($data, 'idx', [$idxm, 'formatIndexmenuItem'], [$idx, 'tagListItem']); $out .= preg_replace('/
    (.*)<\/ul>/s', "$1", $out_tmp); } else { $nodes = $idxm->builddTreeNodes($data, '', false); $out = "ajxnodes = ["; $out .= rtrim($nodes[0], ","); $out .= "];"; } return $out; } /** * Add Js & Css after template is displayed * * @param Event $event */ public function addStylesForSkins(Event $event) { // $event->data["link"][] = [ // "type" => "text/css", // "rel" => "stylesheet", // "href" => DOKU_BASE . "lib/plugins/indexmenu/scripts/fancytree/... etc etc" // ]; // $event->data["link"][] = [ // "type" => "text/css", // "rel" => "stylesheet", // "href" => "//fonts.googleapis.com/icon?family=Material+Icons" // ]; // $event->data["link"][] = [ // "type" => "text/css", // "rel" => "stylesheet", // "href" => "//code.getmdl.io/1.3.0/material.indigo-pink.min.css" // ]; } }