1<?php 2/** 3 * Info Indexmenu: Show a customizable and sortable index for a namespace. 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Samuele Tognini <samuele@samuele.netsons.org> 7 * 8 */ 9 10if(!defined('DOKU_INC')) die(); 11if(!defined('INDEXMENU_IMG_ABSDIR')) define('INDEXMENU_IMG_ABSDIR', DOKU_PLUGIN."indexmenu/images"); 12 13require_once(DOKU_INC.'inc/search.php'); 14 15/** 16 * All DokuWiki plugins to extend the parser/rendering mechanism 17 * need to inherit from this class 18 */ 19class syntax_plugin_indexmenu_indexmenu extends DokuWiki_Syntax_Plugin { 20 21 var $sort = false; 22 var $msort = false; 23 var $rsort = false; 24 var $nsort = false; 25 var $hsort = false; 26 27 /** 28 * What kind of syntax are we? 29 */ 30 public function getType() { 31 return 'substition'; 32 } 33 34 /** 35 * Behavior regarding the paragraph 36 */ 37 public function getPType() { 38 return 'block'; 39 } 40 41 /** 42 * Where to sort in? 43 */ 44 public function getSort() { 45 return 138; 46 } 47 48 /** 49 * Connect pattern to lexer 50 */ 51 public function connectTo($mode) { 52 $this->Lexer->addSpecialPattern('{{indexmenu>.+?}}', $mode, 'plugin_indexmenu_indexmenu'); 53 } 54 55 /** 56 * Handler to prepare matched data for the rendering process 57 * 58 * @param string $match The text matched by the patterns 59 * @param int $state The lexer state for the match 60 * @param int $pos The character position of the matched text 61 * @param Doku_Handler $handler The Doku_Handler object 62 * @return array Return an array with all data you want to use in render 63 */ 64 public function handle($match, $state, $pos, Doku_Handler $handler) { 65 $theme = 'default'; 66 $level = -1; 67 $gen_id = 'random'; 68 $maxjs = 0; 69 $max = 0; 70 $jsajax = ''; 71 $nss = array(); 72 $skipns = array(); 73 $skipfile = array(); 74 75 $defaultsstr = $this->getConf('defaultoptions'); 76 $defaults = explode(' ', $defaultsstr); 77 78 $match = substr($match, 12, -2); 79 //split namespace,level,theme 80 list($nsstr, $optsstr) = explode('|', $match, 2); 81 //split options 82 $opts = explode(' ', $optsstr); 83 84 //Context option 85 $context = $this->hasOption($defaults, $opts, 'context'); 86 87 //split optional namespaces 88 $nss_temp = preg_split("/ /u", $nsstr, -1, PREG_SPLIT_NO_EMPTY); 89 //Array optional namespace => level 90 for($i = 1; $i < count($nss_temp); $i++) { 91 $nsss = preg_split("/#/u", $nss_temp[$i]); 92 if(!$context) { 93 $nsss[0] = $this->_parse_ns($nsss[0]); 94 } 95 $nss[] = array($nsss[0], (is_numeric($nsss[1])) ? $nsss[1] : $level); 96 } 97 //split main requested namespace 98 if(preg_match('/(.*)#(\S*)/u', $nss_temp[0], $ns_opt)) { 99 //split level 100 $ns = $ns_opt[1]; 101 if(is_numeric($ns_opt[2])) $level = $ns_opt[2]; 102 } else { 103 $ns = $nss_temp[0]; 104 } 105 if(!$context) { 106 $ns = $this->_parse_ns($ns); 107 } 108 109 //nocookie option (disable for uncached pages) 110 $nocookie = $context || $this->hasOption($defaults, $opts, 'nocookie'); 111 //noscroll option 112 $noscroll = $this->hasOption($defaults, $opts, 'noscroll'); 113 //Open at current namespace option 114 $navbar = $this->hasOption($defaults, $opts, 'navbar'); 115 //no namespaces options 116 $nons = $this->hasOption($defaults, $opts, 'nons'); 117 //no pages option 118 $nopg = $this->hasOption($defaults, $opts, 'nopg'); 119 //disable toc preview 120 $notoc = $this->hasOption($defaults, $opts, 'notoc'); 121 //disable the right context menu 122 $nomenu = $this->hasOption($defaults, $opts, 'nomenu'); 123 //Main sort method 124 $tsort = $this->hasOption($defaults, $opts, 'tsort'); 125 $dsort = $this->hasOption($defaults, $opts, 'dsort'); 126 if($tsort) { 127 $sort = 't'; 128 } elseif($dsort) { 129 $sort = 'd'; 130 } else $sort = 0; 131 //sort directories in the same way as files 132 $nsort = $this->hasOption($defaults, $opts, 'nsort'); 133 //sort headpages up 134 $hsort = $this->hasOption($defaults, $opts, 'hsort'); 135 //Metadata sort method 136 if($msort = $this->hasOption($defaults, $opts, 'msort')) { 137 $msort = 'indexmenu_n'; 138 } elseif($value = $this->getOption($defaultsstr, $optsstr, '/msort#(\S+)/u')) { 139 $msort = str_replace(':', ' ', $value); 140 } 141 //reverse sort 142 $rsort = $this->hasOption($defaults, $opts, 'rsort'); 143 144 if($sort) $jsajax .= "&sort=" . $sort; 145 if($msort) $jsajax .= "&msort=" . $msort; 146 if($rsort) $jsajax .= "&rsort=1"; 147 if($nsort) $jsajax .= "&nsort=1"; 148 if($hsort) $jsajax .= "&hsort=1"; 149 if($nopg) $jsajax .= "&nopg=1"; 150 151 //javascript option 152 $dir = ''; 153 //check defaults for js,js#theme, #theme 154 if(!$js = in_array('js', $defaults)) { 155 if(preg_match('/(?:^|\s)(js)?#(\S*)/u', $defaultsstr, $match_djs) > 0) { 156 if(!empty($match_djs[1])) $js = true; 157 if(isset($match_djs[2])) $dir = $match_djs[2]; 158 } 159 } 160 //check opts for nojs,#theme or js,js#theme 161 if($js) { 162 if(in_array('nojs', $opts)) { 163 $js = false; 164 } else { 165 if(preg_match('/(?:^|\s)(?:js)?#(\S*)/u', $optsstr, $match_ojs) > 0) { 166 if(isset($match_ojs[1])) $dir = $match_ojs[1]; 167 } 168 } 169 } else { 170 if($js = in_array('js', $opts)) { 171 //use theme from the defaults 172 } else { 173 if(preg_match('/(?:^|\s)js#(\S*)/u', $optsstr, $match_ojs) > 0) { 174 $js = true; 175 if(isset($match_ojs[1])) $dir = $match_ojs[1]; 176 } 177 } 178 } 179 180 if($js) { 181 //exist theme? 182 if(!empty($dir) && is_dir(INDEXMENU_IMG_ABSDIR . "/" . $dir)) { 183 $theme = $dir; 184 } 185 186 //id generation method 187 $gen_id = $this->getOption($defaultsstr, $optsstr, '/id#(\S+)/u'); 188 189 //max option 190 if($maxmatches = $this->getOption($defaultsstr, $optsstr, '/max#(\d+)($|\s+|#(\d+))/u', true)) { 191 $max = $maxmatches[1]; 192 if($maxmatches[3]) { 193 $jsajax .= "&max=" . $maxmatches[3]; 194 } 195 //disable cookie to avoid javascript errors 196 $nocookie = true; 197 } else { 198 $max = 0; 199 } 200 201 //max js option 202 if($maxjsvalue = $this->getOption($defaultsstr, $optsstr, '/maxjs#(\d+)/u')) { 203 $maxjs = $maxjsvalue; 204 } 205 } 206 if(is_numeric($gen_id)) { 207 $identifier = $gen_id; 208 } elseif($gen_id == 'ns') { 209 $identifier = sprintf("%u", crc32($ns)); 210 } else { 211 $identifier = uniqid(rand()); 212 } 213 214 //skip namespaces in index 215 $skipns[] = $this->getConf('skip_index'); 216 if(preg_match('/skipns[\+=](\S+)/u', $optsstr, $sns) > 0) { 217 //first sign is: '+' (parallel to conf) or '=' (replace conf) 218 $action = $sns[0][6]; 219 $index = 0; 220 if($action == '+') { 221 $index = 1; 222 } 223 $skipns[$index] = $sns[1]; 224 $jsajax .= "&skipns=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $sns[1]); 225 } 226 //skip file 227 $skipfile[] = $this->getConf('skip_file'); 228 if(preg_match('/skipfile[\+=](\S+)/u', $optsstr, $sf) > 0) { 229 //first sign is: '+' (parallel to conf) or '=' (replace conf) 230 $action = $sf[0][8]; 231 $index = 0; 232 if($action == '+') { 233 $index = 1; 234 } 235 $skipfile[$index] = $sf[1]; 236 $jsajax .= "&skipfile=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $sf[1]); 237 } 238 239 //js options 240 $js_opts = compact('theme', 'identifier', 'nocookie', 'navbar', 'noscroll', 'maxjs', 'notoc', 'jsajax', 'context', 'nomenu'); 241 242 return array( 243 $ns, 244 $js_opts, 245 $sort, 246 $msort, 247 $rsort, 248 $nsort, 249 array( 250 'level' => $level, 251 'nons' => $nons, 252 'nopg' => $nopg, 253 'nss' => $nss, 254 'max' => $max, 255 'js' => $js, 256 'skip_index' => $skipns, 257 'skip_file' => $skipfile, 258 'headpage' => $this->getConf('headpage'), 259 'hide_headpage' => $this->getConf('hide_headpage') 260 ), 261 $hsort 262 ); 263 } 264 265 266 /** 267 * Looks if the default options and syntax options has the requested option 268 * 269 * @param array $defaultsopts array of default options 270 * @param array $opts array of options provided via syntax 271 * @param string $optionname name of requested option 272 * @return bool has optionname? 273 */ 274 private function hasOption($defaultsopts, $opts, $optionname) { 275 $name = $optionname; 276 if(substr($optionname, 0, 2) == 'no') { 277 $inversename = substr($optionname, 2); 278 } else { 279 $inversename = 'no' . $optionname; 280 } 281 282 if(in_array($name, $defaultsopts)) { 283 return !in_array($inversename, $opts); 284 } else { 285 return in_array($name, $opts); 286 } 287 } 288 289 /** 290 * Looks for the value of the requested option in the default options and syntax options 291 * 292 * @param string $defaultsstr default options string 293 * @param string $optsstr syntax options string 294 * @param string $matchpattern pattern to search for 295 * @param bool $multiplematches if multiple returns array, otherwise the first match 296 * @return string|array 297 */ 298 private function getOption($defaultsstr, $optsstr, $matchpattern, $multiplematches = false) { 299 if(preg_match($matchpattern, $optsstr, $match_o) > 0) { 300 if($multiplematches) { 301 return $match_o; 302 } else { 303 return $match_o[1]; 304 } 305 } elseif(preg_match($matchpattern, $defaultsstr, $match_d) > 0) { 306 if($multiplematches) { 307 return $match_d; 308 } else { 309 return $match_d[1]; 310 } 311 } 312 return false; 313 } 314 315 /** 316 * Handles the actual output creation. 317 * 318 * @param string $format output format being rendered 319 * @param Doku_Renderer $renderer the current renderer object 320 * @param array $data data created by handler() 321 * @return boolean rendered correctly? 322 */ 323 public function render($format, Doku_Renderer $renderer, $data) { 324 global $ACT; 325 global $conf; 326 global $INFO; 327 if($format == 'xhtml') { 328 /** @var Doku_Renderer_xhtml $renderer */ 329 if($ACT == 'preview') { 330 //Check user permission to display indexmenu in a preview page 331 if($this->getConf('only_admins') && 332 $conf['useacl'] && 333 $INFO['perm'] < AUTH_ADMIN 334 ) 335 return false; 336 //disable cookies 337 $data[1]['nocookie'] = true; 338 } 339 if($data[6]['js'] & $conf['defer_js']) { 340 msg('Indexmenu Plugin: If you use the \'js\'-option of the indexmenu plugin, you have to disable the <a href="https://www.dokuwiki.org/config:defer_js">\'defer_js\'</a>-setting. This setting is temporary, in the future the indexmenu plugin will be improved.',-1); 341 } 342 //Navbar with nojs 343 if($data[1]['navbar'] && !$data[6]['js']) { 344 if(!isset($data[0])) $data[0] = '..'; 345 $data[6]['nss'][] = array(getNS($INFO['id'])); 346 $renderer->info['cache'] = FALSE; 347 } 348 349 if($data[1]['context']) { 350 //resolve current id relative namespaces 351 $data[0] = $this->_parse_ns($data[0], $INFO['id']); 352 foreach($data[6]['nss'] as $key=> $value) { 353 $data[6]['nss'][$key][0] = $this->_parse_ns($value[0], $INFO['id']); 354 } 355 $renderer->info['cache'] = FALSE; 356 } 357 $n = $this->_indexmenu($data); 358 if(!@$n) { 359 $n = $this->getConf('empty_msg'); 360 $n = str_replace('{{ns}}', cleanID($data[0]), $n); 361 $n = p_render('xhtml', p_get_instructions($n), $info); 362 } 363 $renderer->doc .= $n; 364 return true; 365 } else if($format == 'metadata') { 366 /** @var Doku_Renderer_metadata $renderer */ 367 if(!($data[1]['navbar'] && !$data[6]['js']) && !$data[1]['context']) { 368 //this is an indexmenu page that needs the PARSER_CACHE_USE event trigger; 369 $renderer->meta['indexmenu'] = TRUE; 370 } 371 $renderer->doc .= ((empty($data[0])) ? $conf['title'] : nons($data[0]))." index\n\n"; 372 unset($renderer->persistent['indexmenu']); 373 return true; 374 } else { 375 return false; 376 } 377 } 378 379 /** 380 * Return the index 381 * 382 * @author Samuele Tognini <samuele@samuele.netsons.org> 383 * 384 * This function is a simple hack of Dokuwiki @see html_index($ns) 385 * @author Andreas Gohr <andi@splitbrain.org> 386 * 387 * @param array $myns the options for indexmenu 388 * @return bool|string return html for a nojs index and when enabled the js rendered index, otherwise false 389 */ 390 private function _indexmenu($myns) { 391 global $conf; 392 $ns = $myns[0]; 393 $js_opts = $myns[1]; //theme, identifier, nocookie, navbar, noscroll, maxjs, notoc, jsajax, context, nomenu 394 $this->sort = $myns[2]; 395 $this->msort = $myns[3]; 396 $this->rsort = $myns[4]; 397 $this->nsort = $myns[5]; 398 $opts = $myns[6]; //level, nons, nopg, nss, max, js, skip_index, skip_file, headpage, hide_headpage 399 $this->hsort = $myns[7]; 400 $data = array(); 401 $js_name = "indexmenu_".$js_opts['identifier']; 402 $fsdir = "/".utf8_encodeFN(str_replace(':', '/', $ns)); 403 if($this->sort || $this->msort || $this->rsort || $this->hsort) { 404 $this->_search($data, $conf['datadir'], array($this, '_search_index'), $opts, $fsdir); 405 } else { 406 search($data, $conf['datadir'], array($this, '_search_index'), $opts, $fsdir); 407 } 408 if(!$data) return false; 409 410 // javascript index 411 $output_tmp = ""; 412 if($opts['js']) { 413 $ns = str_replace('/', ':', $ns); 414 $output_tmp = $this->_jstree($data, $ns, $js_opts, $js_name, $opts['max']); 415 416 //remove unwanted nodes from standard index 417 $this->_clean_data($data); 418 } 419 420 // Nojs dokuwiki index 421 // extra div needed when index is first element in sidebar of dokuwiki template, template uses this to toggle sidebar 422 // the toggle interacts with hide needed for js option. 423 $output = "\n"; 424 $output .= '<div><div id="nojs_'.$js_name.'" data-jsajax="'.utf8_encodeFN($js_opts['jsajax']).'" class="indexmenu_nojs">'."\n"; 425 $output .= html_buildlist($data, 'idx', array($this, "_html_list_index"), "html_li_index"); 426 $output .= "</div></div>\n"; 427 $output .= $output_tmp; 428 return $output; 429 } 430 431 /** 432 * Build the browsable index of pages using javascript 433 * 434 * @author Samuele Tognini <samuele@samuele.netsons.org> 435 * @author Rene Hadler 436 * 437 * @param array $data array with items of the tree 438 * @param string $ns requested namespace 439 * @param array $js_opts options for javascript renderer 440 * @param string $js_name identifier for this index 441 * @param int $max the node at $max level will retrieve all its child nodes through the AJAX mechanism 442 * @return bool|string returns inline javascript or false 443 */ 444 private function _jstree($data, $ns, $js_opts, $js_name, $max) { 445 global $conf; 446 $hns = false; 447 if(empty($data)) return false; 448 449 //Render requested ns as root 450 $headpage = $this->getConf('headpage'); 451 //if rootnamespace and headpage, then add startpage as headpage - TODO seems not logic, when desired use $conf[headpage]=:start: ?? 452 if(empty($ns) && !empty($headpage)) $headpage .= ','.$conf['start']; 453 $title = $this->_getTitle($ns, $headpage, $hns); 454 if(empty($title)) { 455 if(empty($ns)){ 456 $title = htmlspecialchars($conf['title'], ENT_QUOTES); 457 } else{ 458 $title = $ns; 459 } 460 } 461 // inline javascript 462 $out = "<script type='text/javascript' charset='utf-8'>\n"; 463 $out .= "<!--//--><![CDATA[//><!--\n"; 464 $out .= "var $js_name = new dTree('".$js_name."','".$js_opts['theme']."');\n"; 465 //javascript config options 466 $sepchar = idfilter(':', false); 467 $out .= "$js_name.config.urlbase='".substr(wl(":"), 0, -1)."';\n"; 468 $out .= "$js_name.config.sepchar='".$sepchar."';\n"; 469 if($js_opts['notoc']) $out .= "$js_name.config.toc=false;\n"; 470 if($js_opts['nocookie']) $out .= "$js_name.config.useCookies=false;\n"; 471 if($js_opts['noscroll']) $out .= "$js_name.config.scroll=false;\n"; 472 if($js_opts['maxjs'] > 0) $out .= "$js_name.config.maxjs=".$js_opts['maxjs'].";\n"; 473 if(!empty($js_opts['jsajax'])) $out .= "$js_name.config.jsajax='".utf8_encodeFN($js_opts['jsajax'])."';\n"; 474 //add root node 475 $out .= $js_name.".add('".idfilter(cleanID($ns), false)."',0,-1,".json_encode($title); 476 if($hns) $out .= ",'".idfilter(cleanID($hns), false)."'"; 477 $out .= ");\n"; 478 //add nodes 479 $anodes = $this->_jsnodes($data, $js_name); 480 $out .= $anodes[0]; 481 //write to document 482 $out .= "document.write(".$js_name.");\n"; 483 //initialize index 484 $out .= "jQuery(function(){".$js_name.".init("; 485 $out .= (int) is_file(INDEXMENU_IMG_ABSDIR.'/'.$js_opts['theme'].'/style.css').","; 486 $out .= (int) $js_opts['nocookie'].","; 487 $out .= '"'.$anodes[1].'",'; 488 $out .= (int) $js_opts['navbar'].","; 489 $out .= (int) $max; 490 if($js_opts['nomenu']) $out .= ",1"; 491 $out .= ");});\n"; 492 493 $out .= "//--><!]]>\n"; 494 $out .= "</script>\n"; 495 return $out; 496 } 497 498 /** 499 * Return array of javascript nodes and nodes to open. 500 * 501 * @author Samuele Tognini <samuele@samuele.netsons.org> 502 * @param array $data array with items of the tree 503 * @param string $js_name identifier for this index 504 * @param int $noajax return as inline js (=1) or array for ajax response (=0) 505 * @return array|bool returns array with 506 * - a string of the javascript nodes 507 * - and a string of space separated numbers of the opened nodes 508 * or false when no data provided 509 */ 510 public function _jsnodes($data, $js_name, $noajax = 1) { 511 if(empty($data)) return false; 512 //Array of nodes to check 513 $q = array('0'); 514 //Current open node 515 $node = 0; 516 $out = ''; 517 $extra = ''; 518 if($noajax) { 519 $jscmd = $js_name.".add"; 520 $separator = ";\n"; 521 } else { 522 $jscmd = "new Array "; 523 $separator = ","; 524 } 525 foreach($data as $i=> $item) { 526 $i++; 527 //Remove already processed nodes (greater level = lower level) 528 while($item['level'] <= $data[end($q) - 1]['level']) { 529 array_pop($q); 530 } 531 532 //till i found its father node 533 if($item['level'] == 1) { 534 //root node 535 $father = '0'; 536 } else { 537 //Father node 538 $father = end($q); 539 } 540 //add node and its options 541 if($item['type'] == 'd') { 542 //Search the lowest open node of a tree branch in order to open it. 543 if($item['open']) ($item['level'] < $data[$node]['level']) ? $node = $i : $extra .= "$i "; 544 //insert node in last position 545 $q[] = $i; 546 } 547 $out .= $jscmd."('".idfilter($item['id'], false)."',$i,".$father.",".json_encode($item['title']); 548 //hns 549 ($item['hns']) ? $out .= ",'".idfilter($item['hns'], false)."'" : $out .= ",0"; 550 ($item['type'] == 'd' || $item['type'] == 'l') ? $out .= ",1" : $out .= ",0"; 551 //MAX option 552 ($item['type'] == 'l') ? $out .= ",1" : $out .= ",0"; 553 $out .= ")".$separator; 554 } 555 $extra = rtrim($extra, ' '); 556 return array($out, $extra); 557 } 558 559 /** 560 * Get namespace title, checking for headpages 561 * 562 * @author Samuele Tognini <samuele@samuele.netsons.org> 563 * @param string $ns namespace 564 * @param string $headpage commaseparated headpages options and headpages 565 * @param string $hns reference pageid of headpage, false when not existing 566 * @return string when headpage & heading on: title of headpage, otherwise: namespace name 567 */ 568 private function _getTitle($ns, $headpage, &$hns) { 569 global $conf; 570 $hns = false; 571 $title = noNS($ns); 572 if(empty($headpage)) return $title; 573 $ahp = explode(",", $headpage); 574 foreach($ahp as $hp) { 575 switch($hp) { 576 case ":inside:": 577 $page = $ns.":".noNS($ns); 578 break; 579 case ":same:": 580 $page = $ns; 581 break; 582 //it's an inside start 583 case ":start:": 584 $page = ltrim($ns.":".$conf['start'], ":"); 585 break; 586 //inside pages 587 default: 588 $page = $ns.":".$hp; 589 } 590 //check headpage 591 if(@file_exists(wikiFN($page)) && auth_quickaclcheck($page) >= AUTH_READ) { 592 if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { 593 $title_tmp = p_get_first_heading($page, FALSE); 594 if(!is_null($title_tmp)) $title = $title_tmp; 595 } 596 $title = htmlspecialchars($title, ENT_QUOTES); 597 $hns = $page; 598 //headpage found, exit for 599 break; 600 } 601 } 602 return $title; 603 } 604 605 /** 606 * Parse namespace request 607 * 608 * @author Samuele Tognini <samuele@samuele.netsons.org> 609 * @param string $ns namespaceid 610 * @param bool $id page id to resolve $ns relative to. 611 * @return string id of namespace 612 */ 613 public function _parse_ns($ns, $id = FALSE) { 614 if(!$id) { 615 global $ID; 616 $id = $ID; 617 } 618 //Just for old reelases compatibility 619 if(empty($ns) || $ns == '..') $ns = ":.."; 620 return resolve_id(getNS($id), $ns); 621 } 622 623 /** 624 * Clean index data from unwanted nodes in nojs mode. 625 * 626 * @author Samuele Tognini <samuele@samuele.netsons.org> 627 * @param array $data nodes of the tree 628 * @return void 629 */ 630 private function _clean_data(&$data) { 631 foreach($data as $i=> $item) { 632 //closed node 633 if($item['type'] == "d" && !$item['open']) { 634 $a = $i + 1; 635 $level = $data[$i]['level']; 636 //search and remove every lower and closed nodes 637 while($data[$a]['level'] > $level && !$data[$a]['open']) { 638 unset($data[$a]); 639 $a++; 640 } 641 } 642 } 643 } 644 645 /** 646 * Callback that adds an item of namespace/page to the browsable index, if it fits in the specified options 647 * 648 * $opts['skip_index'] string regexp matching namespaceids to skip 649 * $opts['skip_file'] string regexp matching pageids to skip 650 * $opts['headpage'] string headpages options or pageids 651 * $opts['level'] int desired depth of main namespace, -1 = all levels 652 * $opts['nss'] array with entries: array(namespaceid,level) specifying namespaces with their own level 653 * $opts['nons'] bool exclude namespace nodes 654 * $opts['max'] int If initially closed, the node at max level will retrieve all its child nodes through the AJAX mechanism 655 * $opts['nopg'] bool exclude page nodes 656 * $opts['hide_headpage'] int don't hide (0) or hide (1) 657 * $opts['js'] bool use js-render 658 * 659 * @author Andreas Gohr <andi@splitbrain.org> 660 * modified by Samuele Tognini <samuele@samuele.netsons.org> 661 * @param array $data Already collected nodes 662 * @param string $base Where to start the search, usually this is $conf['datadir'] 663 * @param string $file Current file or directory relative to $base 664 * @param string $type Type either 'd' for directory or 'f' for file 665 * @param int $lvl Current recursion depht 666 * @param array $opts Option array as given to search(), see above. 667 * @return bool if this directory should be traversed (true) or not (false) 668 */ 669 public function _search_index(&$data, $base, $file, $type, $lvl, $opts) { 670 global $conf; 671 $hns = false; 672 $isopen = false; 673 $title = null; 674 $skip_index = $opts['skip_index']; 675 $skip_file = $opts['skip_file']; 676 $headpage = $opts['headpage']; 677 $id = pathID($file); 678 if($type == 'd') { 679 // Skip folders in plugin conf 680 foreach($skip_index as $skipi) { 681 if(!empty($skipi) && preg_match($skipi, $id)) 682 return false; 683 } 684 //check ACL (for sneaky_index namespaces too). 685 if($conf['sneaky_index'] && auth_quickaclcheck($id.':') < AUTH_READ) return false; 686 //Open requested level 687 if($opts['level'] > $lvl || $opts['level'] == -1) $isopen = true; 688 //Search optional namespaces 689 if(!empty($opts['nss'])) { 690 $nss = $opts['nss']; 691 for($a = 0; $a < count($nss); $a++) { 692 if(preg_match("/^".$id."($|:.+)/i", $nss[$a][0], $match)) { 693 //It contains an optional namespace 694 $isopen = true; 695 } elseif(preg_match("/^".$nss[$a][0]."(:.*)/i", $id, $match)) { 696 //It's inside an optional namespace 697 if($nss[$a][1] == -1 || substr_count($match[1], ":") < $nss[$a][1]) { 698 $isopen = true; 699 } else { 700 $isopen = false; 701 } 702 } 703 } 704 } 705 if($opts['nons']) { 706 return $isopen; 707 } elseif($opts['max'] > 0 && !$isopen && $lvl >= $opts['max']) { 708 $isopen = false; 709 //Stop recursive searching 710 $return = false; 711 //change type 712 $type = "l"; 713 } elseif($opts['js']) { 714 $return = true; 715 } else { 716 $return = $isopen; 717 } 718 //Set title and headpage 719 $title = $this->_getTitle($id, $headpage, $hns); 720 //link namespace nodes to start pages when excluding page nodes 721 if(!$hns && $opts['nopg']) $hns = $id.":".$conf['start']; 722 } else { 723 //Nopg.Dont show pages 724 if($opts['nopg']) return false; 725 $return = true; 726 //Nons.Set all pages at first level 727 if($opts['nons']) $lvl = 1; 728 //don't add 729 if(substr($file, -4) != '.txt') return false; 730 //check hiddens and acl 731 if(isHiddenPage($id) || auth_quickaclcheck($id) < AUTH_READ) return false; 732 //Skip files in plugin conf 733 foreach($skip_file as $skipf) { 734 if(!empty($skipf) && preg_match($skipf, $id)) 735 return false; 736 } 737 //Skip headpages to hide 738 if(!$opts['nons'] && !empty($headpage) && $opts['hide_headpage']) { 739 //start page is in root 740 if($id == $conf['start']) return false; 741 $ahp = explode(",", $headpage); 742 foreach($ahp as $hp) { 743 switch($hp) { 744 case ":inside:": 745 if(noNS($id) == noNS(getNS($id))) return false; 746 break; 747 case ":same:": 748 if(@is_dir(dirname(wikiFN($id))."/".utf8_encodeFN(noNS($id)))) return false; 749 break; 750 //it' s an inside start 751 case ":start:": 752 if(noNS($id) == $conf['start']) return false; 753 break; 754 default: 755 if(noNS($id) == cleanID($hp)) return false; 756 } 757 } 758 } 759 760 //Set title 761 if($conf['useheading'] == 1 || $conf['useheading'] === 'navigation') { 762 $title = p_get_first_heading($id, FALSE); 763 } 764 if(is_null($title)) $title = noNS($id); 765 $title = htmlspecialchars($title, ENT_QUOTES); 766 } 767 768 $item = array( 769 'id' => $id, 770 'type' => $type, 771 'level' => $lvl, 772 'open' => $isopen, 773 'title' => $title, 774 'hns' => $hns, 775 'file' => $file, 776 'return' => $return 777 ); 778 $item['sort'] = $this->_setorder($item); 779 $data[] = $item; 780 return $return; 781 } 782 783 /** 784 * Callback Index item formatter 785 * 786 * User function for @see html_buildlist() 787 * 788 * @author Andreas Gohr <andi@splitbrain.org> 789 * @author Samuele Tognini <samuele@samuele.netsons.org> 790 * @author Rik Blok 791 * 792 * @param array $item item described by array with at least the entries 793 * - id page id/namespace id 794 * - type 'd', 'l'(directory which is not yet opened) or 'f' 795 * - open is node open 796 * - title title of link 797 * - hns page id of headpage of the namespace or false 798 * @return string html of the content of a list item 799 */ 800 public function _html_list_index($item) { 801 global $INFO; 802 $ret = ''; 803 804 //namespace 805 if($item['type'] == 'd' || $item['type'] == 'l') { 806 $markCurrentPage = false; 807 808 $link = $item['id']; 809 $more = 'idx='.$item['id']; 810 //namespace link 811 if($item['hns']) { 812 $link = $item['hns']; 813 $tagid = "indexmenu_idx_head"; 814 $more = ''; 815 //current page is shown? 816 $markCurrentPage = $this->getConf('hide_headpage') && $item['hns'] == $INFO['id']; 817 } else { 818 //namespace without headpage 819 $tagid = "indexmenu_idx"; 820 if($item['open']) $tagid .= ' open'; 821 } 822 823 if($markCurrentPage) $ret .= '<span class="curid">'; 824 $ret .= '<a href="'.wl($link, $more).'" class="'.$tagid.'">'; 825 $ret .= $item['title']; 826 $ret .= '</a>'; 827 if($markCurrentPage) $ret .= '</span>'; 828 } else { 829 //page link 830 $ret .= html_wikilink(':'.$item['id']); 831 } 832 return $ret; 833 } 834 835 /** 836 * callback that recurse directory 837 * 838 * This function recurses into a given base directory 839 * and calls the supplied function for each file and directory 840 * 841 * Similar to search() of inc/search.php, but has extended sorting options 842 * 843 * @param array $data The results of the search are stored here 844 * @param string $base Where to start the search 845 * @param callback $func Callback (function name or array with object,method) 846 * @param array $opts List of indexmenu options 847 * @param string $dir Current directory beyond $base 848 * @param int $lvl Recursion Level 849 * 850 * @author Andreas Gohr <andi@splitbrain.org> 851 * @author modified by Samuele Tognini <samuele@samuele.netsons.org> 852 */ 853 public function _search(&$data, $base, $func, $opts, $dir = '', $lvl = 1) { 854 $dirs = array(); 855 $files = array(); 856 $files_tmp = array(); 857 $dirs_tmp = array(); 858 $count = count($data); 859 860 //read in directories and files 861 $dh = @opendir($base.'/'.$dir); 862 if(!$dh) return; 863 while(($file = readdir($dh)) !== false) { 864 //skip hidden files and upper dirs 865 if(preg_match('/^[\._]/', $file)) continue; 866 if(is_dir($base.'/'.$dir.'/'.$file)) { 867 $dirs[] = $dir.'/'.$file; 868 continue; 869 } 870 $files[] = $dir.'/'.$file; 871 } 872 closedir($dh); 873 874 //Collect and sort dirs 875 if($this->nsort) { 876 //collect the wanted directories in dirs_tmp 877 foreach($dirs as $dir) { 878 call_user_func_array($func, array(&$dirs_tmp, $base, $dir, 'd', $lvl, $opts)); 879 } 880 //sort directories 881 usort($dirs_tmp, array($this, "_cmp")); 882 //add and search each directory 883 foreach($dirs_tmp as $dir) { 884 $data[] = $dir; 885 if($dir['return']) { 886 $this->_search($data, $base, $func, $opts, $dir['file'], $lvl + 1); 887 } 888 } 889 } else { 890 //sort by page name 891 sort($dirs); 892 //collect directories 893 foreach($dirs as $dir) { 894 if(call_user_func_array($func, array(&$data, $base, $dir, 'd', $lvl, $opts))) { 895 $this->_search($data, $base, $func, $opts, $dir, $lvl + 1); 896 } 897 } 898 } 899 900 //Collect and sort files 901 foreach($files as $file) { 902 call_user_func_array($func, array(&$files_tmp, $base, $file, 'f', $lvl, $opts)); 903 } 904 usort($files_tmp, array($this, "_cmp")); 905 906 //count added items 907 $added = count($data) - $count; 908 909 if($added === 0 && empty($files_tmp)) { 910 //remove empty directory again, only if it has not a headpage associated 911 $v = end($data); 912 if(!$v['hns']) array_pop($data); 913 } else { 914 //add files to index 915 $data = array_merge($data, $files_tmp); 916 } 917 } 918 919 /** 920 * callback that sorts nodes 921 * 922 * @param array $a first node as array with 'sort' entry 923 * @param array $b second node as array with 'sort' entry 924 * @return int if less than zero 1st node is less than 2nd, otherwise equal respectively larger 925 */ 926 private function _cmp($a, $b) { 927 if($this->rsort) { 928 return strnatcasecmp($b['sort'], $a['sort']); 929 } else { 930 return strnatcasecmp($a['sort'], $b['sort']); 931 } 932 } 933 934 /** 935 * Add sort information to item. 936 * 937 * @author Samuele Tognini <samuele@samuele.netsons.org> 938 * 939 * @param array $item 940 * @return bool|int|mixed|string 941 */ 942 private function _setorder($item) { 943 global $conf; 944 945 $sort = false; 946 $page = false; 947 if($item['type'] == 'd' || $item['type'] == 'l') { 948 //Fake order info when nsort is not requested 949 ($this->nsort) ? $page = $item['hns'] : $sort = 0; 950 } 951 if($item['type'] == 'f') $page = $item['id']; 952 if($page) { 953 if($this->hsort && noNS($item['id']) == $conf['start']) $sort = 1; 954 if($this->msort) $sort = p_get_metadata($page, $this->msort); 955 if(!$sort && $this->sort) { 956 switch($this->sort) { 957 case 't': 958 $sort = $item['title']; 959 break; 960 case 'd': 961 $sort = @filectime(wikiFN($page)); 962 break; 963 } 964 } 965 } 966 if($sort === false) $sort = noNS($item['id']); 967 return $sort; 968 } 969} //Indexmenu class end 970