MediaWiki master
SpecialWhatLinksHere.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
39use stdClass;
43
51 protected $target;
52
59 private array $formData;
60 private IConnectionProvider $dbProvider;
61 private LinkBatchFactory $linkBatchFactory;
62 private IContentHandlerFactory $contentHandlerFactory;
63 private SearchEngineFactory $searchEngineFactory;
64 private NamespaceInfo $namespaceInfo;
65 private TitleFactory $titleFactory;
66 private LinksMigration $linksMigration;
67
68 private const LIMITS = [ 20, 50, 100, 250, 500 ];
69
79 public function __construct(
80 IConnectionProvider $dbProvider,
81 LinkBatchFactory $linkBatchFactory,
82 IContentHandlerFactory $contentHandlerFactory,
83 SearchEngineFactory $searchEngineFactory,
84 NamespaceInfo $namespaceInfo,
85 TitleFactory $titleFactory,
86 LinksMigration $linksMigration
87 ) {
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;
97 }
98
103 protected function setParameter( $par ) {
104 if ( $par ) {
105 // The only difference that subpage syntax can have is the underscore.
106 $par = str_replace( '_', ' ', $par );
107 }
108 parent::setParameter( $par );
109 }
110
114 public function onSuccess() {
115 $this->getSkin()->setRelevantTitle( $this->target );
116
117 $out = $this->getOutput();
118 $out->setPageTitleMsg(
119 $this->msg( 'whatlinkshere-title' )->plaintextParams( $this->target->getPrefixedText() )
120 );
121 $out->addBacklinkSubtitle( $this->target );
122
123 [ $offsetNamespace, $offsetPageID, $dir ] = $this->parseOffsetAndDir();
124
125 $this->showIndirectLinks(
126 0,
127 $this->target,
128 $this->formData['limit'],
129 $offsetNamespace,
130 $offsetPageID,
131 $dir
132 );
133 }
134
145 private function parseOffsetAndDir(): array {
146 $from = (int)$this->formData['from'];
147
148 if ( $from ) {
149 $dir = 'next';
150 $offsetNamespace = null;
151 $offsetPageID = $from - 1;
152 } else {
153 $dir = $this->formData['dir'] ?? 'next';
154 [ $offsetNamespaceString, $offsetPageIDString ] = explode(
155 '|',
156 $this->formData['offset'] . '|'
157 );
158 if ( !$offsetPageIDString ) {
159 $offsetPageIDString = $offsetNamespaceString;
160 $offsetNamespaceString = '';
161 }
162 if ( is_numeric( $offsetNamespaceString ) ) {
163 $offsetNamespace = (int)$offsetNamespaceString;
164 } else {
165 $offsetNamespace = null;
166 }
167 $offsetPageID = (int)$offsetPageIDString;
168 }
169
170 if ( $offsetNamespace === null ) {
171 $offsetTitle = $this->titleFactory->newFromID( $offsetPageID );
172 $offsetNamespace = $offsetTitle ? $offsetTitle->getNamespace() : NS_MAIN;
173 }
174
175 return [ $offsetNamespace, $offsetPageID, $dir ];
176 }
177
186 private function showIndirectLinks(
187 $level, LinkTarget $target, $limit, $offsetNamespace = 0, $offsetPageID = 0, $dir = 'next'
188 ) {
189 $out = $this->getOutput();
190 $dbr = $this->dbProvider->getReplicaDatabase();
191 $hookRunner = $this->getHookRunner();
192
193 $hidelinks = $this->formData['hidelinks'];
194 $hideredirs = $this->formData['hideredirs'];
195 $hidetrans = $this->formData['hidetrans'];
196 $hideimages = $target->getNamespace() !== NS_FILE || ( $this->formData['hideimages'] ?? false );
197
198 // For historical reasons `pagelinks` always contains an entry for the redirect target.
199 // So we only need to query `redirect` if `pagelinks` isn't being queried.
200 $fetchredirs = $hidelinks && !$hideredirs;
201
202 // Build query conds in concert for all four tables...
203 $conds = [];
204 $conds['redirect'] = [
205 'rd_namespace' => $target->getNamespace(),
206 'rd_title' => $target->getDBkey(),
207 'rd_interwiki' => '',
208 ];
209 $conds['pagelinks'] = $this->linksMigration->getLinksConditions( 'pagelinks', $target );
210 $conds['templatelinks'] = $this->linksMigration->getLinksConditions( 'templatelinks', $target );
211 $conds['imagelinks'] = [
212 'il_to' => $target->getDBkey(),
213 ];
214
215 $namespace = $this->formData['namespace'];
216 if ( $namespace !== '' ) {
217 $namespace = intval( $this->formData['namespace'] );
218 $invert = $this->formData['invert'];
219 if ( $invert ) {
220 // Select all namespaces except for the specified one.
221 // This allows the database to use the *_from_namespace index. (T241837)
222 $namespaces = array_diff(
223 $this->namespaceInfo->getValidNamespaces(), [ $namespace ] );
224 } else {
225 $namespaces = $namespace;
226 }
227 } else {
228 // Select all namespaces.
229 // This allows the database to use the *_from_namespace index. (T297754)
230 $namespaces = $this->namespaceInfo->getValidNamespaces();
231 }
232 $conds['redirect']['page_namespace'] = $namespaces;
233 $conds['pagelinks']['pl_from_namespace'] = $namespaces;
234 $conds['templatelinks']['tl_from_namespace'] = $namespaces;
235 $conds['imagelinks']['il_from_namespace'] = $namespaces;
236
237 if ( $offsetPageID ) {
238 $op = $dir === 'prev' ? '<' : '>';
239 $conds['redirect'][] = $dbr->buildComparison( $op, [
240 'rd_from' => $offsetPageID,
241 ] );
242 $conds['templatelinks'][] = $dbr->buildComparison( $op, [
243 'tl_from_namespace' => $offsetNamespace,
244 'tl_from' => $offsetPageID,
245 ] );
246 $conds['pagelinks'][] = $dbr->buildComparison( $op, [
247 'pl_from_namespace' => $offsetNamespace,
248 'pl_from' => $offsetPageID,
249 ] );
250 $conds['imagelinks'][] = $dbr->buildComparison( $op, [
251 'il_from_namespace' => $offsetNamespace,
252 'il_from' => $offsetPageID,
253 ] );
254 }
255
256 if ( $hideredirs ) {
257 // For historical reasons `pagelinks` always contains an entry for the redirect target.
258 // So we hide that link when $hideredirs is set. There's unfortunately no way to tell when a
259 // redirect's content also links to the target.
260 $conds['pagelinks']['rd_from'] = null;
261 }
262
263 $sortDirection = $dir === 'prev' ? SelectQueryBuilder::SORT_DESC : SelectQueryBuilder::SORT_ASC;
264
265 $fname = __METHOD__;
266 $queryFunc = function ( IReadableDatabase $dbr, $table, $fromCol ) use (
267 $conds, $target, $limit, $sortDirection, $fname, $hookRunner
268 ) {
269 // Read an extra row as an at-end check
270 $queryLimit = $limit + 1;
271 $on = [
272 "rd_from = $fromCol",
273 'rd_title' => $target->getDBkey(),
274 'rd_namespace' => $target->getNamespace(),
275 'rd_interwiki' => '',
276 ];
277 // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
278 $subQuery = $dbr->newSelectQueryBuilder()
279 ->table( $table )
280 ->fields( [ $fromCol, 'rd_from', 'rd_fragment' ] )
281 ->conds( $conds[$table] )
282 ->orderBy( [ $fromCol . '_namespace', $fromCol ], $sortDirection )
283 ->limit( 2 * $queryLimit )
284 ->leftJoin( 'redirect', 'redirect', $on );
285
286 $queryBuilder = $dbr->newSelectQueryBuilder()
287 ->table( $subQuery, 'temp_backlink_range' )
288 ->join( 'page', 'page', "$fromCol = page_id" )
289 ->fields( [ 'page_id', 'page_namespace', 'page_title',
290 'rd_from', 'rd_fragment', 'page_is_redirect' ] )
291 ->orderBy( [ 'page_namespace', 'page_id' ], $sortDirection )
292 ->limit( $queryLimit );
293
294 $hookRunner->onSpecialWhatLinksHereQuery( $table, $this->formData, $queryBuilder );
295
296 return $queryBuilder->caller( $fname )->fetchResultSet();
297 };
298
299 if ( $fetchredirs ) {
300 $queryBuilder = $dbr->newSelectQueryBuilder()
301 ->table( 'redirect' )
302 ->fields( [ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'rd_fragment', 'page_is_redirect' ] )
303 ->conds( $conds['redirect'] )
304 ->orderBy( 'rd_from', $sortDirection )
305 ->limit( $limit + 1 )
306 ->join( 'page', 'page', 'rd_from = page_id' );
307
308 $hookRunner->onSpecialWhatLinksHereQuery( 'redirect', $this->formData, $queryBuilder );
309
310 $rdRes = $queryBuilder->caller( __METHOD__ )->fetchResultSet();
311 }
312
313 if ( !$hidelinks ) {
314 $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
315 }
316
317 if ( !$hidetrans ) {
318 $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
319 }
320
321 if ( !$hideimages ) {
322 $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
323 }
324
325 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $rdRes is declared when fetching redirs
326 if ( ( !$fetchredirs || !$rdRes->numRows() )
327 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $plRes is declared when fetching links
328 && ( $hidelinks || !$plRes->numRows() )
329 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $tlRes is declared when fetching trans
330 && ( $hidetrans || !$tlRes->numRows() )
331 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $ilRes is declared when fetching images
332 && ( $hideimages || !$ilRes->numRows() )
333 ) {
334 if ( $level == 0 && !$this->including() ) {
335 if ( $hidelinks || $hidetrans || $hideredirs ) {
336 $msgKey = 'nolinkshere-filter';
337 } elseif ( is_int( $namespace ) ) {
338 $msgKey = 'nolinkshere-ns';
339 } else {
340 $msgKey = 'nolinkshere';
341 }
342 $link = $this->getLinkRenderer()->makeLink(
343 $this->target,
344 null,
345 [],
346 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
347 );
348
349 $errMsg = $this->msg( $msgKey )
350 ->params( $this->target->getPrefixedText() )
351 ->rawParams( $link )
352 ->parseAsBlock();
353 $out->addHTML( $errMsg );
354 $out->setStatusCode( 404 );
355 }
356
357 return;
358 }
359
360 // Read the rows into an array and remove duplicates
361 // templatelinks comes third so that the templatelinks row overwrites the
362 // pagelinks/redirect row, so we get (inclusion) rather than nothing
363 $rows = [];
364 if ( $fetchredirs ) {
365 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $rdRes is declared when fetching redirs
366 foreach ( $rdRes as $row ) {
367 $row->is_template = 0;
368 $row->is_image = 0;
369 $rows[$row->page_id] = $row;
370 }
371 }
372 if ( !$hidelinks ) {
373 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $plRes is declared when fetching links
374 foreach ( $plRes as $row ) {
375 $row->is_template = 0;
376 $row->is_image = 0;
377 $rows[$row->page_id] = $row;
378 }
379 }
380 if ( !$hidetrans ) {
381 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $tlRes is declared when fetching trans
382 foreach ( $tlRes as $row ) {
383 $row->is_template = 1;
384 $row->is_image = 0;
385 $rows[$row->page_id] = $row;
386 }
387 }
388 if ( !$hideimages ) {
389 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $ilRes is declared when fetching images
390 foreach ( $ilRes as $row ) {
391 $row->is_template = 0;
392 $row->is_image = 1;
393 $rows[$row->page_id] = $row;
394 }
395 }
396
397 // Sort by namespace + page ID, changing the keys to 0-based indices
398 usort( $rows, static function ( $rowA, $rowB ) {
399 if ( $rowA->page_namespace !== $rowB->page_namespace ) {
400 return $rowA->page_namespace < $rowB->page_namespace ? -1 : 1;
401 }
402 if ( $rowA->page_id !== $rowB->page_id ) {
403 return $rowA->page_id < $rowB->page_id ? -1 : 1;
404 }
405 return 0;
406 } );
407
408 $numRows = count( $rows );
409
410 // Work out the start and end IDs, for prev/next links
411 if ( !$limit ) { // T289351
412 $nextNamespace = $nextPageId = $prevNamespace = $prevPageId = false;
413 $rows = [];
414 } elseif ( $dir === 'prev' ) {
415 if ( $numRows > $limit ) {
416 // More rows available after these ones
417 // Get the next row from the last row in the result set
418 $nextNamespace = $rows[$limit]->page_namespace;
419 $nextPageId = $rows[$limit]->page_id;
420 // Remove undisplayed rows, for dir='prev' we need to discard first record after sorting
421 $rows = array_slice( $rows, 1, $limit );
422 // Get the prev row from the first displayed row
423 $prevNamespace = $rows[0]->page_namespace;
424 $prevPageId = $rows[0]->page_id;
425 } else {
426 // Get the next row from the last displayed row
427 $nextNamespace = $rows[$numRows - 1]->page_namespace;
428 $nextPageId = $rows[$numRows - 1]->page_id;
429 $prevNamespace = false;
430 $prevPageId = false;
431 }
432 } else {
433 // If offset is not set disable prev link
434 $prevNamespace = $offsetPageID ? $rows[0]->page_namespace : false;
435 $prevPageId = $offsetPageID ? $rows[0]->page_id : false;
436 if ( $numRows > $limit ) {
437 // Get the next row from the last displayed row
438 $nextNamespace = $rows[$limit - 1]->page_namespace ?? false;
439 $nextPageId = $rows[$limit - 1]->page_id ?? false;
440 // Remove undisplayed rows
441 $rows = array_slice( $rows, 0, $limit );
442 } else {
443 $nextNamespace = false;
444 $nextPageId = false;
445 }
446 }
447
448 // use LinkBatch to make sure, that all required data (associated with Titles)
449 // is loaded in one query
450 $lb = $this->linkBatchFactory->newLinkBatch();
451 foreach ( $rows as $row ) {
452 $lb->add( $row->page_namespace, $row->page_title );
453 }
454 $lb->execute();
455
456 if ( $level == 0 && !$this->including() ) {
457 $link = $this->getLinkRenderer()->makeLink(
458 $this->target,
459 null,
460 [],
461 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
462 );
463
464 $msg = $this->msg( 'linkshere' )
465 ->params( $this->target->getPrefixedText() )
466 ->rawParams( $link )
467 ->parseAsBlock();
468 $out->addHTML( $msg );
469
470 $out->addWikiMsg( 'whatlinkshere-count', Message::numParam( count( $rows ) ) );
471
472 $prevnext = $this->getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId );
473 $out->addHTML( $prevnext );
474 }
475 $out->addHTML( $this->listStart( $level ) );
476 foreach ( $rows as $row ) {
477 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
478
479 if ( $row->rd_from && $level < 2 ) {
480 $out->addHTML( $this->listItem( $row, $nt, $target, true ) );
481 $this->showIndirectLinks(
482 $level + 1,
483 $nt,
484 $this->getConfig()->get( MainConfigNames::MaxRedirectLinksRetrieved )
485 );
486 $out->addHTML( Xml::closeElement( 'li' ) );
487 } else {
488 $out->addHTML( $this->listItem( $row, $nt, $target ) );
489 }
490 }
491
492 $out->addHTML( $this->listEnd() );
493
494 if ( $level == 0 && !$this->including() ) {
495 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable $prevnext is defined with $level is 0
496 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable prevnext is set when used
497 $out->addHTML( $prevnext );
498 }
499 }
500
501 protected function listStart( $level ) {
502 return Xml::openElement( 'ul', ( $level ? [] : [ 'id' => 'mw-whatlinkshere-list' ] ) );
503 }
504
505 private function listItem( stdClass $row, PageIdentity $nt, LinkTarget $target, bool $notClose = false ) {
506 $legacyTitle = $this->titleFactory->newFromPageIdentity( $nt );
507
508 if ( $row->rd_from ) {
509 $query = [ 'redirect' => 'no' ];
510 } else {
511 $query = [];
512 }
513
514 $dir = $this->getLanguage()->getDir();
515 $link = Html::rawElement( 'bdi', [ 'dir' => $dir ], $this->getLinkRenderer()->makeKnownLink(
516 $nt,
517 null,
518 $row->page_is_redirect ? [ 'class' => 'mw-redirect' ] : [],
519 $query
520 ) );
521
522 // Display properties (redirect or template)
523 $propsText = '';
524 $props = [];
525 if ( (string)$row->rd_fragment !== '' ) {
526 $props[] = $this->msg( 'whatlinkshere-sectionredir' )
527 ->rawParams( $this->getLinkRenderer()->makeLink(
528 $target->createFragmentTarget( $row->rd_fragment ),
529 $row->rd_fragment
530 ) )->escaped();
531 } elseif ( $row->rd_from ) {
532 $props[] = $this->msg( 'isredirect' )->escaped();
533 }
534 if ( $row->is_template ) {
535 $props[] = $this->msg( 'istemplate' )->escaped();
536 }
537 if ( $row->is_image ) {
538 $props[] = $this->msg( 'isimage' )->escaped();
539 }
540
541 $legacyTarget = $this->titleFactory->newFromLinkTarget( $target );
542 $this->getHookRunner()->onWhatLinksHereProps( $row, $legacyTitle, $legacyTarget, $props );
543
544 if ( count( $props ) ) {
545 $propsText = $this->msg( 'parentheses' )
546 ->rawParams( $this->getLanguage()->semicolonList( $props ) )->escaped();
547 }
548
549 # Space for utilities links, with a what-links-here link provided
550 $wlhLink = $this->wlhLink(
551 $legacyTitle,
552 $this->msg( 'whatlinkshere-links' )->text(),
553 $this->msg( 'editlink' )->text()
554 );
555 $wlh = Html::rawElement(
556 'span',
557 [ 'class' => 'mw-whatlinkshere-tools' ],
558 $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped()
559 );
560
561 return $notClose ?
562 Xml::openElement( 'li' ) . "$link $propsText $wlh\n" :
563 Xml::tags( 'li', null, "$link $propsText $wlh" ) . "\n";
564 }
565
566 protected function listEnd() {
567 return Xml::closeElement( 'ul' );
568 }
569
570 protected function wlhLink( Title $target, $text, $editText ) {
571 static $title = null;
572 $title ??= $this->getPageTitle();
573
574 $linkRenderer = $this->getLinkRenderer();
575
576 // always show a "<- Links" link
577 $links = [
578 'links' => $linkRenderer->makeKnownLink(
579 $title,
580 $text,
581 [],
582 [ 'target' => $target->getPrefixedText() ]
583 ),
584 ];
585
586 // if the page is editable, add an edit link
587 if (
588 // check user permissions
589 $this->getAuthority()->isAllowed( 'edit' ) &&
590 // check, if the content model is editable through action=edit
591 $this->contentHandlerFactory->getContentHandler( $target->getContentModel() )
592 ->supportsDirectEditing()
593 ) {
594 $links['edit'] = $linkRenderer->makeKnownLink(
595 $target,
596 $editText,
597 [],
598 [ 'action' => 'edit' ]
599 );
600 }
601
602 // build the links html
603 return $this->getLanguage()->pipeList( $links );
604 }
605
606 private function getPrevNext( $prevNamespace, $prevPageId, $nextNamespace, $nextPageId ) {
607 $navBuilder = new PagerNavigationBuilder( $this->getContext() );
608
609 $navBuilder
610 ->setPage( $this->getPageTitle( $this->target->getPrefixedDBkey() ) )
611 // Remove 'target', already included in the request title
612 ->setLinkQuery(
613 array_diff_key(
614 array_filter(
615 $this->formData,
616 fn ( $value ) => $value !== null && $value !== '' && $value !== false
617 ),
618 [ 'target' => null, 'from' => null ]
619 )
620 )
621 ->setLimits( self::LIMITS )
622 ->setLimitLinkQueryParam( 'limit' )
623 ->setCurrentLimit( $this->formData['limit'] )
624 ->setPrevMsg( 'whatlinkshere-prev' )
625 ->setNextMsg( 'whatlinkshere-next' );
626
627 if ( $prevPageId != 0 ) {
628 $navBuilder->setPrevLinkQuery( [ 'dir' => 'prev', 'offset' => "$prevNamespace|$prevPageId" ] );
629 }
630 if ( $nextPageId != 0 ) {
631 $navBuilder->setNextLinkQuery( [ 'dir' => 'next', 'offset' => "$nextNamespace|$nextPageId" ] );
632 }
633
634 return $navBuilder->getHtml();
635 }
636
637 protected function getFormFields() {
638 $this->addHelpLink( 'Help:What links here' );
639 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
640
641 $fields = [
642 'target' => [
643 'type' => 'title',
644 'name' => 'target',
645 'id' => 'mw-whatlinkshere-target',
646 'label-message' => 'whatlinkshere-page',
647 'section' => 'whatlinkshere-target',
648 'creatable' => true,
649 ],
650 'namespace' => [
651 'type' => 'namespaceselect',
652 'name' => 'namespace',
653 'id' => 'namespace',
654 'label-message' => 'namespace',
655 'all' => '',
656 'default' => '',
657 'in-user-lang' => true,
658 'section' => 'whatlinkshere-ns',
659 ],
660 'invert' => [
661 'type' => 'check',
662 'name' => 'invert',
663 'id' => 'nsinvert',
664 'hide-if' => [ '===', 'namespace', '' ],
665 'label-message' => 'invert',
666 'help-message' => 'tooltip-whatlinkshere-invert',
667 'help-inline' => false,
668 'section' => 'whatlinkshere-ns'
669 ],
670 'limit' => [
671 'type' => 'hidden',
672 'name' => 'limit',
673 'default' => $this->getConfig()->get( MainConfigNames::QueryPageDefaultLimit ),
674 'filter-callback' => static fn ( $value ) => max( 0, min( intval( $value ), 5000 ) ),
675 ],
676 'offset' => [
677 'type' => 'api',
678 'name' => 'offset',
679 'default' => '',
680 ],
681 'dir' => [
682 'type' => 'api',
683 'name' => 'dir',
684 ],
685 'from' => [
686 'type' => 'api',
687 'name' => 'from',
688 'default' => 0,
689 ]
690 ];
691
692 $filters = [ 'hidetrans', 'hidelinks', 'hideredirs' ];
693
694 // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
695 // 'whatlinkshere-hidelinks'
696 // To be sure they will be found by grep
697 foreach ( $filters as $filter ) {
698 // Parameter only provided for backwards-compatibility with old translations
699 $hide = $this->msg( 'hide' )->text();
700 $msg = $this->msg( "whatlinkshere-{$filter}", $hide )->text();
701 $fields[$filter] = [
702 'type' => 'check',
703 'name' => $filter,
704 'label' => $msg,
705 'section' => 'whatlinkshere-filter',
706 ];
707 }
708
709 return $fields;
710 }
711
712 protected function alterForm( HTMLForm $form ) {
713 // This parameter from the subpage syntax is only added after constructing the form,
714 // so we should add the dynamic field that depends on the user input here.
715
716 // TODO: This looks not good. Ideally we can initialize it in onSubmit().
717 // Maybe extend the hide-if feature to match prefixes on the client side.
718 $this->target = Title::newFromText( $this->getRequest()->getText( 'target' ) );
719 if ( $this->target && $this->target->getNamespace() == NS_FILE ) {
720 $hide = $this->msg( 'hide' )->text();
721 $msg = $this->msg( 'whatlinkshere-hideimages', $hide )->text();
722 $form->addFields( [
723 'hideimages' => [
724 'type' => 'check',
725 'name' => 'hideimages',
726 'label' => $msg,
727 'section' => 'whatlinkshere-filter',
728 ]
729 ] );
730 }
731
732 $form->setWrapperLegendMsg( 'whatlinkshere' )
733 ->setSubmitTextMsg( 'whatlinkshere-submit' );
734 }
735
736 protected function getShowAlways() {
737 return true;
738 }
739
740 protected function getSubpageField() {
741 return 'target';
742 }
743
744 public function onSubmit( array $data ) {
745 $this->formData = $data;
746 return true;
747 }
748
749 public function requiresPost() {
750 return false;
751 }
752
753 protected function getDisplayFormat() {
754 return 'ooui';
755 }
756
765 public function prefixSearchSubpages( $search, $limit, $offset ) {
766 return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
767 }
768
769 protected function getGroupName() {
770 return 'pagetools';
771 }
772}
773
778class_alias( SpecialWhatLinksHere::class, 'SpecialWhatLinksHere' );
const NS_FILE
Definition Defines.php:71
const NS_MAIN
Definition Defines.php:65
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
setWrapperLegendMsg( $msg)
Prompt the whole form to be wrapped in a "<fieldset>", with this message as its "<legend>" element.
addFields( $descriptor)
Add fields to the form.
Definition HTMLForm.php:496
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition Html.php:240
static closeElement( $element)
Returns "</$element>".
Definition Html.php:304
Service for compat reading of links tables.
A class containing constants representing the names of configuration variables.
const MaxRedirectLinksRetrieved
Name constant for the MaxRedirectLinksRetrieved setting, for use with Config::get()
const QueryPageDefaultLimit
Name constant for the QueryPageDefaultLimit setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:155
Build the navigation for a pager, with links to prev/next page, links to change limits,...
Special page which uses an HTMLForm to handle processing.
string null $par
The subpage of the special page.
getSkin()
Shortcut to get the skin being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
Implements Special:Whatlinkshere.
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.
requiresPost()
Whether this action should using POST method to submit, default to true.
setParameter( $par)
Get a better-looking target title from the subpage syntax.
wlhLink(Title $target, $text, $editText)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
__construct(IConnectionProvider $dbProvider, LinkBatchFactory $linkBatchFactory, IContentHandlerFactory $contentHandlerFactory, SearchEngineFactory $searchEngineFactory, NamespaceInfo $namespaceInfo, TitleFactory $titleFactory, LinksMigration $linksMigration)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getDisplayFormat()
Get display format for the form.
getFormFields()
Get an HTMLForm descriptor array.
onSubmit(array $data)
Process the form on submission.
alterForm(HTMLForm $form)
Play with the HTMLForm if you need to more substantially.
onSuccess()
We want the result displayed after the form, so we use this instead of onSubmit()
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates Title objects.
Represents a title within MediaWiki.
Definition Title.php:78
getContentModel( $flags=0)
Get the page's content model id, see the CONTENT_MODEL_XXX constants.
Definition Title.php:1065
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1858
Module of static functions for generating XML.
Definition Xml.php:37
Factory class for SearchEngine.
Build SELECT queries with a fluent interface.
Represents the target of a wiki link.
createFragmentTarget(string $fragment)
Create a new LinkTarget with a different fragment on the same page.
Interface for objects (potentially) representing an editable wiki page.
Provide primary and replica IDatabase connections.
A database connection without write operations.