]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/PageList.php
added SearchReplace support. some thoughts on paging support
[SourceForge/phpwiki.git] / lib / PageList.php
1 <?php rcs_id('$Id: PageList.php,v 1.55 2004-02-17 12:14:07 rurban Exp $');
2
3 /**
4  * List a number of pagenames, optionally as table with various columns.
5  * This library relieves some work for these plugins:
6  *
7  * AllPages, BackLinks, LikePages, Mostpopular, TitleSearch and more
8  *
9  * It also allows dynamic expansion of those plugins to include more
10  * columns in their output.
11  *
12  * Column 'info=' arguments:
13  *
14  * 'pagename' _("Page Name")
15  * 'mtime'    _("Last Modified")
16  * 'hits'     _("Hits")
17  * 'summary'  _("Last Summary")
18  * 'version'  _("Version")),
19  * 'author'   _("Last Author")),
20  * 'locked'   _("Locked"), _("locked")
21  * 'minor'    _("Minor Edit"), _("minor")
22  * 'markup'   _("Markup")
23  * 'size'     _("Size")
24  * 'remove'   _("Remove") //todo: move this admin action away, not really an info column
25  *
26  * 'checkbox'  A selectable checkbox appears at the left.
27  * 'all'       All columns except remove, content and renamed_pagename
28  * 'most'      pagename, mtime, author, size, hits, ...
29  * 'some'      pagename, mtime, author
30  *
31  * FIXME: In this refactoring I have un-implemented _ctime, _cauthor, and
32  * number-of-revision.  Note the _ctime and _cauthor as they were implemented
33  * were somewhat flawed: revision 1 of a page doesn't have to exist in the
34  * database.  If lots of revisions have been made to a page, it's more than likely
35  * that some older revisions (include revision 1) have been cleaned (deleted).
36  *
37  * TODO: 
38  *   limit, offset, rows arguments for multiple pages/multiple rows.
39  *
40  *   check PagePerm "list" access-type
41  *
42  *   ->supportedArgs() which arguments are supported, so that the plugin 
43  *                     doesn't explictly need to declare it
44  */
45 class _PageList_Column_base {
46     var $_tdattr = array();
47
48     function _PageList_Column_base ($default_heading, $align = false) {
49         $this->_heading = $default_heading;
50
51         if ($align) {
52             // align="char" isn't supported by any browsers yet :(
53             //if (is_array($align))
54             //    $this->_tdattr = $align;
55             //else
56             $this->_tdattr['align'] = $align;
57         }
58     }
59
60     function format ($pagelist, $page_handle, &$revision_handle) {
61         return HTML::td($this->_tdattr,
62                         HTML::raw('&nbsp;'),
63                         $this->_getValue($page_handle, $revision_handle),
64                         HTML::raw('&nbsp;'));
65     }
66
67     function setHeading ($heading) {
68         $this->_heading = $heading;
69     }
70
71     function heading () {
72         if (in_array($this->_field,array('pagename','mtime','hits'))) {
73             // multiple comma-delimited sortby args: "+hits,+pagename"
74             // asc or desc: +pagename, -pagename
75             $sortby = PageList::sortby($this->_field,'flip_order');
76             $s = HTML::a(array('href' => $GLOBALS['request']->GetURLtoSelf(array('sortby' => $sortby)),'class' => 'pagetitle', 'title' => sprintf(_("Sort by %s"),$this->_field)), HTML::raw('&nbsp;'), HTML::u($this->_heading), HTML::raw('&nbsp;'));
77         } else {
78             $s = HTML(HTML::raw('&nbsp;'), HTML::u($this->_heading), HTML::raw('&nbsp;'));
79         }
80         return HTML::td(array('align' => 'center'),$s);
81     }
82 };
83
84 class _PageList_Column extends _PageList_Column_base {
85     function _PageList_Column ($field, $default_heading, $align = false) {
86         $this->_PageList_Column_base($default_heading, $align);
87
88         $this->_need_rev = substr($field, 0, 4) == 'rev:';
89         if ($this->_need_rev)
90             $this->_field = substr($field, 4);
91         else
92             $this->_field = $field;
93     }
94
95     function _getValue ($page_handle, &$revision_handle) {
96         if ($this->_need_rev) {
97             if (!$revision_handle)
98                 $revision_handle = $page_handle->getCurrentRevision();
99             return $revision_handle->get($this->_field);
100         }
101         else {
102             return $page_handle->get($this->_field);
103         }
104     }
105 };
106
107 class _PageList_Column_size extends _PageList_Column {
108     function _getValue ($page_handle, &$revision_handle) {
109         if (!$revision_handle)
110             $revision_handle = $page_handle->getCurrentRevision();
111         return $this->_getSize($revision_handle);
112     }
113
114     function _getSize($revision_handle) {
115         $bytes = strlen($revision_handle->_data['%content']);
116         return ByteFormatter($bytes);
117     }
118 }
119
120
121 class _PageList_Column_bool extends _PageList_Column {
122     function _PageList_Column_bool ($field, $default_heading, $text = 'yes') {
123         $this->_PageList_Column($field, $default_heading, 'center');
124         $this->_textIfTrue = $text;
125         $this->_textIfFalse = new RawXml('&#8212;'); //mdash
126     }
127
128     function _getValue ($page_handle, &$revision_handle) {
129         $val = _PageList_Column::_getValue($page_handle, $revision_handle);
130         return $val ? $this->_textIfTrue : $this->_textIfFalse;
131     }
132 };
133
134 class _PageList_Column_checkbox extends _PageList_Column {
135     function _PageList_Column_checkbox ($field, $default_heading, $name='p') {
136         $this->_name = $name;
137         $heading = HTML::input(array('type'  => 'button',
138                                      'title' => _("Click to de-/select all pages"),
139                                      'name'  => $default_heading,
140                                      'value' => $default_heading,
141                                      'onclick' => "flipAll(this.form)"
142                                      ));
143         $this->_PageList_Column($field, $heading, 'center');
144     }
145     function _getValue ($pagelist, $page_handle, &$revision_handle) {
146         $pagename = $page_handle->getName();
147         if (!empty($pagelist->_selected[$pagename])) {
148             return HTML::input(array('type' => 'checkbox',
149                                      'name' => $this->_name . "[$pagename]",
150                                      'value' => $pagename,
151                                      'checked' => 'CHECKED'));
152         } else {
153             return HTML::input(array('type' => 'checkbox',
154                                      'name' => $this->_name . "[$pagename]",
155                                      'value' => $pagename));
156         }
157     }
158     function format ($pagelist, $page_handle, &$revision_handle) {
159         return HTML::td($this->_tdattr,
160                         HTML::raw('&nbsp;'),
161                         $this->_getValue($pagelist, $page_handle, $revision_handle),
162                         HTML::raw('&nbsp;'));
163     }
164 };
165
166 class _PageList_Column_time extends _PageList_Column {
167     function _PageList_Column_time ($field, $default_heading) {
168         $this->_PageList_Column($field, $default_heading, 'right');
169         global $Theme;
170         $this->Theme = &$Theme;
171     }
172
173     function _getValue ($page_handle, &$revision_handle) {
174         $time = _PageList_Column::_getValue($page_handle, $revision_handle);
175         return $this->Theme->formatDateTime($time);
176     }
177 };
178
179 class _PageList_Column_version extends _PageList_Column {
180     function _getValue ($page_handle, &$revision_handle) {
181         if (!$revision_handle)
182             $revision_handle = $page_handle->getCurrentRevision();
183         return $revision_handle->getVersion();
184     }
185 };
186
187 // If needed this could eventually become a subclass
188 // of a new _PageList_Column_action class for other actions.
189 // only for WikiAdminRemove or WikiAdminSelect
190 class _PageList_Column_remove extends _PageList_Column {
191     function _getValue ($page_handle, &$revision_handle) {
192         return Button(array('action' => 'remove'), _("Remove"),
193                       $page_handle->getName());
194     }
195 };
196
197 // only for WikiAdminRename
198 class _PageList_Column_renamed_pagename extends _PageList_Column {
199     function _getValue ($page_handle, &$revision_handle) {
200         $post_args = $GLOBALS['request']->getArg('admin_rename');
201         $value = str_replace($post_args['from'], $post_args['to'],$page_handle->getName());
202         return HTML::div(" => ",HTML::input(array('type' => 'text',
203                                                   'name' => 'rename[]',
204                                                   'value' => $value)));
205     }
206 };
207
208 // Output is hardcoded to limit of first 50 bytes. Otherwise
209 // on very large Wikis this will fail if used with AllPages
210 // (PHP memory limit exceeded)
211 class _PageList_Column_content extends _PageList_Column {
212     function _PageList_Column_content ($field, $default_heading, $align = false) {
213         _PageList_Column::_PageList_Column($field, $default_heading, $align);
214         $this->bytes = 50;
215         if ($field == 'content') {
216             $this->_heading .= sprintf(_(" ... first %d bytes"),
217                                        $this->bytes);
218         } elseif ($field == 'hi_content') {
219             if (!empty($_POST['admin_replace'])) {
220               $search = $_POST['admin_replace']['from'];
221               $this->_heading .= sprintf(_(" ... around %s"),
222                                        '»'.$search.'«');
223             }
224         }
225     }
226     function _getValue ($page_handle, &$revision_handle) {
227         if (!$revision_handle)
228             $revision_handle = $page_handle->getCurrentRevision();
229         // Not sure why implode is needed here, I thought
230         // getContent() already did this, but it seems necessary.
231         $c = implode("\n", $revision_handle->getContent());
232         if ($this->_field == 'hi_content') {
233             $search = $_POST['admin_replace']['from'];
234             if ($search and ($i = strpos($c,$search))) {
235                 $l = strlen($search);
236                 $j = max(0,$i - ($this->bytes / 2));
237                 return HTML::div(array('style' => 'font-size:x-small'),
238                                  HTML::div(array('class' => 'transclusion'),
239                                            HTML::span(substr($c, $j, ($this->bytes / 2))),
240                                            HTML::span(array("style"=>"background:yellow"),$search),
241                                            HTML::span(substr($c, $i+$l, ($this->bytes / 2))))
242                                  );
243             } else {
244                 $c = sprintf(_("%s not found"),
245                              '»'.$search.'«');
246                 return HTML::div(array('style' => 'font-size:x-small','align'=>'center'),
247                                  $c);
248             }
249         } elseif (($len = strlen($c)) > $this->bytes) {
250             $c = substr($c, 0, $this->bytes);
251         }
252         include_once('lib/BlockParser.php');
253         // false --> don't bother processing hrefs for embedded WikiLinks
254         $ct = TransformText($c, $revision_handle->get('markup'), false);
255         return HTML::div(array('style' => 'font-size:x-small'),
256                          HTML::div(array('class' => 'transclusion'), $ct),
257                          // Don't show bytes here if size column present too
258                          ($this->parent->_columns_seen['size'] or !$len) ? "" :
259                            ByteFormatter($len, /*$longformat = */true));
260     }
261 };
262
263 class _PageList_Column_author extends _PageList_Column {
264     function _PageList_Column_author ($field, $default_heading, $align = false) {
265         _PageList_Column::_PageList_Column($field, $default_heading, $align);
266         global $WikiNameRegexp, $request;
267         $this->WikiNameRegexp = $WikiNameRegexp;
268         $this->dbi = &$request->getDbh();
269     }
270
271     function _getValue ($page_handle, &$revision_handle) {
272         $author = _PageList_Column::_getValue($page_handle, $revision_handle);
273         if (preg_match("/^$this->WikiNameRegexp\$/", $author) && $this->dbi->isWikiPage($author))
274             return WikiLink($author);
275         else
276             return $author;
277     }
278 };
279
280 class _PageList_Column_pagename extends _PageList_Column_base {
281     var $_field = 'pagename';
282
283     function _PageList_Column_pagename () {
284         $this->_PageList_Column_base(_("Page Name"));
285         global $request;
286         $this->dbi = &$request->getDbh();
287     }
288
289     function _getValue ($page_handle, &$revision_handle) {
290         if ($this->dbi->isWikiPage($pagename = $page_handle->getName()))
291             return WikiLink($page_handle);
292         else
293             return WikiLink($page_handle, 'unknown');
294     }
295 };
296
297
298
299 class PageList {
300     var $_group_rows = 3;
301     var $_columns = array();
302     var $_excluded_pages = array();
303     var $_rows = array();
304     var $_caption = "";
305     var $_pagename_seen = false;
306     var $_types = array();
307     var $_options = array();
308     var $_selected = array();
309
310     function PageList ($columns = false, $exclude = false, $options = false) {
311         $this->_initAvailableColumns();
312         $symbolic_columns = 
313             array(
314                   'all' =>  array_diff(array_keys($this->_types),
315                                        array('checkbox','remove','renamed_pagename','content')),
316                   'most' => array('pagename','mtime','author','size','hits'),
317                   'some' => array('pagename','mtime','author')
318                   );
319         if ($columns) {
320             if (!is_array($columns))
321                 $columns = explode(',', $columns);
322             // expand symbolic columns:
323             foreach ($symbolic_columns as $symbol => $cols) {
324                 if (in_array($symbol,$columns)) { // e.g. 'checkbox,all'
325                     $columns = array_diff(array_merge($columns,$cols),array($symbol));
326                 }
327             }
328             foreach ($columns as $col) {
329                 $this->_addColumn($col);
330             }
331         }
332         $this->_addColumn('pagename');
333
334         if ($exclude) {
335             if (!is_array($exclude))
336                 $exclude = explode(',', $exclude);
337             $this->_excluded_pages = $exclude;
338         }
339
340         $this->_options = $options;
341         $this->_messageIfEmpty = _("<no matches>");
342     }
343
344     // Currently PageList takes these arguments:
345     // 1: info, 2: exclude, 3: hash of options
346     // Here we declare which options are supported, so that 
347     // the calling plugin may simply merge this with its own default arguments 
348     function supportedArgs () {
349         return array(//Currently supported options:
350                      'info'              => 'pagename',
351                      'exclude'           => '',          // also wildcards and comma-seperated lists
352
353                      // for the sort buttons in <th>
354                      'sortby'            => '',   // same as for WikiDB::getAllPages
355
356                      //PageList pager options:
357                      // These options may also be given to _generate(List|Table) later
358                      // But limit and offset might help the query WikiDB::getAllPages()
359                      //cols    => 1,       // side-by-side display of list (1-3)
360                      //limit   => 50,      // length of one column
361                      //offset  => 0,       // needed internally for the pager
362                      //paging  => 'auto',  // '':     normal paging mode
363                      //                    // 'auto': drop 'info' columns and enhance rows 
364                      //                    //         when the list becomes large
365                      //                    // 'none': don't page at all
366                      );
367     }
368
369     function setCaption ($caption_string) {
370         $this->_caption = $caption_string;
371     }
372
373     function getCaption () {
374         // put the total into the caption if needed
375         if (is_string($this->_caption) && strstr($this->_caption, '%d'))
376             return sprintf($this->_caption, $this->getTotal());
377         return $this->_caption;
378     }
379
380     function setMessageIfEmpty ($msg) {
381         $this->_messageIfEmpty = $msg;
382     }
383
384
385     function getTotal () {
386         return count($this->_rows);
387     }
388
389     function isEmpty () {
390         return empty($this->_rows);
391     }
392
393     function addPage ($page_handle) {
394         if (is_string($page_handle)) {
395             if (in_array($page_handle, $this->_excluded_pages))
396                 return;             // exclude page.
397             $dbi = $GLOBALS['request']->getDbh();
398             $page_handle = $dbi->getPage($page_handle);
399         } elseif (is_object($page_handle)) {
400           if (in_array($page_handle->getName(), $this->_excluded_pages))
401             return;             // exclude page.
402         }
403
404         $group = (int)(count($this->_rows) / $this->_group_rows);
405         $class = ($group % 2) ? 'oddrow' : 'evenrow';
406         $revision_handle = false;
407
408         if (count($this->_columns) > 1) {
409             $row = HTML::tr(array('class' => $class));
410             foreach ($this->_columns as $col)
411                 $row->pushContent($col->format($this, $page_handle, $revision_handle));
412         }
413         else {
414             $col = $this->_columns[0];
415             $row = HTML::li(array('class' => $class),
416                             $col->_getValue($page_handle, $revision_handle));
417         }
418
419         $this->_rows[] = $row;
420     }
421
422     function addPages ($page_iter) {
423         while ($page = $page_iter->next())
424             $this->addPage($page);
425     }
426
427     function addPageList (&$list) {
428         reset ($list);
429         while ($page = next($list))
430             $this->addPage($page);
431     }
432
433     function getContent() {
434         // Note that the <caption> element wants inline content.
435         $caption = $this->getCaption();
436
437         if ($this->isEmpty())
438             return $this->_emptyList($caption);
439         elseif (count($this->_columns) == 1)
440             return $this->_generateList($caption);
441         else
442             return $this->_generateTable($caption);
443     }
444
445     function printXML() {
446         PrintXML($this->getContent());
447     }
448
449     function asXML() {
450         return AsXML($this->getContent());
451     }
452
453     /** 
454      * handles sortby requests for the DB iterator and table header links
455      * prefix the column with + or - like "+pagename","-mtime", ...
456      * supported column: 'pagename','mtime','hits'
457      * supported action: 'flip_order', 'db'
458      */
459     function sortby ($column, $action) {
460         if (substr($column,0,1) == '+') {
461             $order = '+'; $column = substr($column,1);
462         } elseif (substr($column,0,1) == '-') {
463             $order = '-'; $column = substr($column,1);
464         }
465         if (in_array($column,array('pagename','mtime','hits'))) {
466             // default order: +pagename, -mtime, -hits
467             if (empty($order))
468                 if (in_array($column,array('mtime','hits')))
469                     $order = '-';
470                 else
471                     $order = '+';
472             //TODO: multiple comma-delimited sortby args: "+hits,+pagename"
473             if ($action == 'flip_order') {
474                 return ($order == '+' ? '-' : '+') . $column;
475             } elseif ($action == 'db') {
476                 // asc or desc: +pagename, -pagename
477                 return $column . ($order == '+' ? ' ASC' : ' DESC');
478             }
479         }
480         return '';
481     }
482
483     // echo implode(":",explodeList("Test*",array("xx","Test1","Test2")));
484     function explodePageList($input, $perm = false, $sortby = '') {
485         // expand wildcards from list of all pages
486         if (preg_match('/[\?\*]/',$input)) {
487             $dbi = $GLOBALS['request']->getDbh();
488             $allPagehandles = $dbi->getAllPages($perm,$sortby);
489             while ($pagehandle = $allPagehandles->next()) {
490                 $allPages[] = $pagehandle->getName();
491             }
492             return explodeList($input, $allPages);
493         } else {
494             //TODO: do the sorting
495             return explode(',',$input);
496         }
497     }
498
499
500     ////////////////////
501     // private
502     ////////////////////
503     //Performance Fixme: Initialize only the requested objects
504     function _initAvailableColumns() {
505         if (!empty($this->_types))
506             return;
507
508         $this->_types =
509             array(
510                   'content'
511                   => new _PageList_Column_content('content', _("Content")),
512
513                   'hi_content'
514                   => new _PageList_Column_content('hi_content', _("Content")),
515                   
516                   'remove'
517                   => new _PageList_Column_remove('remove', _("Remove")),
518
519                   'renamed_pagename'
520                   => new _PageList_Column_renamed_pagename('rename', _("Rename to")),
521
522                   'checkbox'
523                   => new _PageList_Column_checkbox('p', _("Select")),
524
525                   'pagename'
526                   => new _PageList_Column_pagename,
527
528                   'mtime'
529                   => new _PageList_Column_time('rev:mtime',
530                                                _("Last Modified")),
531                   'hits'
532                   => new _PageList_Column('hits', _("Hits"), 'right'),
533
534                   'size'
535                   => new _PageList_Column_size('size', _("Size"), 'right'),
536                                                /*array('align' => 'char', 'char' => ' ')*/
537
538                   'summary'
539                   => new _PageList_Column('rev:summary', _("Last Summary")),
540
541                   'version'
542                   => new _PageList_Column_version('rev:version', _("Version"),
543                                                   'right'),
544                   'author'
545                   => new _PageList_Column_author('rev:author',
546                                                  _("Last Author")),
547                   'locked'
548                   => new _PageList_Column_bool('locked', _("Locked"),
549                                                _("locked")),
550                   'minor'
551                   => new _PageList_Column_bool('rev:is_minor_edit',
552                                                _("Minor Edit"), _("minor")),
553                   'markup'
554                   => new _PageList_Column('rev:markup', _("Markup"))
555                   );
556     }
557
558     function _addColumn ($column) {
559
560         $this->_initAvailableColumns();
561
562         if (isset($this->_columns_seen[$column]))
563             return false;       // Already have this one.
564         $this->_columns_seen[$column] = true;
565
566         if (strstr($column, ':'))
567             list ($column, $heading) = explode(':', $column, 2);
568
569         if (!isset($this->_types[$column])) {
570             trigger_error(sprintf("%s: Bad column", $column), E_USER_NOTICE);
571             return false;
572         }
573
574         $col = $this->_types[$column];
575         if (!empty($heading))
576             $col->setHeading($heading);
577
578         $this->_columns[] = $col;
579
580         return true;
581     }
582
583     // make a table given the caption
584     function _generateTable($caption) {
585         $table = HTML::table(array('cellpadding' => 0,
586                                    'cellspacing' => 1,
587                                    'border'      => 0,
588                                    'class'       => 'pagelist'));
589         if ($caption)
590             $table->pushContent(HTML::caption(array('align'=>'top'), $caption));
591
592         //Warning: This is quite fragile. It depends solely on a private variable
593         //         in ->_addColumn()
594         if (in_array('checkbox',$this->_columns_seen)) {
595             $table->pushContent($this->_jsFlipAll());
596         }
597         $row = HTML::tr();
598         $table_summary = array();
599         foreach ($this->_columns as $col) {
600             $row->pushContent($col->heading());
601             if (is_string($col->_heading))
602                 $table_summary[] = $col->_heading;
603         }
604         // Table summary for non-visual browsers.
605         $table->setAttr('summary', sprintf(_("Columns: %s."), 
606                                            implode(", ", $table_summary)));
607
608         $table->pushContent(HTML::thead($row),
609                             HTML::tbody(false, $this->_rows));
610         return $table;
611     }
612
613     function _jsFlipAll() {
614       return JavaScript("
615 function flipAll(formObj) {
616   var isFirstSet = -1;
617   for (var i=0;i < formObj.length;i++) {
618       fldObj = formObj.elements[i];
619       if (fldObj.type == 'checkbox') { 
620          if (isFirstSet == -1)
621            isFirstSet = (fldObj.checked) ? true : false;
622          fldObj.checked = (isFirstSet) ? false : true;
623        }
624    }
625 }");
626     }
627
628     function _generateList($caption) {
629         $list = HTML::ul(array('class' => 'pagelist'), $this->_rows);
630         $out = HTML();
631         //Warning: This is quite fragile. It depends solely on a private variable
632         //         in ->_addColumn()
633         if (in_array('checkbox',$this->_columns_seen)) {
634             $out->pushContent($this->_jsFlipAll());
635         }
636         if ($caption)
637             $out->pushContent(HTML::p($caption));
638         $out->pushContent($list);
639         return $out;
640     }
641
642     function _emptyList($caption) {
643         $html = HTML();
644         if ($caption)
645             $html->pushContent(HTML::p($caption));
646         if ($this->_messageIfEmpty)
647             $html->pushContent(HTML::blockquote(HTML::p($this->_messageIfEmpty)));
648         return $html;
649     }
650
651 };
652
653 /* List pages with checkboxes to select from.
654  * The [Select] button toggles via _jsFlipAll
655  */
656
657 class PageList_Selectable
658 extends PageList {
659
660     function PageList_Selectable ($columns=false, $exclude=false) {
661         if ($columns) {
662             if (!is_array($columns))
663                 $columns = explode(',', $columns);
664             if (!in_array('checkbox',$columns))
665                 array_unshift($columns,'checkbox');
666         } else {
667             $columns = array('checkbox','pagename');
668         }
669         PageList::PageList($columns,$exclude);
670     }
671
672     function addPageList ($array) {
673         while (list($pagename,$selected) = each($array)) {
674             if ($selected) $this->addPageSelected($pagename);
675             $this->addPage($pagename);
676         }
677     }
678
679     function addPageSelected ($pagename) {
680         $this->_selected[$pagename] = 1;
681     }
682     //Todo:
683     //insert javascript when clicked on Selected Select/Deselect all
684 }
685
686 // (c-file-style: "gnu")
687 // Local Variables:
688 // mode: php
689 // tab-width: 8
690 // c-basic-offset: 4
691 // c-hanging-comment-ender-p: nil
692 // indent-tabs-mode: nil
693 // End:
694 ?>