64 protected $limits = [ 20, 50, 100, 250, 500 ];
84 parent::__construct(
'Whatlinkshere' );
85 $this->mIncludable =
true;
86 $this->dbProvider = $dbProvider;
87 $this->linkBatchFactory = $linkBatchFactory;
88 $this->contentHandlerFactory = $contentHandlerFactory;
89 $this->searchEngineFactory = $searchEngineFactory;
90 $this->namespaceInfo = $namespaceInfo;
91 $this->titleFactory = $titleFactory;
92 $this->linksMigration = $linksMigration;
102 $par = str_replace(
'_',
' ',
$par );
104 parent::setParameter(
$par );
113 $opts->
add(
'namespace',
null, FormOptions::INTNULL );
114 $opts->
add(
'limit',
null, FormOptions::INTNULL );
126 if ( $opts->
getValue(
'limit' ) ===
null ) {
134 $this->
getSkin()->setRelevantTitle( $this->target );
137 $out->setPageTitleMsg(
138 $this->
msg(
'whatlinkshere-title' )->plaintextParams( $this->target->getPrefixedText() )
140 $out->addBacklinkSubtitle( $this->target );
142 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir(
$opts );
144 $this->showIndirectLinks(
166 $from =
$opts->getValue(
'from' );
171 $offsetNamespace =
null;
172 $offsetPageID = $from - 1;
175 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
179 if ( !$offsetPageIDString ) {
180 $offsetPageIDString = $offsetNamespaceString;
181 $offsetNamespaceString =
'';
183 if ( is_numeric( $offsetNamespaceString ) ) {
184 $offsetNamespace = (int)$offsetNamespaceString;
186 $offsetNamespace =
null;
188 $offsetPageID = (int)$offsetPageIDString;
191 if ( $offsetNamespace ===
null ) {
192 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
193 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() :
NS_MAIN;
196 return [ $offsetNamespace, $offsetPageID, $dir ];
207 private function showIndirectLinks(
208 $level, $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir =
'next'
210 $out = $this->getOutput();
211 $dbr = $this->dbProvider->getReplicaDatabase();
213 $hidelinks = $this->opts->getValue(
'hidelinks' );
214 $hideredirs = $this->opts->getValue(
'hideredirs' );
215 $hidetrans = $this->opts->getValue(
'hidetrans' );
216 $hideimages = $target->
getNamespace() !==
NS_FILE || $this->opts->getValue(
'hideimages' );
220 $fetchredirs = $hidelinks && !$hideredirs;
224 $conds[
'redirect'] = [
227 'rd_interwiki' =>
'',
229 $conds[
'pagelinks'] = $this->linksMigration->getLinksConditions(
'pagelinks', $target );
230 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
231 $conds[
'imagelinks'] = [
235 $namespace = $this->opts->getValue(
'namespace' );
236 if ( is_int( $namespace ) ) {
237 $invert = $this->opts->getValue(
'invert' );
241 $namespaces = array_diff(
242 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
244 $namespaces = $namespace;
249 $namespaces = $this->namespaceInfo->getValidNamespaces();
251 $conds[
'redirect'][
'page_namespace'] = $namespaces;
252 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
253 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
254 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
256 if ( $offsetPageID ) {
257 $op = $dir ===
'prev' ?
'<' :
'>';
258 $conds[
'redirect'][] = $dbr->buildComparison( $op, [
259 'rd_from' => $offsetPageID,
261 $conds[
'templatelinks'][] = $dbr->buildComparison( $op, [
262 'tl_from_namespace' => $offsetNamespace,
263 'tl_from' => $offsetPageID,
265 $conds[
'pagelinks'][] = $dbr->buildComparison( $op, [
266 'pl_from_namespace' => $offsetNamespace,
267 'pl_from' => $offsetPageID,
269 $conds[
'imagelinks'][] = $dbr->buildComparison( $op, [
270 'il_from_namespace' => $offsetNamespace,
271 'il_from' => $offsetPageID,
279 $conds[
'pagelinks'][
'rd_from'] =
null;
282 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
285 $queryFunc =
static function ( IReadableDatabase $dbr, $table, $fromCol ) use (
286 $conds, $target, $limit, $sortDirection, $fname
289 $queryLimit = $limit + 1;
291 "rd_from = $fromCol",
294 'rd_interwiki' =>
'',
297 $subQuery = $dbr->newSelectQueryBuilder()
299 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
300 ->conds( $conds[$table] )
301 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
302 ->limit( 2 * $queryLimit )
303 ->leftJoin(
'redirect',
'redirect', $on );
305 return $dbr->newSelectQueryBuilder()
306 ->table( $subQuery,
'temp_backlink_range' )
307 ->join(
'page',
'page',
"$fromCol = page_id" )
308 ->fields( [
'page_id',
'page_namespace',
'page_title',
309 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
310 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
311 ->limit( $queryLimit )
316 if ( $fetchredirs ) {
317 $rdRes = $dbr->newSelectQueryBuilder()
318 ->table(
'redirect' )
319 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
320 ->conds( $conds[
'redirect'] )
321 ->orderBy(
'rd_from', $sortDirection )
322 ->limit( $limit + 1 )
323 ->join(
'page',
'page',
'rd_from = page_id' )
324 ->caller( __METHOD__ )
329 $plRes = $queryFunc( $dbr,
'pagelinks',
'pl_from' );
333 $tlRes = $queryFunc( $dbr,
'templatelinks',
'tl_from' );
336 if ( !$hideimages ) {
337 $ilRes = $queryFunc( $dbr,
'imagelinks',
'il_from' );
341 if ( ( !$fetchredirs || !$rdRes->numRows() )
343 && ( $hidelinks || !$plRes->numRows() )
345 && ( $hidetrans || !$tlRes->numRows() )
347 && ( $hideimages || !$ilRes->numRows() )
349 if ( $level == 0 && !$this->including() ) {
350 if ( $hidelinks || $hidetrans || $hideredirs ) {
351 $msgKey =
'nolinkshere-filter';
352 } elseif ( is_int( $namespace ) ) {
353 $msgKey =
'nolinkshere-ns';
355 $msgKey =
'nolinkshere';
357 $link = $this->getLinkRenderer()->makeLink(
361 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
364 $errMsg = $this->msg( $msgKey )
365 ->params( $this->target->getPrefixedText() )
368 $out->addHTML( $errMsg );
369 $out->setStatusCode( 404 );
379 if ( $fetchredirs ) {
381 foreach ( $rdRes as $row ) {
382 $row->is_template = 0;
384 $rows[$row->page_id] = $row;
389 foreach ( $plRes as $row ) {
390 $row->is_template = 0;
392 $rows[$row->page_id] = $row;
397 foreach ( $tlRes as $row ) {
398 $row->is_template = 1;
400 $rows[$row->page_id] = $row;
403 if ( !$hideimages ) {
405 foreach ( $ilRes as $row ) {
406 $row->is_template = 0;
408 $rows[$row->page_id] = $row;
413 usort( $rows,
static function ( $rowA, $rowB ) {
414 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
415 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
417 if ( $rowA->page_id !== $rowB->page_id ) {
418 return $rowA->page_id < $rowB->page_id ? -1 : 1;
423 $numRows = count( $rows );
427 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
429 } elseif ( $dir ===
'prev' ) {
430 if ( $numRows > $limit ) {
433 $nextNamespace = $rows[$limit]->page_namespace;
434 $nextPageId = $rows[$limit]->page_id;
436 $rows = array_slice( $rows, 1, $limit );
438 $prevNamespace = $rows[0]->page_namespace;
439 $prevPageId = $rows[0]->page_id;
442 $nextNamespace = $rows[$numRows - 1]->page_namespace;
443 $nextPageId = $rows[$numRows - 1]->page_id;
444 $prevNamespace =
false;
449 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
450 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
451 if ( $numRows > $limit ) {
453 $nextNamespace = $rows[$limit - 1]->page_namespace ??
false;
454 $nextPageId = $rows[$limit - 1]->page_id ??
false;
456 $rows = array_slice( $rows, 0, $limit );
458 $nextNamespace =
false;
465 $lb = $this->linkBatchFactory->newLinkBatch();
466 foreach ( $rows as $row ) {
467 $lb->add( $row->page_namespace, $row->page_title );
471 if ( $level == 0 && !$this->including() ) {
472 $link = $this->getLinkRenderer()->makeLink(
476 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
479 $msg = $this->msg(
'linkshere' )
480 ->params( $this->target->getPrefixedText() )
483 $out->addHTML( $msg );
487 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
488 $out->addHTML( $prevnext );
490 $out->addHTML( $this->listStart( $level ) );
491 foreach ( $rows as $row ) {
492 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
494 if ( $row->rd_from && $level < 2 ) {
495 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
496 $this->showIndirectLinks(
501 $out->addHTML( Xml::closeElement(
'li' ) );
503 $out->addHTML( $this->listItem( $row, $nt, $target ) );
507 $out->addHTML( $this->listEnd() );
509 if ( $level == 0 && !$this->including() ) {
512 $out->addHTML( $prevnext );
517 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
520 protected function listItem( $row, $nt, $target, $notClose =
false ) {
521 $dirmark = $this->getLanguage()->getDirMark();
523 if ( $row->rd_from ) {
524 $query = [
'redirect' =>
'no' ];
529 $link = $this->getLinkRenderer()->makeKnownLink(
532 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
539 if ( (
string)$row->rd_fragment !==
'' ) {
540 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
541 ->rawParams( $this->getLinkRenderer()->makeLink(
545 } elseif ( $row->rd_from ) {
546 $props[] = $this->msg(
'isredirect' )->escaped();
548 if ( $row->is_template ) {
549 $props[] = $this->msg(
'istemplate' )->escaped();
551 if ( $row->is_image ) {
552 $props[] = $this->msg(
'isimage' )->escaped();
555 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
557 if ( count( $props ) ) {
558 $propsText = $this->msg(
'parentheses' )
559 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
562 # Space for utilities links, with a what-links-here link provided
563 $wlhLink = $this->wlhLink(
565 $this->msg(
'whatlinkshere-links' )->text(),
566 $this->msg(
'editlink' )->text()
568 $wlh = Html::rawElement(
570 [
'class' =>
'mw-whatlinkshere-tools' ],
571 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped()
575 Xml::openElement(
'li' ) .
"$link $propsText $dirmark $wlh\n" :
576 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
580 return Xml::closeElement(
'ul' );
584 static $title =
null;
585 if ( $title ===
null ) {
586 $title = $this->getPageTitle();
589 $linkRenderer = $this->getLinkRenderer();
593 'links' => $linkRenderer->makeKnownLink(
606 $this->contentHandlerFactory->getContentHandler( $target->
getContentModel() )
607 ->supportsDirectEditing()
609 $links[
'edit'] = $linkRenderer->makeKnownLink(
613 [
'action' =>
'edit' ]
618 return $this->getLanguage()->pipeList( $links );
621 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
625 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
627 ->setLinkQuery( array_diff_key( $this->opts->getChangedValues(), [
'target' =>
null ] ) )
628 ->setLimits( $this->limits )
629 ->setLimitLinkQueryParam(
'limit' )
630 ->setCurrentLimit( $this->opts->getValue(
'limit' ) )
631 ->setPrevMsg(
'whatlinkshere-prev' )
632 ->setNextMsg(
'whatlinkshere-next' );
634 if ( $prevPageId != 0 ) {
635 $navBuilder->setPrevLinkQuery( [
'dir' =>
'prev',
'offset' =>
"$prevNamespace|$prevPageId" ] );
637 if ( $nextPageId != 0 ) {
638 $navBuilder->setNextLinkQuery( [
'dir' =>
'next',
'offset' =>
"$nextNamespace|$nextPageId" ] );
641 return $navBuilder->getHtml();
645 $this->addHelpLink(
'Help:What links here' );
646 $this->getOutput()->addModuleStyles(
'mediawiki.special' );
652 'id' =>
'mw-whatlinkshere-target',
653 'label-message' =>
'whatlinkshere-page',
654 'section' =>
'whatlinkshere-target',
658 'type' =>
'namespaceselect',
659 'name' =>
'namespace',
661 'label-message' =>
'namespace',
663 'in-user-lang' =>
true,
664 'section' =>
'whatlinkshere-ns',
670 'hide-if' => [
'===',
'namespace',
'' ],
671 'label-message' =>
'invert',
672 'help-message' =>
'tooltip-whatlinkshere-invert',
673 'help-inline' =>
false,
674 'section' =>
'whatlinkshere-ns',
678 $filters = [
'hidetrans',
'hidelinks',
'hideredirs' ];
683 foreach ( $filters as $filter ) {
685 $hide = $this->msg(
'hide' )->text();
686 $msg = $this->msg(
"whatlinkshere-{$filter}", $hide )->text();
691 'section' =>
'whatlinkshere-filter',
704 $this->target = Title::newFromText( $this->
getRequest()->getText(
'target' ) );
705 if ( $this->target && $this->target->getNamespace() ==
NS_FILE ) {
706 $hide = $this->msg(
'hide' )->text();
707 $msg = $this->msg(
'whatlinkshere-hideimages', $hide )->text();
711 'name' =>
'hideimages',
713 'section' =>
'whatlinkshere-filter',
719 ->setSubmitTextMsg(
'whatlinkshere-submit' );
751 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );