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