dokuwiki-indexmenu-plugin – Blame information for rev 1

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