63 protected $limits = [ 20, 50, 100, 250, 500 ];
83 parent::__construct(
'Whatlinkshere' );
84 $this->mIncludable =
true;
85 $this->dbProvider = $dbProvider;
86 $this->linkBatchFactory = $linkBatchFactory;
87 $this->contentHandlerFactory = $contentHandlerFactory;
88 $this->searchEngineFactory = $searchEngineFactory;
89 $this->namespaceInfo = $namespaceInfo;
90 $this->titleFactory = $titleFactory;
91 $this->linksMigration = $linksMigration;
101 $par = str_replace(
'_',
' ',
$par );
103 parent::setParameter(
$par );
112 $opts->
add(
'namespace',
null, FormOptions::INTNULL );
113 $opts->
add(
'limit',
null, FormOptions::INTNULL );
125 if ( $opts->
getValue(
'limit' ) ===
null ) {
133 $this->
getSkin()->setRelevantTitle( $this->target );
136 $out->setPageTitleMsg(
137 $this->
msg(
'whatlinkshere-title' )->plaintextParams( $this->target->getPrefixedText() )
139 $out->addBacklinkSubtitle( $this->target );
141 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir(
$opts );
143 $this->showIndirectLinks(
165 $from =
$opts->getValue(
'from' );
170 $offsetNamespace =
null;
171 $offsetPageID = $from - 1;
174 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
178 if ( !$offsetPageIDString ) {
179 $offsetPageIDString = $offsetNamespaceString;
180 $offsetNamespaceString =
'';
182 if ( is_numeric( $offsetNamespaceString ) ) {
183 $offsetNamespace = (int)$offsetNamespaceString;
185 $offsetNamespace =
null;
187 $offsetPageID = (int)$offsetPageIDString;
190 if ( $offsetNamespace ===
null ) {
191 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
192 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() :
NS_MAIN;
195 return [ $offsetNamespace, $offsetPageID, $dir ];
206 private function showIndirectLinks(
207 $level, $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir =
'next'
209 $out = $this->getOutput();
210 $dbr = $this->dbProvider->getReplicaDatabase();
212 $hidelinks = $this->opts->getValue(
'hidelinks' );
213 $hideredirs = $this->opts->getValue(
'hideredirs' );
214 $hidetrans = $this->opts->getValue(
'hidetrans' );
215 $hideimages = $target->
getNamespace() !==
NS_FILE || $this->opts->getValue(
'hideimages' );
219 $fetchredirs = $hidelinks && !$hideredirs;
223 $conds[
'redirect'] = [
226 'rd_interwiki' =>
'',
228 $conds[
'pagelinks'] = $this->linksMigration->getLinksConditions(
'pagelinks', $target );
229 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
230 $conds[
'imagelinks'] = [
234 $namespace = $this->opts->getValue(
'namespace' );
235 if ( is_int( $namespace ) ) {
236 $invert = $this->opts->getValue(
'invert' );
240 $namespaces = array_diff(
241 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
243 $namespaces = $namespace;
248 $namespaces = $this->namespaceInfo->getValidNamespaces();
250 $conds[
'redirect'][
'page_namespace'] = $namespaces;
251 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
252 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
253 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
255 if ( $offsetPageID ) {
256 $op = $dir ===
'prev' ?
'<' :
'>';
257 $conds[
'redirect'][] = $dbr->buildComparison( $op, [
258 'rd_from' => $offsetPageID,
260 $conds[
'templatelinks'][] = $dbr->buildComparison( $op, [
261 'tl_from_namespace' => $offsetNamespace,
262 'tl_from' => $offsetPageID,
264 $conds[
'pagelinks'][] = $dbr->buildComparison( $op, [
265 'pl_from_namespace' => $offsetNamespace,
266 'pl_from' => $offsetPageID,
268 $conds[
'imagelinks'][] = $dbr->buildComparison( $op, [
269 'il_from_namespace' => $offsetNamespace,
270 'il_from' => $offsetPageID,
278 $conds[
'pagelinks'][
'rd_from'] =
null;
281 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
284 $queryFunc =
static function ( IReadableDatabase $dbr, $table, $fromCol ) use (
285 $conds, $target, $limit, $sortDirection, $fname
288 $queryLimit = $limit + 1;
290 "rd_from = $fromCol",
293 'rd_interwiki' =>
'',
296 $subQuery = $dbr->newSelectQueryBuilder()
298 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
299 ->conds( $conds[$table] )
300 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
301 ->limit( 2 * $queryLimit )
302 ->leftJoin(
'redirect',
'redirect', $on );
304 return $dbr->newSelectQueryBuilder()
305 ->table( $subQuery,
'temp_backlink_range' )
306 ->join(
'page',
'page',
"$fromCol = page_id" )
307 ->fields( [
'page_id',
'page_namespace',
'page_title',
308 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
309 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
310 ->limit( $queryLimit )
315 if ( $fetchredirs ) {
316 $rdRes = $dbr->newSelectQueryBuilder()
317 ->table(
'redirect' )
318 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
319 ->conds( $conds[
'redirect'] )
320 ->orderBy(
'rd_from', $sortDirection )
321 ->limit( $limit + 1 )
322 ->join(
'page',
'page',
'rd_from = page_id' )
323 ->caller( __METHOD__ )
328 $plRes = $queryFunc( $dbr,
'pagelinks',
'pl_from' );
332 $tlRes = $queryFunc( $dbr,
'templatelinks',
'tl_from' );
335 if ( !$hideimages ) {
336 $ilRes = $queryFunc( $dbr,
'imagelinks',
'il_from' );
340 if ( ( !$fetchredirs || !$rdRes->numRows() )
342 && ( $hidelinks || !$plRes->numRows() )
344 && ( $hidetrans || !$tlRes->numRows() )
346 && ( $hideimages || !$ilRes->numRows() )
348 if ( $level == 0 && !$this->including() ) {
349 if ( $hidelinks || $hidetrans || $hideredirs ) {
350 $msgKey =
'nolinkshere-filter';
351 } elseif ( is_int( $namespace ) ) {
352 $msgKey =
'nolinkshere-ns';
354 $msgKey =
'nolinkshere';
356 $link = $this->getLinkRenderer()->makeLink(
360 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
363 $errMsg = $this->msg( $msgKey )
364 ->params( $this->target->getPrefixedText() )
367 $out->addHTML( $errMsg );
368 $out->setStatusCode( 404 );
378 if ( $fetchredirs ) {
380 foreach ( $rdRes as $row ) {
381 $row->is_template = 0;
383 $rows[$row->page_id] = $row;
388 foreach ( $plRes as $row ) {
389 $row->is_template = 0;
391 $rows[$row->page_id] = $row;
396 foreach ( $tlRes as $row ) {
397 $row->is_template = 1;
399 $rows[$row->page_id] = $row;
402 if ( !$hideimages ) {
404 foreach ( $ilRes as $row ) {
405 $row->is_template = 0;
407 $rows[$row->page_id] = $row;
412 usort( $rows,
static function ( $rowA, $rowB ) {
413 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
414 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
416 if ( $rowA->page_id !== $rowB->page_id ) {
417 return $rowA->page_id < $rowB->page_id ? -1 : 1;
422 $numRows = count( $rows );
426 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
428 } elseif ( $dir ===
'prev' ) {
429 if ( $numRows > $limit ) {
432 $nextNamespace = $rows[$limit]->page_namespace;
433 $nextPageId = $rows[$limit]->page_id;
435 $rows = array_slice( $rows, 1, $limit );
437 $prevNamespace = $rows[0]->page_namespace;
438 $prevPageId = $rows[0]->page_id;
441 $nextNamespace = $rows[$numRows - 1]->page_namespace;
442 $nextPageId = $rows[$numRows - 1]->page_id;
443 $prevNamespace =
false;
448 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
449 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
450 if ( $numRows > $limit ) {
452 $nextNamespace = $rows[$limit - 1]->page_namespace ??
false;
453 $nextPageId = $rows[$limit - 1]->page_id ??
false;
455 $rows = array_slice( $rows, 0, $limit );
457 $nextNamespace =
false;
464 $lb = $this->linkBatchFactory->newLinkBatch();
465 foreach ( $rows as $row ) {
466 $lb->add( $row->page_namespace, $row->page_title );
470 if ( $level == 0 && !$this->including() ) {
471 $link = $this->getLinkRenderer()->makeLink(
475 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
478 $msg = $this->msg(
'linkshere' )
479 ->params( $this->target->getPrefixedText() )
482 $out->addHTML( $msg );
486 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
487 $out->addHTML( $prevnext );
489 $out->addHTML( $this->listStart( $level ) );
490 foreach ( $rows as $row ) {
491 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
493 if ( $row->rd_from && $level < 2 ) {
494 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
495 $this->showIndirectLinks(
500 $out->addHTML( Xml::closeElement(
'li' ) );
502 $out->addHTML( $this->listItem( $row, $nt, $target ) );
506 $out->addHTML( $this->listEnd() );
508 if ( $level == 0 && !$this->including() ) {
511 $out->addHTML( $prevnext );
516 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
519 protected function listItem( $row, $nt, $target, $notClose =
false ) {
520 $dirmark = $this->getLanguage()->getDirMark();
522 if ( $row->rd_from ) {
523 $query = [
'redirect' =>
'no' ];
528 $link = $this->getLinkRenderer()->makeKnownLink(
531 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
538 if ( (
string)$row->rd_fragment !==
'' ) {
539 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
540 ->rawParams( $this->getLinkRenderer()->makeLink(
544 } elseif ( $row->rd_from ) {
545 $props[] = $this->msg(
'isredirect' )->escaped();
547 if ( $row->is_template ) {
548 $props[] = $this->msg(
'istemplate' )->escaped();
550 if ( $row->is_image ) {
551 $props[] = $this->msg(
'isimage' )->escaped();
554 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
556 if ( count( $props ) ) {
557 $propsText = $this->msg(
'parentheses' )
558 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
561 # Space for utilities links, with a what-links-here link provided
562 $wlhLink = $this->wlhLink(
564 $this->msg(
'whatlinkshere-links' )->text(),
565 $this->msg(
'editlink' )->text()
567 $wlh = Xml::wrapClass(
568 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped(),
569 'mw-whatlinkshere-tools'
573 Xml::openElement(
'li' ) .
"$link $propsText $dirmark $wlh\n" :
574 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
578 return Xml::closeElement(
'ul' );
582 static $title =
null;
583 if ( $title ===
null ) {
584 $title = $this->getPageTitle();
587 $linkRenderer = $this->getLinkRenderer();
591 'links' => $linkRenderer->makeKnownLink(
604 $this->contentHandlerFactory->getContentHandler( $target->
getContentModel() )
605 ->supportsDirectEditing()
607 $links[
'edit'] = $linkRenderer->makeKnownLink(
611 [
'action' =>
'edit' ]
616 return $this->getLanguage()->pipeList( $links );
619 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
623 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
625 ->setLinkQuery( array_diff_key( $this->opts->getChangedValues(), [
'target' =>
null ] ) )
626 ->setLimits( $this->limits )
627 ->setLimitLinkQueryParam(
'limit' )
628 ->setCurrentLimit( $this->opts->getValue(
'limit' ) )
629 ->setPrevMsg(
'whatlinkshere-prev' )
630 ->setNextMsg(
'whatlinkshere-next' );
632 if ( $prevPageId != 0 ) {
633 $navBuilder->setPrevLinkQuery( [
'dir' =>
'prev',
'offset' =>
"$prevNamespace|$prevPageId" ] );
635 if ( $nextPageId != 0 ) {
636 $navBuilder->setNextLinkQuery( [
'dir' =>
'next',
'offset' =>
"$nextNamespace|$nextPageId" ] );
639 return $navBuilder->getHtml();
643 $this->addHelpLink(
'Help:What links here' );
644 $this->getOutput()->addModuleStyles(
'mediawiki.special' );
650 'id' =>
'mw-whatlinkshere-target',
651 'label-message' =>
'whatlinkshere-page',
652 'section' =>
'whatlinkshere-target',
656 'type' =>
'namespaceselect',
657 'name' =>
'namespace',
659 'label-message' =>
'namespace',
661 'in-user-lang' =>
true,
662 'section' =>
'whatlinkshere-ns',
668 'hide-if' => [
'===',
'namespace',
'' ],
669 'label-message' =>
'invert',
670 'help-message' =>
'tooltip-whatlinkshere-invert',
671 'help-inline' =>
false,
672 'section' =>
'whatlinkshere-ns',
676 $filters = [
'hidetrans',
'hidelinks',
'hideredirs' ];
681 foreach ( $filters as $filter ) {
683 $hide = $this->msg(
'hide' )->text();
684 $msg = $this->msg(
"whatlinkshere-{$filter}", $hide )->text();
689 'section' =>
'whatlinkshere-filter',
702 $this->target = Title::newFromText( $this->
getRequest()->getText(
'target' ) );
703 if ( $this->target && $this->target->getNamespace() ==
NS_FILE ) {
704 $hide = $this->msg(
'hide' )->text();
705 $msg = $this->msg(
'whatlinkshere-hideimages', $hide )->text();
709 'name' =>
'hideimages',
711 'section' =>
'whatlinkshere-filter',
717 ->setSubmitTextMsg(
'whatlinkshere-submit' );
749 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );