559 lines
17 KiB
PHP
559 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Indexmenu Action Plugin: Indexmenu Component.
|
|
*
|
|
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
|
|
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 <samuele@samuele.netsons.org>
|
|
*/
|
|
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 <klapinklapin@gmail.com>
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
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 <samuele@samuele.netsons.org>
|
|
*/
|
|
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 <samuele@samuele.netsons.org>
|
|
*/
|
|
public function loadOwnIndexPage(Event $event)
|
|
{
|
|
if ('index' != $event->data) return;
|
|
if (!file_exists(wikiFN($this->getConf('page_index')))) return;
|
|
|
|
global $lang;
|
|
|
|
echo '<h1><a id="index">' . $lang['btn_index'] . "</a></h1>\n";
|
|
echo p_wiki_xhtml($this->getConf('page_index'));
|
|
$event->preventDefault();
|
|
$event->stopPropagation();
|
|
}
|
|
|
|
/**
|
|
* Display the indexmenu sort number.
|
|
*
|
|
* @param Event $event
|
|
*
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
*/
|
|
public function showSortNumberAtTopOfPage(Event $event)
|
|
{
|
|
global $ID, $ACT, $INFO;
|
|
if ($INFO['isadmin'] && $ACT == 'show') {
|
|
if ($n = p_get_metadata($ID, 'indexmenu_n')) {
|
|
echo '<div class="info">';
|
|
echo $this->getLang('showsort') . $n;
|
|
echo '</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 <samuele@samuele.netsons.org>
|
|
* @author Gerrit Uitslag <klapinklapin@gmail.com>
|
|
*/
|
|
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 <samuele@samuele.netsons.org>
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
*/
|
|
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 <andi@splitbrain.org>
|
|
* @author Gerrit Uitslag <klapinklapin@gmail.com>
|
|
*/
|
|
private function renderToc($toc)
|
|
{
|
|
global $lang;
|
|
$out = '<div class="tocheader">';
|
|
$out .= $lang['toc'];
|
|
$out .= '</div>';
|
|
$out .= '<div class="indexmenu_toc_inside">';
|
|
$out .= html_buildlist($toc, 'toc', [$this, 'formatIndexmenuListTocItem'], null, true);
|
|
$out .= '</div>';
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Return the page abstract rendered to XHTML
|
|
*
|
|
* @param $id
|
|
* @param array $meta by reference
|
|
* @return string
|
|
*/
|
|
private function renderAbstract($id, $meta)
|
|
{
|
|
$out = '<div class="tocheader">';
|
|
$out .= '<a href="' . wl($id) . '">';
|
|
$out .= $meta['title'] ? hsc($meta['title']) : hsc(noNS($id));
|
|
$out .= '</a>';
|
|
$out .= '</div>';
|
|
if ($meta['description']['abstract']) {
|
|
$out .= '<div class="indexmenu_toc_inside">';
|
|
$out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info);
|
|
$out .= '</div></div>';
|
|
}
|
|
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 '<a href="' . $link . '">' . hsc($item['title']) . '</a>';
|
|
}
|
|
|
|
/**
|
|
* Print index nodes
|
|
*
|
|
* @param $ns
|
|
* @return string
|
|
*
|
|
* @author Rene Hadler <rene.hadler@iteas.at>
|
|
* @author Samuele Tognini <samuele@samuele.netsons.org>
|
|
* @author Andreas Gohr <andi@splitbrain.org>
|
|
*/
|
|
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 class="idx">(.*)<\/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"
|
|
// ];
|
|
}
|
|
}
|