* User Pager for special:log
[lhc/web/wiklou.git] / includes / LogEventList.php
1 <?php
2 # Copyright (C) 2004 Brion Vibber <brion@pobox.com>, 2008 Aaron Schulz
3 # http://www.mediawiki.org/
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # http://www.gnu.org/copyleft/gpl.html
19
20 class LogEventList {
21 const NO_ACTION_LINK = 1;
22
23 function __construct( &$skin, $flags = 0 ) {
24 $this->skin =& $skin;
25 $this->flags = $flags;
26 $this->preCacheMessages();
27 }
28
29 /**
30 * As we use the same small set of messages in various methods and that
31 * they are called often, we call them once and save them in $this->message
32 */
33 private function preCacheMessages() {
34 // Precache various messages
35 if( !isset( $this->message ) ) {
36 foreach( explode(' ', 'viewpagelogs revhistory filehist rev-delundel' ) as $msg ) {
37 $this->message[$msg] = wfMsgExt( $msg, array( 'escape') );
38 }
39 }
40 }
41
42 /**
43 * Set page title and show header for this log type
44 * @param OutputPage $out where to send output
45 * @param strin $type
46 */
47 public function showHeader( $out, $type ) {
48 if( LogPage::isLogType( $type ) ) {
49 $out->setPageTitle( LogPage::logName( $type ) );
50 $out->addWikiText( LogPage::logHeader( $type ) );
51 }
52 }
53
54 /**
55 * Show options for the log list
56 * @param OutputPage $out where to send output
57 * @param string $type,
58 * @param string $user,
59 * @param string $page,
60 * @param string $pattern
61 */
62 public function showOptions( $out, $type, $user, $page, $pattern ) {
63 global $wgScript, $wgMiserMode;
64 $action = htmlspecialchars( $wgScript );
65 $title = SpecialPage::getTitleFor( 'Log' );
66 $special = htmlspecialchars( $title->getPrefixedDBkey() );
67 $out->addHTML( "<form action=\"$action\" method=\"get\"><fieldset>" .
68 Xml::element( 'legend', array(), wfMsg( 'log' ) ) .
69 Xml::hidden( 'title', $special ) . "\n" .
70 $this->getTypeMenu( $type ) . "\n" .
71 $this->getUserInput( $user ) . "\n" .
72 $this->getTitleInput( $page ) . "\n" .
73 ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) .
74 Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" .
75 "</fieldset></form>" );
76 }
77
78 /**
79 * @return string Formatted HTML
80 */
81 private function getTypeMenu( $queryType ) {
82 global $wgLogRestrictions, $wgUser;
83
84 $out = "<select name='type'>\n";
85
86 $validTypes = LogPage::validTypes();
87 $m = array(); // Temporary array
88
89 // First pass to load the log names
90 foreach( $validTypes as $type ) {
91 $text = LogPage::logName( $type );
92 $m[$text] = $type;
93 }
94
95 // Second pass to sort by name
96 ksort($m);
97
98 // Third pass generates sorted XHTML content
99 foreach( $m as $text => $type ) {
100 $selected = ($type == $queryType);
101 // Restricted types
102 if ( isset($wgLogRestrictions[$type]) ) {
103 if ( $wgUser->isAllowed( $wgLogRestrictions[$type] ) ) {
104 $out .= Xml::option( $text, $type, $selected ) . "\n";
105 }
106 } else {
107 $out .= Xml::option( $text, $type, $selected ) . "\n";
108 }
109 }
110
111 $out .= '</select>';
112 return $out;
113 }
114
115 /**
116 * @return string Formatted HTML
117 */
118 private function getUserInput( $user ) {
119 return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 12, $user );
120 }
121
122 /**
123 * @return string Formatted HTML
124 */
125 private function getTitleInput( $title ) {
126 return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title );
127 }
128
129 /**
130 * @return boolean Checkbox
131 */
132 private function getTitlePattern( $pattern ) {
133 return Xml::checkLabel( wfMsg( 'log-title-wildcard' ), 'pattern', 'pattern', $pattern );
134 }
135
136 /**
137 * @param Row $row a single row from the result set
138 * @return string Formatted HTML list item
139 * @private
140 */
141 public function logLine( $row ) {
142 global $wgLang, $wgUser, $wgContLang;
143 $skin = $wgUser->getSkin();
144 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
145 $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->log_timestamp), true );
146 // Enter the existence or non-existence of this page into the link cache,
147 // for faster makeLinkObj() in LogPage::actionText()
148 $linkCache =& LinkCache::singleton();
149 $linkCache->addLinkObj( $title );
150 // User links
151 if( LogPage::isDeleted($row,LogPage::DELETED_USER) ) {
152 $userLink = '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
153 } else {
154 $userLink = $this->skin->userLink( $row->log_user, $row->user_name ) .
155 $this->skin->userToolLinksRedContribs( $row->log_user, $row->user_name );
156 }
157 // Comment
158 if( $row->log_action == 'create2' ) {
159 $comment = ''; // Suppress from old account creations, useless and can contain incorrect links
160 } else if( LogPage::isDeleted($row,LogPage::DELETED_COMMENT) ) {
161 $comment = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>';
162 } else {
163 $comment = $wgContLang->getDirMark() . $this->skin->commentBlock( $row->log_comment );
164 }
165 // Extract extra parameters
166 $paramArray = LogPage::extractParams( $row->log_params );
167 $revert = $del = '';
168 // Some user can hide log items and have review links
169 if( $wgUser->isAllowed( 'deleterevision' ) ) {
170 $del = $this->showhideLinks( $row ) . ' ';
171 }
172 // Add review links and such...
173 if( !($this->flags & self::NO_ACTION_LINK) && !($row->log_deleted & LogPage::DELETED_ACTION) ) {
174 if( $row->log_type == 'move' && isset( $paramArray[0] ) && $wgUser->isAllowed( 'move' ) ) {
175 $destTitle = Title::newFromText( $paramArray[0] );
176 if( $destTitle ) {
177 $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Movepage' ),
178 wfMsg( 'revertmove' ),
179 'wpOldTitle=' . urlencode( $destTitle->getPrefixedDBkey() ) .
180 '&wpNewTitle=' . urlencode( $title->getPrefixedDBkey() ) .
181 '&wpReason=' . urlencode( wfMsgForContent( 'revertmove' ) ) .
182 '&wpMovetalk=0' ) . ')';
183 }
184 // Show undelete link
185 } else if( $row->log_action == 'delete' && $wgUser->isAllowed( 'delete' ) ) {
186 $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Undelete' ),
187 wfMsg( 'undeletelink' ) ,
188 'target='. urlencode( $title->getPrefixedDBkey() ) ) . ')';
189 // Show unblock link
190 } else if( $row->log_action == 'block' && $wgUser->isAllowed( 'block' ) ) {
191 $revert = '(' . $this->skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Ipblocklist' ),
192 wfMsg( 'unblocklink' ),
193 'action=unblock&ip=' . urlencode( $row->log_title ) ) . ')';
194 // Show change protection link
195 } else if( ( $row->log_action == 'protect' || $row->log_action == 'modify' ) && $wgUser->isAllowed( 'protect' ) ) {
196 $revert = '(' . $this->skin->makeKnownLinkObj( $title, wfMsg( 'protect_change' ), 'action=unprotect' ) . ')';
197 // Show unmerge link
198 } else if ( $row->log_action == 'merge' ) {
199 $merge = SpecialPage::getTitleFor( 'Mergehistory' );
200 $revert = '(' . $this->skin->makeKnownLinkObj( $merge, wfMsg('revertmerge'),
201 wfArrayToCGI(
202 array('target' => $paramArray[0], 'dest' => $title->getPrefixedText(), 'mergepoint' => $paramArray[1] )
203 )
204 ) . ')';
205 // If an edit was hidden from a page give a review link to the history
206 } else if( $row->log_action == 'revision' && $wgUser->isAllowed( 'deleterevision' ) ) {
207 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
208 // Different revision types use different URL params...
209 $subtype = isset($paramArray[2]) ? $paramArray[1] : '';
210 // Link to each hidden object ID, $paramArray[1] is the url param. List if several...
211 $Ids = explode( ',', $paramArray[2] );
212 if( count($Ids) == 1 ) {
213 $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
214 wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $Ids[0] ) ) );
215 } else {
216 $revert .= wfMsgHtml('revdel-restore').':';
217 foreach( $Ids as $n => $id ) {
218 $revert .= ' '.$this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
219 wfArrayToCGI( array('target' => $paramArray[0], $paramArray[1] => $id ) ) );
220 }
221 }
222 $revert = "($revert)";
223 // Hidden log items, give review link
224 } else if( $row->log_action == 'event' && $wgUser->isAllowed( 'deleterevision' ) ) {
225 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
226 $revert .= wfMsgHtml('revdel-restore');
227 $Ids = explode( ',', $paramArray[0] );
228 // Link to each hidden object ID, $paramArray[1] is the url param. List if several...
229 if( count($Ids) == 1 ) {
230 $revert = $this->skin->makeKnownLinkObj( $revdel, wfMsgHtml('revdel-restore'),
231 wfArrayToCGI( array('logid' => $Ids[0] ) ) );
232 } else {
233 foreach( $Ids as $n => $id ) {
234 $revert .= $this->skin->makeKnownLinkObj( $revdel, '#'.($n+1),
235 wfArrayToCGI( array('logid' => $id ) ) );
236 }
237 }
238 $revert = "($revert)";
239 } else {
240 wfRunHooks( 'LogLine', array( $row->log_type, $row->log_action, $title, $paramArray,
241 &$comment, &$revert, $row->log_timestamp ) );
242 // wfDebug( "Invoked LogLine hook for " $row->log_type . ", " . $row->log_action . "\n" );
243 // Do nothing. The implementation is handled by the hook modifiying the passed-by-ref parameters.
244 }
245 }
246 // Event description
247 if( $row->log_deleted & LogPage::DELETED_ACTION ) {
248 $action = '<span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>';
249 } else {
250 $action = LogPage::actionText( $row->log_type, $row->log_action, $title, $this->skin, $paramArray, true );
251 }
252
253 return "<li>$del$time $userLink $action $comment $revert</li>";
254 }
255
256 /**
257 * @param Row $row
258 * @private
259 */
260 private function showhideLinks( $row ) {
261 global $wgAllowLogDeletion;
262
263 if( !$wgAllowLogDeletion )
264 return "";
265
266 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
267 // If event was hidden from sysops
268 if( !LogPage::userCan( $row, Revision::DELETED_RESTRICTED ) ) {
269 $del = $this->message['rev-delundel'];
270 } else if( $row->log_type == 'suppress' ) {
271 return ''; // No one should be hiding from the oversight log
272 } else {
273 $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'], 'logid='.$row->log_id );
274 // Bolden oversighted content
275 if( LogPage::isDeleted( $row, Revision::DELETED_RESTRICTED ) )
276 $del = "<strong>$del</strong>";
277 }
278 return "<tt>(<small>$del</small>)</tt>";
279 }
280 }
281
282
283 /**
284 *
285 * @addtogroup SpecialPage
286 */
287 class LogReader {
288 const NO_ACTION_LINK = 1;
289
290 var $db, $joinClauses, $whereClauses;
291 var $type = '', $user = '', $title = null, $pattern = false;
292
293 /**
294 * @param WebRequest $request For internal use use a FauxRequest object to pass arbitrary parameters.
295 */
296 function LogReader( $request ) {
297 $this->db = wfGetDB( DB_SLAVE );
298 $this->setupQuery( $request );
299 }
300
301 /**
302 * Basic setup and applies the limiting factors from the WebRequest object.
303 * @param WebRequest $request
304 * @private
305 */
306 function setupQuery( $request ) {
307 $page = $this->db->tableName( 'page' );
308 $user = $this->db->tableName( 'user' );
309 $this->joinClauses = array(
310 "LEFT OUTER JOIN $page ON log_namespace=page_namespace AND log_title=page_title",
311 "INNER JOIN $user ON user_id=log_user" );
312 $this->whereClauses = array();
313
314 $this->limitType( $request->getVal( 'type' ) );
315 $this->limitUser( $request->getText( 'user' ) );
316 $this->limitTitle( $request->getText( 'page' ) , $request->getBool( 'pattern' ) );
317 $this->limitTime( $request->getVal( 'from' ), '>=' );
318 $this->limitTime( $request->getVal( 'until' ), '<=' );
319
320 list( $this->limit, $this->offset ) = $request->getLimitOffset();
321
322 // XXX This all needs to use Pager, ugly hack for now.
323 global $wgMiserMode;
324 if( $wgMiserMode )
325 $this->offset = min( $this->offset, 10000 );
326 }
327
328 /**
329 * Set the log reader to return only entries of the given type.
330 * Type restrictions enforced here
331 * @param string $type A log type ('upload', 'delete', etc)
332 * @private
333 */
334 function limitType( $type ) {
335 global $wgLogRestrictions, $wgUser;
336 // Reset the array, clears extra "where" clauses when $par is used
337 $this->whereClauses = $hiddenLogs = array();
338 // Nothing to show the user requested a log they can't see
339 if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) {
340 $this->whereClauses[] = "1 = 0";
341 return false;
342 }
343 // Don't show private logs to unpriviledged users
344 foreach( $wgLogRestrictions as $logtype => $right ) {
345 if( !$wgUser->isAllowed($right) || empty($type) ) {
346 $safetype = $this->db->strencode( $logtype );
347 $hiddenLogs[] = "'$safetype'";
348 }
349 }
350 if( !empty($hiddenLogs) ) {
351 $this->whereClauses[] = 'log_type NOT IN('.implode(',',$hiddenLogs).')';
352 }
353
354 if( empty($type) ) {
355 return false;
356 }
357 $this->type = $type;
358 $safetype = $this->db->strencode( $type );
359 $this->whereClauses[] = "log_type='$safetype'";
360 }
361
362 /**
363 * Set the log reader to return only entries by the given user.
364 * @param string $name (In)valid user name
365 * @private
366 */
367 function limitUser( $name ) {
368 if ( $name == '' )
369 return false;
370 $usertitle = Title::makeTitleSafe( NS_USER, $name );
371 if ( is_null( $usertitle ) )
372 return false;
373 $this->user = $usertitle->getText();
374
375 /* Fetch userid at first, if known, provides awesome query plan afterwards */
376 $userid = $this->db->selectField('user','user_id',array('user_name'=>$this->user));
377 if (!$userid)
378 /* It should be nicer to abort query at all,
379 but for now it won't pass anywhere behind the optimizer */
380 $this->whereClauses[] = "NULL";
381 else
382 $this->whereClauses[] = "log_user=$userid";
383 }
384
385 /**
386 * Set the log reader to return only entries affecting the given page.
387 * (For the block and rights logs, this is a user page.)
388 * @param string $page Title name as text
389 * @private
390 */
391 function limitTitle( $page , $pattern ) {
392 global $wgMiserMode;
393
394 $title = Title::newFromText( $page );
395
396 if( strlen( $page ) == 0 || !$title instanceof Title )
397 return false;
398
399 $this->title =& $title;
400 $this->pattern = $pattern;
401 $ns = $title->getNamespace();
402 if ( $pattern && !$wgMiserMode ) {
403 $safetitle = $this->db->escapeLike( $title->getDBkey() ); // use escapeLike to avoid expensive search patterns like 't%st%'
404 $this->whereClauses[] = "log_namespace=$ns AND log_title LIKE '$safetitle%'";
405 } else {
406 $safetitle = $this->db->strencode( $title->getDBkey() );
407 $this->whereClauses[] = "log_namespace=$ns AND log_title = '$safetitle'";
408 }
409 }
410
411 /**
412 * Set the log reader to return only entries in a given time range.
413 * @param string $time Timestamp of one endpoint
414 * @param string $direction either ">=" or "<=" operators
415 * @private
416 */
417 function limitTime( $time, $direction ) {
418 # Direction should be a comparison operator
419 if( empty( $time ) ) {
420 return false;
421 }
422 $safetime = $this->db->strencode( wfTimestamp( TS_MW, $time ) );
423 $this->whereClauses[] = "log_timestamp $direction '$safetime'";
424 }
425
426 /**
427 * Build an SQL query from all the set parameters.
428 * @return string the SQL query
429 * @private
430 */
431 function getQuery() {
432 global $wgAllowLogDeletion;
433
434 $logging = $this->db->tableName( "logging" );
435 $sql = "SELECT /*! STRAIGHT_JOIN */ log_type, log_action, log_timestamp,
436 log_user, user_name, log_namespace, log_title, page_id,
437 log_comment, log_params, log_deleted ";
438 if( $wgAllowLogDeletion )
439 $sql .= ", log_id ";
440
441 $sql .= "FROM $logging ";
442 if( !empty( $this->joinClauses ) ) {
443 $sql .= implode( ' ', $this->joinClauses );
444 }
445 if( !empty( $this->whereClauses ) ) {
446 $sql .= " WHERE " . implode( ' AND ', $this->whereClauses );
447 }
448 $sql .= " ORDER BY log_timestamp DESC ";
449 $sql = $this->db->limitResult($sql, $this->limit, $this->offset );
450 return $sql;
451 }
452
453 /**
454 * Execute the query and start returning results.
455 * @return ResultWrapper result object to return the relevant rows
456 */
457 function getRows() {
458 $res = $this->db->query( $this->getQuery(), __METHOD__ );
459 return $this->db->resultObject( $res );
460 }
461
462 /**
463 * @return string The query type that this LogReader has been limited to.
464 */
465 function queryType() {
466 return $this->type;
467 }
468
469 /**
470 * @return string The username type that this LogReader has been limited to, if any.
471 */
472 function queryUser() {
473 return $this->user;
474 }
475
476 /**
477 * @return boolean The checkbox, if titles should be searched by a pattern too
478 */
479 function queryPattern() {
480 return $this->pattern;
481 }
482
483 /**
484 * @return string The text of the title that this LogReader has been limited to.
485 */
486 function queryTitle() {
487 if( is_null( $this->title ) ) {
488 return '';
489 } else {
490 return $this->title->getPrefixedText();
491 }
492 }
493
494 /**
495 * Is there at least one row?
496 *
497 * @return bool
498 */
499 public function hasRows() {
500 # Little hack...
501 $limit = $this->limit;
502 $this->limit = 1;
503 $res = $this->db->query( $this->getQuery() );
504 $this->limit = $limit;
505 $ret = $this->db->numRows( $res ) > 0;
506 $this->db->freeResult( $res );
507 return $ret;
508 }
509
510 }
511
512 /**
513 *
514 * @addtogroup SpecialPage
515 */
516 class LogViewer {
517 /**
518 * @var LogReader $reader
519 */
520 var $reader;
521 var $numResults = 0;
522 var $flags = 0;
523
524 /**
525 * @param LogReader &$reader where to get our data from
526 * @param integer $flags Bitwise combination of flags:
527 * LogEventList::NO_ACTION_LINK Don't show restore/unblock/block links
528 */
529 function LogViewer( &$reader, $flags = 0 ) {
530 global $wgUser;
531 $this->skin = $wgUser->getSkin();
532 $this->reader =& $reader;
533 $this->flags = $flags;
534 $this->logList = new LogEventList( $wgUser->getSkin(), $flags );
535 }
536
537 /**
538 * Take over the whole output page in $wgOut with the log display.
539 */
540 function show() {
541 global $wgOut;
542 $this->logList->showHeader( $wgOut, $this->reader->queryType() );
543 $this->logList->showOptions( $wgOut, $this->reader->queryType(), $this->reader->queryUser(),
544 $this->reader->queryTitle(), $this->reader->queryPattern() );
545 $result = $this->getLogRows();
546 if ( $this->numResults > 0 ) {
547 $this->showPrevNext( $wgOut );
548 $this->doShowList( $wgOut, $result );
549 $this->showPrevNext( $wgOut );
550 } else {
551 $this->showError( $wgOut );
552 }
553 }
554
555 /**
556 * Load the data from the linked LogReader
557 * Preload the link cache
558 * Initialise numResults
559 *
560 * Must be called before calling showPrevNext
561 *
562 * @return object database result set
563 */
564 function getLogRows() {
565 $result = $this->reader->getRows();
566 $this->numResults = 0;
567
568 // Fetch results and form a batch link existence query
569 $batch = new LinkBatch;
570 while ( $s = $result->fetchObject() ) {
571 // User link
572 $batch->addObj( Title::makeTitleSafe( NS_USER, $s->user_name ) );
573 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $s->user_name ) );
574
575 // Move destination link
576 if ( $s->log_type == 'move' ) {
577 $paramArray = LogPage::extractParams( $s->log_params );
578 $title = Title::newFromText( $paramArray[0] );
579 $batch->addObj( $title );
580 }
581 ++$this->numResults;
582 }
583 $batch->execute();
584
585 return $result;
586 }
587
588
589 /**
590 * Output just the list of entries given by the linked LogReader,
591 * with extraneous UI elements. Use for displaying log fragments in
592 * another page (eg at Special:Undelete)
593 * @param OutputPage $out where to send output
594 */
595 function showList( &$out ) {
596 $result = $this->getLogRows();
597 if ( $this->numResults > 0 ) {
598 $this->doShowList( $out, $result );
599 } else {
600 $this->showError( $out );
601 }
602 }
603
604 function doShowList( &$out, $result ) {
605 // Rewind result pointer and go through it again, making the HTML
606 $html = "\n<ul>\n";
607 $result->seek( 0 );
608 while( $s = $result->fetchObject() ) {
609 $html .= $this->logList->logLine( $s );
610 }
611 $html .= "\n</ul>\n";
612 $out->addHTML( $html );
613 $result->free();
614 }
615
616 function showError( &$out ) {
617 $out->addWikiMsg( 'logempty' );
618 }
619
620 /**
621 * @param OutputPage &$out where to send output
622 * @private
623 */
624 function showPrevNext( &$out ) {
625 global $wgContLang,$wgRequest;
626 $pieces = array();
627 $pieces[] = 'type=' . urlencode( $this->reader->queryType() );
628 $pieces[] = 'user=' . urlencode( $this->reader->queryUser() );
629 $pieces[] = 'page=' . urlencode( $this->reader->queryTitle() );
630 $pieces[] = 'pattern=' . urlencode( $this->reader->queryPattern() );
631 $bits = implode( '&', $pieces );
632 list( $limit, $offset ) = $wgRequest->getLimitOffset();
633
634 # TODO: use timestamps instead of offsets to make it more natural
635 # to go huge distances in time
636 $html = wfViewPrevNext( $offset, $limit,
637 $wgContLang->specialpage( 'Log' ),
638 $bits,
639 $this->numResults < $limit);
640 $out->addHTML( '<p>' . $html . '</p>' );
641 }
642 }
643