]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/PageList.php
Spelling: seperator --> separator
[SourceForge/phpwiki.git] / lib / PageList.php
1 <?php
2 /* Copyright (C) 2004-2010 $ThePhpWikiProgrammingTeam
3  * Copyright (C) 2008-2010 Marc-Etienne Vargenau, Alcatel-Lucent
4  *
5  * This file is part of PhpWiki.
6  *
7  * PhpWiki is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * PhpWiki is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License along
18  * with PhpWiki; if not, write to the Free Software Foundation, Inc.,
19  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20  */
21
22 /**
23  * List a number of pagenames, optionally as table with various columns.
24  *
25  * See pgsrc/Help%2FPageList for arguments and details
26  *
27  * FIXME: In this refactoring I (Jeff) have un-implemented _ctime, _cauthor, and
28  * number-of-revision.  Note the _ctime and _cauthor as they were implemented
29  * were somewhat flawed: revision 1 of a page doesn't have to exist in the
30  * database.  If lots of revisions have been made to a page, it's more than likely
31  * that some older revisions (include revision 1) have been cleaned (deleted).
32  *
33  * DONE:
34  *   paging support: limit, offset args
35  *   check PagePerm "list" access-type,
36  *   all columns are sortable. Thanks to the wikilens team.
37  *   cols > 1, comma, azhead, ordered (OL lists)
38  *   ->supportedArgs() which arguments are supported, so that the plugin
39  *                     doesn't explictly need to declare it
40  *   added slice option when the page_iter (e.g. ->_pages) is already sliced.
41  *
42  * TODO:
43  *   fix sortby logic, fix multiple sortby and other paging args per page.
44  *   info=relation,linkto nopage=1
45  *   use custom format method (RecentChanges, rss, ...)
46  *
47  * FIXED:
48  *   fix memory exhaustion on large pagelists with old --memory-limit php's only.
49  *   Status: improved 2004-06-25 16:19:36 rurban
50  */
51 class _PageList_Column_base
52 {
53     public $_tdattr = array();
54     public $_field;
55
56     function __construct($default_heading, $align = false)
57     {
58         $this->_heading = $default_heading;
59
60         if ($align) {
61             $this->_tdattr['class'] = "align-".$align;
62         }
63     }
64
65     function format($pagelist, $page_handle, &$revision_handle)
66     {
67         return HTML::td($this->_tdattr,
68             $this->_getValue($page_handle, $revision_handle));
69     }
70
71     function getHeading()
72     {
73         return $this->_heading;
74     }
75
76     function setHeading($heading)
77     {
78         $this->_heading = $heading;
79     }
80
81     // old-style heading
82     function heading()
83     {
84         global $request;
85         // allow sorting?
86         if (1 /* or in_array($this->_field, PageList::sortable_columns())*/) {
87             // multiple comma-delimited sortby args: "+hits,+pagename"
88             // asc or desc: +pagename, -pagename
89             $sortby = PageList::sortby($this->_field, 'flip_order');
90             //Fixme: pass all also other GET args along. (limit, p[])
91             //TODO: support GET and POST
92             $s = HTML::a(array('href' =>
93                 $request->GetURLtoSelf(array('sortby' => $sortby)),
94                     'class' => 'pagetitle',
95                     'title' => sprintf(_("Sort by %s"), $this->_field)),
96                 HTML::u($this->_heading));
97         } else {
98             $s = HTML::u($this->_heading);
99         }
100         return HTML::th($s);
101     }
102
103     // new grid-style sortable heading
104     // TODO: via activeui.js ? (fast dhtml sorting)
105     function button_heading($pagelist, $colNum)
106     {
107         global $WikiTheme, $request;
108         // allow sorting?
109         if (!$WikiTheme->DUMP_MODE /* or in_array($this->_field, PageList::sortable_columns()) */) {
110             // TODO: add to multiple comma-delimited sortby args: "+hits,+pagename"
111             $src = false;
112             $noimg_src = $WikiTheme->getButtonURL('sort_none');
113             if ($noimg_src)
114                 $noimg = HTML::img(array('src' => $noimg_src,
115                     'alt' => '.'));
116             else
117                 $noimg = HTML::raw('');
118             $reverse = false;
119             if ($pagelist->sortby($colNum, 'check')) { // show icon? request or plugin arg
120                 $sortby = $pagelist->sortby($colNum, 'flip_order');
121                 $desc = (substr($sortby, 0, 1) == '-'); // +pagename or -pagename
122                 $src = $WikiTheme->getButtonURL($desc ? 'sort_up' : 'sort_down');
123                 $reverse = $desc;
124             } else {
125                 // initially unsorted
126                 $sortby = $pagelist->sortby($colNum, 'get');
127             }
128             if (!$src) {
129                 $img = $noimg;
130                 $reverse = false;
131                 $img->setAttr('alt', ".");
132             } else {
133                 $img = HTML::img(array('src' => $src,
134                     'alt' => _("Click to reverse sort order")));
135             }
136             if ($reverse) {
137                 $title = _("Click to sort by reverse %s");
138             } else {
139                 $title = _("Click to sort by %s");
140             }
141             $s = HTML::a(array('href' =>
142                 //Fixme: pass all also other GET args along. (limit is ok, p[])
143                 $request->GetURLtoSelf(array('sortby' => $sortby,
144                     'id' => $pagelist->id)),
145                     'class' => 'gridbutton',
146                     'title' => sprintf($title, $this->_heading)),
147                 $this->_heading,
148                 $img);
149         } else {
150             $s = HTML($this->_heading);
151         }
152         return HTML::th(array('class' => 'gridbutton'), $s);
153     }
154
155     /**
156      * Take two columns of this type and compare them.
157      * An undefined value is defined to be < than the smallest defined value.
158      * This base class _compare only works if the value is simple (e.g., a number).
159      *
160      * @param  mixed $colvala  $this->_getValue() of column a
161      * @param  mixed $colvalb  $this->_getValue() of column b
162      *
163      * @return int -1 if $a < $b, 1 if $a > $b, 0 otherwise.
164      */
165     function _compare($colvala, $colvalb)
166     {
167         if (is_string($colvala))
168             return strcmp($colvala, $colvalb);
169         $ret = 0;
170         if (($colvala === $colvalb) || (!isset($colvala) && !isset($colvalb))) {
171             ;
172         } else {
173             $ret = (!isset($colvala) || ($colvala < $colvalb)) ? -1 : 1;
174         }
175         return $ret;
176     }
177 }
178
179 class _PageList_Column extends _PageList_Column_base
180 {
181     function _PageList_Column($field, $default_heading, $align = false)
182     {
183         parent::__construct($default_heading, $align);
184
185         $this->_need_rev = substr($field, 0, 4) == 'rev:';
186         $this->_iscustom = substr($field, 0, 7) == 'custom:';
187         if ($this->_iscustom) {
188             $this->_field = substr($field, 7);
189         } elseif ($this->_need_rev)
190             $this->_field = substr($field, 4);
191         else
192             $this->_field = $field;
193     }
194
195     /**
196      * @param WikiDB_Page $page_handle
197      * @param WikiDB_PageRevision $revision_handle
198      * @return mixed
199      */
200     function _getValue($page_handle, &$revision_handle)
201     {
202         if ($this->_need_rev) {
203             if (!$revision_handle)
204                 // columns which need the %content should override this. (size, hi_content)
205                 $revision_handle = $page_handle->getCurrentRevision(false);
206             return $revision_handle->get($this->_field);
207         } else {
208             return $page_handle->get($this->_field);
209         }
210     }
211
212     /**
213      * @param WikiDB_Page $page_handle
214      * @param WikiDB_PageRevision $revision_handle
215      * @return int|string
216      */
217     function _getSortableValue($page_handle, &$revision_handle)
218     {
219         $val = $this->_getValue($page_handle, $revision_handle);
220         if ($this->_field == 'hits')
221             return (int)$val;
222         elseif (is_object($val) && method_exists($val, 'asString'))
223             return $val->asString(); else
224             return (string)$val;
225     }
226 }
227
228 /* overcome a call_user_func limitation by not being able to do:
229  * call_user_func_array(array(&$class, $class_name), $params);
230  * So we need $class = new $classname($params);
231  * And we add a 4th param to get at the parent $pagelist object
232  */
233 class _PageList_Column_custom extends _PageList_Column
234 {
235     function __construct($params)
236     {
237         $this->_pagelist =& $params[3];
238         parent::__construct($params[0], $params[1], $params[2]);
239     }
240 }
241
242 class _PageList_Column_size extends _PageList_Column
243 {
244     function format($pagelist, $page_handle, &$revision_handle)
245     {
246         return HTML::td($this->_tdattr,
247             $this->_getValuePageList($pagelist, $page_handle, $revision_handle));
248     }
249
250     function _getValuePageList($pagelist, $page_handle, &$revision_handle)
251     {
252         if (!$revision_handle or (!$revision_handle->_data['%content']
253             or $revision_handle->_data['%content'] === true)
254         ) {
255             $revision_handle = $page_handle->getCurrentRevision(true);
256             unset($revision_handle->_data['%pagedata']['_cached_html']);
257         }
258         $size = $this->_getSize($revision_handle);
259         // we can safely purge the content when it is not sortable
260         if (empty($pagelist->_sortby[$this->_field]))
261             unset($revision_handle->_data['%content']);
262         return $size;
263     }
264
265     function _getSortableValue($page_handle, &$revision_handle)
266     {
267         if (!$revision_handle)
268             $revision_handle = $page_handle->getCurrentRevision(true);
269         return (empty($revision_handle->_data['%content']))
270             ? 0 : strlen($revision_handle->_data['%content']);
271     }
272
273     function _getSize($revision_handle)
274     {
275         $bytes = @strlen($revision_handle->_data['%content']);
276         return ByteFormatter($bytes);
277     }
278 }
279
280 class _PageList_Column_bool extends _PageList_Column
281 {
282     function __construct($field, $default_heading, $text = 'yes')
283     {
284         parent::__construct($field, $default_heading, 'center');
285         $this->_textIfTrue = $text;
286         $this->_textIfFalse = new RawXml('&#8212;'); //mdash
287     }
288
289     function _getValue($page_handle, &$revision_handle)
290     {
291         //FIXME: check if $this is available in the parent (->need_rev)
292         $val = parent::_getValue($page_handle, $revision_handle);
293         return $val ? $this->_textIfTrue : $this->_textIfFalse;
294     }
295 }
296
297 class _PageList_Column_checkbox extends _PageList_Column
298 {
299     function __construct($field, $default_heading, $name = 'p')
300     {
301         $this->_name = $name;
302         $heading = HTML::input(array('type' => 'button',
303             'title' => _("Click to de-/select all pages"),
304             'name' => $default_heading,
305             'value' => $default_heading,
306             'onclick' => "flipAll(this.form)"
307         ));
308         parent::__construct($field, $heading, 'center');
309     }
310
311     function _getValuePageList($pagelist, $page_handle, &$revision_handle)
312     {
313         $pagename = $page_handle->getName();
314         $selected = !empty($pagelist->_selected[$pagename]);
315         if (strstr($pagename, '[') or strstr($pagename, ']')) {
316             $pagename = str_replace(array('[', ']'), array('%5B', '%5D'), $pagename);
317         }
318         if ($selected) {
319             return HTML::input(array('type' => 'checkbox',
320                 'name' => $this->_name . "[$pagename]",
321                 'value' => 1,
322                 'checked' => 'checked'));
323         } else {
324             return HTML::input(array('type' => 'checkbox',
325                 'name' => $this->_name . "[$pagename]",
326                 'value' => 1));
327         }
328     }
329
330     function format($pagelist, $page_handle, &$revision_handle)
331     {
332         return HTML::td($this->_tdattr,
333             $this->_getValuePageList($pagelist, $page_handle, $revision_handle));
334     }
335
336     // don't sort this javascript button
337     function button_heading($pagelist, $colNum)
338     {
339         $s = HTML($this->_heading);
340         return HTML::th(array('class' => 'gridbutton'), $s);
341     }
342 }
343
344 class _PageList_Column_time extends _PageList_Column
345 {
346     function __construct($field, $default_heading)
347     {
348         parent::__construct($field, $default_heading, 'right');
349         global $WikiTheme;
350         $this->WikiTheme = &$WikiTheme;
351     }
352
353     function _getValue($page_handle, &$revision_handle)
354     {
355         $time = parent::_getValue($page_handle, $revision_handle);
356         return $this->WikiTheme->formatDateTime($time);
357     }
358
359     function _getSortableValue($page_handle, &$revision_handle)
360     {
361         return parent::_getValue($page_handle, $revision_handle);
362     }
363 }
364
365 class _PageList_Column_version extends _PageList_Column
366 {
367     function _getValue($page_handle, &$revision_handle)
368     {
369         if (!$revision_handle)
370             $revision_handle = $page_handle->getCurrentRevision();
371         return $revision_handle->getVersion();
372     }
373 }
374
375 // Output is hardcoded to limit of first 50 bytes. Otherwise
376 // on very large Wikis this will fail if used with AllPages
377 // (PHP memory limit exceeded)
378 class _PageList_Column_content extends _PageList_Column
379 {
380     function __construct($field, $default_heading, $align = false,
381                          $search = false, $hilight_re = false)
382     {
383         parent::__construct($field, $default_heading, $align);
384         $this->bytes = 50;
385         $this->search = $search;
386         $this->hilight_re = $hilight_re;
387         if ($field == 'content') {
388             $this->_heading .= sprintf(_(" ... first %d bytes"),
389                 $this->bytes);
390         } elseif ($field == 'rev:hi_content') {
391             global $HTTP_POST_VARS;
392             if (!$this->search and !empty($HTTP_POST_VARS['admin_replace'])) {
393                 $this->search = $HTTP_POST_VARS['admin_replace']['from'];
394             }
395             $this->_heading .= sprintf(_(" ... around “%s”"), $this->search);
396         }
397     }
398
399     function _getValue($page_handle, &$revision_handle)
400     {
401         if (!$revision_handle or (!$revision_handle->_data['%content']
402             or $revision_handle->_data['%content'] === true)
403         ) {
404             $revision_handle = $page_handle->getCurrentRevision(true);
405         }
406
407         if ($this->_field == 'hi_content') {
408             if (!empty($revision_handle->_data['%pagedata'])) {
409                 $revision_handle->_data['%pagedata']['_cached_html'] = '';
410             }
411             $search = $this->search;
412             $score = '';
413             if (is_object($page_handle) and !empty($page_handle->score))
414                 $score = $page_handle->score;
415             elseif (is_array($page_handle) and !empty($page_handle['score']))
416                 $score = $page_handle['score'];
417
418             $hilight_re = $this->hilight_re;
419             // use the TextSearchQuery highlighter
420             if ($search and $hilight_re) {
421                 $matches = preg_grep("/$hilight_re/i", $revision_handle->getContent());
422                 $html = array();
423                 foreach (array_slice($matches, 0, 5) as $line) {
424                     $line = WikiPlugin_FullTextSearch::highlight_line($line, $hilight_re);
425                     $html[] = HTML::p(HTML::small(array('class' => 'search-context'), $line));
426                 }
427                 if ($score)
428                     $html[] = HTML::small(sprintf("... [%0.1f]", $score));
429                 return $html;
430             }
431             // Remove special characters so that highlighting works
432             $search = preg_replace('/^[\^\*]/', '', $search);
433             $search = preg_replace('/[\^\*]$/', '', $search);
434             $c =& $revision_handle->getPackedContent();
435             if ($search and ($i = strpos(strtolower($c), strtolower($search))) !== false) {
436                 $l = strlen($search);
437                 $j = max(0, $i - ($this->bytes / 2));
438                 return HTML::div(array('style' => 'font-size:x-small'),
439                     HTML::div(array('class' => 'transclusion'),
440                         HTML::span(($j ? '...' : '')
441                             . substr($c, $j, ($j ? $this->bytes / 2 : $i))),
442                         HTML::span(array("style" => "background:yellow"),
443                             substr($c, $i, $l)),
444                         HTML::span(substr($c, $i + $l, ($this->bytes / 2))
445                             . "..." . " "
446                             . ($score ? sprintf("[%0.1f]", $score) : ""))));
447             } else {
448                 $c = sprintf(_("“%s” not found"), $search);
449                 return HTML::div(array('class' => 'align-center'),
450                     $c . " " . ($score ? sprintf("[%0.1f]", $score) : ""));
451             }
452         } elseif (($len = strlen($c)) > $this->bytes) {
453             $c = substr($c, 0, $this->bytes);
454         }
455         include_once 'lib/BlockParser.php';
456         // false --> don't bother processing hrefs for embedded WikiLinks
457         $ct = TransformText($c);
458         if (empty($pagelist->_sortby[$this->_field]))
459             unset($revision_handle->_data['%pagedata']['_cached_html']);
460         return HTML::div(array('style' => 'font-size:x-small'),
461             HTML::div(array('class' => 'transclusion'), $ct),
462             // Don't show bytes here if size column present too
463             ($this->parent->_columns_seen['size'] or !$len) ? "" :
464                 ByteFormatter($len, /*$longformat = */
465                     true));
466     }
467
468     function _getSortableValue($page_handle, &$revision_handle)
469     {
470         if (is_object($page_handle) and !empty($page_handle->score))
471             return $page_handle->score;
472         elseif (is_array($page_handle) and !empty($page_handle['score']))
473             return $page_handle['score']; else
474             return substr(parent::_getValue($page_handle, $revision_handle), 0, 50);
475     }
476 }
477
478 class _PageList_Column_author extends _PageList_Column
479 {
480     function __construct($field, $default_heading, $align = false)
481     {
482         /**
483          * @var WikiRequest $request
484          */
485         global $request;
486
487         parent::__construct($field, $default_heading, $align);
488         $this->dbi =& $request->getDbh();
489     }
490
491     function _getValue($page_handle, &$revision_handle)
492     {
493         $author = parent::_getValue($page_handle, $revision_handle);
494         if ($this->dbi->isWikiPage($author))
495             return WikiLink($author);
496         else
497             return $author;
498     }
499
500     function _getSortableValue($page_handle, &$revision_handle)
501     {
502         return parent::_getValue($page_handle, $revision_handle);
503     }
504 }
505
506 class _PageList_Column_owner extends _PageList_Column_author
507 {
508     function _getValue($page_handle, &$revision_handle)
509     {
510         $author = $page_handle->getOwner();
511         if ($this->dbi->isWikiPage($author))
512             return WikiLink($author);
513         else
514             return $author;
515     }
516
517     function _getSortableValue($page_handle, &$revision_handle)
518     {
519         return parent::_getValue($page_handle, $revision_handle);
520     }
521 }
522
523 class _PageList_Column_creator extends _PageList_Column_author
524 {
525     function _getValue($page_handle, &$revision_handle)
526     {
527         $author = $page_handle->getCreator();
528         if ($this->dbi->isWikiPage($author))
529             return WikiLink($author);
530         else
531             return $author;
532     }
533
534     function _getSortableValue($page_handle, &$revision_handle)
535     {
536         return parent::_getValue($page_handle, $revision_handle);
537     }
538 }
539
540 class _PageList_Column_pagename extends _PageList_Column_base
541 {
542     public $_field = 'pagename';
543
544     function __construct()
545     {
546         parent::__construct(_("Page Name"));
547         global $request;
548         $this->dbi = &$request->getDbh();
549     }
550
551     function _getValue($page_handle, &$revision_handle)
552     {
553         if ($this->dbi->isWikiPage($page_handle->getName()))
554             return WikiLink($page_handle, 'known');
555         else
556             return WikiLink($page_handle, 'unknown');
557     }
558
559     function _getSortableValue($page_handle, &$revision_handle)
560     {
561         return $page_handle->getName();
562     }
563
564     /**
565      * Compare two pagenames for sorting.  See _PageList_Column::_compare.
566      **/
567     function _compare($colvala, $colvalb)
568     {
569         return strcmp($colvala, $colvalb);
570     }
571 }
572
573 class _PageList_Column_perm extends _PageList_Column
574 {
575     function _getValue($page_handle, &$revision_handle)
576     {
577         $perm_array = pagePermissions($page_handle->_pagename);
578         return pagePermissionsSimpleFormat($perm_array,
579             $page_handle->get('author'),
580             $page_handle->get('group'));
581     }
582 }
583
584 class _PageList_Column_acl extends _PageList_Column
585 {
586     function _getValue($page_handle, &$revision_handle)
587     {
588         $perm_tree = pagePermissions($page_handle->_pagename);
589
590         list($type, $perm) = pagePermissionsAcl($perm_tree[0], $perm_tree);
591         if ($type == 'inherited') {
592             $type = sprintf(_("page permission inherited from %s"), $perm_tree[1][0]);
593         } elseif ($type == 'page') {
594             $type = _("individual page permission");
595         } elseif ($type == 'default') {
596             $type = _("default page permission");
597         }
598         $result = HTML::span();
599         $result->pushContent($type);
600         $result->pushContent(HTML::br());
601         $result->pushContent($perm->asAclLines());
602         return $result;
603     }
604 }
605
606 class PageList
607 {
608     public $_group_rows = 3;
609     public $_columns = array();
610     private $_columnsMap = array(); // Maps column name to column number.
611     private $_excluded_pages = array();
612     public $_pages = array();
613     public $_caption = "";
614     public $_types = array();
615     public $_options = array();
616     public $_selected = array();
617     public $_sortby = array();
618     public $_maxlen = 0;
619     private $_messageIfEmpty = '';
620     public $_columns_seen = array();
621     private $_stack;
622
623     function __construct($columns = array(), $exclude = array(), $options = array())
624     {
625         /**
626          * @var WikiRequest $request
627          */
628         global $request;
629
630         // unique id per pagelist on each page.
631         if (!isset($request->_pagelist))
632             $request->_pagelist = 0;
633         else
634             $request->_pagelist++;
635         $this->id = $request->_pagelist;
636         if ($request->getArg('count'))
637             $options['count'] = $request->getArg('count');
638         if ($options)
639             $this->_options = $options;
640
641         $this->initAvailableColumns();
642         // let plugins predefine only certain objects, such its own custom pagelist columns
643         $symbolic_columns =
644             array(
645                 'all' => array_diff(array_keys($this->_types), // all but...
646                     array('checkbox', 'remove', 'renamed_pagename',
647                         'content', 'hi_content', 'perm', 'acl')),
648                 'most' => array('pagename', 'mtime', 'author', 'hits'),
649                 'some' => array('pagename', 'mtime', 'author')
650             );
651         if (isset($this->_options['listtype'])
652             and $this->_options['listtype'] == 'dl'
653         )
654             $this->_options['nopage'] = 1;
655         if ($columns) {
656             if (!is_array($columns))
657                 $columns = explode(',', $columns);
658             // expand symbolic columns:
659             foreach ($symbolic_columns as $symbol => $cols) {
660                 if (in_array($symbol, $columns)) { // e.g. 'checkbox,all'
661                     $columns = array_diff(array_merge($columns, $cols), array($symbol));
662                 }
663             }
664             unset($cols);
665             if (empty($this->_options['nopage']) and !in_array('pagename', $columns))
666                 $this->_addColumn('pagename');
667             foreach ($columns as $col) {
668                 if (!empty($col))
669                     $this->_addColumn($col);
670             }
671             unset($col);
672         }
673         // If 'pagename' is already present, _addColumn() will not add it again
674         if (empty($this->_options['nopage']))
675             $this->_addColumn('pagename');
676
677         if (!empty($this->_options['types'])) {
678             foreach ($this->_options['types'] as $type) {
679                 $this->_types[$type->_field] = $type;
680                 $this->_addColumn($type->_field);
681             }
682             unset($this->_options['types']);
683         }
684
685         global $request;
686         // explicit header options: ?id=x&sortby=... override options[]
687         // support multiple sorts. check multiple, no nested elseif
688         if (($this->id == $request->getArg("id"))
689             and $request->getArg('sortby')
690         ) {
691             // add it to the front of the sortby array
692             $this->sortby($request->getArg('sortby'), 'init');
693             $this->_options['sortby'] = $request->getArg('sortby');
694         } // plugin options
695         if (!empty($options['sortby'])) {
696             if (empty($this->_options['sortby']))
697                 $this->_options['sortby'] = $options['sortby'];
698             $this->sortby($options['sortby'], 'init');
699         } // global options
700         if (!isset($request->args["id"]) and $request->getArg('sortby')
701             and empty($this->_options['sortby'])
702         ) {
703             $this->_options['sortby'] = $request->getArg('sortby');
704             $this->sortby($this->_options['sortby'], 'init');
705         }
706         // same as above but without the special sortby push, and mutually exclusive (elseif)
707         foreach ($this->pagingArgs() as $key) {
708             if ($key == 'sortby') continue;
709             if (($this->id == $request->getArg("id"))
710                 and $request->getArg($key)
711             ) {
712                 $this->_options[$key] = $request->getArg($key);
713             } // plugin options
714             elseif (!empty($options) and !empty($options[$key])) {
715                 $this->_options[$key] = $options[$key];
716             } // global options
717             elseif (!isset($request->args["id"]) and $request->getArg($key)) {
718                 $this->_options[$key] = $request->getArg($key);
719             } else
720                 $this->_options[$key] = false;
721         }
722         if ($exclude) {
723             if (is_string($exclude) and !is_array($exclude))
724                 $exclude = $this->explodePageList($exclude, false,
725                     $this->_options['sortby'],
726                     $this->_options['limit']);
727             $this->_excluded_pages = $exclude;
728         }
729         $this->_messageIfEmpty = _("No matches");
730     }
731
732     // Currently PageList takes these arguments:
733     // 1: info, 2: exclude, 3: hash of options
734     // Here we declare which options are supported, so that
735     // the calling plugin may simply merge this with its own default arguments
736     public static function supportedArgs()
737     {
738         // Todo: add all supported Columns, like locked, minor, ...
739         return array( // Currently supported options:
740             /* what columns, what pages */
741             'info' => 'pagename',
742             'exclude' => '', // also wildcards, comma-separated lists
743             // and <!plugin-list !> arrays
744             /* select pages by meta-data: */
745             'author' => false, // current user by []
746             'owner' => false, // current user by []
747             'creator' => false, // current user by []
748
749             /* for the sort buttons in <th> */
750             'sortby' => '', // same as for WikiDB::getAllPages
751             // (unsorted is faster)
752
753             /* PageList pager options:
754              * These options may also be given to _generate(List|Table) later
755              * But limit and offset might help the query WikiDB::getAllPages()
756              */
757             'limit' => 50, // number of rows (pagesize)
758             'paging' => 'auto', // 'auto'   top + bottom rows if applicable
759             //                      // 'top'    top only if applicable
760             //                      // 'bottom' bottom only if applicable
761             //                      // 'none'   don't page at all
762             // (TODO: clarify what if $paging==false ?)
763
764             /* list-style options (with single pagename column only so far) */
765             'cols' => 1, // side-by-side display of list (1-3)
766             'azhead' => 0, // 1: group by initials
767             // 2: provide shortcut links to initials also
768             'comma' => 0, // condensed comma-separated list,
769             // 1 if without links, 2 if with
770             'commasep' => false, // Default: ', '
771             'listtype' => '', // ul (default), ol, dl, comma
772             'ordered' => false, // OL or just UL lists (ignored for comma)
773             'linkmore' => '', // If count>0 and limit>0 display a link with
774             // the number of all results, linked to the given pagename.
775
776             'nopage' => false, // for info=col omit the pagename column
777             // array_keys($this->_types). filter by columns: e.g. locked=1
778             'pagename' => null, // string regex
779             'locked' => null,
780             'minor' => null,
781             'mtime' => null,
782             'hits' => null,
783             'size' => null,
784             'version' => null,
785             'external' => null,
786         );
787     }
788
789     private function pagingArgs()
790     {
791         return array('sortby', 'limit', 'paging', 'count', 'dosort');
792     }
793
794     /**
795      * @param    mixed $caption    string or HTML
796      */
797     public function setCaption($caption)
798     {
799         $this->_caption = $caption;
800     }
801
802     /**
803      * @param    mixed $caption    string or HTML
804      */
805     public function addCaption($caption)
806     {
807         $this->_caption = HTML($this->_caption, " ", $caption);
808     }
809
810     private function getCaption()
811     {
812         // put the total into the caption if needed
813         if (is_string($this->_caption) && strstr($this->_caption, '%d'))
814             return sprintf($this->_caption, $this->getTotal());
815         return $this->_caption;
816     }
817
818     public function getTotal()
819     {
820         return !empty($this->_options['count'])
821             ? (integer)$this->_options['count'] : count($this->_pages);
822     }
823
824     public function isEmpty()
825     {
826         return empty($this->_pages);
827     }
828
829     public function addPage($page_handle)
830     {
831         $pagename = is_string($page_handle) ? $page_handle : $page_handle->getName();
832         if (in_array($pagename, $this->pageNames())) {
833             return;
834         }
835         if (!empty($this->_excluded_pages)) {
836             if (!in_array($pagename, $this->_excluded_pages))
837                 $this->_pages[] = $page_handle;
838         } else {
839             $this->_pages[] = $page_handle;
840         }
841     }
842
843     public function pageNames()
844     {
845         $pages = array();
846         $limit = @$this->_options['limit'];
847         foreach ($this->_pages as $page_handle) {
848             if (is_object($page_handle)) {
849                 $pages[] = $page_handle->getName();
850             } else {
851                 $pages[] = $page_handle;
852             }
853             if ($limit and count($pages) > $limit)
854                 break;
855         }
856         return $pages;
857     }
858
859     private function getPageFromHandle($page_handle)
860     {
861         /**
862          * @var WikiRequest $request
863          */
864         global $request;
865
866         if (is_string($page_handle)) {
867             if (empty($page_handle))
868                 return $page_handle;
869             $page_handle = $request->_dbi->getPage($page_handle);
870         }
871         return $page_handle;
872     }
873
874     /**
875      * Take a PageList_Page object, and return an HTML object to display
876      * it in a table or list row.
877      */
878     private function renderPageRow(&$page_handle, $i = 0)
879     {
880         $page_handle = $this->getPageFromHandle($page_handle);
881         //FIXME. only on sf.net
882         if (!is_object($page_handle)) {
883             trigger_error("PageList: Invalid page_handle $page_handle", E_USER_WARNING);
884             return false;
885         }
886         if (!isset($page_handle)
887             or empty($page_handle)
888             or (!empty($this->_excluded_pages)
889                 and in_array($page_handle->getName(), $this->_excluded_pages))) {
890             return false; // exclude page.
891         }
892         // enforce view permission
893         if (!mayAccessPage('view', $page_handle->getName())) {
894             return false;
895         }
896
897         $group = (int)($i / $this->_group_rows);
898         $class = ($group % 2) ? 'oddrow' : 'evenrow';
899         $revision_handle = false;
900         $this->_maxlen = max($this->_maxlen, strlen($page_handle->getName()));
901
902         if (count($this->_columns) > 1) {
903             $row = HTML::tr(array('class' => $class));
904             $j = 0;
905             foreach ($this->_columns as $col) {
906                 $col->current_row = $i;
907                 $col->current_column = $j;
908                 $row->pushContent($col->format($this, $page_handle, $revision_handle));
909                 $j++;
910             }
911         } else {
912             $col = $this->_columns[0];
913             $col->current_row = $i;
914             $col->current_column = 0;
915             $row = $col->_getValue($page_handle, $revision_handle);
916         }
917
918         return $row;
919     }
920
921     /* ignore from, but honor limit */
922     public function addPages($page_iter)
923     {
924         // TODO: if limit check max(strlen(pagename))
925         $limit = $page_iter->limit();
926         $i = 0;
927         if ($limit) {
928             list($from, $limit) = $this->limit($limit);
929             $this->_options['slice'] = 0;
930             $limit += $from;
931             while ($page = $page_iter->next()) {
932                 $i++;
933                 if ($from and $i < $from)
934                     continue;
935                 if (!$limit or ($limit and $i < $limit))
936                     $this->addPage($page);
937             }
938         } else {
939             $this->_options['slice'] = 0;
940             while ($page = $page_iter->next()) {
941                 $this->addPage($page);
942             }
943         }
944         if (!is_array($page_iter->_options) || !array_key_exists('limit_by_db', $page_iter->_options) || !$page_iter->_options['limit_by_db'])
945             $this->_options['slice'] = 1;
946         if ($i and empty($this->_options['count']))
947             $this->_options['count'] = $i;
948     }
949
950     public function addPageList($list)
951     {
952         if (empty($list)) return; // Protect reset from a null arg
953         if (isset($this->_options['limit'])) { // extract from,count from limit
954             list($from, $limit) = WikiDB_backend::limit($this->_options['limit']);
955             $limit += $from;
956         } else {
957             $limit = 0;
958         }
959         $this->_options['slice'] = 0;
960         $i = 0;
961         foreach ($list as $page) {
962             $i++;
963             if ($from and $i < $from)
964                 continue;
965             if (!$limit or ($limit and $i < $limit)) {
966                 if (is_object($page)) $page = $page->_pagename;
967                 $this->addPage((string)$page);
968             }
969         }
970     }
971
972     private function maxLen()
973     {
974         global $request;
975         $dbi =& $request->getDbh();
976         if (is_a($dbi, 'WikiDB_SQL')) {
977             extract($dbi->_backend->_table_names);
978             $res = $dbi->_backend->_dbh->getOne("SELECT max(length(pagename)) FROM $page_tbl");
979             if (DB::isError($res) || empty($res)) return false;
980             else return $res;
981         } elseif (is_a($dbi, 'WikiDB_ADODB')) {
982             extract($dbi->_backend->_table_names);
983             $row = $dbi->_backend->_dbh->getRow("SELECT max(length(pagename)) FROM $page_tbl");
984             return $row ? $row[0] : false;
985         } else
986             return false;
987     }
988
989     /**
990      * @return bool|WikiDB_Page
991      */
992     public function first()
993     {
994         if (count($this->_pages) > 0) {
995             return $this->_pages[0];
996         }
997         return false;
998     }
999
1000     public function getContent()
1001     {
1002         // Note that the <caption> element wants inline content.
1003         $caption = $this->getCaption();
1004
1005         if ($this->isEmpty())
1006             return $this->_emptyList($caption);
1007         elseif (isset($this->_options['listtype'])
1008             and in_array($this->_options['listtype'], array('ol', 'ul', 'comma', 'dl'))
1009         )
1010             return $this->generateList($caption); elseif (count($this->_columns) == 1)
1011             return $this->generateList($caption); else
1012             return $this->_generateTable($caption);
1013     }
1014
1015     function printXML()
1016     {
1017         PrintXML($this->getContent());
1018     }
1019
1020     function asXML()
1021     {
1022         return AsXML($this->getContent());
1023     }
1024
1025     /**
1026      * Handle sortby requests for the DB iterator and table header links.
1027      * Prefix the column with + or - like "+pagename","-mtime", ...
1028      *
1029      * Supported actions:
1030      *   'init'       :   unify with predefined order. "pagename" => "+pagename"
1031      *   'flip_order' :   "mtime" => "+mtime" => "-mtime" ...
1032      *   'db'         :   "-pagename" => "pagename DESC"
1033      *   'check'      :
1034      *
1035      * Now all columns are sortable. (patch by Dan Frankowski)
1036      * Some columns have native DB backend methods, some not.
1037      */
1038     public function sortby($column, $action, $valid_fields = false)
1039     {
1040         global $request;
1041
1042         if (empty($column)) return '';
1043         if (is_int($column)) {
1044             $column = $this->_columns[$column - 1]->_field;
1045         }
1046
1047         // support multiple comma-delimited sortby args: "+hits,+pagename"
1048         // recursive concat
1049         if (strstr($column, ',')) {
1050             $result = ($action == 'check') ? true : array();
1051             foreach (explode(',', $column) as $col) {
1052                 if ($action == 'check')
1053                     $result = $result && $this->sortby($col, $action, $valid_fields);
1054                 else
1055                     $result[] = $this->sortby($col, $action, $valid_fields);
1056             }
1057             // 'check' returns true/false for every col. return true if all are true.
1058             // i.e. the unsupported 'every' operator in functional languages.
1059             if ($action == 'check')
1060                 return $result;
1061             else
1062                 return join(",", $result);
1063         }
1064         if (substr($column, 0, 1) == '+') {
1065             $order = '+';
1066             $column = substr($column, 1);
1067         } elseif (substr($column, 0, 1) == '-') {
1068             $order = '-';
1069             $column = substr($column, 1);
1070         }
1071         // default initial order: +pagename, -mtime, -hits
1072         if (empty($order)) {
1073             if (!empty($this->_sortby[$column]))
1074                 $order = $this->_sortby[$column];
1075             else {
1076                 if (in_array($column, array('mtime', 'hits')))
1077                     $order = '-';
1078                 else
1079                     $order = '+';
1080             }
1081         }
1082         if ($action == 'get') {
1083             return $order . $column;
1084         } elseif ($action == 'flip_order') {
1085             if (0 and DEBUG)
1086                 trigger_error("flip $order $column " . $this->id, E_USER_NOTICE);
1087             return ($order == '+' ? '-' : '+') . $column;
1088         } elseif ($action == 'init') { // only allowed from PageList::PageList
1089             if (0 and DEBUG) {
1090                 if ($this->sortby($column, 'clicked')) {
1091                     trigger_error("clicked $order $column $this->id", E_USER_NOTICE);
1092                 }
1093             }
1094             $this->_sortby[$column] = $order; // forces show icon
1095             return $order . $column;
1096         } elseif ($action == 'check') { // show icon?
1097             //if specified via arg or if clicked
1098             $show = (!empty($this->_sortby[$column]) or $this->sortby($column, 'clicked'));
1099             if (0 and $show and DEBUG) {
1100                 trigger_error("show $order $column " . $this->id, E_USER_NOTICE);
1101             }
1102             return $show;
1103         } elseif ($action == 'clicked') { // flip sort order?
1104             global $request;
1105             $arg = $request->getArg('sortby');
1106             return ($arg
1107                 and strstr($arg, $column)
1108                     and (!isset($request->args['id'])
1109                         or $this->id == $request->getArg('id')));
1110         } elseif ($action == 'db') {
1111             // Performance enhancement: use native DB sort if possible.
1112             if (($valid_fields and in_array($column, $valid_fields))
1113                 or (method_exists($request->_dbi->_backend, 'sortable_columns')
1114                     and (in_array($column, $request->_dbi->_backend->sortable_columns())))
1115             ) {
1116                 // omit this sort method from the sortPages call at rendering
1117                 // asc or desc: +pagename, -pagename
1118                 return $column . ($order == '+' ? ' ASC' : ' DESC');
1119             } else {
1120                 return '';
1121             }
1122         }
1123         return '';
1124     }
1125
1126     /* Splits pagelist string into array.
1127      * Test* or Test1,Test2
1128      * Limitation: Doesn't split into comma-sep and then expand wildcards.
1129      * "Test1*,Test2*" is expanded into TextSearch "Test1* Test2*"
1130      */
1131     public static function explodePageList($input, $include_empty = false, $sortby = '',
1132                                     $limit = '', $exclude = '')
1133     {
1134         /**
1135          * @var WikiRequest $request
1136          */
1137         global $request;
1138
1139         if (empty($input)) {
1140             return array();
1141         }
1142         if (is_array($input)) {
1143             return $input;
1144         }
1145         // expand wildcards from list of all pages
1146         if (preg_match('/[\?\*]/', $input) or substr($input, 0, 1) == "^") {
1147             include_once 'lib/TextSearchQuery.php';
1148             $search = new TextSearchQuery(str_replace(",", " or ", $input), true,
1149                 (substr($input, 0, 1) == "^") ? 'posix' : 'glob');
1150             $dbi = $request->getDbh();
1151             $iter = $dbi->titleSearch($search, $sortby, $limit, $exclude);
1152             $pages = array();
1153             while ($pagehandle = $iter->next()) {
1154                 $pages[] = trim($pagehandle->getName());
1155             }
1156             return $pages;
1157         } else {
1158             //TODO: do the sorting, normally not needed if used for exclude only
1159             return array_map("trim", explode(',', $input));
1160         }
1161     }
1162
1163     // TODO: optimize getTotal => store in count
1164     public static function allPagesByAuthor($wildcard, $include_empty = false, $sortby = '',
1165                                             $limit = '', $exclude = '')
1166     {
1167         /**
1168          * @var WikiRequest $request
1169          */
1170         global $request;
1171
1172         $dbi = $request->getDbh();
1173         $allPagehandles = $dbi->getAllPages($include_empty, $sortby, $limit, $exclude);
1174         $allPages = array();
1175         if ($wildcard === '[]') {
1176             $wildcard = $request->_user->getAuthenticatedId();
1177             if (!$wildcard) return $allPages;
1178         }
1179         $do_glob = preg_match('/[\?\*]/', $wildcard);
1180         while ($pagehandle = $allPagehandles->next()) {
1181             $name = $pagehandle->getName();
1182             $author = $pagehandle->getAuthor();
1183             if ($author) {
1184                 if ($do_glob) {
1185                     if (glob_match($wildcard, $author))
1186                         $allPages[] = $name;
1187                 } elseif ($wildcard == $author) {
1188                     $allPages[] = $name;
1189                 }
1190             }
1191             // TODO: purge versiondata_cache
1192         }
1193         return $allPages;
1194     }
1195
1196     public static function allPagesByOwner($wildcard, $include_empty = false, $sortby = '',
1197                                            $limit = '', $exclude = '')
1198     {
1199         /**
1200          * @var WikiRequest $request
1201          */
1202         global $request;
1203
1204         $dbi = $request->getDbh();
1205         $allPagehandles = $dbi->getAllPages($include_empty, $sortby, $limit, $exclude);
1206         $allPages = array();
1207         if ($wildcard === '[]') {
1208             $wildcard = $request->_user->getAuthenticatedId();
1209             if (!$wildcard) return $allPages;
1210         }
1211         $do_glob = preg_match('/[\?\*]/', $wildcard);
1212         while ($pagehandle = $allPagehandles->next()) {
1213             $name = $pagehandle->getName();
1214             $owner = $pagehandle->getOwner();
1215             if ($owner) {
1216                 if ($do_glob) {
1217                     if (glob_match($wildcard, $owner))
1218                         $allPages[] = $name;
1219                 } elseif ($wildcard == $owner) {
1220                     $allPages[] = $name;
1221                 }
1222             }
1223         }
1224         return $allPages;
1225     }
1226
1227     public static function allPagesByCreator($wildcard, $include_empty = false, $sortby = '',
1228                                $limit = '', $exclude = '')
1229     {
1230         /**
1231          * @var WikiRequest $request
1232          */
1233         global $request;
1234
1235         $dbi = $request->getDbh();
1236         $allPagehandles = $dbi->getAllPages($include_empty, $sortby, $limit, $exclude);
1237         $allPages = array();
1238         if ($wildcard === '[]') {
1239             $wildcard = $request->_user->getAuthenticatedId();
1240             if (!$wildcard) return $allPages;
1241         }
1242         $do_glob = preg_match('/[\?\*]/', $wildcard);
1243         while ($pagehandle = $allPagehandles->next()) {
1244             $name = $pagehandle->getName();
1245             $creator = $pagehandle->getCreator();
1246             if ($creator) {
1247                 if ($do_glob) {
1248                     if (glob_match($wildcard, $creator))
1249                         $allPages[] = $name;
1250                 } elseif ($wildcard == $creator) {
1251                     $allPages[] = $name;
1252                 }
1253             }
1254         }
1255         return $allPages;
1256     }
1257
1258     // UserPages are pages NOT owned by ADMIN_USER
1259     public static function allUserPages($include_empty = false, $sortby = '',
1260                                         $limit = '', $exclude = '')
1261     {
1262         /**
1263          * @var WikiRequest $request
1264          */
1265         global $request;
1266
1267         $dbi = $request->getDbh();
1268         $allPagehandles = $dbi->getAllPages($include_empty, $sortby, $limit, $exclude);
1269         $allPages = array();
1270         while ($pagehandle = $allPagehandles->next()) {
1271             $name = $pagehandle->getName();
1272             $owner = $pagehandle->getOwner();
1273             if ($owner !== ADMIN_USER) {
1274                 $allPages[] = $name;
1275             }
1276         }
1277         return $allPages;
1278     }
1279
1280     /** Plugin and theme hooks:
1281      *  If the pageList is initialized with $options['types'] these types are also initialized,
1282      *  overriding the standard types.
1283      */
1284     private function initAvailableColumns()
1285     {
1286         global $customPageListColumns;
1287         $standard_types =
1288             array(
1289                 'content'
1290                 => new _PageList_Column_content('rev:content', _("Content")),
1291                 // new: plugin specific column types initialised by the relevant plugins
1292                 /*
1293                 'hi_content' // with highlighted search for SearchReplace
1294                 => new _PageList_Column_content('rev:hi_content', _("Content")),
1295                 'remove'
1296                 => new _PageList_Column_remove('remove', _("Remove")),
1297                 // initialised by the plugin
1298                 'renamed_pagename'
1299                 => new _PageList_Column_renamed_pagename('rename', _("Rename to")),
1300                 */
1301                 'perm'
1302                 => new _PageList_Column_perm('perm', _("Permission")),
1303                 'acl'
1304                 => new _PageList_Column_acl('acl', _("ACL")),
1305                 'checkbox'
1306                 => new _PageList_Column_checkbox('p', _("All")),
1307                 'pagename'
1308                 => new _PageList_Column_pagename,
1309                 'mtime'
1310                 => new _PageList_Column_time('rev:mtime', _("Last Modified")),
1311                 'hits'
1312                 => new _PageList_Column('hits', _("Hits"), 'right'),
1313                 'size'
1314                 => new _PageList_Column_size('rev:size', _("Size"), 'right'),
1315                 'summary'
1316                 => new _PageList_Column('rev:summary', _("Last Summary")),
1317                 'version'
1318                 => new _PageList_Column_version('rev:version', _("Version"),
1319                     'right'),
1320                 'author'
1321                 => new _PageList_Column_author('rev:author', _("Last Author")),
1322                 'owner'
1323                 => new _PageList_Column_owner('author_id', _("Owner")),
1324                 'creator'
1325                 => new _PageList_Column_creator('author_id', _("Creator")),
1326                 /*
1327                 'group'
1328                 => new _PageList_Column_author('group', _("Group")),
1329                 */
1330                 'locked'
1331                 => new _PageList_Column_bool('locked', _("Locked"),
1332                     _("locked")),
1333                 'external'
1334                 => new _PageList_Column_bool('external', _("External"),
1335                     _("external")),
1336                 'minor'
1337                 => new _PageList_Column_bool('rev:is_minor_edit',
1338                     _("Minor Edit"), _("minor")),
1339                 // 'rating' initialised by the wikilens theme hook: addPageListColumn
1340                 /*
1341                 'rating'
1342                 => new _PageList_Column_rating('rating', _("Rate")),
1343                 */
1344             );
1345         if (empty($this->_types))
1346             $this->_types = array();
1347         // add plugin specific pageList columns, initialized by $options['types']
1348         $this->_types = array_merge($standard_types, $this->_types);
1349         // add theme custom specific pageList columns:
1350         //   set the 4th param as the current pagelist object.
1351         if (!empty($customPageListColumns)) {
1352             foreach ($customPageListColumns as $column => $params) {
1353                 $class_name = array_shift($params);
1354                 $params[3] =& $this;
1355                 // ref to a class does not work with php-4
1356                 $this->_types[$column] = new $class_name($params);
1357             }
1358         }
1359     }
1360
1361     public function getOption($option)
1362     {
1363         if (array_key_exists($option, $this->_options)) {
1364             return $this->_options[$option];
1365         } else {
1366             return null;
1367         }
1368     }
1369
1370     /**
1371      * Add a column to this PageList, given a column name.
1372      * The name is a type, and optionally has a : and a label. Examples:
1373      *
1374      *   pagename
1375      *   pagename:This page
1376      *   mtime
1377      *   mtime:Last modified
1378      *
1379      * If this function is called multiple times for the same type, the
1380      * column will only be added the first time, and ignored the succeeding times.
1381      * If you wish to add multiple columns of the same type, use addColumnObject().
1382      *
1383      * @param string $column column name
1384      * @return bool true if column is added, false otherwise
1385      */
1386     public function _addColumn($column)
1387     {
1388         if (isset($this->_columns_seen[$column]))
1389             return false; // Already have this one.
1390         if (!isset($this->_types[$column]))
1391             $this->initAvailableColumns();
1392         $this->_columns_seen[$column] = true;
1393
1394         if (strstr($column, ':'))
1395             list ($column, $heading) = explode(':', $column, 2);
1396
1397         // FIXME: these column types have hooks (objects) elsewhere
1398         // Omitting this warning should be overridable by the extension
1399         if (!isset($this->_types[$column])) {
1400             $silently_ignore = array('numbacklinks',
1401                 'rating', 'ratingvalue',
1402                 'coagreement', 'minmisery',
1403                 'averagerating', 'top3recs',
1404                 'relation', 'linkto');
1405             if (!in_array($column, $silently_ignore))
1406                 trigger_error(sprintf("%s: Bad column", $column), E_USER_NOTICE);
1407             return false;
1408         }
1409         if (!(defined('FUSIONFORGE') && FUSIONFORGE)) {
1410             // FIXME: anon users might rate and see ratings also.
1411             // Defer this logic to the plugin.
1412             if ($column == 'rating' and !$request->_user->isSignedIn()) {
1413                 return false;
1414             }
1415         }
1416
1417         $this->addColumnObject($this->_types[$column]);
1418
1419         return true;
1420     }
1421
1422     /**
1423      * Add a column to this PageList, given a column object.
1424      *
1425      * @param array|object $col   An object derived from _PageList_Column.
1426      **/
1427     public function addColumnObject($col)
1428     {
1429         if (is_array($col)) { // custom column object
1430             $params =& $col;
1431             $class_name = array_shift($params);
1432             $params[3] =& $this;
1433             $col = new $class_name($params);
1434         }
1435         $heading = $col->getHeading();
1436         if (!empty($heading))
1437             $col->setHeading($heading);
1438
1439         $this->_columns[] = $col;
1440         $this->_columnsMap[$col->_field] = count($this->_columns); // start with 1
1441     }
1442
1443     /**
1444      * Compare _PageList_Page objects.
1445      **/
1446     private function pageCompare(&$a, &$b)
1447     {
1448         if (empty($this->_sortby) or count($this->_sortby) == 0) {
1449             // No columns to sort by
1450             return 0;
1451         } else {
1452             $pagea = $this->getPageFromHandle($a); // If a string, convert to page
1453             assert(is_a($pagea, 'WikiDB_Page'));
1454             $pageb = $this->getPageFromHandle($b); // If a string, convert to page
1455             assert(is_a($pageb, 'WikiDB_Page'));
1456             foreach ($this->_sortby as $colNum => $direction) {
1457                 // get column type object
1458                 if (!is_int($colNum)) { // or column fieldname
1459                     if (isset($this->_columnsMap[$colNum]))
1460                         $col = $this->_columns[$this->_columnsMap[$colNum] - 1];
1461                     elseif (isset($this->_types[$colNum]))
1462                         $col = $this->_types[$colNum];
1463                 }
1464                 if (empty($col)) {
1465                     return 0;
1466                 }
1467                 assert(isset($col));
1468                 $revision_handle = false;
1469                 $aval = $col->_getSortableValue($pagea, $revision_handle);
1470                 $revision_handle = false;
1471                 $bval = $col->_getSortableValue($pageb, $revision_handle);
1472
1473                 $cmp = $col->_compare($aval, $bval);
1474                 if ($direction === "-") // Reverse the sense of the comparison
1475                     $cmp *= -1;
1476
1477                 if ($cmp !== 0)
1478                     // This is the first comparison that is not equal-- go with it
1479                     return $cmp;
1480             }
1481             return 0;
1482         }
1483     }
1484
1485     /**
1486      * Put pages in order according to the sortby arg, if given
1487      * If the sortby cols are already sorted by the DB call, don't do usort.
1488      * TODO: optimize for multiple sortable cols
1489      */
1490     private function sortPages()
1491     {
1492         if (count($this->_sortby) > 0) {
1493             $need_sort = $this->_options['dosort'];
1494             if (!$need_sort)
1495                 foreach ($this->_sortby as $col => $dir) {
1496                     if (!$this->sortby($col, 'db'))
1497                         $need_sort = true;
1498                 }
1499             if ($need_sort) { // There are some columns to sort by
1500                 // TODO: consider nopage
1501                 usort($this->_pages, array($this, 'pageCompare'));
1502             }
1503         }
1504     }
1505
1506     public function limit($limit)
1507     {
1508         if (is_array($limit)) {
1509             list($from, $count) = $limit;
1510             if ((!empty($from) && !is_numeric($from)) or (!empty($count) && !is_numeric($count))) {
1511                 trigger_error(_("Illegal “limit” argument: must be an integer or two integers separated by comma"));
1512                 return array(0, 0);
1513             }
1514             return $limit;
1515         }
1516         if (strstr($limit, ',')) {
1517             list($from, $limit) = explode(',', $limit);
1518             if ((!empty($from) && !is_numeric($from)) or (!empty($limit) && !is_numeric($limit))) {
1519                 trigger_error(_("Illegal “limit” argument: must be an integer or two integers separated by comma"));
1520                 return array(0, 0);
1521             }
1522             return array($from, $limit);
1523         } else {
1524             if (!empty($limit) && !is_numeric($limit)) {
1525                 trigger_error(_("Illegal “limit” argument: must be an integer or two integers separated by comma"));
1526                 return array(0, 0);
1527             }
1528             return array(0, $limit);
1529         }
1530     }
1531
1532     public function pagingTokens($numrows = false, $ncolumns = false, $limit = false)
1533     {
1534         /**
1535          * @var WikiRequest $request
1536          */
1537         global $request;
1538
1539         if ($numrows === false)
1540             $numrows = $this->getTotal();
1541         if ($limit === false)
1542             $limit = $this->_options['limit'];
1543         if ($ncolumns === false)
1544             $ncolumns = count($this->_columns);
1545
1546         list($offset, $pagesize) = $this->limit($limit);
1547         if (!$pagesize or
1548             (!$offset and $numrows < $pagesize) or
1549             (($offset + $pagesize) < 0)
1550         )
1551             return false;
1552
1553         $pagename = $request->getArg('pagename');
1554         $defargs = array_merge(array('id' => $this->id), $request->args);
1555         if (USE_PATH_INFO) unset($defargs['pagename']);
1556         if (isset($defargs['action']) and ($defargs['action'] == 'browse')) {
1557             unset($defargs['action']);
1558         }
1559         $prev = $defargs;
1560
1561         $tokens = array();
1562         $tokens['PREV'] = false;
1563         $tokens['PREV_LINK'] = "";
1564         $tokens['COLS'] = $ncolumns;
1565         $tokens['COUNT'] = $numrows;
1566         $tokens['OFFSET'] = $offset;
1567         $tokens['SIZE'] = $pagesize;
1568         $tokens['NUMPAGES'] = (int)ceil($numrows / $pagesize);
1569         $tokens['ACTPAGE'] = (int)ceil(($offset / $pagesize) + 1);
1570         if ($offset > 0) {
1571             $prev['limit'] = max(0, $offset - $pagesize) . ",$pagesize";
1572             $prev['count'] = $numrows;
1573             $tokens['LIMIT'] = $prev['limit'];
1574             $tokens['PREV'] = true;
1575             $tokens['PREV_LINK'] = WikiURL($pagename, $prev);
1576             $prev['limit'] = "0,$pagesize"; // FIRST_LINK
1577             $tokens['FIRST_LINK'] = WikiURL($pagename, $prev);
1578         }
1579         $next = $defargs;
1580         $tokens['NEXT'] = false;
1581         $tokens['NEXT_LINK'] = "";
1582         if (($offset + $pagesize) < $numrows) {
1583             $next['limit'] = min($offset + $pagesize, $numrows - $pagesize)
1584                 . ",$pagesize";
1585             $next['count'] = $numrows;
1586             $tokens['LIMIT'] = $next['limit'];
1587             $tokens['NEXT'] = true;
1588             $tokens['NEXT_LINK'] = WikiURL($pagename, $next);
1589             $next['limit'] = $numrows - $pagesize . ",$pagesize"; // LAST_LINK
1590             $tokens['LAST_LINK'] = WikiURL($pagename, $next);
1591         }
1592         return $tokens;
1593     }
1594
1595     // make a table given the caption
1596     public function _generateTable($caption = '')
1597     {
1598         if (count($this->_sortby) > 0) $this->sortPages();
1599
1600         // wikiadminutils hack. that's a way to pagelist non-pages
1601         $rows = isset($this->_rows) ? $this->_rows : array();
1602         $i = 0;
1603         $count = $this->getTotal();
1604         $do_paging = (isset($this->_options['paging'])
1605             and !empty($this->_options['limit'])
1606                 and $count
1607                     and $this->_options['paging'] != 'none');
1608         if ($do_paging) {
1609             $tokens = $this->pagingTokens($count,
1610                 count($this->_columns),
1611                 $this->_options['limit']);
1612             if ($tokens and !empty($this->_options['slice']))
1613                 $this->_pages = array_slice($this->_pages, $tokens['OFFSET'], $tokens['SIZE']);
1614         }
1615         foreach ($this->_pages as $pagenum => $page) {
1616             $one_row = $this->renderPageRow($page, $i++);
1617             $rows[] = $one_row;
1618         }
1619         $table = HTML::table(array('class' => 'fullwidth pagelist'));
1620         if ($caption) {
1621             $table->pushContent(HTML::caption(array('class' => 'top'), $caption));
1622         }
1623
1624         $row = HTML::tr();
1625         $i = 1; // start with 1!
1626         foreach ($this->_columns as $col) {
1627             $heading = $col->button_heading($this, $i);
1628             if ($do_paging
1629                 and isset($col->_field)
1630                     and $col->_field == 'pagename'
1631                         and ($maxlen = $this->maxLen())
1632             ) {
1633             }
1634             $row->pushContent($heading);
1635             $i++;
1636         }
1637         $table->pushContent(HTML::colgroup(array('span' => count($this->_columns))));
1638         if ($do_paging) {
1639             if ($tokens === false) {
1640                 $table->pushContent(HTML::thead($row),
1641                     HTML::tbody(false, $rows));
1642                 return $table;
1643             }
1644
1645             $paging = Template("pagelink", $tokens);
1646             if ($this->_options['paging'] != 'bottom')
1647                 $table->pushContent(HTML::thead($paging));
1648             if ($this->_options['paging'] != 'top')
1649                 $table->pushContent(HTML::tfoot($paging));
1650             $table->pushContent(HTML::tbody(false, HTML($row, $rows)));
1651             return $table;
1652         } else {
1653             $table->pushContent(HTML::thead($row),
1654                 HTML::tbody(false, $rows));
1655             return $table;
1656         }
1657     }
1658
1659     /* recursive stack for private sublist options (azhead, cols) */
1660     private function saveOptions($opts)
1661     {
1662         $stack = array('pages' => $this->_pages);
1663         foreach ($opts as $k => $v) {
1664             $stack[$k] = $this->_options[$k];
1665             $this->_options[$k] = $v;
1666         }
1667         if (empty($this->_stack))
1668             $this->_stack = new Stack();
1669         $this->_stack->push($stack);
1670     }
1671
1672     private function restoreOptions()
1673     {
1674         assert($this->_stack);
1675         $stack = $this->_stack->pop();
1676         $this->_pages = $stack['pages'];
1677         unset($stack['pages']);
1678         foreach ($stack as $k => $v) {
1679             $this->_options[$k] = $v;
1680         }
1681     }
1682
1683     // 'cols'   - split into several columns
1684     // 'azhead' - support <h3> grouping into initials
1685     // 'ordered' - OL or UL list (not yet inherited to all plugins)
1686     // 'comma'  - condensed comma-list only, 1: no links, >1: with links
1687     // FIXME: only unique list entries, esp. with nopage
1688     private function generateList($caption = '')
1689     {
1690         if (empty($this->_pages)) {
1691             return false; // stop recursion
1692         }
1693         if (!isset($this->_options['listtype']))
1694             $this->_options['listtype'] = '';
1695         foreach ($this->_pages as $pagenum => $page) {
1696             $one_row = $this->renderPageRow($page);
1697             $rows[] = array('header' => WikiLink($page), 'render' => $one_row);
1698         }
1699         $out = HTML();
1700         if ($caption) {
1701             $out->pushContent(HTML::p($caption));
1702         }
1703
1704         // Semantic Search et al: only unique list entries, esp. with nopage
1705         if (!is_array($this->_pages[0]) and is_string($this->_pages[0])) {
1706             $this->_pages = array_unique($this->_pages);
1707         }
1708         if (count($this->_sortby) > 0) $this->sortPages();
1709         $count = $this->getTotal();
1710         $do_paging = (isset($this->_options['paging'])
1711             and !empty($this->_options['limit'])
1712                 and $count
1713                     and $this->_options['paging'] != 'none');
1714         if ($do_paging) {
1715             $tokens = $this->pagingTokens($count,
1716                 count($this->_columns),
1717                 $this->_options['limit']);
1718             if ($tokens) {
1719                 $paging = Template("pagelink", $tokens);
1720                 $out->pushContent(HTML::table(array('class' => 'fullwidth'), $paging));
1721             }
1722         }
1723
1724         if (!empty($this->_options['limit']) and !empty($this->_options['slice'])) {
1725             list($offset, $count) = $this->limit($this->_options['limit']);
1726         } else {
1727             $offset = 0;
1728             $count = count($this->_pages);
1729         }
1730         // need a recursive switch here for the azhead and cols grouping.
1731         if (!empty($this->_options['cols']) and $this->_options['cols'] > 1) {
1732             $length = intval($count / ($this->_options['cols']));
1733             // If division does not give an integer, we need one more line
1734             // E.g. 13 pages to display in 3 columns.
1735             if (($length * ($this->_options['cols'])) != $count) {
1736                 $length += 1;
1737             }
1738             $width = sprintf("%d", 100 / $this->_options['cols']) . '%';
1739             $cols = HTML::tr(array('class' => 'top'));
1740             for ($i = $offset; $i < $offset + $count; $i += $length) {
1741                 $this->saveOptions(array('cols' => 0, 'paging' => 'none'));
1742                 $this->_pages = array_slice($this->_pages, $i, $length);
1743                 $cols->pushContent(HTML::td($this->generateList()));
1744                 $this->restoreOptions();
1745             }
1746             // speed up table rendering by defining colgroups
1747             $out->pushContent(HTML::table(HTML::colgroup
1748                 (array('span' => $this->_options['cols'],
1749                        'style' => 'width: '.$width)),
1750                 $cols));
1751             return $out;
1752         }
1753
1754         // Ignore azhead if not sorted by pagename
1755         if (!empty($this->_options['azhead'])
1756             and strstr($this->sortby($this->_options['sortby'], 'init'), "pagename")
1757         ) {
1758             $cur_h = substr($this->_pages[0]->getName(), 0, 1);
1759             $out->pushContent(HTML::h3($cur_h));
1760             // group those pages together with same $h
1761             $j = 0;
1762             for ($i = 0; $i < count($this->_pages); $i++) {
1763                 $page =& $this->_pages[$i];
1764                 $h = substr($page->getName(), 0, 1);
1765                 if ($h != $cur_h and $i > $j) {
1766                     $this->saveOptions(array('cols' => 0, 'azhead' => 0, 'ordered' => $j + 1));
1767                     $this->_pages = array_slice($this->_pages, $j, $i - $j);
1768                     $out->pushContent($this->generateList());
1769                     $this->restoreOptions();
1770                     $j = $i;
1771                     $out->pushContent(HTML::h3($h));
1772                     $cur_h = $h;
1773                 }
1774             }
1775             if ($i > $j) { // flush the rest
1776                 $this->saveOptions(array('cols' => 0, 'azhead' => 0, 'ordered' => $j + 1));
1777                 $this->_pages = array_slice($this->_pages, $j, $i - $j);
1778                 $out->pushContent($this->generateList());
1779                 $this->restoreOptions();
1780             }
1781             return $out;
1782         }
1783
1784         if ($this->_options['listtype'] == 'comma')
1785             $this->_options['comma'] = 2;
1786         if (!empty($this->_options['comma'])) {
1787             if ($this->_options['comma'] == 1)
1788                 $out->pushContent($this->generateCommaListAsString());
1789             else
1790                 $out->pushContent($this->generateCommaList());
1791             return $out;
1792         }
1793
1794         if ($this->_options['listtype'] == 'ol') {
1795             if (empty($this->_options['ordered'])) {
1796                 $this->_options['ordered'] = $offset + 1;
1797             }
1798         } elseif ($this->_options['listtype'] == 'ul')
1799             $this->_options['ordered'] = 0;
1800         if ($this->_options['listtype'] == 'ol' and !empty($this->_options['ordered'])) {
1801             $list = HTML::ol(array('class' => 'pagelist',
1802                 'start' => $this->_options['ordered']));
1803         } elseif ($this->_options['listtype'] == 'dl') {
1804             $list = HTML::dl(array('class' => 'pagelist'));
1805         } else {
1806             $list = HTML::ul(array('class' => 'pagelist'));
1807         }
1808         $i = 0;
1809         //TODO: currently we ignore limit here and hope that the backend didn't ignore it. (BackLinks)
1810         if (!empty($this->_options['limit']))
1811             list($offset, $pagesize) = $this->limit($this->_options['limit']);
1812         else
1813             $pagesize = 0;
1814         foreach (array_reverse($rows) as $one_row) {
1815             $pagehtml = $one_row['render'];
1816             if (!$pagehtml) continue;
1817             $group = ($i++ / $this->_group_rows);
1818             //TODO: here we switch every row, in tables every third.
1819             //      unification or parametrized?
1820             $class = ($group % 2) ? 'oddrow' : 'evenrow';
1821             if ($this->_options['listtype'] == 'dl') {
1822                 $header = $one_row['header'];
1823                 $list->pushContent(HTML::dt(array('class' => $class), $header),
1824                     HTML::dd(array('class' => $class), $pagehtml));
1825             } else
1826                 $list->pushContent(HTML::li(array('class' => $class), $pagehtml));
1827             if ($pagesize and $i > $pagesize) break;
1828         }
1829         $out->pushContent($list);
1830         if ($do_paging and $tokens) {
1831             $out->pushContent(HTML::table(array('class' => 'fullwidth'), $paging));
1832         }
1833         return $out;
1834     }
1835
1836     // comma=1
1837     // Condense list without a href links: "Page1, Page2, ..."
1838     // FIXME: only unique list entries, esp. with nopage
1839     private function generateCommaListAsString()
1840     {
1841         if (defined($this->_options['commasep']))
1842             $separator = $this->_options['commasep'];
1843         else
1844             $separator = ', ';
1845         $pages = array();
1846         foreach ($this->_pages as $pagenum => $page) {
1847             if ($s = $this->renderPageRow($page)) // some pages are not viewable
1848                 $pages[] = is_string($s) ? $s : $s->asString();
1849         }
1850         return HTML(join($separator, $pages));
1851     }
1852
1853     // comma=2
1854     // Normal WikiLink list.
1855     // Future: 1 = reserved for plain string (see above)
1856     //         2 and more => HTML link specialization?
1857     // FIXME: only unique list entries, esp. with nopage
1858     private function generateCommaList()
1859     {
1860         if (defined($this->_options['commasep']))
1861             $separator = HTML::raw($this->_options['commasep']);
1862         else
1863             $separator = ', ';
1864         $html = HTML();
1865         $html->pushContent($this->renderPageRow($this->_pages[0]));
1866         next($this->_pages);
1867         foreach ($this->_pages as $pagenum => $page) {
1868             if ($s = $this->renderPageRow($page)) // some pages are not viewable
1869                 $html->pushContent($separator, $s);
1870         }
1871         return $html;
1872     }
1873
1874     function _emptyList($caption)
1875     {
1876         $html = HTML();
1877         if ($caption) {
1878             $html->pushContent(HTML::p($caption));
1879         }
1880         if (!empty($this->_messageIfEmpty))
1881             $html->pushContent(HTML::blockquote(HTML::p($this->_messageIfEmpty)));
1882         return $html;
1883     }
1884
1885 }
1886
1887 /* List pages with checkboxes to select from.
1888  * The [Select] button toggles via Javascript flipAll
1889  */
1890
1891 class PageList_Selectable
1892     extends PageList
1893 {
1894
1895     function __construct($columns = false, $exclude = '', $options = array())
1896     {
1897         if ($columns) {
1898             if (!is_array($columns))
1899                 $columns = explode(',', $columns);
1900             if (!in_array('checkbox', $columns))
1901                 array_unshift($columns, 'checkbox');
1902         } else {
1903             $columns = array('checkbox', 'pagename');
1904         }
1905         parent::__construct($columns, $exclude, $options);
1906     }
1907
1908     function addPageList($array)
1909     {
1910         while (list($pagename, $selected) = each($array)) {
1911             if ($selected) $this->addPageSelected((string)$pagename);
1912             $this->addPage((string)$pagename);
1913         }
1914     }
1915
1916     function addPageSelected($pagename)
1917     {
1918         $this->_selected[$pagename] = 1;
1919     }
1920 }
1921
1922 class PageList_Unselectable
1923     extends PageList
1924 {
1925
1926     function __construct($columns = false, $exclude = '', $options = array())
1927     {
1928         if ($columns) {
1929             if (!is_array($columns))
1930                 $columns = explode(',', $columns);
1931         } else {
1932             $columns = array('pagename');
1933         }
1934         parent::__construct($columns, $exclude, $options);
1935     }
1936
1937     function addPageList($array)
1938     {
1939         while (list($pagename, $selected) = each($array)) {
1940             if ($selected) $this->addPageSelected((string)$pagename);
1941             $this->addPage((string)$pagename);
1942         }
1943     }
1944
1945     function addPageSelected($pagename)
1946     {
1947         $this->_selected[$pagename] = 1;
1948     }
1949 }
1950
1951 // Local Variables:
1952 // mode: php
1953 // tab-width: 8
1954 // c-basic-offset: 4
1955 // c-hanging-comment-ender-p: nil
1956 // indent-tabs-mode: nil
1957 // End: