1 <?php rcs_id('$Id: PageList.php,v 1.55 2004-02-17 12:14:07 rurban Exp $');
4 * List a number of pagenames, optionally as table with various columns.
5 * This library relieves some work for these plugins:
7 * AllPages, BackLinks, LikePages, Mostpopular, TitleSearch and more
9 * It also allows dynamic expansion of those plugins to include more
10 * columns in their output.
12 * Column 'info=' arguments:
14 * 'pagename' _("Page Name")
15 * 'mtime' _("Last Modified")
17 * 'summary' _("Last Summary")
18 * 'version' _("Version")),
19 * 'author' _("Last Author")),
20 * 'locked' _("Locked"), _("locked")
21 * 'minor' _("Minor Edit"), _("minor")
22 * 'markup' _("Markup")
24 * 'remove' _("Remove") //todo: move this admin action away, not really an info column
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
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).
38 * limit, offset, rows arguments for multiple pages/multiple rows.
40 * check PagePerm "list" access-type
42 * ->supportedArgs() which arguments are supported, so that the plugin
43 * doesn't explictly need to declare it
45 class _PageList_Column_base {
46 var $_tdattr = array();
48 function _PageList_Column_base ($default_heading, $align = false) {
49 $this->_heading = $default_heading;
52 // align="char" isn't supported by any browsers yet :(
53 //if (is_array($align))
54 // $this->_tdattr = $align;
56 $this->_tdattr['align'] = $align;
60 function format ($pagelist, $page_handle, &$revision_handle) {
61 return HTML::td($this->_tdattr,
63 $this->_getValue($page_handle, $revision_handle),
67 function setHeading ($heading) {
68 $this->_heading = $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(' '), HTML::u($this->_heading), HTML::raw(' '));
78 $s = HTML(HTML::raw(' '), HTML::u($this->_heading), HTML::raw(' '));
80 return HTML::td(array('align' => 'center'),$s);
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);
88 $this->_need_rev = substr($field, 0, 4) == 'rev:';
90 $this->_field = substr($field, 4);
92 $this->_field = $field;
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);
102 return $page_handle->get($this->_field);
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);
114 function _getSize($revision_handle) {
115 $bytes = strlen($revision_handle->_data['%content']);
116 return ByteFormatter($bytes);
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('—'); //mdash
128 function _getValue ($page_handle, &$revision_handle) {
129 $val = _PageList_Column::_getValue($page_handle, $revision_handle);
130 return $val ? $this->_textIfTrue : $this->_textIfFalse;
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)"
143 $this->_PageList_Column($field, $heading, 'center');
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'));
153 return HTML::input(array('type' => 'checkbox',
154 'name' => $this->_name . "[$pagename]",
155 'value' => $pagename));
158 function format ($pagelist, $page_handle, &$revision_handle) {
159 return HTML::td($this->_tdattr,
161 $this->_getValue($pagelist, $page_handle, $revision_handle),
162 HTML::raw(' '));
166 class _PageList_Column_time extends _PageList_Column {
167 function _PageList_Column_time ($field, $default_heading) {
168 $this->_PageList_Column($field, $default_heading, 'right');
170 $this->Theme = &$Theme;
173 function _getValue ($page_handle, &$revision_handle) {
174 $time = _PageList_Column::_getValue($page_handle, $revision_handle);
175 return $this->Theme->formatDateTime($time);
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();
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());
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)));
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);
215 if ($field == 'content') {
216 $this->_heading .= sprintf(_(" ... first %d bytes"),
218 } elseif ($field == 'hi_content') {
219 if (!empty($_POST['admin_replace'])) {
220 $search = $_POST['admin_replace']['from'];
221 $this->_heading .= sprintf(_(" ... around %s"),
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))))
244 $c = sprintf(_("%s not found"),
246 return HTML::div(array('style' => 'font-size:x-small','align'=>'center'),
249 } elseif (($len = strlen($c)) > $this->bytes) {
250 $c = substr($c, 0, $this->bytes);
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));
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();
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);
280 class _PageList_Column_pagename extends _PageList_Column_base {
281 var $_field = 'pagename';
283 function _PageList_Column_pagename () {
284 $this->_PageList_Column_base(_("Page Name"));
286 $this->dbi = &$request->getDbh();
289 function _getValue ($page_handle, &$revision_handle) {
290 if ($this->dbi->isWikiPage($pagename = $page_handle->getName()))
291 return WikiLink($page_handle);
293 return WikiLink($page_handle, 'unknown');
300 var $_group_rows = 3;
301 var $_columns = array();
302 var $_excluded_pages = array();
303 var $_rows = array();
305 var $_pagename_seen = false;
306 var $_types = array();
307 var $_options = array();
308 var $_selected = array();
310 function PageList ($columns = false, $exclude = false, $options = false) {
311 $this->_initAvailableColumns();
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')
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));
328 foreach ($columns as $col) {
329 $this->_addColumn($col);
332 $this->_addColumn('pagename');
335 if (!is_array($exclude))
336 $exclude = explode(',', $exclude);
337 $this->_excluded_pages = $exclude;
340 $this->_options = $options;
341 $this->_messageIfEmpty = _("<no matches>");
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
353 // for the sort buttons in <th>
354 'sortby' => '', // same as for WikiDB::getAllPages
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
369 function setCaption ($caption_string) {
370 $this->_caption = $caption_string;
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;
380 function setMessageIfEmpty ($msg) {
381 $this->_messageIfEmpty = $msg;
385 function getTotal () {
386 return count($this->_rows);
389 function isEmpty () {
390 return empty($this->_rows);
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.
404 $group = (int)(count($this->_rows) / $this->_group_rows);
405 $class = ($group % 2) ? 'oddrow' : 'evenrow';
406 $revision_handle = false;
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));
414 $col = $this->_columns[0];
415 $row = HTML::li(array('class' => $class),
416 $col->_getValue($page_handle, $revision_handle));
419 $this->_rows[] = $row;
422 function addPages ($page_iter) {
423 while ($page = $page_iter->next())
424 $this->addPage($page);
427 function addPageList (&$list) {
429 while ($page = next($list))
430 $this->addPage($page);
433 function getContent() {
434 // Note that the <caption> element wants inline content.
435 $caption = $this->getCaption();
437 if ($this->isEmpty())
438 return $this->_emptyList($caption);
439 elseif (count($this->_columns) == 1)
440 return $this->_generateList($caption);
442 return $this->_generateTable($caption);
445 function printXML() {
446 PrintXML($this->getContent());
450 return AsXML($this->getContent());
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'
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);
465 if (in_array($column,array('pagename','mtime','hits'))) {
466 // default order: +pagename, -mtime, -hits
468 if (in_array($column,array('mtime','hits')))
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');
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();
492 return explodeList($input, $allPages);
494 //TODO: do the sorting
495 return explode(',',$input);
503 //Performance Fixme: Initialize only the requested objects
504 function _initAvailableColumns() {
505 if (!empty($this->_types))
511 => new _PageList_Column_content('content', _("Content")),
514 => new _PageList_Column_content('hi_content', _("Content")),
517 => new _PageList_Column_remove('remove', _("Remove")),
520 => new _PageList_Column_renamed_pagename('rename', _("Rename to")),
523 => new _PageList_Column_checkbox('p', _("Select")),
526 => new _PageList_Column_pagename,
529 => new _PageList_Column_time('rev:mtime',
532 => new _PageList_Column('hits', _("Hits"), 'right'),
535 => new _PageList_Column_size('size', _("Size"), 'right'),
536 /*array('align' => 'char', 'char' => ' ')*/
539 => new _PageList_Column('rev:summary', _("Last Summary")),
542 => new _PageList_Column_version('rev:version', _("Version"),
545 => new _PageList_Column_author('rev:author',
548 => new _PageList_Column_bool('locked', _("Locked"),
551 => new _PageList_Column_bool('rev:is_minor_edit',
552 _("Minor Edit"), _("minor")),
554 => new _PageList_Column('rev:markup', _("Markup"))
558 function _addColumn ($column) {
560 $this->_initAvailableColumns();
562 if (isset($this->_columns_seen[$column]))
563 return false; // Already have this one.
564 $this->_columns_seen[$column] = true;
566 if (strstr($column, ':'))
567 list ($column, $heading) = explode(':', $column, 2);
569 if (!isset($this->_types[$column])) {
570 trigger_error(sprintf("%s: Bad column", $column), E_USER_NOTICE);
574 $col = $this->_types[$column];
575 if (!empty($heading))
576 $col->setHeading($heading);
578 $this->_columns[] = $col;
583 // make a table given the caption
584 function _generateTable($caption) {
585 $table = HTML::table(array('cellpadding' => 0,
588 'class' => 'pagelist'));
590 $table->pushContent(HTML::caption(array('align'=>'top'), $caption));
592 //Warning: This is quite fragile. It depends solely on a private variable
594 if (in_array('checkbox',$this->_columns_seen)) {
595 $table->pushContent($this->_jsFlipAll());
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;
604 // Table summary for non-visual browsers.
605 $table->setAttr('summary', sprintf(_("Columns: %s."),
606 implode(", ", $table_summary)));
608 $table->pushContent(HTML::thead($row),
609 HTML::tbody(false, $this->_rows));
613 function _jsFlipAll() {
615 function flipAll(formObj) {
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;
628 function _generateList($caption) {
629 $list = HTML::ul(array('class' => 'pagelist'), $this->_rows);
631 //Warning: This is quite fragile. It depends solely on a private variable
633 if (in_array('checkbox',$this->_columns_seen)) {
634 $out->pushContent($this->_jsFlipAll());
637 $out->pushContent(HTML::p($caption));
638 $out->pushContent($list);
642 function _emptyList($caption) {
645 $html->pushContent(HTML::p($caption));
646 if ($this->_messageIfEmpty)
647 $html->pushContent(HTML::blockquote(HTML::p($this->_messageIfEmpty)));
653 /* List pages with checkboxes to select from.
654 * The [Select] button toggles via _jsFlipAll
657 class PageList_Selectable
660 function PageList_Selectable ($columns=false, $exclude=false) {
662 if (!is_array($columns))
663 $columns = explode(',', $columns);
664 if (!in_array('checkbox',$columns))
665 array_unshift($columns,'checkbox');
667 $columns = array('checkbox','pagename');
669 PageList::PageList($columns,$exclude);
672 function addPageList ($array) {
673 while (list($pagename,$selected) = each($array)) {
674 if ($selected) $this->addPageSelected($pagename);
675 $this->addPage($pagename);
679 function addPageSelected ($pagename) {
680 $this->_selected[$pagename] = 1;
683 //insert javascript when clicked on Selected Select/Deselect all
686 // (c-file-style: "gnu")
691 // c-hanging-comment-ender-p: nil
692 // indent-tabs-mode: nil