Remove useless comments from search output
[mediawiki.git] / includes / QueryPage.php
blob29bbd52b78ce36fdc5c5887138b306f985ea98fc
1 <?php
2 /**
3 * Base code for "query" special pages.
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.
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.
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
20 * @file
21 * @ingroup SpecialPage
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
30 abstract class QueryPage extends SpecialPage {
31 /**
32 * Whether or not we want plain listoutput rather than an ordered list
34 * @var bool
36 var $listoutput = false;
38 /**
39 * The offset and limit in use, as passed to the query() function
41 * @var int
43 var $offset = 0;
44 var $limit = 0;
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().
51 protected $numRows;
53 protected $cachedTimestamp = null;
55 /**
56 * Wheter to show prev/next links
58 protected $shownavigation = true;
60 /**
61 * Get a list of query page classes and their associated special pages,
62 * for periodic updates.
64 * DO NOT CHANGE THIS LIST without testing that
65 * maintenance/updateSpecialPages.php still works.
66 * @return array
68 public static function getPages() {
69 global $wgDisableCounters;
70 static $qp = null;
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' ),
109 wfRunHooks( 'wgQueryPages', array( &$qp ) );
111 if ( !$wgDisableCounters ) {
112 $qp[] = array( 'PopularPagesPage', 'Popularpages' );
116 return $qp;
120 * A mutator for $this->listoutput;
122 * @param bool $bool
124 function setListoutput( $bool ) {
125 $this->listoutput = $bool;
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
137 * Note that the query itself should return the following three columns:
138 * 'namespace', 'title', and 'value'. 'value' is used for sorting.
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()).
146 * Don't include an ORDER or LIMIT clause, they will be added.
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
154 function getQueryInfo() {
155 return null;
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
164 function getSQL() {
165 /* Implement getQueryInfo() instead */
166 throw new MWException( "Bug in a QueryPage: doesn't implement getQueryInfo() nor "
167 . "getQuery() properly" );
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
177 function getOrderFields() {
178 return array( 'value' );
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
191 function usesTimestamps() {
192 return false;
196 * Override to sort by increasing values
198 * @return bool
200 function sortDescending() {
201 return true;
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.
209 * @return bool
211 function isExpensive() {
212 global $wgDisableQueryPages;
213 return $wgDisableQueryPages;
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
223 public function isCacheable() {
224 return true;
228 * Whether or not the output of the page in question is retrieved from
229 * the database cache.
231 * @return bool
233 function isCached() {
234 global $wgMiserMode;
236 return $this->isExpensive() && $wgMiserMode;
240 * Sometime we don't want to build rss / atom feeds.
242 * @return bool
244 function isSyndicated() {
245 return true;
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
257 abstract function formatResult( $skin, $result );
260 * The content returned by this function will be output before any result
262 * @return string
264 function getPageHeader() {
265 return '';
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).
273 * @return array
275 function linkParameters() {
276 return array();
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
287 function tryLastResult() {
288 return false;
292 * Clear the cache and save new results
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
299 function recache( $limit, $ignoreErrors = true ) {
300 if ( !$this->isCacheable() ) {
301 return 0;
304 $fname = get_class( $this ) . '::recache';
305 $dbw = wfGetDB( DB_MASTER );
306 if ( !$dbw ) {
307 return false;
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
326 } else {
327 $value = 0;
330 $vals[] = array( 'qc_type' => $this->getName(),
331 'qc_namespace' => $row->namespace,
332 'qc_title' => $row->title,
333 'qc_value' => $value );
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__ );
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__ );
350 } catch ( DBError $e ) {
351 if ( !$ignoreErrors ) {
352 throw $e; // report query error
354 $num = false; // set result to false to indicate error
357 return $num;
361 * Get a DB connection to be used for slow recache queries
363 function getRecacheDB() {
364 return wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) );
368 * Run the query and return the result
369 * @param int|bool $limit Numerical limit or false for no limit
370 * @param int|bool $offset Numerical offset or false for no offset
371 * @return ResultWrapper
372 * @since 1.18
374 function reallyDoQuery( $limit, $offset = false ) {
375 $fname = get_class( $this ) . "::reallyDoQuery";
376 $dbr = $this->getRecacheDB();
377 $query = $this->getQueryInfo();
378 $order = $this->getOrderFields();
380 if ( $this->sortDescending() ) {
381 foreach ( $order as &$field ) {
382 $field .= ' DESC';
386 if ( is_array( $query ) ) {
387 $tables = isset( $query['tables'] ) ? (array)$query['tables'] : array();
388 $fields = isset( $query['fields'] ) ? (array)$query['fields'] : array();
389 $conds = isset( $query['conds'] ) ? (array)$query['conds'] : array();
390 $options = isset( $query['options'] ) ? (array)$query['options'] : array();
391 $join_conds = isset( $query['join_conds'] ) ? (array)$query['join_conds'] : array();
393 if ( count( $order ) ) {
394 $options['ORDER BY'] = $order;
397 if ( $limit !== false ) {
398 $options['LIMIT'] = intval( $limit );
401 if ( $offset !== false ) {
402 $options['OFFSET'] = intval( $offset );
405 $res = $dbr->select( $tables, $fields, $conds, $fname,
406 $options, $join_conds
408 } else {
409 // Old-fashioned raw SQL style, deprecated
410 $sql = $this->getSQL();
411 $sql .= ' ORDER BY ' . implode( ', ', $order );
412 $sql = $dbr->limitResult( $sql, $limit, $offset );
413 $res = $dbr->query( $sql, $fname );
416 return $dbr->resultObject( $res );
420 * Somewhat deprecated, you probably want to be using execute()
421 * @param int|bool $offset
422 * @param int|bool $limit
423 * @return ResultWrapper
425 function doQuery( $offset = false, $limit = false ) {
426 if ( $this->isCached() && $this->isCacheable() ) {
427 return $this->fetchFromCache( $limit, $offset );
428 } else {
429 return $this->reallyDoQuery( $limit, $offset );
434 * Fetch the query results from the query cache
435 * @param int|bool $limit Numerical limit or false for no limit
436 * @param int|bool $offset Numerical offset or false for no offset
437 * @return ResultWrapper
438 * @since 1.18
440 function fetchFromCache( $limit, $offset = false ) {
441 $dbr = wfGetDB( DB_SLAVE );
442 $options = array();
443 if ( $limit !== false ) {
444 $options['LIMIT'] = intval( $limit );
446 if ( $offset !== false ) {
447 $options['OFFSET'] = intval( $offset );
449 if ( $this->sortDescending() ) {
450 $options['ORDER BY'] = 'qc_value DESC';
451 } else {
452 $options['ORDER BY'] = 'qc_value ASC';
454 $res = $dbr->select( 'querycache', array( 'qc_type',
455 'namespace' => 'qc_namespace',
456 'title' => 'qc_title',
457 'value' => 'qc_value' ),
458 array( 'qc_type' => $this->getName() ),
459 __METHOD__, $options
461 return $dbr->resultObject( $res );
464 public function getCachedTimestamp() {
465 if ( is_null( $this->cachedTimestamp ) ) {
466 $dbr = wfGetDB( DB_SLAVE );
467 $fname = get_class( $this ) . '::getCachedTimestamp';
468 $this->cachedTimestamp = $dbr->selectField( 'querycache_info', 'qci_timestamp',
469 array( 'qci_type' => $this->getName() ), $fname );
471 return $this->cachedTimestamp;
475 * This is the actual workhorse. It does everything needed to make a
476 * real, honest-to-gosh query page.
477 * @param string $par
478 * @return int
480 function execute( $par ) {
481 global $wgQueryCacheLimit, $wgDisableQueryPageUpdate;
483 $user = $this->getUser();
484 if ( !$this->userCanExecute( $user ) ) {
485 $this->displayRestrictionError();
486 return;
489 $this->setHeaders();
490 $this->outputHeader();
492 $out = $this->getOutput();
494 if ( $this->isCached() && !$this->isCacheable() ) {
495 $out->addWikiMsg( 'querypage-disabled' );
496 return 0;
499 $out->setSyndicated( $this->isSyndicated() );
501 if ( $this->limit == 0 && $this->offset == 0 ) {
502 list( $this->limit, $this->offset ) = $this->getRequest()->getLimitOffset();
505 // TODO: Use doQuery()
506 if ( !$this->isCached() ) {
507 # select one extra row for navigation
508 $res = $this->reallyDoQuery( $this->limit + 1, $this->offset );
509 } else {
510 # Get the cached result, select one extra row for navigation
511 $res = $this->fetchFromCache( $this->limit + 1, $this->offset );
512 if ( !$this->listoutput ) {
514 # Fetch the timestamp of this update
515 $ts = $this->getCachedTimestamp();
516 $lang = $this->getLanguage();
517 $maxResults = $lang->formatNum( $wgQueryCacheLimit );
519 if ( $ts ) {
520 $updated = $lang->userTimeAndDate( $ts, $user );
521 $updateddate = $lang->userDate( $ts, $user );
522 $updatedtime = $lang->userTime( $ts, $user );
523 $out->addMeta( 'Data-Cache-Time', $ts );
524 $out->addJsConfigVars( 'dataCacheTime', $ts );
525 $out->addWikiMsg( 'perfcachedts', $updated, $updateddate, $updatedtime, $maxResults );
526 } else {
527 $out->addWikiMsg( 'perfcached', $maxResults );
530 # If updates on this page have been disabled, let the user know
531 # that the data set won't be refreshed for now
532 if ( is_array( $wgDisableQueryPageUpdate )
533 && in_array( $this->getName(), $wgDisableQueryPageUpdate )
535 $out->wrapWikiMsg(
536 "<div class=\"mw-querypage-no-updates\">\n$1\n</div>",
537 'querypage-no-updates'
543 $this->numRows = $res->numRows();
545 $dbr = wfGetDB( DB_SLAVE );
546 $this->preprocessResults( $dbr, $res );
548 $out->addHTML( Xml::openElement( 'div', array( 'class' => 'mw-spcontent' ) ) );
550 # Top header and navigation
551 if ( $this->shownavigation ) {
552 $out->addHTML( $this->getPageHeader() );
553 if ( $this->numRows > 0 ) {
554 $out->addHTML( $this->msg( 'showingresultsinrange' )->numParams(
555 min( $this->numRows, $this->limit ), # do not show the one extra row, if exist
556 $this->offset + 1, ( min( $this->numRows, $this->limit ) + $this->offset ) )->parseAsBlock() );
557 # Disable the "next" link when we reach the end
558 $paging = $this->getLanguage()->viewPrevNext( $this->getPageTitle( $par ), $this->offset,
559 $this->limit, $this->linkParameters(), ( $this->numRows <= $this->limit ) );
560 $out->addHTML( '<p>' . $paging . '</p>' );
561 } else {
562 # No results to show, so don't bother with "showing X of Y" etc.
563 # -- just let the user know and give up now
564 $out->addWikiMsg( 'specialpage-empty' );
565 $out->addHTML( Xml::closeElement( 'div' ) );
566 return;
570 # The actual results; specialist subclasses will want to handle this
571 # with more than a straight list, so we hand them the info, plus
572 # an OutputPage, and let them get on with it
573 $this->outputResults( $out,
574 $this->getSkin(),
575 $dbr, # Should use a ResultWrapper for this
576 $res,
577 min( $this->numRows, $this->limit ), # do not format the one extra row, if exist
578 $this->offset );
580 # Repeat the paging links at the bottom
581 if ( $this->shownavigation ) {
582 $out->addHTML( '<p>' . $paging . '</p>' );
585 $out->addHTML( Xml::closeElement( 'div' ) );
587 return min( $this->numRows, $this->limit ); # do not return the one extra row, if exist
591 * Format and output report results using the given information plus
592 * OutputPage
594 * @param OutputPage $out OutputPage to print to
595 * @param Skin $skin User skin to use
596 * @param DatabaseBase $dbr Database (read) connection to use
597 * @param int $res Result pointer
598 * @param int $num Number of available result rows
599 * @param int $offset Paging offset
601 protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
602 global $wgContLang;
604 if ( $num > 0 ) {
605 $html = array();
606 if ( !$this->listoutput ) {
607 $html[] = $this->openList( $offset );
610 # $res might contain the whole 1,000 rows, so we read up to
611 # $num [should update this to use a Pager]
612 for ( $i = 0; $i < $num && $row = $res->fetchObject(); $i++ ) {
613 $line = $this->formatResult( $skin, $row );
614 if ( $line ) {
615 $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
616 ? ' class="not-patrolled"'
617 : '';
618 $html[] = $this->listoutput
619 ? $line
620 : "<li{$attr}>{$line}</li>\n";
624 # Flush the final result
625 if ( $this->tryLastResult() ) {
626 $row = null;
627 $line = $this->formatResult( $skin, $row );
628 if ( $line ) {
629 $attr = ( isset( $row->usepatrol ) && $row->usepatrol && $row->patrolled == 0 )
630 ? ' class="not-patrolled"'
631 : '';
632 $html[] = $this->listoutput
633 ? $line
634 : "<li{$attr}>{$line}</li>\n";
638 if ( !$this->listoutput ) {
639 $html[] = $this->closeList();
642 $html = $this->listoutput
643 ? $wgContLang->listToText( $html )
644 : implode( '', $html );
646 $out->addHTML( $html );
651 * @param $offset
652 * @return string
654 function openList( $offset ) {
655 return "\n<ol start='" . ( $offset + 1 ) . "' class='special'>\n";
659 * @return string
661 function closeList() {
662 return "</ol>\n";
666 * Do any necessary preprocessing of the result object.
667 * @param DatabaseBase $db
668 * @param ResultWrapper $res
670 function preprocessResults( $db, $res ) {}
673 * Similar to above, but packaging in a syndicated feed instead of a web page
674 * @param string $class
675 * @param int $limit
676 * @return bool
678 function doFeed( $class = '', $limit = 50 ) {
679 global $wgFeed, $wgFeedClasses, $wgFeedLimit;
681 if ( !$wgFeed ) {
682 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
683 return false;
686 $limit = min( $limit, $wgFeedLimit );
688 if ( isset( $wgFeedClasses[$class] ) ) {
689 $feed = new $wgFeedClasses[$class](
690 $this->feedTitle(),
691 $this->feedDesc(),
692 $this->feedUrl() );
693 $feed->outHeader();
695 $res = $this->reallyDoQuery( $limit, 0 );
696 foreach ( $res as $obj ) {
697 $item = $this->feedResult( $obj );
698 if ( $item ) {
699 $feed->outItem( $item );
703 $feed->outFooter();
704 return true;
705 } else {
706 return false;
711 * Override for custom handling. If the titles/links are ok, just do
712 * feedItemDesc()
713 * @param object $row
714 * @return FeedItem|null
716 function feedResult( $row ) {
717 if ( !isset( $row->title ) ) {
718 return null;
720 $title = Title::makeTitle( intval( $row->namespace ), $row->title );
721 if ( $title ) {
722 $date = isset( $row->timestamp ) ? $row->timestamp : '';
723 $comments = '';
724 if ( $title ) {
725 $talkpage = $title->getTalkPage();
726 $comments = $talkpage->getFullURL();
729 return new FeedItem(
730 $title->getPrefixedText(),
731 $this->feedItemDesc( $row ),
732 $title->getFullURL(),
733 $date,
734 $this->feedItemAuthor( $row ),
735 $comments );
736 } else {
737 return null;
741 function feedItemDesc( $row ) {
742 return isset( $row->comment ) ? htmlspecialchars( $row->comment ) : '';
745 function feedItemAuthor( $row ) {
746 return isset( $row->user_text ) ? $row->user_text : '';
749 function feedTitle() {
750 global $wgLanguageCode, $wgSitename;
751 $desc = $this->getDescription();
752 return "$wgSitename - $desc [$wgLanguageCode]";
755 function feedDesc() {
756 return $this->msg( 'tagline' )->text();
759 function feedUrl() {
760 return $this->getPageTitle()->getFullURL();
765 * Class definition for a wanted query page like
766 * WantedPages, WantedTemplates, etc
768 abstract class WantedQueryPage extends QueryPage {
769 function isExpensive() {
770 return true;
773 function isSyndicated() {
774 return false;
778 * Cache page existence for performance
779 * @param DatabaseBase $db
780 * @param ResultWrapper $res
782 function preprocessResults( $db, $res ) {
783 if ( !$res->numRows() ) {
784 return;
787 $batch = new LinkBatch;
788 foreach ( $res as $row ) {
789 $batch->add( $row->namespace, $row->title );
791 $batch->execute();
793 // Back to start for display
794 $res->seek( 0 );
798 * Should formatResult() always check page existence, even if
799 * the results are fresh? This is a (hopefully temporary)
800 * kluge for Special:WantedFiles, which may contain false
801 * positives for files that exist e.g. in a shared repo (bug
802 * 6220).
803 * @return bool
805 function forceExistenceCheck() {
806 return false;
810 * Format an individual result
812 * @param Skin $skin Skin to use for UI elements
813 * @param object $result Result row
814 * @return string
816 public function formatResult( $skin, $result ) {
817 $title = Title::makeTitleSafe( $result->namespace, $result->title );
818 if ( $title instanceof Title ) {
819 if ( $this->isCached() || $this->forceExistenceCheck() ) {
820 $pageLink = $title->isKnown()
821 ? '<del>' . Linker::link( $title ) . '</del>'
822 : Linker::link(
823 $title,
824 null,
825 array(),
826 array(),
827 array( 'broken' )
829 } else {
830 $pageLink = Linker::link(
831 $title,
832 null,
833 array(),
834 array(),
835 array( 'broken' )
838 return $this->getLanguage()->specialList( $pageLink, $this->makeWlhLink( $title, $result ) );
839 } else {
840 return $this->msg( 'wantedpages-badtitle', $result->title )->escaped();
845 * Make a "what links here" link for a given title
847 * @param Title $title Title to make the link for
848 * @param object $result Result row
849 * @return string
851 private function makeWlhLink( $title, $result ) {
852 $wlh = SpecialPage::getTitleFor( 'Whatlinkshere', $title->getPrefixedText() );
853 $label = $this->msg( 'nlinks' )->numParams( $result->value )->escaped();
854 return Linker::link( $wlh, $label );