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   $mode   string          output format being rendered
319     * @param   $renderer Doku_Renderer the current renderer object
320     * @param   $data     array         data created by handler()
321     * @return  boolean                 rendered correctly?
322     */
323    public function render($mode, Doku_Renderer $renderer, $data) {
324        global $ACT;
325        global $conf;
326        global $INFO;
327        if($mode == '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($mode == '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        $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
972