1<?php 2 3/** 4 * Indexmenu Action Plugin: Indexmenu Component. 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Samuele Tognini <samuele@samuele.netsons.org> 8 */ 9 10use dokuwiki\Extension\ActionPlugin; 11use dokuwiki\Extension\Event; 12use dokuwiki\Extension\EventHandler; 13use dokuwiki\plugin\indexmenu\Search; 14use dokuwiki\Ui\Index; 15 16/** 17 * Class action_plugin_indexmenu 18 */ 19class action_plugin_indexmenu extends ActionPlugin 20{ 21 /** 22 * plugin should use this method to register its handlers with the dokuwiki's event controller 23 * 24 * @param EventHandler $controller DokuWiki's event controller object. 25 */ 26 public function register(EventHandler $controller) 27 { 28 if ($this->getConf('only_admins')) { 29 $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'removeSyntaxIfNotAdmin'); 30 } 31 if ($this->getConf('page_index') != '') { 32 $controller->register_hook('TPL_ACT_RENDER', 'BEFORE', $this, 'loadOwnIndexPage'); 33 } 34 $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'extendJSINFO'); 35 $controller->register_hook('PARSER_CACHE_USE', 'BEFORE', $this, 'purgeCache'); 36 if ($this->getConf('show_sort')) { 37 $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'showSortNumberAtTopOfPage'); 38 } 39 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'ajaxCalls'); 40 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'addStylesForSkins'); 41 } 42 43 /** 44 * Check if user has permission to insert indexmenu 45 * 46 * @param Event $event 47 * 48 * @author Samuele Tognini <samuele@samuele.netsons.org> 49 */ 50 public function removeSyntaxIfNotAdmin(Event $event) 51 { 52 global $INFO; 53 if (!$INFO['ismanager']) { 54 $event->data[0][1] = preg_replace("/{{indexmenu(|_n)>.+?}}/", "", $event->data[0][1]); 55 } 56 } 57 58 /** 59 * Add additional info to $JSINFO 60 * 61 * @param Event $event 62 * 63 * @author Gerrit Uitslag <klapinklapin@gmail.com> 64 * @author Samuele Tognini <samuele@samuele.netsons.org> 65 */ 66 public function extendJSINFO(Event $event) 67 { 68 global $INFO, $JSINFO; 69 70 $JSINFO['isadmin'] = (int)$INFO['isadmin']; 71 $JSINFO['isauth'] = isset($INFO['userinfo']) ? (int) $INFO['userinfo'] : 0; 72 } 73 74 /** 75 * Check for pages changes and eventually purge cache. 76 * 77 * @param Event $event 78 * 79 * @author Samuele Tognini <samuele@samuele.netsons.org> 80 */ 81 public function purgeCache(Event $event) 82 { 83 global $ID; 84 global $conf; 85 global $INPUT; 86 global $INFO; 87 88 /** @var cache_parser $cache */ 89 $cache = &$event->data; 90 91 if (!isset($cache->page)) return; 92 //purge only xhtml cache 93 if ($cache->mode != "xhtml") return; 94 //Check if it is an indexmenu page 95 if (!p_get_metadata($ID, 'indexmenu hasindexmenu')) return; 96 97 $aclcache = $this->getConf('aclcache'); 98 if ($conf['useacl']) { 99 $newkey = false; 100 if ($aclcache == 'user') { 101 //Cache per user 102 if ($INPUT->server->str('REMOTE_USER')) { 103 $newkey = $INPUT->server->str('REMOTE_USER'); 104 } 105 } elseif ($aclcache == 'groups') { 106 //Cache per groups 107 if (isset($INFO['userinfo']['grps'])) { 108 $newkey = implode('#', $INFO['userinfo']['grps']); 109 } 110 } 111 if ($newkey) { 112 $cache->key .= "#" . $newkey; 113 $cache->cache = getCacheName($cache->key, $cache->ext); 114 } 115 } 116 //Check if a page is more recent than purgefile. 117 if (@filemtime($cache->cache) < @filemtime($conf['cachedir'] . '/purgefile')) { 118 $event->preventDefault(); 119 $event->stopPropagation(); 120 $event->result = false; 121 } 122 } 123 124 /** 125 * Render a defined page as index. 126 * 127 * @param Event $event 128 * 129 * @author Samuele Tognini <samuele@samuele.netsons.org> 130 */ 131 public function loadOwnIndexPage(Event $event) 132 { 133 if ('index' != $event->data) return; 134 if (!file_exists(wikiFN($this->getConf('page_index')))) return; 135 136 global $lang; 137 138 echo '<h1><a id="index">' . $lang['btn_index'] . "</a></h1>\n"; 139 echo p_wiki_xhtml($this->getConf('page_index')); 140 $event->preventDefault(); 141 $event->stopPropagation(); 142 } 143 144 /** 145 * Display the indexmenu sort number. 146 * 147 * @param Event $event 148 * 149 * @author Samuele Tognini <samuele@samuele.netsons.org> 150 */ 151 public function showSortNumberAtTopOfPage(Event $event) 152 { 153 global $ID, $ACT, $INFO; 154 if ($INFO['isadmin'] && $ACT == 'show') { 155 if ($n = p_get_metadata($ID, 'indexmenu_n')) { 156 echo '<div class="info">'; 157 echo $this->getLang('showsort') . $n; 158 echo '</div>'; 159 } 160 } 161 } 162 163 /** 164 * Handles ajax requests for indexmenu 165 * 166 * @param Event $event 167 */ 168 public function ajaxCalls(Event $event) 169 { 170 if ($event->data !== 'indexmenu') { 171 return; 172 } 173 //no other ajax call handlers needed 174 $event->stopPropagation(); 175 $event->preventDefault(); 176 177 global $INPUT; 178 switch ($INPUT->str('req')) { 179 case 'local': 180 //list themes 181 $this->getlocalThemes(); 182 break; 183 184 case 'toc': 185 //print toc preview 186 if ($INPUT->has('id')) { 187 echo $this->printToc($INPUT->str('id')); 188 } 189 break; 190 191 case 'index': 192 //for dTree 193 //retrieval of data of the extra nodes for the indexmenu (if ajax loading set with max#m(#n) 194 if ($INPUT->has('idx')) { 195 echo $this->printIndex($INPUT->str('idx')); 196 } 197 break; 198 199 case 'fancytree': 200 //data for new index build with Fancytree 201 $this->getDataFancyTree(); 202 break; 203 } 204 } 205 206 /** 207 * Handles ajax requests for FancyTree 208 * 209 * @return void 210 */ 211 private function getDataFancyTree() 212 { 213 global $INPUT; 214 215 $ns = $INPUT->str('ns', ''); 216 $ns = rtrim($ns, ':'); 217 //key of directory has extra : on the end 218 $level = -1; //opened levels. -1=all levels open 219 $max = 1; //levels to load by lazyloading. Before the default was 0. CHANGED to 1. 220 $skipFileCombined = []; 221 $skipNsCombined = []; 222 223 if ($INPUT->int('max') > 0) { 224 $max = $INPUT->int('max'); // max#n#m, if init: #n, otherwise #m 225 $level = $max; 226 } 227 if ($INPUT->int('level', -10) >= -1) { 228 $level = $INPUT->int('level'); 229 } 230 $isInit = $INPUT->bool('init'); 231 232 $currentPage = $INPUT->str('currentpage'); 233 if ($isInit) { 234 $subnss = $INPUT->arr('subnss'); 235 // if 'navbar' is enabled add current ns to list 236 if ($INPUT->bool('navbar')) { 237 $currentNs = getNS($currentPage); 238 if ($currentNs !== false) { 239 $subnss[] = [$currentNs, 1]; 240 } 241 } 242 // alternative, via javascript.. https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree.html#loadKeyPath 243 } else { 244 //not set via javascript at the moment.. ajax opens per level, so subnss has no use here 245 $subnss = $INPUT->str('subnss'); 246 if ($subnss !== '') { 247 $subnss = [[cleanID($subnss), 1]]; 248 } 249 } 250 251 $skipf = $INPUT->str('skipfile'); 252 $skipFileCombined[] = $this->getConf('skip_file'); 253 if (!empty($skipf)) { 254 $index = 0; 255 //prefix is '=' or '+' 256 if ($skipf[0] == '+') { 257 $index = 1; 258 } 259 $skipFileCombined[$index] = substr($skipf, 1); 260 } 261 $skipn = $INPUT->str('skipns'); 262 $skipNsCombined[] = $this->getConf('skip_index'); 263 if (!empty($skipn)) { 264 $index = 0; 265 //prefix is '=' or '+' 266 if ($skipn[0] == '+') { 267 $index = 1; 268 } 269 $skipNsCombined[$index] = substr($skipn, 1); 270 } 271 272 $opts = [ 273 //only set for init, lazy requests equal to max 274 'level' => $level, 275 //nons only needed for init as it has no nested nodes 276 'nons' => $INPUT->bool('nons'), 277 'nopg' => $INPUT->bool('nopg'), 278 //init with complex array, empty if lazy loading 279 'subnss' => $subnss, 280 'max' => $max, 281 'skipnscombined' => $skipNsCombined, 282 'skipfilecombined' => $skipFileCombined, 283 'headpage' => $this->getConf('headpage'), 284 'hide_headpage' => $this->getConf('hide_headpage'), 285 ]; 286 287 $sort = [ 288 'sort' => $INPUT->str('sort'), 289 'msort' => $INPUT->str('msort'), 290 'rsort' => $INPUT->bool('rsort'), 291 'nsort' => $INPUT->bool('nsort'), 292 'hsort' => $INPUT->bool('hsort') 293 ]; 294 295 $opts['tempNew'] = true; //TODO temporary for recognizing treenew in the search function 296 297 $search = new Search($sort); 298 $data = $search->search($ns, $opts); 299 $fancytreeData = $search->buildFancytreeData($data, $isInit, $currentPage, $opts['nopg']); 300 301 //add eventually debug info 302 if ($isInit) { 303 //for lazy loading are other items than children not supported. 304// $fancytreeData['opts'] = $opts; 305// $fancytreeData['sort'] = $sort; 306// $fancytreeData['debug'] = $data; 307 } else { 308 //returns only children, therefore, add debug info to first child 309// $fancytreeData[0]['opts'] = $opts; 310// $fancytreeData[0]['sort'] = $sort; 311// $fancytreeData[0]['debug'] = $data; 312 } 313 314 header('Content-Type: application/json'); 315 echo json_encode($fancytreeData); 316 } 317 318 /** 319 * Print a list of local themes 320 * 321 * @author Samuele Tognini <samuele@samuele.netsons.org> 322 * @author Gerrit Uitslag <klapinklapin@gmail.com> 323 */ 324 private function getlocalThemes() 325 { 326 header('Content-Type: application/json'); 327 328 $themebase = 'lib/plugins/indexmenu/images'; 329 330 $handle = @opendir(DOKU_INC . $themebase); 331 $themes = []; 332 while (false !== ($file = readdir($handle))) { 333 if ( 334 is_dir(DOKU_INC . $themebase . '/' . $file) 335 && $file != "." 336 && $file != ".." 337 && $file != "repository" 338 && $file != "tmp" 339 && $file != ".svn" 340 ) { 341 $themes[] = $file; 342 } 343 } 344 closedir($handle); 345 sort($themes); 346 347 echo json_encode([ 348 'themebase' => $themebase, 349 'themes' => $themes 350 ]); 351 } 352 353 /** 354 * Print a toc preview 355 * 356 * @param string $id 357 * @return string 358 * 359 * @author Samuele Tognini <samuele@samuele.netsons.org> 360 * @author Andreas Gohr <andi@splitbrain.org> 361 */ 362 private function printToc($id) 363 { 364 $id = cleanID($id); 365 if (auth_quickaclcheck($id) < AUTH_READ) return ''; 366 367 $meta = p_get_metadata($id); 368 $toc = $meta['description']['tableofcontents'] ?? []; 369 370 if (count($toc) > 1) { 371 //display ToC of two or more headings 372 $out = $this->renderToc($toc); 373 } else { 374 //display page abstract 375 $out = $this->renderAbstract($id, $meta); 376 } 377 return $out; 378 } 379 380 /** 381 * Return the TOC rendered to XHTML 382 * 383 * @param $toc 384 * @return string 385 * 386 * @author Andreas Gohr <andi@splitbrain.org> 387 * @author Gerrit Uitslag <klapinklapin@gmail.com> 388 */ 389 private function renderToc($toc) 390 { 391 global $lang; 392 $out = '<div class="tocheader">'; 393 $out .= $lang['toc']; 394 $out .= '</div>'; 395 $out .= '<div class="indexmenu_toc_inside">'; 396 $out .= html_buildlist($toc, 'toc', [$this, 'formatIndexmenuListTocItem'], null, true); 397 $out .= '</div>'; 398 return $out; 399 } 400 401 /** 402 * Return the page abstract rendered to XHTML 403 * 404 * @param $id 405 * @param array $meta by reference 406 * @return string 407 */ 408 private function renderAbstract($id, $meta) 409 { 410 $out = '<div class="tocheader">'; 411 $out .= '<a href="' . wl($id) . '">'; 412 $out .= $meta['title'] ? hsc($meta['title']) : hsc(noNS($id)); 413 $out .= '</a>'; 414 $out .= '</div>'; 415 if ($meta['description']['abstract']) { 416 $out .= '<div class="indexmenu_toc_inside">'; 417 $out .= p_render('xhtml', p_get_instructions($meta['description']['abstract']), $info); 418 $out .= '</div></div>'; 419 } 420 return $out; 421 } 422 423 /** 424 * Callback for html_buildlist 425 * 426 * @param $item 427 * @return string 428 */ 429 public function formatIndexmenuListTocItem($item) 430 { 431 global $INPUT; 432 433 $id = cleanID($INPUT->str('id')); 434 435 if (isset($item['hid'])) { 436 $link = '#' . $item['hid']; 437 } else { 438 $link = $item['link']; 439 } 440 441 //prefix anchers with page id 442 if ($link[0] == '#') { 443 $link = wl($id, $link, false, ''); 444 } 445 return '<a href="' . $link . '">' . hsc($item['title']) . '</a>'; 446 } 447 448 /** 449 * Print index nodes 450 * 451 * @param $ns 452 * @return string 453 * 454 * @author Rene Hadler <rene.hadler@iteas.at> 455 * @author Samuele Tognini <samuele@samuele.netsons.org> 456 * @author Andreas Gohr <andi@splitbrain.org> 457 */ 458 private function printIndex($ns) 459 { 460 global $conf, $INPUT; 461 $idxm = new syntax_plugin_indexmenu_indexmenu(); 462 $ns = $idxm->parseNs(rawurldecode($ns)); 463 $level = -1; 464 $max = 0; 465 $data = []; 466 $skipfilecombined = []; 467 $skipnscombined = []; 468 469 if ($INPUT->int('max') > 0) { 470 $max = $INPUT->int('max'); 471 $level = $max; 472 } 473 $nss = $INPUT->str('nss', '', true); 474 $sort['sort'] = $INPUT->str('sort', '', true); 475 $sort['msort'] = $INPUT->str('msort', '', true); 476 $sort['rsort'] = $INPUT->bool('rsort', false, true); 477 $sort['nsort'] = $INPUT->bool('nsort', false, true); 478 $sort['hsort'] = $INPUT->bool('hsort', false, true); 479 $search = new Search($sort); 480 $fsdir = "/" . utf8_encodeFN(str_replace(':', '/', $ns)); 481 482 $skipf = utf8_decodeFN($INPUT->str('skipfile')); 483 $skipfilecombined[] = $this->getConf('skip_file'); 484 if (!empty($skipf)) { 485 $index = 0; 486 if ($skipf[0] == '+') { 487 $index = 1; 488 } 489 $skipfilecombined[$index] = substr($skipf, 1); 490 } 491 $skipn = utf8_decodeFN($INPUT->str('skipns')); 492 $skipnscombined[] = $this->getConf('skip_index'); 493 if (!empty($skipn)) { 494 $index = 0; 495 if ($skipn[0] == '+') { 496 $index = 1; 497 } 498 $skipnscombined[$index] = substr($skipn, 1); 499 } 500 501 $opts = [ 502 'level' => $level, 503 'nons' => $INPUT->bool('nons', false, true), 504 'nss' => [[$nss, 1]], 505 'max' => $max, 506 'js' => false, 507 'nopg' => $INPUT->bool('nopg', false, true), 508 'skipnscombined' => $skipnscombined, 509 'skipfilecombined' => $skipfilecombined, 510 'headpage' => $idxm->getConf('headpage'), 511 'hide_headpage' => $idxm->getConf('hide_headpage') 512 ]; 513 if ($sort['sort'] || $sort['msort'] || $sort['rsort'] || $sort['hsort']) { 514 $search->customSearch($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir); 515 } else { 516 search($data, $conf['datadir'], [$search, 'searchIndexmenuItems'], $opts, $fsdir); 517 } 518 519 $out = ''; 520 if ($INPUT->int('nojs') === 1) { 521 $idx = new Index(); 522 $out_tmp = html_buildlist($data, 'idx', [$idxm, 'formatIndexmenuItem'], [$idx, 'tagListItem']); 523 $out .= preg_replace('/<ul class="idx">(.*)<\/ul>/s', "$1", $out_tmp); 524 } else { 525 $nodes = $idxm->builddTreeNodes($data, '', false); 526 $out = "ajxnodes = ["; 527 $out .= rtrim($nodes[0], ","); 528 $out .= "];"; 529 } 530 return $out; 531 } 532 533 /** 534 * Add Js & Css after template is displayed 535 * 536 * @param Event $event 537 */ 538 public function addStylesForSkins(Event $event) 539 { 540 541// $event->data["link"][] = [ 542// "type" => "text/css", 543// "rel" => "stylesheet", 544// "href" => DOKU_BASE . "lib/plugins/indexmenu/scripts/fancytree/... etc etc" 545// ]; 546 547// $event->data["link"][] = [ 548// "type" => "text/css", 549// "rel" => "stylesheet", 550// "href" => "//fonts.googleapis.com/icon?family=Material+Icons" 551// ]; 552 553// $event->data["link"][] = [ 554// "type" => "text/css", 555// "rel" => "stylesheet", 556// "href" => "//code.getmdl.io/1.3.0/material.indigo-pink.min.css" 557// ]; 558 } 559} 560