1<?php
2/**
3 * Dir Plugin: Shows pages in one or namespaces in a table or list
4 *
5 * @license  GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author   Jacobus Geluk <Jacobus.Geluk@gmail.com>
7 * @based_on "pageindex" plugin by Kite <Kite@puzzlers.org>
8 * @based_on "externallink" plugin by Otto Vainio <plugins@valjakko.net>
9 * @based_on "pagelist" plugin by Esther Brunner <wikidesign@gmail.com>
10 *
11 * Contributions by:
12 *
13 * - Jean-Philippe Prade
14 * - Gunther Hartmann
15 * - Sebastian Menge
16 * - Matthias Schulte
17 * - Geert Janssens
18 * - Gerry Weißbach
19 */
20
21if(!defined('DOKU_INC')) {
22    define ('DOKU_INC', realpath(dirname(__FILE__).'/../../').'/');
23}
24if(!defined('DOKU_PLUGIN')) {
25    define ('DOKU_PLUGIN', DOKU_INC.'lib/plugins/');
26}
27
28require_once (DOKU_PLUGIN.'syntax.php');
29require_once (DOKU_INC.'inc/search.php');
30require_once (DOKU_INC.'inc/pageutils.php');
31
32define ("DIR_PLUGIN_PATTERN", "DIR");
33
34/**
35 * The main DIR plugin class...
36 */
37class syntax_plugin_dir extends DokuWiki_Syntax_Plugin {
38    var $debug = false;
39    var $plugins = Array();
40    var $opts = Array();
41    var $cols = Array();
42    var $hdrs = Array();
43    var $pages = Array();
44    var $includeTags = Array();
45    var $excludeTags = Array();
46    var $hasTags = false;
47    var $style = "default";
48    var $rdr = NULL;
49    var $rdrMode = NULL;
50    var $start = "start";
51    var $dformat = NULL;
52    var $sortKeys = Array();
53    var $nbrOfSortKeys = 0;
54    var $useDefaultTitle = true;
55    var $modeIsXHTML = false;
56    var $modeIsLatex = false;
57    var $processedLatex = false;
58    var $rowNumber = 0;
59    var $ucnames = false;
60
61    /**
62     * Constructor
63     */
64    function syntax_plugin_dir() {
65        global $conf;
66
67        //
68        // In the config you can set allowdebug, but you can also
69        // specify the debug attribute in the ~~DIR~~ line...
70        //
71        if($conf ["allowdebug"] == 1)
72            $this->debug = true;
73
74        $this->start   = $conf ["start"];
75        $this->dformat = $conf ["dformat"];
76        $this->style   = $this->getConf("style");
77    }
78
79    /**
80     * return some info
81     */
82    function getInfo() {
83        return array(
84            'author' => 'Jacobus Geluk',
85            'email'  => 'Jacobus.Geluk@gmail.com',
86            'date'   => '2008-06-28',
87            'name'   => 'Dir Plugin',
88            'desc'   => 'Shows pages in one or namespaces in a table or list',
89            'url'    => 'http://www.dokuwiki.org/plugin:dir',
90        );
91    }
92
93    /**
94     * What kind of syntax are we?
95     */
96    function getType() {
97        return "substition";
98    }
99
100    /**
101     * Just before build in links
102     */
103    function getSort() {
104        return 299;
105    }
106
107    /**
108     * What about paragraphs?
109     */
110    function getPType() {
111        return "block";
112    }
113
114    /**
115     * Register the ~~DIR~~ verb...
116     * Supported signatures:
117     *
118     * 1. ~~DIR~~
119     * 2. ~~DIR:...~~
120     * 3. ~~DIR?...~~
121     */
122    function connectTo($mode) {
123        $this->Lexer->addSpecialPattern('~~'.DIR_PLUGIN_PATTERN.'~~', $mode, 'plugin_dir');
124        $this->Lexer->addSpecialPattern('~~'.DIR_PLUGIN_PATTERN.'[:?][^~]*~~', $mode, 'plugin_dir');
125    }
126
127    /**
128     * Handle the match
129     */
130    function handle($match, $state, $pos, Doku_Handler $handler) {
131        return preg_replace("%~~".DIR_PLUGIN_PATTERN.":(=(.*))?~~%", "\\2", $match);
132    }
133
134    /**
135     * Initialize the current object for each rendering pass
136     */
137    function _initRender($mode, &$renderer) {
138        $rc                = FALSE;
139        $this->rowNumber   = 0;
140        $this->opts        = Array();
141        $this->cols        = Array();
142        $this->hdrs        = Array();
143        $this->pages       = Array();
144        $this->hasTags     = false;
145        $this->excludeTags = Array();
146        $this->includeTags = Array();
147        $this->rdr         =& $renderer;
148        $this->rdrMode     = $mode;
149
150        switch($mode) {
151            case 'latex':
152                $this->modeIsXHTML = false;
153                $this->modeIsLatex = true;
154                $rc                = TRUE;
155                break;
156            case 'xhtml':
157                $this->modeIsXHTML = true;
158                $this->modeIsLatex = false;
159                $rc                = TRUE;
160                break;
161            default:
162                $this->modeIsXHTML = false;
163                $this->modeIsLatex = false;
164        }
165
166        return $rc;
167    }
168
169    /**
170     * Create output
171     */
172    function render($mode, Doku_Renderer $renderer, $data) {
173        if(!$this->_initRender($mode, $renderer)) return false;
174
175        $rc = $this->_dir($data);
176
177        if($this->modeIsLatex) $this->processedLatex = true;
178
179        $this->_showDebugMsg("Leaving syntax_plugin_dir.render()");
180
181        return $rc;
182    }
183
184    /**
185     * Put a debug message on screen...
186     */
187    function _showDebugMsg($msg) {
188        if(!$this->debug) return;
189
190        if(is_array($msg)) {
191            foreach($msg as $index => $m) {
192                $this->_showDebugMsg("Array [$index]: ".$m);
193            }
194            return;
195        }
196
197        $this->_putNewLine();
198
199        switch($this->rdrMode) {
200            case 'xhtml':
201                $this->_put(DOKU_LF."<span style=\"color:red;\">~~");
202                $this->_put(DIR_PLUGIN_PATTERN."~~: ".hsc($msg)."</span>");
203                break;
204            case 'latex':
205                $this->_put(DOKU_LF."~~");
206                $this->_put(DIR_PLUGIN_PATTERN."~~: ".$msg);
207                break;
208        }
209    }
210
211    /**
212     * Load the specified plugin (like the tag or discussion plugin)
213     */
214    function _loadPlugin($plugin) {
215        if(plugin_isdisabled($plugin))
216            return false;
217
218        $plug = plugin_load('helper', $plugin);
219
220        if(!$plug) {
221            $this->_showDebugMsg("Plugin \"$plugin\" NOT loaded!");
222            return false;
223        }
224
225        $this->plugins [$plugin] = $plug;
226        $this->_showDebugMsg("Plugin \"$plugin\" loaded!");
227
228        return true;
229    }
230
231    /**
232     * Let another plugin generate the content...
233     */
234    function _pluginCell($plugin, $id) {
235        $plug = $this->plugins [$plugin];
236
237        if(!$plug)
238            return 'Plugin '.$plugin.' not loaded!';
239
240        $html = $plug->td(cleanID($id));
241
242        return $html;
243    }
244
245    /**
246     * Shows parsed options (in debug mode)
247     */
248    function _parseOptionsShow($data, $dir, $ns) {
249        if(!$this->debug) return;
250
251        $this->_put(DOKU_LF."<xmp style=\"font-family: Courier; color: red;\">");
252        $this->_put(DOKU_LF."  data = $data");
253        $this->_put(DOKU_LF."  dir  = $dir");
254        $this->_put(DOKU_LF."  ns   = $ns");
255
256        foreach($this->opts as $key => $opt) {
257            if(is_array($opt)) {
258                foreach($opt as $optkey => $optval) {
259                    $this->_put(DOKU_LF."  opts[$key][$optkey] = $optval");
260                }
261            } else if(is_bool($opt)) {
262                $this->_put(DOKU_LF."  opts[$key] = ".($opt ? "true" : "false"));
263            } else {
264                $this->_put(DOKU_LF."  opts[$key] = $opt");
265            }
266        }
267        $this->_put(DOKU_LF.DOKU_LF."date: ".date("D M j G:i:s T Y"));
268        $this->_put(DOKU_LF."</xmp>".DOKU_LF);
269    }
270
271    /**
272     * Get the namespace of the parent directory
273     * (always prefixed and postfixed with a colon, root is ':')
274     */
275    function _getParentNS($id) {
276        // global $ID ;
277        $curNS = getNS($id);
278
279        if($curNS == '') return ':';
280
281        if(substr($curNS, 0, 1) != ':') {
282            $curNS = ':'.$curNS;
283        }
284
285        return $curNS.':';
286    }
287
288    /**
289     * Create a fully qualified namespace from the specified one.
290     * The second parameter must be true when the given namespace
291     * is never a page id. In that case, the returned namespace
292     * always ends with a colon.
293     */
294    function _parseNS($ns, $mustBeNSnoPage) {
295        global $ID;
296
297        if(substr($ns, 0, 2) == '.:') {
298            $ns = ':'.getNS($ID).substr($ns, 1);
299        } elseif(substr($ns, 0, 3) == '..:') {
300            $ns = $this->_getParentNS($ID).substr($ns, 3);
301        } elseif($ns == '..') {
302            $ns = $this->_getParentNS($ID);
303        } elseif(substr($ns, 0, 1) == ':') {
304        } elseif($ns == '.' || $ns == '*') {
305            $ns = ':'.getNS($ID);
306        } else {
307            $ns = ':'.getNS($ID).':'.$ns;
308        }
309
310        if($mustBeNSnoPage && substr($ns, -1) <> ':') $ns .= ':';
311
312        return $ns;
313    }
314
315    /**
316     * Convert namespace to its path
317     */
318    function _ns2path($ns) {
319        global $conf;
320
321        if($ns == ':' || $ns == '') return $conf ['datadir'];
322
323        $ns = trim($ns, ':');
324
325        $path = $conf ['datadir'].'/'.utf8_encodeFN(str_replace(':', '/', $ns));
326
327        return $path;
328    }
329
330    /**
331     * Initialize the opts array...
332     */
333    function _initOpts($flags) {
334        $this->opts                   = array();
335        $this->opts ["noheader"]      = false;
336        $this->opts ["collapse"]      = false;
337        $this->opts ["ego"]           = false;
338        $this->opts ["namespacename"] = false;
339
340        $flags = explode('&', $flags);
341
342        foreach($flags as $index => $par) {
343            $tmp = explode("=", $par);
344            $key = $tmp [0];
345            $val = $tmp [1];
346
347            switch($key) {
348                case "skip":
349                case "cols":
350                case "hdrs":
351                case "sort":
352                case "tag":
353                    $val = explode(';', trim($val, ';'));
354                    $this->_loadPlugin("tag");
355                    break;
356                case "noheader":
357                case "nohead":
358                case "nohdr":
359                    $key = "noheader";
360                    $val = true;
361                    break;
362                case "showheader":
363                case "header":
364                    $key = "noheader";
365                    $val = false;
366                    break;
367                case "collapse":
368                    $key = "collapse";
369                    $val = true;
370                    break;
371                case "ego":
372                    $key = "ego";
373                    $val = true;
374                    break;
375                case "nodefaulttitle":
376                case "ndt":
377                    $key                   = "nodefaulttitle";
378                    $val                   = true;
379                    $this->useDefaultTitle = false;
380                    break;
381                case "widedesc":
382                    $val = true;
383                    break;
384                case "table":
385                    $this->style = "table";
386                    break;
387                case "list":
388                    $this->style = "list";
389                    break;
390                case "namespacename":
391                    $key = "namespacename";
392                    $val = true;
393                    break;
394                case "ucnames":
395                    $this->ucnames = true;
396                    break;
397
398                case "debug":
399                    $this->debug = true;
400		    break;
401                case  "last":
402		    $key = 'maxrows';
403		    $val = intval($val);
404		    break;
405            }
406            $this->opts [$key] = $val;
407        }
408    }
409
410    /**
411     * Check the supplied column names
412     */
413    function _parseColumnNames() {
414        if(is_array($this->opts ["cols"])) {
415            $this->cols = $this->opts ["cols"];
416        } else {
417            $this->cols = Array("page");
418        }
419
420        if(count($this->cols) == 0) {
421            $cols [] = "page";
422            $cols [] = "desc";
423        }
424
425        $newCols = Array();
426
427        foreach($this->cols as $index => $col) {
428            switch($col) {
429                case "page":
430                case "desc":
431                case "user":
432                case "userid":
433                case "mdate":
434                case "cdate":
435                case "rowno":
436                    break;
437                case "comments":
438                    $this->_loadPlugin("discussion");
439                    break;
440                case "tags":
441                    $this->_loadPlugin("tag");
442                    break;
443                case "date":
444                    $col = "mdate";
445                    break;
446                case "description":
447                    $col = "desc";
448                    break;
449                default:
450                    $this->_showDebugMsg("Unrecognized column name: \"$col\"");
451                    $col = '';
452            }
453            if($col != '') {
454                $this->_showDebugMsg("Recognized column name: $col");
455                $newCols [] = $col;
456            }
457        }
458        $this->cols = $newCols;
459        if(count($this->opts ["hdrs"]) != count($this->cols)) {
460            $this->_showDebugMsg(
461                "The number of specified headers (".count($this->opts ["hdrs"]).
462                    ") is not equal to the number of specified columns (".
463                    count($this->cols).")!"
464            );
465        }
466    }
467
468    /**
469     * Check the supplied tags
470     */
471    function _parseTags() {
472        $this->hasTags = false;
473
474        if(is_array($this->opts ["tag"])) {
475            foreach($this->opts ["tag"] as $tag) {
476                if($tag == NULL || $tag == '')
477                    continue;
478                $tag = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8");
479                if(substr($tag, 0, 1) == '!') {
480                    $this->excludeTags [] = substr($tag, 1);
481                } else {
482                    $this->includeTags [] = $tag;
483                }
484                $this->hasTags = true;
485            }
486            foreach($this->excludeTags as $tag) {
487                $this->_showDebugMsg("Specified exclude tag: $tag");
488            }
489            foreach($this->includeTags as $tag) {
490                $this->_showDebugMsg("Specified include tag: $tag");
491            }
492        }
493    }
494
495    /**
496     * Check the supplied sort keys
497     */
498    function _parseSortKeys() {
499        if(is_array($this->opts ["sort"])) {
500            $this->sortKeys = $this->opts ["sort"];
501        }
502
503        $sortKeys = Array();
504
505        foreach($this->sortKeys as $index => $sortKey) {
506
507            $array = explode('-', strtolower($sortKey));
508            if(count($array) == 1) {
509                $array = Array($sortKey, "a");
510            }
511
512            switch($array [1]) {
513                case NULL:
514                case "a":
515                case "asc":
516                case "ascending":
517                    $array [1] = false;
518                    break;
519                case "d":
520                case "desc":
521                case "descending":
522                    $array [1] = true;
523                    break;
524                default:
525                    $this->_showDebugMsg(
526                        "Unrecognized sort column name modifier: ".
527                            $array [1]
528                    );
529                    $array [1] = false;
530            }
531
532            switch($array [0]) {
533                case "page":
534                case "desc":
535                case "user":
536                case "userid":
537                case "mdate":
538                case "cdate":
539                case "rowno":
540                    break;
541                case "comments":
542                    $this->_loadPlugin("discussion");
543                    break;
544                case "tags":
545                    $this->_loadPlugin("tag");
546                    break;
547                case "date":
548                    $array [0] = "mdate";
549                    break;
550                case "description":
551                    $array [0] = "desc";
552                    break;
553                default:
554                    $this->_showDebugMsg(
555                        "Unrecognized sort column name: ".$array [0]
556                    );
557                    $array [0] = NULL;
558            }
559            if($array [0]) {
560                $this->_showDebugMsg(
561                    "Sort column ".$array [0]." ".
562                        ($array [1] ? "descending" : "ascending")
563                );
564                $sortKeys [] = $array;
565            }
566        }
567
568        $this->sortKeys      = $sortKeys;
569        $this->nbrOfSortKeys = count($this->sortKeys);
570    }
571
572    /**
573     * Add a page to the collection of $pages. Check first if it should
574     * not be skipped...
575     */
576    function _addFoundPage(&$data, $ns, $id, $type, $level) {
577        global $ID;
578        $fqid = $ns.$id; // Fully qualified id...
579
580        //
581        // If this file or directory should be skipped, do so
582        //
583        switch($type) {
584            case "f":
585                if(($fqid == ':'.$ID) && !$this->opts ["ego"]) // If we found ourself, skip it
586                    return false;
587                $pageName = noNS($id);
588                if($pageName == $this->start)
589                    return false;
590                foreach($this->opts ["skipfqid"] as $index => $skipitem) {
591                    if($skipitem) {
592                        if($skipitem == $fqid) {
593                            //
594                            // Remove the skip rule, it has no use any more...
595                            //
596                            $this->opts ["skipfqid"] [$index] = NULL;
597                            $this->_showDebugMsg("Skipping $fqid due to skip rule $skipitem");
598                            return false;
599                        }
600                    }
601                }
602                if($this->opts ["collapse"]) {
603                    // With collapse, only show:
604                    // - pages within the same namespace as the current page
605                    if($this->_getParentNS($fqid) != $this->_getParentNS($ID)) {
606                        return false;
607                    }
608                }
609                $linkid = $fqid;
610                break;
611            case "d":
612                $fqid .= ':';
613                foreach($this->opts ["skipns"] as $skipitem) {
614                    if($skipitem == $fqid) {
615                        $this->_showDebugMsg("Skipping $fqid due to skip rule $skipitem");
616                        return false;
617                    }
618                }
619
620                // Don't add startpages the user isn't authorized to read
621                if(auth_quickaclcheck(substr($linkid, 1)) < AUTH_READ)
622                    return false;
623
624                if($this->opts ["collapse"]) {
625                    // With collapse, only show:
626                    // - sibling namespaces of the current namespace and it's ancestors
627                    $curPathSplit  = explode(":", trim(getNS($ID), ":"));
628                    $fqidPathSplit = explode(":", trim(getNS($fqid), ":"));
629
630                    // Find the last parent namespace that matches
631                    // If there is only one more child namespace in the namespace under evaluation,
632                    // Then this is a sibling of one of the parent namespaces of the current page.
633                    // Siblings are ok, grandchild namespaces and below should be skipped (for collapse).
634                    $clevel = 0;
635                    if(count($curPathSplit) > 0) {
636                        while(($clevel < count($fqidPathSplit) - 1) && ($clevel < count($curPathSplit))) {
637                            if($curPathSplit[$clevel] == $fqidPathSplit[$clevel]) {
638                                $clevel++;
639                            } else {
640                                break;
641                            }
642                        }
643                    }
644                    if(count($fqidPathSplit) > $clevel + 1) {
645                        return false;
646                    }
647                }
648
649                $linkid = $fqid.$this->start;
650                break;
651        }
652
653        //  $this->_showDebugMsg ("$level $type $ns$id:");
654
655        if($this->ucnames) {
656            $fqid =  str_replace('_'," ",$fqid);
657           // $fqid = ltrim($fqid , ':');
658            $fqid = preg_replace_callback(
659                '|:\w|',
660                function ($matches) {
661                    return strtoupper($matches[0]);
662                },
663                $fqid
664            );
665            $fqid = ucwords($fqid);
666        }
667        $data [] = array(
668            'id'         =>  $fqid,
669            'type'       => $type,
670            'level'      => $level,
671            'linkid'     => $linkid,
672            'timestamp'  => NULL
673        );
674
675        return true;
676    }
677
678    /**
679     * Callback method for the search function in _parseOptions
680     */
681    function _searchDir(&$data, $base, $file, $type, $level, $opts) {
682        global $ID;
683        $ns = $opts ["ns"];
684
685        switch($type) {
686            case "d":
687                return $this->_addFoundPage($data, $ns, pathID($file), $type, $level);
688            case "f":
689                if(!preg_match('#\.txt$#', $file))
690                    return false;
691                //check ACL
692                $id = pathID($file);
693                if(auth_quickaclcheck($id) < AUTH_READ)
694                    return false;
695                $this->_addFoundPage($data, $ns, $id, $type, $level);
696        }
697
698        return false;
699    }
700
701    /**
702     * Parse the options after the ~~DIR: string and
703     * return true if the table can be generated...
704     *
705     * A namespace specifaction should start with a colon and the flags
706     * should start with a question mark, like this:
707     *
708     * ~~DIR[[:<namespace>][?<flags>]]~~
709     *
710     * To not break pages created with an older version of this plugin, this
711     * syntax is also supported:
712     *
713     * ~~DIR:<flags>~~
714     *
715     * This assumes that no other colon is put in <flags>
716     */
717    function _parseOptions($data) {
718        global $conf;
719        global $ID;
720        $ns    = '.';
721        $flags = trim($data, '~');
722        $flags = substr($flags, strlen(DIR_PLUGIN_PATTERN));
723        $flags = trim($flags);
724
725        $this->_showDebugMsg("specified arguments=".$flags);
726
727        if(
728            substr($flags, 0, 1) == ':' &&
729            strpos(substr($flags, 1), '?') === FALSE &&
730            strpos(substr($flags, 1), ':') === FALSE
731        ) {
732            //
733            // This is the "old" syntax where flags do not start with a question mark
734            //
735            $this->_showDebugMsg("parseOptions A");
736            $flags = substr($flags, 1);
737        } else if(
738            substr($flags, 0, 1) == ':' &&
739            strpos(substr($flags, 1), '?') === FALSE
740        ) {
741            //
742            // There is no questionmark so it's all namespace specification
743            //
744            $this->_showDebugMsg("parseOptions B");
745            $ns    = substr($flags, 1);
746            $flags = '';
747        } else if(substr($flags, 0, 1) == '?') {
748            $this->_showDebugMsg("parseOptions C");
749            $flags = substr($flags, 1);
750        } else if(strlen($flags) == 0) {
751            $this->_showDebugMsg("parseOptions D");
752        } else if(
753            strpos(substr($flags, 1), '?') !== FALSE
754        ) {
755            $this->_showDebugMsg("parseOptions E");
756            $tmp = explode('?', $flags);
757
758            if(count($tmp) == 2) {
759                $ns    = substr($tmp [0], 1);
760                $flags = $tmp [1];
761            } else {
762                $this->_showDebugMsg("ERROR: Multiple questionmarks are not supported");
763                $flags = '';
764            }
765        } else {
766            $this->_showDebugMsg("parseOptions E");
767            $ns    = $flags;
768            $flags = '';
769        }
770
771        $this->_showDebugMsg("specified namespace=$ns");
772        $this->_showDebugMsg("specified flags=$flags");
773
774        $ns = $this->_parseNS($ns, true);
775
776        $path = $this->_ns2path($ns);
777        $this->_showDebugMsg("path=$path");
778
779        $this->_initOpts($flags);
780        $this->_parseColumnNames();
781        $this->_parseSortKeys();
782        $this->_parseTags();
783
784        //
785        // Check the column headers
786        //
787        $this->hdrs = $this->cols;
788        if(is_array($this->opts ["hdrs"])) {
789            foreach($this->opts ["hdrs"] as $index => $hdr) {
790                $this->hdrs [$index] = $hdr;
791            }
792        }
793
794        //
795        // Check the skip items
796        //
797        $this->opts ["skipfqid"] = Array();
798        $this->opts ["skipns"]   = Array();
799        if(is_array($this->opts ["skip"])) {
800            foreach($this->opts ["skip"] as $skipitem) {
801                $item = $this->_parseNS($skipitem, false);
802                if(substr($item, -1) == ":") {
803                    $this->opts ["skipns"] [] = $item;
804                } else {
805                    $this->opts ["skipfqid"] [] = $item;
806                }
807            }
808        }
809
810        $this->_parseOptionsShow($data, $path, $ns);
811
812        //
813        // Search the directory $dir, only if the pages array
814        // is empty, since we can pass here several times (xhtml, latex).
815        //
816        $this->_showDebugMsg("Search directory $path");
817        $this->_showDebugMsg("for namespace $ns");
818
819        if(count($this->pages) == 0) {
820            search(
821                $this->pages, // results
822                $path, // folder root
823                array($this, '_searchDir'), // handler
824                array('ns' => $ns) // namespace
825            );
826        }
827        $count = count($this->pages);
828
829        $this->_showDebugMsg("Found ".$count." pages!");
830
831        if($count == 0) {
832            $this->_put(DOKU_LF."\t<p>There are no documents to show.</p>".DOKU_LF);
833            return false;
834        }
835
836        $this->_sortResult();
837	    if ( !empty($this->opts['maxrows']) && $this->opts['maxrows'] > 0 ) {
838		    $this->pages = array_slice($this->pages, 0, $this->opts['maxrows']);
839	    }
840
841        return true;
842    }
843
844    /**
845     * Sort the found pages according to the settings
846     */
847    function _sortResult() {
848
849        if($this->nbrOfSortKeys == 0)
850            return;
851
852        usort($this->pages, array($this, "_sortPage"));
853    }
854
855    /**
856     * Compare function for usort
857     */
858    function _sortPage($a, $b) {
859        return $this->_sortPageByKey($a, $b, 0);
860    }
861
862    function _sortPageByKey(&$a, &$b, $index) {
863        if($index >= $this->nbrOfSortKeys)
864            return 0;
865
866        $keyType  = $this->sortKeys [$index];
867        $sortKeyA = $this->_getSortKey($a, $keyType [0]);
868        $sortKeyB = $this->_getSortKey($b, $keyType [0]);
869
870        if($sortKeyA == $sortKeyB)
871            return $this->_sortPageByKey($a, $b, $index + 1);
872
873        if($keyType [1]) {
874            $tmp      = $sortKeyA;
875            $sortKeyA = $sortKeyB;
876            $sortKeyB = $tmp;
877        }
878
879        return ($sortKeyA < $sortKeyB) ? -1 : 1;
880    }
881
882    /**
883     * Produces a sortable key
884     */
885    function _getSortKey(&$page, $keyType) {
886        switch($keyType) {
887            case "page":
888                return html_wikilink($page ["id"]);
889            case "desc":
890            case "widedesc":
891                return $this->_getMeta($page, "description", "abstract");
892            case "mdate":
893                return $this->_getMeta($page, "date", "modified");
894            case "cdate":
895                return $this->_getMeta($page, "date", "created");
896            case "user":
897                $users = $this->_getMeta($page, "contributor");
898                if(is_array($users)) {
899                    $index = 0;
900                    foreach($users as $userid => $user) {
901                        if($user && $user <> "") {
902                            return $user;
903                        }
904                    }
905                }
906                return $users;
907            case "userid":
908                $users = $this->_getMeta($page, "contributor");
909                if(is_array($users)) {
910                    $index = 0;
911                    foreach($users as $userid => $user) {
912                        if($userid && $userid <> "") {
913                            return $userid;
914                        }
915                    }
916                }
917                return $users;
918            case "comments":
919                return $this->_pluginCell("discussion", $page ["linkid"]);
920            case "tags":
921                return $this->_pluginCell("tag", $page ["linkid"]);
922            case "rowno":
923                return '0';
924        }
925
926        return NULL;
927    }
928
929    /**
930     * Generate the content for the cell with the page link...
931     */
932    function _tableCellContentID(&$page) {
933        $fqid        = $page ["id"];
934        $tmplvl      = $page ["level"] - 1;
935        $spacerWidth = $tmplvl * 20;
936        $pageid      = $fqid;
937        $name        = NULL;
938
939        if($page ["type"] == 'd') {
940            if($this->opts ["namespacename"]) {
941                $pieces = explode(':', trim($pageid, ':'));
942                $name   = array_pop($pieces);
943            }
944            $pageid .= ':'.$this->start;
945        }
946
947        if(!$this->useDefaultTitle) {
948            $name = explode(':', $fqid);
949            $name = ucfirst($name [count($name) - 1]);
950        }
951
952        switch($this->rdrMode) {
953            case 'latex':
954                $this->rdr->internallink($pageid, $name);
955                break;
956            case 'xhtml':
957                if($spacerWidth > 0) {
958                    $this->_put('<div style="margin-left: '.$spacerWidth.'px;">');
959                }
960
961                if($page ["type"] == 'd' && $this->ucnames) {
962                   $dirlnk = html_wikilink($pageid, $name);
963                   $dirlnk = str_replace('wikilink2', 'wikilink',$dirlnk);
964                   $this->_put($dirlnk);
965                }
966                else $this->_put(html_wikilink($pageid, $name));
967                if($spacerWidth > 0) {
968                    $this->_put('</div>');
969                }
970                break;
971        }
972    }
973
974    /**
975     * Get default value for an unset element
976     */
977    function _getMeta(&$page, $key1, $key2 = NULL) {
978        if(!isset ($page ["meta"]))
979            $page ["meta"] = p_get_metadata($page ["linkid"], false, true);
980
981        //
982        // Use "created" instead of "modified" if null
983        //
984        if(
985            $key1 == "date" &&
986            $key2 == "modified" &&
987            !isset ($page ["meta"]["date"]["modified"])
988        ) {
989            $key2 = "created";
990        }
991        //
992        // Return "creator" if "contributor" is null
993        //
994        if($key1 == "contributor" && !isset ($page ["meta"]["contributor"])) {
995            $key1 = "creator";
996        }
997
998        if(is_string($key2)) return $page ["meta"] [$key1] [$key2];
999
1000        return $page ["meta"] [$key1];
1001    }
1002
1003    /**
1004     * Generate the table cell content...
1005     */
1006    function _tableCellContent(&$page, $col) {
1007        switch($col) {
1008            case "page":
1009                $this->_tableCellContentID($page);
1010                break;
1011            case "desc":
1012            case "widedesc":
1013                $this->_put($this->_getMeta($page, "description", "abstract"));
1014                break;
1015            case "mdate":
1016                $this->_putDate($this->_getMeta($page, "date", "modified"));
1017                break;
1018            case "cdate":
1019                $this->_putDate($this->_getMeta($page, "date", "created"));
1020                break;
1021            case "user":
1022                $users = $this->_getMeta($page, "contributor");
1023                if(is_array($users)) {
1024                    $index = 0;
1025                    foreach($users as $userid => $user) {
1026                        if($user && $user <> '') {
1027                            if($index++ > 0) {
1028                                $this->_putNewLine();
1029                            }
1030                            $this->_put($user);
1031                        }
1032                    }
1033                }
1034                break;
1035            case "userid":
1036                $users = $this->_getMeta($page, "contributor");
1037                if(is_array($users)) {
1038                    $index = 0;
1039                    foreach($users as $userid => $user) {
1040                        if($userid && $userid <> '') {
1041                            if($index++ > 0) {
1042                                $this->_putNewLine();
1043                            }
1044                            $this->_put($userid);
1045                        }
1046                    }
1047                }
1048                break;
1049            case "comments":
1050                if(!$this->modeIsLatex)
1051                    $this->_put($this->_pluginCell("discussion", $page ["linkid"]));
1052                break;
1053            case "tags":
1054                if(!$this->modeIsLatex)
1055                    $this->_put($this->_pluginCell("tag", $page ["linkid"]));
1056                break;
1057            case "rowno":
1058                $this->_put($this->rowNumber);
1059                break;
1060            default:
1061                $this->_put($col);
1062        }
1063    }
1064
1065    /**
1066     * Rewrite of renderer->table_open () because of class
1067     */
1068    function _tableOpen() {
1069
1070        if($this->modeIsLatex) {
1071            $rdr = $this->rdr;
1072            $rdr->_counter['row_counter'] = 0;
1073            $rdr->_current_tab_cols = 0;
1074            if($rdr->info ['usetablefigure'] == "on") {
1075                $this->_putCmdNl("begin{figure}[h]");
1076            } else {
1077                $this->_putCmdNl("vspace{0.8em}");
1078            }
1079            $rdr->putcmd("begin{tabular}");
1080            $rdr->put("{");
1081            foreach($this->hdrs as $index => $hdr) {
1082                $rdr->put("l");
1083                if($index + 1 < sizeof($this->hdrs))
1084                    $rdr->put('|');
1085            }
1086            $rdr->putnl("}");
1087            return;
1088        }
1089
1090        switch($this->style) {
1091            case "table":
1092                $class = "inline";
1093                break;
1094            case "list":
1095                $class = "ul";
1096                break;
1097            default:
1098                $class = "pagelist";
1099        }
1100        $this->_showDebugMsg("Style=".$this->style." table class=$class");
1101        $this->rdr->table_open (null, null, null, $class) ;
1102    }
1103
1104    /**
1105     * Rewrite of renderer->table_close ()
1106     */
1107    function _tableClose() {
1108        if($this->modeIsLatex) {
1109            $this->rdr->tabular_close();
1110            return;
1111        }
1112
1113        $this->rdr->table_close () ;
1114    }
1115
1116    /**
1117     * Rewrite of renderer->tableheader_open () because of class
1118     */
1119    function _tableHeaderCellOpen($class) {
1120        if($this->modeIsLatex)
1121            return;
1122
1123        $this->_put(DOKU_LF.DOKU_TAB.DOKU_TAB.'<th class="'.$class.'">');
1124    }
1125
1126    /**
1127     * Rewrite of renderer->tableheader_close ()
1128     */
1129    function _tableHeaderCellClose($index) {
1130        if($this->modeIsLatex) {
1131            if(($index + 1) == sizeof($this->hdrs)) {
1132                $this->rdr->putnl('\\\\');
1133            } else {
1134                $this->rdr->put('&');
1135            }
1136            return;
1137        }
1138        return $this->rdr->tableheader_close();
1139    }
1140
1141    /**
1142     * Rewrite of renderer->tablecell_open () because of class
1143     */
1144    function _tableCellOpen($colspan, $class) {
1145        if($this->modeIsLatex) return;
1146
1147        $this->_put(DOKU_LF.DOKU_TAB.DOKU_TAB.'<td class="'.$class.'"');
1148
1149        if($colspan > 1)
1150            $this->_put(' colspan="'.$colspan.'"');
1151
1152        $this->_put(">");
1153    }
1154
1155    /**
1156     * Rewrite of renderer->tablecell_close () because of class
1157     */
1158    function _tableCellClose($index) {
1159        if($this->modeIsLatex) {
1160            if(($index + 1) == sizeof($this->hdrs)) {
1161                $this->rdr->putnl('\\\\');
1162            } else {
1163                $this->rdr->put('&');
1164            }
1165            return;
1166        }
1167
1168        return $this->rdr->tablecell_close();
1169    }
1170
1171    /**
1172     * Return the class name to be used for the <td> showing $col.
1173     */
1174    function _getCellClassForCol($col) {
1175        switch($col) {
1176            case "page":
1177                return "dpage";
1178            case "date":
1179            case "user":
1180            case "desc":
1181            case "comments":
1182            case "tags":
1183            case "rowno":
1184                return $col;
1185            case "mdate":
1186            case "cdate":
1187                return "date";
1188            case "userid":
1189                return "user";
1190            case "":
1191                return "";
1192        }
1193        $this->_showDebugMsg("Unknown style class for col $col");
1194        return "desc";
1195    }
1196
1197    function _tableHeaderRowOpen() {
1198        if($this->modeIsLatex) {
1199            $this->_putCmdNl('hline');
1200            return;
1201        }
1202
1203        $this->rdr->tablerow_open();
1204    }
1205
1206    function _tableHeaderRowClose() {
1207        if($this->modeIsLatex) {
1208            $this->_putCmdNl('hline');
1209            return;
1210        }
1211
1212        $this->rdr->tablerow_close();
1213    }
1214
1215    function _tableRowOpen() {
1216        if($this->modeIsLatex)
1217            return;
1218
1219        $this->rdr->tablerow_open();
1220    }
1221
1222    function _tableRowClose() {
1223        if($this->modeIsLatex)
1224            return;
1225
1226        $this->rdr->tablerow_close();
1227    }
1228
1229    /**
1230     * Return true if the tag plugin is not loaded,
1231     * if the ~~DIR~~ line has no tag attribute or
1232     * if the given page has one of the specified tags.
1233     */
1234    function _hasTag($page) {
1235        if(!$this->hasTags) return true;
1236
1237        $plug = $this->plugins ['tag'];
1238        if(!$plug) return true;
1239
1240        // Get the tags of the current page
1241        $tmp = $this->_getMeta($page, "subject");
1242
1243        if(!is_array($tmp)) return false;
1244
1245        $tags = Array();
1246
1247        // Convert them to lowercase
1248        foreach($tmp as $tag) {
1249            $tags [] = mb_convert_case($tag, MB_CASE_LOWER, "UTF-8");
1250        }
1251
1252        #
1253        # If there is an intersection with the exclude tags then we can not show
1254        # the current document
1255        #
1256        if(count($this->excludeTags) > 0) {
1257            if(count(array_intersect($tags, $this->excludeTags)) > 0) {
1258                $this->_showDebugMsg('skip');
1259                return false;
1260            }
1261        }
1262
1263        # If the intersection with the include tags is not equal (in size) to the
1264        # array of include tags, we must skip the current document.
1265        if(count($this->includeTags) > 0) {
1266            $intersection = array_intersect($tags, $this->includeTags);
1267            if(count($intersection) != count($this->includeTags)) {
1268                return false;
1269            }
1270        }
1271
1272        return true;
1273    }
1274
1275    /**
1276     * Generate the actual table content...
1277     */
1278    function _tableContent() {
1279        $doWideDesc = $this->opts ["widedesc"];
1280
1281        if(!$this->opts ["noheader"]) {
1282            $this->_tableHeaderRowOpen();
1283            foreach($this->hdrs as $index => $hdr) {
1284                $this->_tableHeaderCellOpen(
1285                    $this->_getCellClassForCol($this->cols [$index])
1286                );
1287                $this->_put($hdr);
1288                $this->_tableHeaderCellClose($index);
1289            }
1290            $this->_tableHeaderRowClose();
1291        }
1292
1293        foreach($this->pages as $page) {
1294
1295            if(!$this->_hasTag($page)) continue;
1296
1297            $this->rowNumber += 1;
1298
1299            $this->_tableRowOpen();
1300            foreach($this->cols as $index => $col) {
1301                $this->_tableCellOpen(1, $this->_getCellClassForCol($col));
1302                $this->_tableCellContent($page, $col);
1303                $this->_tableCellClose($index);
1304            }
1305            $this->_tableRowClose();
1306            if($doWideDesc) {
1307                $this->_tableRowOpen();
1308                $this->_tableCellOpen(count($this->cols), "desc");
1309                $this->_tableCellContent($page, "widedesc");
1310                $this->_tableCellClose(0);
1311                $this->_tableRowClose();
1312            }
1313        }
1314    }
1315
1316    /**
1317     * Write data to the output stream
1318     */
1319    function _put($data) {
1320        if($data == NULL || $data == '')
1321            return;
1322
1323        switch($this->rdrMode) {
1324            case 'xhtml':
1325                $this->rdr->doc .= $data;
1326                break;
1327            case 'latex':
1328                $this->rdr->put($data);
1329                break;
1330        }
1331    }
1332
1333    /**
1334     * Write a date to the output stream
1335     */
1336    function _putDate($date) {
1337        $this->_put(strftime($this->dformat, $date));
1338    }
1339
1340    function _putCmdNl($cmd) {
1341        $this->rdr->putcmdnl($cmd);
1342    }
1343
1344    function _putNewLine() {
1345        if($this->modeIsLatex) {
1346            $this->_putCmdNl('newline');
1347        } else {
1348            $this->_put('<br />');
1349        }
1350    }
1351
1352    /**
1353     * Do the real work
1354     */
1355    function _dir($data) {
1356        if(!$this->_parseOptions($data))
1357            return false;
1358
1359        //
1360        // If we already did the latex pass, skip the xhtml pass
1361        //
1362        if($this->processedLatex && $this->rdrMode == 'xhtml') {
1363            //return false ;
1364        }
1365
1366        //
1367        // Generate the actual table...
1368        //
1369        $this->_tableOpen();
1370        $this->_tableContent();
1371        $this->_tableClose();
1372
1373        return true;
1374    }
1375
1376} // syntax_plugin_dir
1377
1378//Setup VIM: ex: et ts=2 enc=utf-8 :
1379