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