HTMLForm: Add "cloner" type
[lhc/web/wiklou.git] / includes / QueryPage.php
1 <?php
2 /**
3 * Base code for "query" special pages.
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 * @file
21 * @ingroup SpecialPage
22 */
23
24 /**
25 * This is a class for doing query pages; since they're almost all the same,
26 * we factor out some of the functionality into a superclass, and let
27 * subclasses derive from it.
28 * @ingroup SpecialPage
29 */
30 abstract class QueryPage extends SpecialPage {
31 /**
32 * Whether or not we want plain listoutput rather than an ordered list
33 *
34 * @var bool
35 */
36 var $listoutput = false;
37
38 /**
39 * The offset and limit in use, as passed to the query() function
40 *
41 * @var int
42 */
43 var $offset = 0;
44 var $limit = 0;
45
46 /**
47 * The number of rows returned by the query. Reading this variable
48 * only makes sense in functions that are run after the query has been
49 * done, such as preprocessResults() and formatRow().
50 */
51 protected $numRows;
52
53 protected $cachedTimestamp = null;
54
55 /**
56 * Whether to show prev/next links
57 */
58 protected $shownavigation = true;
59
60 /**
61 * Get a list of query page classes and their associated special pages,
62 * for periodic updates.
63 *
64 * DO NOT CHANGE THIS LIST without testing that
65 * maintenance/updateSpecialPages.php still works.
66 * @return array
67 */
68 public static function getPages() {
69 global $wgDisableCounters;
70 static $qp = null;
71
72 if ( $qp === null ) {
73 // QueryPage subclass, Special page name
74 $qp = array(
75 array( 'AncientPagesPage', 'Ancientpages' ),
76 array( 'BrokenRedirectsPage', 'BrokenRedirects' ),
77 array( 'DeadendPagesPage', 'Deadendpages' ),
78 array( 'DoubleRedirectsPage', 'DoubleRedirects' ),
79 array( 'FileDuplicateSearchPage', 'FileDuplicateSearch' ),
80 array( 'ListDuplicatedFilesPage', 'ListDuplicatedFiles'),
81 array( 'LinkSearchPage', 'LinkSearch' ),
82 array( 'ListredirectsPage', 'Listredirects' ),
83 array( 'LonelyPagesPage', 'Lonelypages' ),
84 array( 'LongPagesPage', 'Longpages' ),
85 array( 'MIMEsearchPage', 'MIMEsearch' ),
86 array( 'MostcategoriesPage', 'Mostcategories' ),
87 array( 'MostimagesPage', 'Mostimages' ),
88 array( 'MostinterwikisPage', 'Mostinterwikis' ),
89 array( 'MostlinkedCategoriesPage', 'Mostlinkedcategories' ),
90 array( 'MostlinkedtemplatesPage', 'Mostlinkedtemplates' ),
91 array( 'MostlinkedPage', 'Mostlinked' ),
92 array( 'MostrevisionsPage', 'Mostrevisions' ),
93 array( 'FewestrevisionsPage', 'Fewestrevisions' ),
94 array( 'ShortPagesPage', 'Shortpages' ),
95 array( 'UncategorizedCategoriesPage', 'Uncategorizedcategories' ),
96 array( 'UncategorizedPagesPage', 'Uncategorizedpages' ),
97 array( 'UncategorizedImagesPage', 'Uncategorizedimages' ),
98 array( 'UncategorizedTemplatesPage', 'Uncategorizedtemplates' ),
99 array( 'UnusedCategoriesPage', 'Unusedcategories' ),
100 array( 'UnusedimagesPage', 'Unusedimages' ),
101 array( 'WantedCategoriesPage', 'Wantedcategories' ),
102 array( 'WantedFilesPage', 'Wantedfiles' ),
103 array( 'WantedPagesPage', 'Wantedpages' ),
104 array( 'WantedTemplatesPage', 'Wantedtemplates' ),
105 array( 'UnwatchedPagesPage', 'Unwatchedpages' ),
106 array( 'UnusedtemplatesPage', 'Unusedtemplates' ),
107 array( 'WithoutInterwikiPage', 'Withoutinterwiki' ),
108 );
109 wfRunHooks( 'wgQueryPages', array( &$qp ) );
110
111 if ( !$wgDisableCounters ) {
112 $qp[] = array( 'PopularPagesPage', 'Popularpages' );
113 }
114 }
115
116 return $qp;
117 }
118
119 /**
120 * A mutator for $this->listoutput;
121 *
122 * @param bool $bool
123 */
124 function setListoutput( $bool ) {
125 $this->listoutput = $bool;
126 }
127
128 /**
129 * Subclasses return an SQL query here, formatted as an array with the
130 * following keys:
131 * tables => Table(s) for passing to Database::select()
132 * fields => Field(s) for passing to Database::select(), may be *
133 * conds => WHERE conditions
134 * options => options
135 * join_conds => JOIN conditions
136 *
137 * Note that the query itself should return the following three columns:
138 * 'namespace', 'title', and 'value'. 'value' is used for sorting.
139 *
140 * These may be stored in the querycache table for expensive queries,
141 * and that cached data will be returned sometimes, so the presence of
142 * extra fields can't be relied upon. The cached 'value' column will be
143 * an integer; non-numeric values are useful only for sorting the
144 * initial query (except if they're timestamps, see usesTimestamps()).
145 *
146 * Don't include an ORDER or LIMIT clause, they will be added.
147 *
148 * If this function is not overridden or returns something other than
149 * an array, getSQL() will be used instead. This is for backwards
150 * compatibility only and is strongly deprecated.
151 * @return array
152 * @since 1.18
153 */
154 function getQueryInfo() {
155 return null;
156 }
157
158 /**
159 * For back-compat, subclasses may return a raw SQL query here, as a string.
160 * This is strongly deprecated; getQueryInfo() should be overridden instead.
161 * @throws MWException
162 * @return string
163 */
164 function getSQL() {
165 /* Implement getQueryInfo() instead */
166 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
167 . "getQuery() properly" );
168 }
169
170 /**
171 * Subclasses return an array of fields to order by here. Don't append
172 * DESC to the field names, that'll be done automatically if
173 * sortDescending() returns true.
174 * @return array
175 * @since 1.18
176 */
177 function getOrderFields() {
178 return array( 'value' );
179 }
180
181 /**
182 * Does this query return timestamps rather than integers in its
183 * 'value' field? If true, this class will convert 'value' to a
184 * UNIX timestamp for caching.
185 * NOTE: formatRow() may get timestamps in TS_MW (mysql), TS_DB (pgsql)
186 * or TS_UNIX (querycache) format, so be sure to always run them
187 * through wfTimestamp()
188 * @return bool
189 * @since 1.18
190 */
191 function usesTimestamps() {
192 return false;
193 }
194
195 /**
196 * Override to sort by increasing values
197 *
198 * @return bool
199 */
200 function sortDescending() {
201 return true;
202 }
203
204 /**
205 * Is this query expensive (for some definition of expensive)? Then we
206 * don't let it run in miser mode. $wgDisableQueryPages causes all query
207 * pages to be declared expensive. Some query pages are always expensive.
208 *
209 * @return bool
210 */
211 function isExpensive() {
212 global $wgDisableQueryPages;
213 return $wgDisableQueryPages;
214 }
215
216 /**
217 * Is the output of this query cacheable? Non-cacheable expensive pages
218 * will be disabled in miser mode and will not have their results written
219 * to the querycache table.
220 * @return bool
221 * @since 1.18
222 */
223 public function isCacheable() {
224 return true;
225 }
226
227 /**
228 * Whether or not the output of the page in question is retrieved from
229 * the database cache.
230 *
231 * @return bool
232 */
233 function isCached() {
234 global $wgMiserMode;
235
236 return $this->isExpensive() && $wgMiserMode;
237 }
238
239 /**
240 * Sometime we don't want to build rss / atom feeds.
241 *
242 * @return bool
243 */
244 function isSyndicated() {
245 return true;
246 }
247
248 /**
249 * Formats the results of the query for display. The skin is the current
250 * skin; you can use it for making links. The result is a single row of
251 * result data. You should be able to grab SQL results off of it.
252 * If the function returns false, the line output will be skipped.
253 * @param Skin $skin
254 * @param object $result Result row
255 * @return string|bool String or false to skip
256 */
257 abstract function formatResult( $skin, $result );
258
259 /**
260 * The content returned by this function will be output before any result
261 *
262 * @return string
263 */
264 function getPageHeader() {
265 return '';
266 }
267
268 /**
269 * If using extra form wheely-dealies, return a set of parameters here
270 * as an associative array. They will be encoded and added to the paging
271 * links (prev/next/lengths).
272 *
273 * @return array
274 */
275 function linkParameters() {
276 return array();
277 }
278
279 /**
280 * Some special pages (for example SpecialListusers) might not return the
281 * current object formatted, but return the previous one instead.
282 * Setting this to return true will ensure formatResult() is called
283 * one more time to make sure that the very last result is formatted
284 * as well.
285 * @return bool
286 */
287 function tryLastResult() {
288 return false;
289 }
290
291 /**
292 * Clear the cache and save new results
293 *
294 * @param int|bool $limit Limit for SQL statement
295 * @param bool $ignoreErrors Whether to ignore database errors
296 * @throws DBError|Exception
297 * @return bool|int
298 */
299 function recache( $limit, $ignoreErrors = true ) {
300 if ( !$this->isCacheable() ) {
301 return 0;
302 }
303
304 $fname = get_class( $this ) . '::recache';
305 $dbw = wfGetDB( DB_MASTER );
306 if ( !$dbw ) {
307 return false;
308 }
309
310 try {
311 # Do query
312 $res = $this->reallyDoQuery( $limit, false );
313 $num = false;
314 if ( $res ) {
315 $num = $res->numRows();
316 # Fetch results
317 $vals = array();
318 foreach ( $res as $row ) {
319 if ( isset( $row->value ) ) {
320 if ( $this->usesTimestamps() ) {
321 $value = wfTimestamp( TS_UNIX,
322 $row->value );
323 } else {
324 $value = intval( $row->value ); // @bug 14414
325 }
326 } else {
327 $value = 0;
328 }
329
330 $vals[] = array( 'qc_type' => $this->getName(),
331 'qc_namespace' => $row->namespace,
332 'qc_title' => $row->title,
333 'qc_value' => $value );
334 }
335
336 $dbw->begin( __METHOD__ );
337 # Clear out any old cached data
338 $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname );
339 # Save results into the querycache table on the master
340 if ( count( $vals ) ) {
341 $dbw->insert( 'querycache', $vals, __METHOD__ );
342 }
343 # Update the querycache_info record for the page
344 $dbw->delete( 'querycache_info', array( 'qci_type' => $this->getName() ), $fname );
345 $dbw->insert( 'querycache_info',
346 array( 'qci_type' => $this->getName(), 'qci_timestamp' => $dbw->timestamp() ),
347 $fname );
348 $dbw->commit( __METHOD__ );
349 }
350 } catch ( DBError $e ) {
351 if ( !$ignoreErrors ) {
352 throw $e; // report query error
353 }
354 $num = false; // set result to false to indicate error
355 }
356
357 return $num;
358 }
359
360 /**
361 * Get a DB connection to be used for slow recache queries
362 * @return DatabaseBase
363 */
364 function getRecacheDB() {
365 return wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
366 }
367
368 /**
369 * Run the query and return the result
370 * @param int|bool $limit Numerical limit or false for no limit
371 * @param int|bool $offset Numerical offset or false for no offset
372 * @return ResultWrapper
373 * @since 1.18
374 */
375 function reallyDoQuery( $limit, $offset = false ) {
376 $fname = get_class( $this ) . "::reallyDoQuery";
377 $dbr = $this->getRecacheDB();
378 $query = $this->getQueryInfo();
379 $order = $this->getOrderFields();
380
381 if ( $this->sortDescending() ) {
382 foreach ( $order as &$field ) {
383 $field .= ' DESC';
384 }
385 }
386
387 if ( is_array( $query ) ) {
388 $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
389 $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
390 $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
391 $options = isset( $query['options'] ) ? (array)$query['options'] : array();
392 $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
393
394 if ( count( $order ) ) {
395 $options['ORDER BY'] = $order;
396 }
397
398 if ( $limit !== false ) {
399 $options['LIMIT'] = intval( $limit );
400 }
401
402 if ( $offset !== false ) {
403 $options['OFFSET'] = intval( $offset );
404 }
405
406 $res = $dbr->select( $tables, $fields, $conds, $fname,
407 $options, $join_conds
408 );
409 } else {
410 // Old-fashioned raw SQL style, deprecated
411 $sql = $this->getSQL();
412 $sql .= ' ORDER BY ' . implode( ', ', $order );
413 $sql = $dbr->limitResult( $sql, $limit, $offset );
414 $res = $dbr->query( $sql, $fname );
415 }
416
417 return $dbr->resultObject( $res );
418 }
419
420 /**
421 * Somewhat deprecated, you probably want to be using execute()
422 * @param int|bool $offset
423 * @param int|bool $limit
424 * @return ResultWrapper
425 */
426 function doQuery( $offset = false, $limit = false ) {
427 if ( $this->isCached() && $this->isCacheable() ) {
428 return $this->fetchFromCache( $limit, $offset );
429 } else {
430 return $this->reallyDoQuery( $limit, $offset );
431 }
432 }
433
434 /**
435 * Fetch the query results from the query cache
436 * @param int|bool $limit Numerical limit or false for no limit
437 * @param int|bool $offset Numerical offset or false for no offset
438 * @return ResultWrapper
439 * @since 1.18
440 */
441 function fetchFromCache( $limit, $offset = false ) {
442 $dbr = wfGetDB( DB_SLAVE );
443 $options = array();
444 if ( $limit !== false ) {
445 $options['LIMIT'] = intval( $limit );
446 }
447 if ( $offset !== false ) {
448 $options['OFFSET'] = intval( $offset );
449 }
450 if ( $this->sortDescending() ) {
451 $options['ORDER BY'] = 'qc_value DESC';
452 } else {
453 $options['ORDER BY'] = 'qc_value ASC';
454 }
455 $res = $dbr->select( 'querycache', array( 'qc_type',
456 'namespace' => 'qc_namespace',
457 'title' => 'qc_title',
458 'value' => 'qc_value' ),
459 array( 'qc_type' => $this->getName() ),
460 __METHOD__, $options
461 );
462 return $dbr->resultObject( $res );
463 }
464
465 public function getCachedTimestamp() {
466 if ( is_null( $this->cachedTimestamp ) ) {
467 $dbr = wfGetDB( DB_SLAVE );
468 $fname = get_class( $this ) . '::getCachedTimestamp';
469 $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
470 array( 'qci_type' => $this->getName() ), $fname );
471 }
472 return $this->cachedTimestamp;
473 }
474
475 /**
476 * This is the actual workhorse. It does everything needed to make a
477 * real, honest-to-gosh query page.
478 * @param string $par
479 * @return int
480 */
481 function execute( $par ) {
482 global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
483
484 $user = $this->getUser();
485 if ( !$this->userCanExecute( $user ) ) {
486 $this->displayRestrictionError();
487 return;
488 }
489
490 $this->setHeaders();
491 $this->outputHeader();
492
493 $out = $this->getOutput();
494
495 if ( $this->isCached() && !$this->isCacheable() ) {
496 $out->addWikiMsg( 'querypage-disabled' );
497 return 0;
498 }
499
500 $out->setSyndicated( $this->isSyndicated() );
501
502 if ( $this->limit == 0 && $this->offset == 0 ) {
503 list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
504 }
505
506 // TODO: Use doQuery()
507 if ( !$this->isCached() ) {
508 # select one extra row for navigation
509 $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
510 } else {
511 # Get the cached result, select one extra row for navigation
512 $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
513 if ( !$this->listoutput ) {
514
515 # Fetch the timestamp of this update
516 $ts = $this->getCachedTimestamp();
517 $lang = $this->getLanguage();
518 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
519
520 if ( $ts ) {
521 $updated = $lang->userTimeAndDate( $ts, $user );
522 $updateddate = $lang->userDate( $ts, $user );
523 $updatedtime = $lang->userTime( $ts, $user );
524 $out->addMeta( 'Data-Cache-Time', $ts );
525 $out->addJsConfigVars( 'dataCacheTime', $ts );
526 $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
527 } else {
528 $out->addWikiMsg( 'perfcached', $maxResults );
529 }
530
531 # If updates on this page have been disabled, let the user know
532 # that the data set won't be refreshed for now
533 if ( is_array( $wgDisableQueryPageUpdate )
534 && in_array( $this->getName(), $wgDisableQueryPageUpdate )
535 ) {
536 $out->wrapWikiMsg(
537 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
538 'querypage-no-updates'
539 );
540 }
541 }
542 }
543
544 $this->numRows = $res->numRows();
545
546 $dbr = wfGetDB( DB_SLAVE );
547 $this->preprocessResults( $dbr, $res );
548
549 $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
550
551 # Top header and navigation
552 if ( $this->shownavigation ) {
553 $out->addHTML( $this->getPageHeader() );
554 if ( $this->numRows > 0 ) {
555 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
556 min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
557 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
558 # Disable the "next" link when we reach the end
559 $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
560 $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
561 $out->addHTML( '<p>' . $paging . '</p>' );
562 } else {
563 # No results to show, so don't bother with "showing X of Y" etc.
564 # -- just let the user know and give up now
565 $out->addWikiMsg( 'specialpage-empty' );
566 $out->addHTML( Xml::closeElement( 'div' ) );
567 return;
568 }
569 }
570
571 # The actual results; specialist subclasses will want to handle this
572 # with more than a straight list, so we hand them the info, plus
573 # an OutputPage, and let them get on with it
574 $this->outputResults( $out,
575 $this->getSkin(),
576 $dbr, # Should use a ResultWrapper for this
577 $res,
578 min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
579 $this->offset );
580
581 # Repeat the paging links at the bottom
582 if ( $this->shownavigation ) {
583 $out->addHTML( '<p>' . $paging . '</p>' );
584 }
585
586 $out->addHTML( Xml::closeElement( 'div' ) );
587
588 return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
589 }
590
591 /**
592 * Format and output report results using the given information plus
593 * OutputPage
594 *
595 * @param OutputPage $out OutputPage to print to
596 * @param Skin $skin User skin to use
597 * @param DatabaseBase $dbr Database (read) connection to use
598 * @param int $res Result pointer
599 * @param int $num Number of available result rows
600 * @param int $offset Paging offset
601 */
602 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
603 global $wgContLang;
604
605 if ( $num > 0 ) {
606 $html = array();
607 if ( !$this->listoutput ) {
608 $html[] = $this->openList( $offset );
609 }
610
611 # $res might contain the whole 1,000 rows, so we read up to
612 # $num [should update this to use a Pager]
613 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
614 $line = $this->formatResult( $skin, $row );
615 if ( $line ) {
616 $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
617 ? ' class="not-patrolled"'
618 : '';
619 $html[] = $this->listoutput
620 ? $line
621 : "<li{$attr}>{$line}</li>\n";
622 }
623 }
624
625 # Flush the final result
626 if ( $this->tryLastResult() ) {
627 $row = null;
628 $line = $this->formatResult( $skin, $row );
629 if ( $line ) {
630 $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
631 ? ' class="not-patrolled"'
632 : '';
633 $html[] = $this->listoutput
634 ? $line
635 : "<li{$attr}>{$line}</li>\n";
636 }
637 }
638
639 if ( !$this->listoutput ) {
640 $html[] = $this->closeList();
641 }
642
643 $html = $this->listoutput
644 ? $wgContLang->listToText( $html )
645 : implode( '', $html );
646
647 $out->addHTML( $html );
648 }
649 }
650
651 /**
652 * @param int $offset
653 * @return string
654 */
655 function openList( $offset ) {
656 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
657 }
658
659 /**
660 * @return string
661 */
662 function closeList() {
663 return "</ol>\n";
664 }
665
666 /**
667 * Do any necessary preprocessing of the result object.
668 * @param DatabaseBase $db
669 * @param ResultWrapper $res
670 */
671 function preprocessResults( $db, $res ) {}
672
673 /**
674 * Similar to above, but packaging in a syndicated feed instead of a web page
675 * @param string $class
676 * @param int $limit
677 * @return bool
678 */
679 function doFeed( $class = '', $limit = 50 ) {
680 global $wgFeed, $wgFeedClasses, $wgFeedLimit;
681
682 if ( !$wgFeed ) {
683 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
684 return false;
685 }
686
687 $limit = min( $limit, $wgFeedLimit );
688
689 if ( isset( $wgFeedClasses[$class] ) ) {
690 $feed = new $wgFeedClasses[$class](
691 $this->feedTitle(),
692 $this->feedDesc(),
693 $this->feedUrl() );
694 $feed->outHeader();
695
696 $res = $this->reallyDoQuery( $limit, 0 );
697 foreach ( $res as $obj ) {
698 $item = $this->feedResult( $obj );
699 if ( $item ) {
700 $feed->outItem( $item );
701 }
702 }
703
704 $feed->outFooter();
705 return true;
706 } else {
707 return false;
708 }
709 }
710
711 /**
712 * Override for custom handling. If the titles/links are ok, just do
713 * feedItemDesc()
714 * @param object $row
715 * @return FeedItem|null
716 */
717 function feedResult( $row ) {
718 if ( !isset( $row->title ) ) {
719 return null;
720 }
721 $title = Title::makeTitle( intval( $row->namespace ), $row->title );
722 if ( $title ) {
723 $date = isset( $row->timestamp ) ? $row->timestamp : '';
724 $comments = '';
725 if ( $title ) {
726 $talkpage = $title->getTalkPage();
727 $comments = $talkpage->getFullURL();
728 }
729
730 return new FeedItem(
731 $title->getPrefixedText(),
732 $this->feedItemDesc( $row ),
733 $title->getFullURL(),
734 $date,
735 $this->feedItemAuthor( $row ),
736 $comments );
737 } else {
738 return null;
739 }
740 }
741
742 function feedItemDesc( $row ) {
743 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
744 }
745
746 function feedItemAuthor( $row ) {
747 return isset( $row->user_text ) ? $row->user_text : '';
748 }
749
750 function feedTitle() {
751 global $wgLanguageCode, $wgSitename;
752 $desc = $this->getDescription();
753 return "$wgSitename - $desc [$wgLanguageCode]";
754 }
755
756 function feedDesc() {
757 return $this->msg( 'tagline' )->text();
758 }
759
760 function feedUrl() {
761 return $this->getPageTitle()->getFullURL();
762 }
763 }
764
765 /**
766 * Class definition for a wanted query page like
767 * WantedPages, WantedTemplates, etc
768 */
769 abstract class WantedQueryPage extends QueryPage {
770 function isExpensive() {
771 return true;
772 }
773
774 function isSyndicated() {
775 return false;
776 }
777
778 /**
779 * Cache page existence for performance
780 * @param DatabaseBase $db
781 * @param ResultWrapper $res
782 */
783 function preprocessResults( $db, $res ) {
784 if ( !$res->numRows() ) {
785 return;
786 }
787
788 $batch = new LinkBatch;
789 foreach ( $res as $row ) {
790 $batch->add( $row->namespace, $row->title );
791 }
792 $batch->execute();
793
794 // Back to start for display
795 $res->seek( 0 );
796 }
797
798 /**
799 * Should formatResult() always check page existence, even if
800 * the results are fresh? This is a (hopefully temporary)
801 * kluge for Special:WantedFiles, which may contain false
802 * positives for files that exist e.g. in a shared repo (bug
803 * 6220).
804 * @return bool
805 */
806 function forceExistenceCheck() {
807 return false;
808 }
809
810 /**
811 * Format an individual result
812 *
813 * @param Skin $skin Skin to use for UI elements
814 * @param object $result Result row
815 * @return string
816 */
817 public function formatResult( $skin, $result ) {
818 $title = Title::makeTitleSafe( $result->namespace, $result->title );
819 if ( $title instanceof Title ) {
820 if ( $this->isCached() || $this->forceExistenceCheck() ) {
821 $pageLink = $title->isKnown()
822 ? '<del>' . Linker::link( $title ) . '</del>'
823 : Linker::link(
824 $title,
825 null,
826 array(),
827 array(),
828 array( 'broken' )
829 );
830 } else {
831 $pageLink = Linker::link(
832 $title,
833 null,
834 array(),
835 array(),
836 array( 'broken' )
837 );
838 }
839 return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
840 } else {
841 return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
842 }
843 }
844
845 /**
846 * Make a "what links here" link for a given title
847 *
848 * @param Title $title Title to make the link for
849 * @param object $result Result row
850 * @return string
851 */
852 private function makeWlhLink( $title, $result ) {
853 $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
854 $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
855 return Linker::link( $wlh, $label );
856 }
857 }