65 protected $limits = [ 20, 50, 100, 250, 500 ];
85 parent::__construct(
'Whatlinkshere' );
86 $this->mIncludable =
true;
87 $this->dbProvider = $dbProvider;
88 $this->linkBatchFactory = $linkBatchFactory;
89 $this->contentHandlerFactory = $contentHandlerFactory;
90 $this->searchEngineFactory = $searchEngineFactory;
91 $this->namespaceInfo = $namespaceInfo;
92 $this->titleFactory = $titleFactory;
93 $this->linksMigration = $linksMigration;
103 $par = str_replace(
'_',
' ',
$par );
105 parent::setParameter(
$par );
114 $opts->
add(
'namespace',
null, FormOptions::INTNULL );
115 $opts->
add(
'limit',
null, FormOptions::INTNULL );
127 if ( $opts->
getValue(
'limit' ) ===
null ) {
135 $this->
getSkin()->setRelevantTitle( $this->target );
138 $out->setPageTitleMsg(
139 $this->
msg(
'whatlinkshere-title' )->plaintextParams( $this->target->getPrefixedText() )
141 $out->addBacklinkSubtitle( $this->target );
143 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir(
$opts );
145 $this->showIndirectLinks(
167 $from =
$opts->getValue(
'from' );
172 $offsetNamespace =
null;
173 $offsetPageID = $from - 1;
176 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
180 if ( !$offsetPageIDString ) {
181 $offsetPageIDString = $offsetNamespaceString;
182 $offsetNamespaceString =
'';
184 if ( is_numeric( $offsetNamespaceString ) ) {
185 $offsetNamespace = (int)$offsetNamespaceString;
187 $offsetNamespace =
null;
189 $offsetPageID = (int)$offsetPageIDString;
192 if ( $offsetNamespace ===
null ) {
193 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
194 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() :
NS_MAIN;
197 return [ $offsetNamespace, $offsetPageID, $dir ];
208 private function showIndirectLinks(
209 $level, LinkTarget $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir =
'next'
211 $out = $this->getOutput();
212 $dbr = $this->dbProvider->getReplicaDatabase();
214 $hidelinks = $this->opts->getValue(
'hidelinks' );
215 $hideredirs = $this->opts->getValue(
'hideredirs' );
216 $hidetrans = $this->opts->getValue(
'hidetrans' );
217 $hideimages = $target->getNamespace() !==
NS_FILE || $this->opts->getValue(
'hideimages' );
221 $fetchredirs = $hidelinks && !$hideredirs;
225 $conds[
'redirect'] = [
226 'rd_namespace' => $target->getNamespace(),
227 'rd_title' => $target->getDBkey(),
228 'rd_interwiki' =>
'',
230 $conds[
'pagelinks'] = $this->linksMigration->getLinksConditions(
'pagelinks', $target );
231 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
232 $conds[
'imagelinks'] = [
233 'il_to' => $target->getDBkey(),
236 $namespace = $this->opts->getValue(
'namespace' );
237 if ( is_int( $namespace ) ) {
238 $invert = $this->opts->getValue(
'invert' );
242 $namespaces = array_diff(
243 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
245 $namespaces = $namespace;
250 $namespaces = $this->namespaceInfo->getValidNamespaces();
252 $conds[
'redirect'][
'page_namespace'] = $namespaces;
253 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
254 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
255 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
257 if ( $offsetPageID ) {
258 $op = $dir ===
'prev' ?
'<' :
'>';
259 $conds[
'redirect'][] = $dbr->buildComparison( $op, [
260 'rd_from' => $offsetPageID,
262 $conds[
'templatelinks'][] = $dbr->buildComparison( $op, [
263 'tl_from_namespace' => $offsetNamespace,
264 'tl_from' => $offsetPageID,
266 $conds[
'pagelinks'][] = $dbr->buildComparison( $op, [
267 'pl_from_namespace' => $offsetNamespace,
268 'pl_from' => $offsetPageID,
270 $conds[
'imagelinks'][] = $dbr->buildComparison( $op, [
271 'il_from_namespace' => $offsetNamespace,
272 'il_from' => $offsetPageID,
280 $conds[
'pagelinks'][
'rd_from'] =
null;
283 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
286 $queryFunc =
static function ( IReadableDatabase $dbr, $table, $fromCol ) use (
287 $conds, $target, $limit, $sortDirection, $fname
290 $queryLimit = $limit + 1;
292 "rd_from = $fromCol",
293 'rd_title' => $target->getDBkey(),
294 'rd_namespace' => $target->getNamespace(),
295 'rd_interwiki' =>
'',
298 $subQuery = $dbr->newSelectQueryBuilder()
300 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
301 ->conds( $conds[$table] )
302 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
303 ->limit( 2 * $queryLimit )
304 ->leftJoin(
'redirect',
'redirect', $on );
306 return $dbr->newSelectQueryBuilder()
307 ->table( $subQuery,
'temp_backlink_range' )
308 ->join(
'page',
'page',
"$fromCol = page_id" )
309 ->fields( [
'page_id',
'page_namespace',
'page_title',
310 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
311 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
312 ->limit( $queryLimit )
317 if ( $fetchredirs ) {
318 $rdRes = $dbr->newSelectQueryBuilder()
319 ->table(
'redirect' )
320 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
321 ->conds( $conds[
'redirect'] )
322 ->orderBy(
'rd_from', $sortDirection )
323 ->limit( $limit + 1 )
324 ->join(
'page',
'page',
'rd_from = page_id' )
325 ->caller( __METHOD__ )
330 $plRes = $queryFunc( $dbr,
'pagelinks',
'pl_from' );
334 $tlRes = $queryFunc( $dbr,
'templatelinks',
'tl_from' );
337 if ( !$hideimages ) {
338 $ilRes = $queryFunc( $dbr,
'imagelinks',
'il_from' );
342 if ( ( !$fetchredirs || !$rdRes->numRows() )
344 && ( $hidelinks || !$plRes->numRows() )
346 && ( $hidetrans || !$tlRes->numRows() )
348 && ( $hideimages || !$ilRes->numRows() )
350 if ( $level == 0 && !$this->including() ) {
351 if ( $hidelinks || $hidetrans || $hideredirs ) {
352 $msgKey =
'nolinkshere-filter';
353 } elseif ( is_int( $namespace ) ) {
354 $msgKey =
'nolinkshere-ns';
356 $msgKey =
'nolinkshere';
358 $link = $this->getLinkRenderer()->makeLink(
362 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
365 $errMsg = $this->msg( $msgKey )
366 ->params( $this->target->getPrefixedText() )
369 $out->addHTML( $errMsg );
370 $out->setStatusCode( 404 );
380 if ( $fetchredirs ) {
382 foreach ( $rdRes as $row ) {
383 $row->is_template = 0;
385 $rows[$row->page_id] = $row;
390 foreach ( $plRes as $row ) {
391 $row->is_template = 0;
393 $rows[$row->page_id] = $row;
398 foreach ( $tlRes as $row ) {
399 $row->is_template = 1;
401 $rows[$row->page_id] = $row;
404 if ( !$hideimages ) {
406 foreach ( $ilRes as $row ) {
407 $row->is_template = 0;
409 $rows[$row->page_id] = $row;
414 usort( $rows,
static function ( $rowA, $rowB ) {
415 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
416 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
418 if ( $rowA->page_id !== $rowB->page_id ) {
419 return $rowA->page_id < $rowB->page_id ? -1 : 1;
424 $numRows = count( $rows );
428 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
430 } elseif ( $dir ===
'prev' ) {
431 if ( $numRows > $limit ) {
434 $nextNamespace = $rows[$limit]->page_namespace;
435 $nextPageId = $rows[$limit]->page_id;
437 $rows = array_slice( $rows, 1, $limit );
439 $prevNamespace = $rows[0]->page_namespace;
440 $prevPageId = $rows[0]->page_id;
443 $nextNamespace = $rows[$numRows - 1]->page_namespace;
444 $nextPageId = $rows[$numRows - 1]->page_id;
445 $prevNamespace =
false;
450 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
451 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
452 if ( $numRows > $limit ) {
454 $nextNamespace = $rows[$limit - 1]->page_namespace ??
false;
455 $nextPageId = $rows[$limit - 1]->page_id ??
false;
457 $rows = array_slice( $rows, 0, $limit );
459 $nextNamespace =
false;
466 $lb = $this->linkBatchFactory->newLinkBatch();
467 foreach ( $rows as $row ) {
468 $lb->add( $row->page_namespace, $row->page_title );
472 if ( $level == 0 && !$this->including() ) {
473 $link = $this->getLinkRenderer()->makeLink(
477 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
480 $msg = $this->msg(
'linkshere' )
481 ->params( $this->target->getPrefixedText() )
484 $out->addHTML( $msg );
488 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
489 $out->addHTML( $prevnext );
491 $out->addHTML( $this->listStart( $level ) );
492 foreach ( $rows as $row ) {
493 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
495 if ( $row->rd_from && $level < 2 ) {
496 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
497 $this->showIndirectLinks(
504 $out->addHTML( $this->listItem( $row, $nt, $target ) );
508 $out->addHTML( $this->listEnd() );
510 if ( $level == 0 && !$this->including() ) {
513 $out->addHTML( $prevnext );
518 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
521 private function listItem( stdClass $row,
PageIdentity $nt,
LinkTarget $target,
bool $notClose =
false ) {
522 $legacyTitle = $this->titleFactory->newFromPageIdentity( $nt );
523 $dirmark = $this->getLanguage()->getDirMark();
525 if ( $row->rd_from ) {
526 $query = [
'redirect' =>
'no' ];
531 $link = $this->getLinkRenderer()->makeKnownLink(
534 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
541 if ( (
string)$row->rd_fragment !==
'' ) {
542 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
543 ->rawParams( $this->getLinkRenderer()->makeLink(
547 } elseif ( $row->rd_from ) {
548 $props[] = $this->msg(
'isredirect' )->escaped();
550 if ( $row->is_template ) {
551 $props[] = $this->msg(
'istemplate' )->escaped();
553 if ( $row->is_image ) {
554 $props[] = $this->msg(
'isimage' )->escaped();
557 $legacyTarget = $this->titleFactory->newFromLinkTarget( $target );
558 $this->getHookRunner()->onWhatLinksHereProps( $row, $legacyTitle, $legacyTarget, $props );
560 if ( count( $props ) ) {
561 $propsText = $this->msg(
'parentheses' )
562 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
565 # Space for utilities links, with a what-links-here link provided
566 $wlhLink = $this->wlhLink(
568 $this->msg(
'whatlinkshere-links' )->text(),
569 $this->msg(
'editlink' )->text()
571 $wlh = Html::rawElement(
573 [
'class' =>
'mw-whatlinkshere-tools' ],
574 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped()
579 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
587 static $title =
null;
588 if ( $title ===
null ) {
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 );