51 private $linkBatchFactory;
54 private $contentHandlerFactory;
57 private $searchEngineFactory;
60 private $namespaceInfo;
63 private $titleFactory;
66 private $linksMigration;
68 protected $limits = [ 20, 50, 100, 250, 500 ];
88 parent::__construct(
'Whatlinkshere' );
89 $this->mIncludable =
true;
90 $this->dbProvider = $dbProvider;
91 $this->linkBatchFactory = $linkBatchFactory;
92 $this->contentHandlerFactory = $contentHandlerFactory;
93 $this->searchEngineFactory = $searchEngineFactory;
94 $this->namespaceInfo = $namespaceInfo;
95 $this->titleFactory = $titleFactory;
96 $this->linksMigration = $linksMigration;
106 $par = str_replace(
'_',
' ',
$par );
108 parent::setParameter(
$par );
117 $opts->
add(
'namespace',
null, FormOptions::INTNULL );
118 $opts->
add(
'limit',
null, FormOptions::INTNULL );
130 if ( $opts->
getValue(
'limit' ) ===
null ) {
138 $this->
getSkin()->setRelevantTitle( $this->target );
141 $out->setPageTitle( $this->
msg(
'whatlinkshere-title', $this->target->getPrefixedText() ) );
142 $out->addBacklinkSubtitle( $this->target );
144 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir(
$opts );
146 $this->showIndirectLinks(
168 $from =
$opts->getValue(
'from' );
173 $offsetNamespace =
null;
174 $offsetPageID = $from - 1;
177 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
181 if ( !$offsetPageIDString ) {
182 $offsetPageIDString = $offsetNamespaceString;
183 $offsetNamespaceString =
'';
185 if ( is_numeric( $offsetNamespaceString ) ) {
186 $offsetNamespace = (int)$offsetNamespaceString;
188 $offsetNamespace =
null;
190 $offsetPageID = (int)$offsetPageIDString;
193 if ( $offsetNamespace ===
null ) {
194 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
195 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() :
NS_MAIN;
198 return [ $offsetNamespace, $offsetPageID, $dir ];
209 private function showIndirectLinks(
210 $level, $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir =
'next'
212 $out = $this->getOutput();
213 $dbr = $this->dbProvider->getReplicaDatabase();
215 $hidelinks = $this->opts->getValue(
'hidelinks' );
216 $hideredirs = $this->opts->getValue(
'hideredirs' );
217 $hidetrans = $this->opts->getValue(
'hidetrans' );
218 $hideimages = $target->
getNamespace() !==
NS_FILE || $this->opts->getValue(
'hideimages' );
222 $fetchredirs = $hidelinks && !$hideredirs;
226 $conds[
'redirect'] = [
230 $conds[
'pagelinks'] = [
234 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
235 $conds[
'imagelinks'] = [
239 $namespace = $this->opts->getValue(
'namespace' );
240 if ( is_int( $namespace ) ) {
241 $invert = $this->opts->getValue(
'invert' );
245 $namespaces = array_diff(
246 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
248 $namespaces = $namespace;
253 $namespaces = $this->namespaceInfo->getValidNamespaces();
255 $conds[
'redirect'][
'page_namespace'] = $namespaces;
256 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
257 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
258 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
260 if ( $offsetPageID ) {
261 $op = $dir ===
'prev' ?
'<' :
'>';
262 $conds[
'redirect'][] =
$dbr->buildComparison( $op, [
263 'rd_from' => $offsetPageID,
265 $conds[
'templatelinks'][] =
$dbr->buildComparison( $op, [
266 'tl_from_namespace' => $offsetNamespace,
267 'tl_from' => $offsetPageID,
269 $conds[
'pagelinks'][] =
$dbr->buildComparison( $op, [
270 'pl_from_namespace' => $offsetNamespace,
271 'pl_from' => $offsetPageID,
273 $conds[
'imagelinks'][] =
$dbr->buildComparison( $op, [
274 'il_from_namespace' => $offsetNamespace,
275 'il_from' => $offsetPageID,
283 $conds[
'pagelinks'][
'rd_from'] =
null;
286 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
290 $conds, $target, $limit, $sortDirection, $fname
293 $queryLimit = $limit + 1;
295 "rd_from = $fromCol",
298 'rd_interwiki = ' .
$dbr->addQuotes(
'' ) .
' OR rd_interwiki IS NULL'
301 $subQuery =
$dbr->newSelectQueryBuilder()
303 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
304 ->conds( $conds[$table] )
305 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
306 ->limit( 2 * $queryLimit )
307 ->leftJoin(
'redirect',
'redirect', $on );
309 return $dbr->newSelectQueryBuilder()
310 ->table( $subQuery,
'temp_backlink_range' )
311 ->join(
'page',
'page',
"$fromCol = page_id" )
312 ->fields( [
'page_id',
'page_namespace',
'page_title',
313 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
314 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
315 ->limit( $queryLimit )
320 if ( $fetchredirs ) {
321 $rdRes =
$dbr->newSelectQueryBuilder()
322 ->table(
'redirect' )
323 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
324 ->conds( $conds[
'redirect'] )
325 ->orderBy(
'rd_from', $sortDirection )
326 ->limit( $limit + 1 )
327 ->join(
'page',
'page',
'rd_from = page_id' )
328 ->caller( __METHOD__ )
333 $plRes = $queryFunc(
$dbr,
'pagelinks',
'pl_from' );
337 $tlRes = $queryFunc(
$dbr,
'templatelinks',
'tl_from' );
340 if ( !$hideimages ) {
341 $ilRes = $queryFunc(
$dbr,
'imagelinks',
'il_from' );
345 if ( ( !$fetchredirs || !$rdRes->numRows() )
347 && ( $hidelinks || !$plRes->numRows() )
349 && ( $hidetrans || !$tlRes->numRows() )
351 && ( $hideimages || !$ilRes->numRows() )
353 if ( $level == 0 && !$this->including() ) {
354 if ( $hidelinks || $hidetrans || $hideredirs ) {
355 $msgKey =
'nolinkshere-filter';
356 } elseif ( is_int( $namespace ) ) {
357 $msgKey =
'nolinkshere-ns';
359 $msgKey =
'nolinkshere';
361 $link = $this->getLinkRenderer()->makeLink(
365 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
368 $errMsg = $this->msg( $msgKey )
369 ->params( $this->target->getPrefixedText() )
372 $out->addHTML( $errMsg );
373 $out->setStatusCode( 404 );
383 if ( $fetchredirs ) {
385 foreach ( $rdRes as $row ) {
386 $row->is_template = 0;
388 $rows[$row->page_id] = $row;
393 foreach ( $plRes as $row ) {
394 $row->is_template = 0;
396 $rows[$row->page_id] = $row;
401 foreach ( $tlRes as $row ) {
402 $row->is_template = 1;
404 $rows[$row->page_id] = $row;
407 if ( !$hideimages ) {
409 foreach ( $ilRes as $row ) {
410 $row->is_template = 0;
412 $rows[$row->page_id] = $row;
417 usort( $rows,
static function ( $rowA, $rowB ) {
418 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
419 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
421 if ( $rowA->page_id !== $rowB->page_id ) {
422 return $rowA->page_id < $rowB->page_id ? -1 : 1;
427 $numRows = count( $rows );
431 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
433 } elseif ( $dir ===
'prev' ) {
434 if ( $numRows > $limit ) {
437 $nextNamespace = $rows[$limit]->page_namespace;
438 $nextPageId = $rows[$limit]->page_id;
440 $rows = array_slice( $rows, 1, $limit );
442 $prevNamespace = $rows[0]->page_namespace;
443 $prevPageId = $rows[0]->page_id;
446 $nextNamespace = $rows[$numRows - 1]->page_namespace;
447 $nextPageId = $rows[$numRows - 1]->page_id;
448 $prevNamespace =
false;
453 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
454 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
455 if ( $numRows > $limit ) {
457 $nextNamespace = $rows[$limit - 1]->page_namespace ??
false;
458 $nextPageId = $rows[$limit - 1]->page_id ??
false;
460 $rows = array_slice( $rows, 0, $limit );
462 $nextNamespace =
false;
469 $lb = $this->linkBatchFactory->newLinkBatch();
470 foreach ( $rows as $row ) {
471 $lb->add( $row->page_namespace, $row->page_title );
475 if ( $level == 0 && !$this->including() ) {
476 $link = $this->getLinkRenderer()->makeLink(
480 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
483 $msg = $this->msg(
'linkshere' )
484 ->params( $this->target->getPrefixedText() )
487 $out->addHTML( $msg );
491 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
492 $out->addHTML( $prevnext );
494 $out->addHTML( $this->listStart( $level ) );
495 foreach ( $rows as $row ) {
496 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
498 if ( $row->rd_from && $level < 2 ) {
499 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
500 $this->showIndirectLinks(
503 $this->getConfig()->
get( MainConfigNames::MaxRedirectLinksRetrieved )
507 $out->addHTML( $this->listItem( $row, $nt, $target ) );
511 $out->addHTML( $this->listEnd() );
513 if ( $level == 0 && !$this->including() ) {
516 $out->addHTML( $prevnext );
521 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
524 protected function listItem( $row, $nt, $target, $notClose =
false ) {
525 $dirmark = $this->getLanguage()->getDirMark();
527 if ( $row->rd_from ) {
528 $query = [
'redirect' =>
'no' ];
533 $link = $this->getLinkRenderer()->makeKnownLink(
536 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
543 if ( (
string)$row->rd_fragment !==
'' ) {
544 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
545 ->rawParams( $this->getLinkRenderer()->makeLink(
549 } elseif ( $row->rd_from ) {
550 $props[] = $this->msg(
'isredirect' )->escaped();
552 if ( $row->is_template ) {
553 $props[] = $this->msg(
'istemplate' )->escaped();
555 if ( $row->is_image ) {
556 $props[] = $this->msg(
'isimage' )->escaped();
559 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
561 if ( count( $props ) ) {
562 $propsText = $this->msg(
'parentheses' )
563 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
566 # Space for utilities links, with a what-links-here link provided
567 $wlhLink = $this->wlhLink(
569 $this->msg(
'whatlinkshere-links' )->text(),
570 $this->msg(
'editlink' )->text()
573 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped(),
574 'mw-whatlinkshere-tools'
579 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
589 $title = $this->getPageTitle();
592 $linkRenderer = $this->getLinkRenderer();
596 'links' => $linkRenderer->makeKnownLink(
609 $this->contentHandlerFactory->getContentHandler( $target->
getContentModel() )
610 ->supportsDirectEditing()
612 $links[
'edit'] = $linkRenderer->makeKnownLink(
616 [
'action' =>
'edit' ]
621 return $this->getLanguage()->pipeList( $links );
624 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
628 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
630 ->setLinkQuery( array_diff_key( $this->opts->getChangedValues(), [
'target' =>
null ] ) )
631 ->setLimits( $this->limits )
632 ->setLimitLinkQueryParam(
'limit' )
633 ->setCurrentLimit( $this->opts->getValue(
'limit' ) )
634 ->setPrevMsg(
'whatlinkshere-prev' )
635 ->setNextMsg(
'whatlinkshere-next' );
637 if ( $prevPageId != 0 ) {
638 $navBuilder->setPrevLinkQuery( [
'dir' =>
'prev',
'offset' =>
"$prevNamespace|$prevPageId" ] );
640 if ( $nextPageId != 0 ) {
641 $navBuilder->setNextLinkQuery( [
'dir' =>
'next',
'offset' =>
"$nextNamespace|$nextPageId" ] );
644 return $navBuilder->getHtml();
648 $this->addHelpLink(
'Help:What links here' );
649 $this->getOutput()->addModuleStyles(
'mediawiki.special' );
655 'id' =>
'mw-whatlinkshere-target',
656 'label-message' =>
'whatlinkshere-page',
657 'section' =>
'whatlinkshere-target',
661 'type' =>
'namespaceselect',
662 'name' =>
'namespace',
664 'label-message' =>
'namespace',
666 'in-user-lang' =>
true,
667 'section' =>
'whatlinkshere-ns',
673 'hide-if' => [
'===',
'namespace',
'' ],
674 'label-message' =>
'invert',
675 'help-message' =>
'tooltip-whatlinkshere-invert',
676 'help-inline' =>
false,
677 'section' =>
'whatlinkshere-ns',
681 $filters = [
'hidetrans',
'hidelinks',
'hideredirs' ];
686 foreach ( $filters as $filter ) {
688 $hide = $this->msg(
'hide' )->text();
689 $msg = $this->msg(
"whatlinkshere-{$filter}", $hide )->text();
694 'section' =>
'whatlinkshere-filter',
707 $this->target = Title::newFromText( $this->
getRequest()->getText(
'target' ) );
708 if ( $this->target && $this->target->getNamespace() ==
NS_FILE ) {
709 $hide = $this->msg(
'hide' )->text();
710 $msg = $this->msg(
'whatlinkshere-hideimages', $hide )->text();
714 'name' =>
'hideimages',
716 'section' =>
'whatlinkshere-filter',
722 ->setSubmitTextMsg(
'whatlinkshere-submit' );
754 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
if(!defined('MW_SETUP_CALLBACK'))
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(IConnectionProvider $dbProvider, 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.
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.