46 private $loadBalancer;
49 private $linkBatchFactory;
52 private $contentHandlerFactory;
55 private $searchEngineFactory;
58 private $namespaceInfo;
61 private $titleFactory;
64 private $linksMigration;
66 protected $limits = [ 20, 50, 100, 250, 500 ];
86 parent::__construct(
'Whatlinkshere' );
87 $this->mIncludable =
true;
88 $this->loadBalancer = $loadBalancer;
89 $this->linkBatchFactory = $linkBatchFactory;
90 $this->contentHandlerFactory = $contentHandlerFactory;
91 $this->searchEngineFactory = $searchEngineFactory;
92 $this->namespaceInfo = $namespaceInfo;
93 $this->titleFactory = $titleFactory;
94 $this->linksMigration = $linksMigration;
104 $par = str_replace(
'_',
' ',
$par );
106 parent::setParameter(
$par );
116 $opts->
add(
'limit', $this->
getConfig()->
get( MainConfigNames::QueryPageDefaultLimit ) );
132 $this->
getSkin()->setRelevantTitle( $this->target );
135 $out->setPageTitle( $this->
msg(
'whatlinkshere-title', $this->target->getPrefixedText() ) );
136 $out->addBacklinkSubtitle( $this->target );
138 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir(
$opts );
140 $this->showIndirectLinks(
162 $from =
$opts->getValue(
'from' );
167 $offsetNamespace =
null;
168 $offsetPageID = $from - 1;
171 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
175 if ( !$offsetPageIDString ) {
176 $offsetPageIDString = $offsetNamespaceString;
177 $offsetNamespaceString =
'';
179 if ( is_numeric( $offsetNamespaceString ) ) {
180 $offsetNamespace = (int)$offsetNamespaceString;
182 $offsetNamespace =
null;
184 $offsetPageID = (int)$offsetPageIDString;
187 if ( $offsetNamespace ===
null ) {
188 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
189 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() :
NS_MAIN;
192 return [ $offsetNamespace, $offsetPageID, $dir ];
203 private function showIndirectLinks(
204 $level, $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir =
'next'
206 $out = $this->getOutput();
209 $hidelinks = $this->opts->getValue(
'hidelinks' );
210 $hideredirs = $this->opts->getValue(
'hideredirs' );
211 $hidetrans = $this->opts->getValue(
'hidetrans' );
212 $hideimages = $target->
getNamespace() !==
NS_FILE || $this->opts->getValue(
'hideimages' );
216 $fetchredirs = $hidelinks && !$hideredirs;
220 $conds[
'redirect'] = [
224 $conds[
'pagelinks'] = [
228 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
229 $conds[
'imagelinks'] = [
233 $namespace = $this->opts->getValue(
'namespace' );
234 if ( is_int( $namespace ) ) {
235 $invert = $this->opts->getValue(
'invert' );
239 $namespaces = array_diff(
240 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
242 $namespaces = $namespace;
247 $namespaces = $this->namespaceInfo->getValidNamespaces();
249 $conds[
'redirect'][
'page_namespace'] = $namespaces;
250 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
251 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
252 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
254 if ( $offsetPageID ) {
255 $op = $dir ===
'prev' ?
'<' :
'>';
256 $conds[
'redirect'][] =
$dbr->buildComparison( $op, [
257 'rd_from' => $offsetPageID,
259 $conds[
'templatelinks'][] =
$dbr->buildComparison( $op, [
260 'tl_from_namespace' => $offsetNamespace,
261 'tl_from' => $offsetPageID,
263 $conds[
'pagelinks'][] =
$dbr->buildComparison( $op, [
264 'pl_from_namespace' => $offsetNamespace,
265 'pl_from' => $offsetPageID,
267 $conds[
'imagelinks'][] =
$dbr->buildComparison( $op, [
268 'il_from_namespace' => $offsetNamespace,
269 'il_from' => $offsetPageID,
277 $conds[
'pagelinks'][
'rd_from'] =
null;
280 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
283 $queryFunc =
static function (
IDatabase $dbr, $table, $fromCol ) use (
284 $conds, $target, $limit, $sortDirection, $fname
287 $queryLimit = $limit + 1;
289 "rd_from = $fromCol",
292 'rd_interwiki = ' .
$dbr->addQuotes(
'' ) .
' OR rd_interwiki IS NULL'
295 $subQuery =
$dbr->newSelectQueryBuilder()
297 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
298 ->conds( $conds[$table] )
299 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
300 ->limit( 2 * $queryLimit )
301 ->leftJoin(
'redirect',
'redirect', $on );
303 return $dbr->newSelectQueryBuilder()
304 ->table( $subQuery,
'temp_backlink_range' )
305 ->join(
'page',
'page',
"$fromCol = page_id" )
306 ->fields( [
'page_id',
'page_namespace',
'page_title',
307 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
308 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
309 ->limit( $queryLimit )
314 if ( $fetchredirs ) {
315 $rdRes =
$dbr->newSelectQueryBuilder()
316 ->table(
'redirect' )
317 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
318 ->conds( $conds[
'redirect'] )
319 ->orderBy(
'rd_from', $sortDirection )
320 ->limit( $limit + 1 )
321 ->join(
'page',
'page',
'rd_from = page_id' )
322 ->caller( __METHOD__ )
327 $plRes = $queryFunc(
$dbr,
'pagelinks',
'pl_from' );
331 $tlRes = $queryFunc(
$dbr,
'templatelinks',
'tl_from' );
334 if ( !$hideimages ) {
335 $ilRes = $queryFunc(
$dbr,
'imagelinks',
'il_from' );
339 if ( ( !$fetchredirs || !$rdRes->numRows() )
341 && ( $hidelinks || !$plRes->numRows() )
343 && ( $hidetrans || !$tlRes->numRows() )
345 && ( $hideimages || !$ilRes->numRows() )
347 if ( $level == 0 && !$this->including() ) {
348 if ( $hidelinks || $hidetrans || $hideredirs ) {
349 $msgKey =
'nolinkshere-filter';
350 } elseif ( is_int( $namespace ) ) {
351 $msgKey =
'nolinkshere-ns';
353 $msgKey =
'nolinkshere';
355 $link = $this->getLinkRenderer()->makeLink(
359 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
362 $errMsg = $this->msg( $msgKey )
363 ->params( $this->target->getPrefixedText() )
366 $out->addHTML( $errMsg );
367 $out->setStatusCode( 404 );
377 if ( $fetchredirs ) {
379 foreach ( $rdRes as $row ) {
380 $row->is_template = 0;
382 $rows[$row->page_id] = $row;
387 foreach ( $plRes as $row ) {
388 $row->is_template = 0;
390 $rows[$row->page_id] = $row;
395 foreach ( $tlRes as $row ) {
396 $row->is_template = 1;
398 $rows[$row->page_id] = $row;
401 if ( !$hideimages ) {
403 foreach ( $ilRes as $row ) {
404 $row->is_template = 0;
406 $rows[$row->page_id] = $row;
411 usort( $rows,
static function ( $rowA, $rowB ) {
412 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
413 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
415 if ( $rowA->page_id !== $rowB->page_id ) {
416 return $rowA->page_id < $rowB->page_id ? -1 : 1;
421 $numRows = count( $rows );
425 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
427 } elseif ( $dir ===
'prev' ) {
428 if ( $numRows > $limit ) {
431 $nextNamespace = $rows[$limit]->page_namespace;
432 $nextPageId = $rows[$limit]->page_id;
434 $rows = array_slice( $rows, 1, $limit );
436 $prevNamespace = $rows[0]->page_namespace;
437 $prevPageId = $rows[0]->page_id;
440 $nextNamespace = $rows[$numRows - 1]->page_namespace;
441 $nextPageId = $rows[$numRows - 1]->page_id;
442 $prevNamespace =
false;
447 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
448 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
449 if ( $numRows > $limit ) {
451 $nextNamespace = $rows[$limit - 1]->page_namespace;
452 $nextPageId = $rows[$limit - 1]->page_id;
454 $rows = array_slice( $rows, 0, $limit );
456 $nextNamespace =
false;
463 $lb = $this->linkBatchFactory->newLinkBatch();
464 foreach ( $rows as $row ) {
465 $lb->add( $row->page_namespace, $row->page_title );
469 if ( $level == 0 && !$this->including() ) {
470 $link = $this->getLinkRenderer()->makeLink(
474 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
477 $msg = $this->msg(
'linkshere' )
478 ->params( $this->target->getPrefixedText() )
481 $out->addHTML( $msg );
485 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
486 $out->addHTML( $prevnext );
488 $out->addHTML( $this->listStart( $level ) );
489 foreach ( $rows as $row ) {
492 if ( $row->rd_from && $level < 2 ) {
493 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
494 $this->showIndirectLinks(
497 $this->getConfig()->
get( MainConfigNames::MaxRedirectLinksRetrieved )
501 $out->addHTML( $this->listItem( $row, $nt, $target ) );
505 $out->addHTML( $this->listEnd() );
507 if ( $level == 0 && !$this->including() ) {
510 $out->addHTML( $prevnext );
515 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
518 protected function listItem( $row, $nt, $target, $notClose =
false ) {
519 $dirmark = $this->getLanguage()->getDirMark();
521 if ( $row->rd_from ) {
522 $query = [
'redirect' =>
'no' ];
527 $link = $this->getLinkRenderer()->makeKnownLink(
530 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
537 if ( (
string)$row->rd_fragment !==
'' ) {
538 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
539 ->rawParams( $this->getLinkRenderer()->makeLink(
543 } elseif ( $row->rd_from ) {
544 $props[] = $this->msg(
'isredirect' )->escaped();
546 if ( $row->is_template ) {
547 $props[] = $this->msg(
'istemplate' )->escaped();
549 if ( $row->is_image ) {
550 $props[] = $this->msg(
'isimage' )->escaped();
553 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
555 if ( count( $props ) ) {
556 $propsText = $this->msg(
'parentheses' )
557 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
560 # Space for utilities links, with a what-links-here link provided
561 $wlhLink = $this->wlhLink(
563 $this->msg(
'whatlinkshere-links' )->text(),
564 $this->msg(
'editlink' )->text()
567 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped(),
568 'mw-whatlinkshere-tools'
573 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
583 $title = $this->getPageTitle();
586 $linkRenderer = $this->getLinkRenderer();
590 'links' => $linkRenderer->makeKnownLink(
603 $this->contentHandlerFactory->getContentHandler( $target->
getContentModel() )
604 ->supportsDirectEditing()
606 $links[
'edit'] = $linkRenderer->makeKnownLink(
610 [
'action' =>
'edit' ]
615 return $this->getLanguage()->pipeList( $links );
618 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
622 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
624 ->setLinkQuery( array_diff_key( $this->opts->getChangedValues(), [
'target' =>
null ] ) )
625 ->setLimits( $this->limits )
626 ->setLimitLinkQueryParam(
'limit' )
627 ->setCurrentLimit( $this->opts->getValue(
'limit' ) )
628 ->setPrevMsg(
'whatlinkshere-prev' )
629 ->setNextMsg(
'whatlinkshere-next' );
631 if ( $prevPageId != 0 ) {
632 $navBuilder->setPrevLinkQuery( [
'dir' =>
'prev',
'offset' =>
"$prevNamespace|$prevPageId" ] );
634 if ( $nextPageId != 0 ) {
635 $navBuilder->setNextLinkQuery( [
'dir' =>
'next',
'offset' =>
"$nextNamespace|$nextPageId" ] );
638 return $navBuilder->getHtml();
642 $this->addHelpLink(
'Help:What links here' );
643 $this->getOutput()->addModuleStyles(
'mediawiki.special' );
649 'id' =>
'mw-whatlinkshere-target',
650 'label-message' =>
'whatlinkshere-page',
651 'section' =>
'whatlinkshere-target',
655 'type' =>
'namespaceselect',
656 'name' =>
'namespace',
658 'label-message' =>
'namespace',
660 'in-user-lang' =>
true,
661 'section' =>
'whatlinkshere-ns',
667 'hide-if' => [
'===',
'namespace',
'' ],
668 'label-message' =>
'invert',
669 'help-message' =>
'tooltip-whatlinkshere-invert',
670 'help-inline' =>
false,
671 'section' =>
'whatlinkshere-ns',
675 $filters = [
'hidetrans',
'hidelinks',
'hideredirs' ];
680 foreach ( $filters as $filter ) {
682 $hide = $this->msg(
'hide' )->text();
683 $msg = $this->msg(
"whatlinkshere-{$filter}", $hide )->text();
688 'section' =>
'whatlinkshere-filter',
702 if ( $this->target && $this->target->getNamespace() ==
NS_FILE ) {
703 $hide = $this->msg(
'hide' )->text();
704 $msg = $this->msg(
'whatlinkshere-hideimages', $hide )->text();
708 'name' =>
'hideimages',
710 'section' =>
'whatlinkshere-filter',
716 ->setSubmitTextMsg(
'whatlinkshere-submit' );
748 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Special page which uses an HTMLForm to handle processing.
string null $par
The sub-page of the special page.
A class containing constants representing the names of configuration variables.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Factory class for SearchEngine.
getOutput()
Get the OutputPage being used for this instance.
getSkin()
Shortcut to get the skin being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
Implements Special:Whatlinkshere.
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
wlhLink(Title $target, $text, $editText)
listItem( $row, $nt, $target, $notClose=false)
__construct(ILoadBalancer $loadBalancer, LinkBatchFactory $linkBatchFactory, IContentHandlerFactory $contentHandlerFactory, SearchEngineFactory $searchEngineFactory, NamespaceInfo $namespaceInfo, TitleFactory $titleFactory, LinksMigration $linksMigration)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
onSuccess()
We want the result displayed after the form, so we use this instead of onSubmit()
requiresPost()
Whether this action should using POST method to submit, default to true.
getDisplayFormat()
Get display format for the form.
setParameter( $par)
Get a better-looking target title from the subpage syntax.
getFormFields()
Get an HTMLForm descriptor array.
onSubmit(array $data)
Process the form on submission.
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getSubpageField()
Override this function to set the field name used in the subpage syntax.
getShowAlways()
Whether the form should always be shown despite the success of submission.
Represents a title within MediaWiki.
getNamespace()
Get the namespace index, i.e.
getDBkey()
Get the main part with underscores.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
createFragmentTarget(string $fragment)
Creates a new Title for a different fragment of the same page.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
getContentModel( $flags=0)
Get the page's content model id, see the CONTENT_MODEL_XXX constants.
getPrefixedText()
Get the prefixed title with spaces.
static closeElement( $element)
Shortcut to close an XML element.
static openElement( $element, $attribs=null)
This opens an XML element.
static wrapClass( $text, $class, $tag='span', $attribs=[])
Shortcut to make a specific element with a class attribute.
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.