45 private $loadBalancer;
48 private $linkBatchFactory;
51 private $contentHandlerFactory;
54 private $searchEngineFactory;
57 private $namespaceInfo;
60 private $titleFactory;
63 private $linksMigration;
65 protected $limits = [ 20, 50, 100, 250, 500 ];
85 parent::__construct(
'Whatlinkshere' );
86 $this->loadBalancer = $loadBalancer;
87 $this->linkBatchFactory = $linkBatchFactory;
88 $this->contentHandlerFactory = $contentHandlerFactory;
89 $this->searchEngineFactory = $searchEngineFactory;
90 $this->namespaceInfo = $namespaceInfo;
91 $this->titleFactory = $titleFactory;
92 $this->linksMigration = $linksMigration;
101 $out->addModuleStyles(
'mediawiki.special' );
106 $opts->
add(
'namespace',
'', FormOptions::INTNULL );
107 $opts->
add(
'limit', $this->
getConfig()->
get( MainConfigNames::QueryPageDefaultLimit ) );
121 if ( $par !==
null ) {
128 $this->target = Title::newFromText(
$opts->
getValue(
'target' ) );
129 if ( !$this->target ) {
131 $out->addHTML( $this->whatlinkshereForm() );
137 $this->
getSkin()->setRelevantTitle( $this->target );
139 $out->setPageTitle( $this->
msg(
'whatlinkshere-title', $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->loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
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'] = [
228 $conds[
'pagelinks'] = [
232 $conds[
'templatelinks'] = $this->linksMigration->getLinksConditions(
'templatelinks', $target );
233 $conds[
'imagelinks'] = [
237 $namespace = $this->opts->getValue(
'namespace' );
238 if ( is_int( $namespace ) ) {
239 $invert = $this->opts->getValue(
'invert' );
243 $namespaces = array_diff(
244 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
246 $namespaces = $namespace;
251 $namespaces = $this->namespaceInfo->getValidNamespaces();
253 $conds[
'redirect'][
'page_namespace'] = $namespaces;
254 $conds[
'pagelinks'][
'pl_from_namespace'] = $namespaces;
255 $conds[
'templatelinks'][
'tl_from_namespace'] = $namespaces;
256 $conds[
'imagelinks'][
'il_from_namespace'] = $namespaces;
258 if ( $offsetPageID ) {
259 $rel = $dir ===
'prev' ?
'<' :
'>';
260 $conds[
'redirect'][] =
"rd_from $rel $offsetPageID";
261 $conds[
'templatelinks'][] =
"(tl_from_namespace = $offsetNamespace AND tl_from $rel $offsetPageID " .
262 "OR tl_from_namespace $rel $offsetNamespace)";
263 $conds[
'pagelinks'][] =
"(pl_from_namespace = $offsetNamespace AND pl_from $rel $offsetPageID " .
264 "OR pl_from_namespace $rel $offsetNamespace)";
265 $conds[
'imagelinks'][] =
"(il_from_namespace = $offsetNamespace AND il_from $rel $offsetPageID " .
266 "OR il_from_namespace $rel $offsetNamespace)";
273 $conds[
'pagelinks'][
'rd_from'] =
null;
276 $sortDirection = $dir ===
'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
279 $queryFunc =
static function (
IDatabase $dbr, $table, $fromCol ) use (
280 $conds, $target, $limit, $sortDirection, $fname
283 $queryLimit = $limit + 1;
285 "rd_from = $fromCol",
288 'rd_interwiki = ' .
$dbr->addQuotes(
'' ) .
' OR rd_interwiki IS NULL'
291 $subQuery =
$dbr->newSelectQueryBuilder()
293 ->fields( [ $fromCol,
'rd_from',
'rd_fragment' ] )
294 ->conds( $conds[$table] )
295 ->orderBy( [ $fromCol .
'_namespace', $fromCol ], $sortDirection )
296 ->limit( 2 * $queryLimit )
297 ->leftJoin(
'redirect',
'redirect', $on );
299 return $dbr->newSelectQueryBuilder()
300 ->table( $subQuery,
'temp_backlink_range' )
301 ->join(
'page',
'page',
"$fromCol = page_id" )
302 ->fields( [
'page_id',
'page_namespace',
'page_title',
303 'rd_from',
'rd_fragment',
'page_is_redirect' ] )
304 ->orderBy( [
'page_namespace',
'page_id' ], $sortDirection )
305 ->limit( $queryLimit )
310 if ( $fetchredirs ) {
311 $rdRes =
$dbr->newSelectQueryBuilder()
312 ->table(
'redirect' )
313 ->fields( [
'page_id',
'page_namespace',
'page_title',
'rd_from',
'rd_fragment',
'page_is_redirect' ] )
314 ->conds( $conds[
'redirect'] )
315 ->orderBy(
'rd_from', $sortDirection )
316 ->limit( $limit + 1 )
317 ->join(
'page',
'page',
'rd_from = page_id' )
318 ->caller( __METHOD__ )
323 $plRes = $queryFunc(
$dbr,
'pagelinks',
'pl_from' );
327 $tlRes = $queryFunc(
$dbr,
'templatelinks',
'tl_from' );
330 if ( !$hideimages ) {
331 $ilRes = $queryFunc(
$dbr,
'imagelinks',
'il_from' );
335 if ( ( !$fetchredirs || !$rdRes->numRows() )
337 && ( $hidelinks || !$plRes->numRows() )
339 && ( $hidetrans || !$tlRes->numRows() )
341 && ( $hideimages || !$ilRes->numRows() )
343 if ( $level == 0 && !$this->including() ) {
344 $out->addHTML( $this->whatlinkshereForm() );
346 $msgKey = is_int( $namespace ) ?
'nolinkshere-ns' :
'nolinkshere';
347 $link = $this->getLinkRenderer()->makeLink(
351 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
354 $errMsg = $this->msg( $msgKey )
355 ->params( $this->target->getPrefixedText() )
358 $out->addHTML( $errMsg );
359 $out->setStatusCode( 404 );
369 if ( $fetchredirs ) {
371 foreach ( $rdRes as $row ) {
372 $row->is_template = 0;
374 $rows[$row->page_id] = $row;
379 foreach ( $plRes as $row ) {
380 $row->is_template = 0;
382 $rows[$row->page_id] = $row;
387 foreach ( $tlRes as $row ) {
388 $row->is_template = 1;
390 $rows[$row->page_id] = $row;
393 if ( !$hideimages ) {
395 foreach ( $ilRes as $row ) {
396 $row->is_template = 0;
398 $rows[$row->page_id] = $row;
403 usort( $rows,
static function ( $rowA, $rowB ) {
404 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
405 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
407 if ( $rowA->page_id !== $rowB->page_id ) {
408 return $rowA->page_id < $rowB->page_id ? -1 : 1;
413 $numRows = count( $rows );
417 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId =
false;
419 } elseif ( $dir ===
'prev' ) {
420 if ( $numRows > $limit ) {
423 $nextNamespace = $rows[$limit]->page_namespace;
424 $nextPageId = $rows[$limit]->page_id;
426 $rows = array_slice( $rows, 1, $limit );
428 $prevNamespace = $rows[0]->page_namespace;
429 $prevPageId = $rows[0]->page_id;
432 $nextNamespace = $rows[$numRows - 1]->page_namespace;
433 $nextPageId = $rows[$numRows - 1]->page_id;
434 $prevNamespace =
false;
439 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace :
false;
440 $prevPageId = $offsetPageID ? $rows[0]->page_id :
false;
441 if ( $numRows > $limit ) {
443 $nextNamespace = $rows[$limit - 1]->page_namespace;
444 $nextPageId = $rows[$limit - 1]->page_id;
446 $rows = array_slice( $rows, 0, $limit );
448 $nextNamespace =
false;
455 $lb = $this->linkBatchFactory->newLinkBatch();
456 foreach ( $rows as $row ) {
457 $lb->add( $row->page_namespace, $row->page_title );
461 if ( $level == 0 && !$this->including() ) {
462 $out->addHTML( $this->whatlinkshereForm() );
464 $link = $this->getLinkRenderer()->makeLink(
468 $this->target->isRedirect() ? [
'redirect' =>
'no' ] : []
471 $msg = $this->msg(
'linkshere' )
472 ->params( $this->target->getPrefixedText() )
475 $out->addHTML( $msg );
479 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
480 $out->addHTML( $prevnext );
482 $out->addHTML( $this->listStart( $level ) );
483 foreach ( $rows as $row ) {
486 if ( $row->rd_from && $level < 2 ) {
487 $out->addHTML( $this->listItem( $row, $nt, $target,
true ) );
488 $this->showIndirectLinks(
491 $this->getConfig()->
get( MainConfigNames::MaxRedirectLinksRetrieved )
493 $out->addHTML( Xml::closeElement(
'li' ) );
495 $out->addHTML( $this->listItem( $row, $nt, $target ) );
499 $out->addHTML( $this->listEnd() );
501 if ( $level == 0 && !$this->including() ) {
504 $out->addHTML( $prevnext );
509 return Xml::openElement(
'ul', ( $level ? [] : [
'id' =>
'mw-whatlinkshere-list' ] ) );
512 protected function listItem( $row, $nt, $target, $notClose =
false ) {
513 $dirmark = $this->getLanguage()->getDirMark();
515 if ( $row->rd_from ) {
516 $query = [
'redirect' =>
'no' ];
521 $link = $this->getLinkRenderer()->makeKnownLink(
524 $row->page_is_redirect ? [
'class' =>
'mw-redirect' ] : [],
531 if ( (
string)$row->rd_fragment !==
'' ) {
532 $props[] = $this->msg(
'whatlinkshere-sectionredir' )
533 ->rawParams( $this->getLinkRenderer()->makeLink(
537 } elseif ( $row->rd_from ) {
538 $props[] = $this->msg(
'isredirect' )->escaped();
540 if ( $row->is_template ) {
541 $props[] = $this->msg(
'istemplate' )->escaped();
543 if ( $row->is_image ) {
544 $props[] = $this->msg(
'isimage' )->escaped();
547 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
549 if ( count( $props ) ) {
550 $propsText = $this->msg(
'parentheses' )
551 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
554 # Space for utilities links, with a what-links-here link provided
555 $wlhLink = $this->wlhLink(
557 $this->msg(
'whatlinkshere-links' )->text(),
558 $this->msg(
'editlink' )->text()
560 $wlh = Xml::wrapClass(
561 $this->msg(
'parentheses' )->rawParams( $wlhLink )->escaped(),
562 'mw-whatlinkshere-tools'
566 Xml::openElement(
'li' ) .
"$link $propsText $dirmark $wlh\n" :
567 Xml::tags(
'li',
null,
"$link $propsText $dirmark $wlh" ) .
"\n";
571 return Xml::closeElement(
'ul' );
577 $title = $this->getPageTitle();
580 $linkRenderer = $this->getLinkRenderer();
584 'links' => $linkRenderer->makeKnownLink(
597 $this->contentHandlerFactory->getContentHandler( $target->
getContentModel() )
598 ->supportsDirectEditing()
600 $links[
'edit'] = $linkRenderer->makeKnownLink(
604 [
'action' =>
'edit' ]
609 return $this->getLanguage()->pipeList( $links );
612 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
616 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
618 ->setLinkQuery( array_diff_key( $this->opts->getChangedValues(), [
'target' =>
null ] ) )
619 ->setLimits( $this->limits )
620 ->setLimitLinkQueryParam(
'limit' )
621 ->setCurrentLimit( $this->opts->getValue(
'limit' ) )
622 ->setPrevMsg(
'whatlinkshere-prev' )
623 ->setNextMsg(
'whatlinkshere-next' );
625 if ( $prevPageId != 0 ) {
626 $navBuilder->setPrevLinkQuery( [
'dir' =>
'prev',
'offset' =>
"$prevNamespace|$prevPageId" ] );
628 if ( $nextPageId != 0 ) {
629 $navBuilder->setNextLinkQuery( [
'dir' =>
'next',
'offset' =>
"$nextNamespace|$nextPageId" ] );
632 return $navBuilder->getHtml();
635 private function whatlinkshereForm() {
637 $this->opts->consumeValue(
'target' );
639 $this->opts->consumeValue(
'namespace' );
640 $this->opts->consumeValue(
'invert' );
646 'default' => $target,
647 'id' =>
'mw-whatlinkshere-target',
648 'label-message' =>
'whatlinkshere-page',
649 'section' =>
'whatlinkshere-target',
652 'type' =>
'namespaceselect',
653 'name' =>
'namespace',
655 'label-message' =>
'namespace',
657 'in-user-lang' =>
true,
658 'section' =>
'whatlinkshere-ns',
664 'hide-if' => [
'===',
'namespace',
'' ],
665 'label-message' =>
'invert',
666 'help-message' =>
'tooltip-whatlinkshere-invert',
667 'help-inline' =>
false,
668 'section' =>
'whatlinkshere-ns',
672 $filters = [
'hidetrans',
'hidelinks',
'hideredirs' ];
673 if ( $this->target instanceof
Title &&
675 $filters[] =
'hideimages';
681 foreach ( $filters as $filter ) {
683 $hide = $this->msg(
'hide' )->text();
684 $msg = $this->msg(
"whatlinkshere-{$filter}", $hide )->text();
689 'section' =>
'whatlinkshere-filter',
695 ->setTitle( $this->getPageTitle() )
696 ->setWrapperLegendMsg(
'whatlinkshere' )
697 ->setSubmitTextMsg(
'whatlinkshere-submit' );
699 return $form->prepareForm()->getHTML(
false );
711 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );