MediaWiki REL1_34
SpecialWhatLinksHere.php
Go to the documentation of this file.
1<?php
26
34 protected $opts;
35
36 protected $selfTitle;
37
39 protected $target;
40
41 protected $limits = [ 20, 50, 100, 250, 500 ];
42
43 public function __construct() {
44 parent::__construct( 'Whatlinkshere' );
45 }
46
47 function execute( $par ) {
48 $out = $this->getOutput();
49
50 $this->setHeaders();
51 $this->outputHeader();
52 $this->addHelpLink( 'Help:What links here' );
53
54 $opts = new FormOptions();
55
56 $opts->add( 'target', '' );
57 $opts->add( 'namespace', '', FormOptions::INTNULL );
58 $opts->add( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
59 $opts->add( 'from', 0 );
60 $opts->add( 'back', 0 );
61 $opts->add( 'hideredirs', false );
62 $opts->add( 'hidetrans', false );
63 $opts->add( 'hidelinks', false );
64 $opts->add( 'hideimages', false );
65 $opts->add( 'invert', false );
66
68 $opts->validateIntBounds( 'limit', 0, 5000 );
69
70 // Give precedence to subpage syntax
71 if ( $par !== null ) {
72 $opts->setValue( 'target', $par );
73 }
74
75 // Bind to member variable
76 $this->opts = $opts;
77
78 $this->target = Title::newFromText( $opts->getValue( 'target' ) );
79 if ( !$this->target ) {
80 if ( !$this->including() ) {
81 $out->addHTML( $this->whatlinkshereForm() );
82 }
83
84 return;
85 }
86
87 $this->getSkin()->setRelevantTitle( $this->target );
88
89 $this->selfTitle = $this->getPageTitle( $this->target->getPrefixedDBkey() );
90
91 $out->setPageTitle( $this->msg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
92 $out->addBacklinkSubtitle( $this->target );
93 $this->showIndirectLinks(
94 0,
95 $this->target,
96 $opts->getValue( 'limit' ),
97 $opts->getValue( 'from' ),
98 $opts->getValue( 'back' )
99 );
100 }
101
109 function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
110 $out = $this->getOutput();
112
113 $hidelinks = $this->opts->getValue( 'hidelinks' );
114 $hideredirs = $this->opts->getValue( 'hideredirs' );
115 $hidetrans = $this->opts->getValue( 'hidetrans' );
116 $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
117
118 $fetchlinks = ( !$hidelinks || !$hideredirs );
119
120 // Build query conds in concert for all three tables...
121 $conds = [];
122 $conds['pagelinks'] = [
123 'pl_namespace' => $target->getNamespace(),
124 'pl_title' => $target->getDBkey(),
125 ];
126 $conds['templatelinks'] = [
127 'tl_namespace' => $target->getNamespace(),
128 'tl_title' => $target->getDBkey(),
129 ];
130 $conds['imagelinks'] = [
131 'il_to' => $target->getDBkey(),
132 ];
133
134 $namespace = $this->opts->getValue( 'namespace' );
135 $invert = $this->opts->getValue( 'invert' );
136 $nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace );
137 if ( is_int( $namespace ) ) {
138 $conds['pagelinks'][] = "pl_from_namespace $nsComparison";
139 $conds['templatelinks'][] = "tl_from_namespace $nsComparison";
140 $conds['imagelinks'][] = "il_from_namespace $nsComparison";
141 }
142
143 if ( $from ) {
144 $conds['templatelinks'][] = "tl_from >= $from";
145 $conds['pagelinks'][] = "pl_from >= $from";
146 $conds['imagelinks'][] = "il_from >= $from";
147 }
148
149 if ( $hideredirs ) {
150 $conds['pagelinks']['rd_from'] = null;
151 } elseif ( $hidelinks ) {
152 $conds['pagelinks'][] = 'rd_from is NOT NULL';
153 }
154
155 $queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use (
156 $conds, $target, $limit
157 ) {
158 // Read an extra row as an at-end check
159 $queryLimit = $limit + 1;
160 $on = [
161 "rd_from = $fromCol",
162 'rd_title' => $target->getDBkey(),
163 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
164 ];
165 $on['rd_namespace'] = $target->getNamespace();
166 // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
167 $subQuery = $dbr->buildSelectSubquery(
168 [ $table, 'redirect', 'page' ],
169 [ $fromCol, 'rd_from' ],
170 $conds[$table],
171 __CLASS__ . '::showIndirectLinks',
172 // Force JOIN order per T106682 to avoid large filesorts
173 [ 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ],
174 [
175 'page' => [ 'JOIN', "$fromCol = page_id" ],
176 'redirect' => [ 'LEFT JOIN', $on ]
177 ]
178 );
179 return $dbr->select(
180 [ 'page', 'temp_backlink_range' => $subQuery ],
181 [ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ],
182 [],
183 __CLASS__ . '::showIndirectLinks',
184 [ 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ],
185 [ 'page' => [ 'JOIN', "$fromCol = page_id" ] ]
186 );
187 };
188
189 if ( $fetchlinks ) {
190 $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
191 }
192
193 if ( !$hidetrans ) {
194 $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
195 }
196
197 if ( !$hideimages ) {
198 $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
199 }
200
201 if ( ( !$fetchlinks || !$plRes->numRows() )
202 && ( $hidetrans || !$tlRes->numRows() )
203 && ( $hideimages || !$ilRes->numRows() )
204 ) {
205 if ( $level == 0 && !$this->including() ) {
206 $out->addHTML( $this->whatlinkshereForm() );
207
208 // Show filters only if there are links
209 if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
210 $out->addHTML( $this->getFilterPanel() );
211 }
212 $msgKey = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
213 $link = $this->getLinkRenderer()->makeLink(
214 $this->target,
215 null,
216 [],
217 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
218 );
219
220 $errMsg = $this->msg( $msgKey )
221 ->params( $this->target->getPrefixedText() )
222 ->rawParams( $link )
223 ->parseAsBlock();
224 $out->addHTML( $errMsg );
225 $out->setStatusCode( 404 );
226 }
227
228 return;
229 }
230
231 // Read the rows into an array and remove duplicates
232 // templatelinks comes second so that the templatelinks row overwrites the
233 // pagelinks row, so we get (inclusion) rather than nothing
234 $rows = [];
235 if ( $fetchlinks ) {
236 foreach ( $plRes as $row ) {
237 $row->is_template = 0;
238 $row->is_image = 0;
239 $rows[$row->page_id] = $row;
240 }
241 }
242 if ( !$hidetrans ) {
243 foreach ( $tlRes as $row ) {
244 $row->is_template = 1;
245 $row->is_image = 0;
246 $rows[$row->page_id] = $row;
247 }
248 }
249 if ( !$hideimages ) {
250 foreach ( $ilRes as $row ) {
251 $row->is_template = 0;
252 $row->is_image = 1;
253 $rows[$row->page_id] = $row;
254 }
255 }
256
257 // Sort by key and then change the keys to 0-based indices
258 ksort( $rows );
259 $rows = array_values( $rows );
260
261 $numRows = count( $rows );
262
263 // Work out the start and end IDs, for prev/next links
264 if ( $numRows > $limit ) {
265 // More rows available after these ones
266 // Get the ID from the last row in the result set
267 $nextId = $rows[$limit]->page_id;
268 // Remove undisplayed rows
269 $rows = array_slice( $rows, 0, $limit );
270 } else {
271 // No more rows after
272 $nextId = false;
273 }
274 $prevId = $from;
275
276 // use LinkBatch to make sure, that all required data (associated with Titles)
277 // is loaded in one query
278 $lb = new LinkBatch();
279 foreach ( $rows as $row ) {
280 $lb->add( $row->page_namespace, $row->page_title );
281 }
282 $lb->execute();
283
284 if ( $level == 0 && !$this->including() ) {
285 $out->addHTML( $this->whatlinkshereForm() );
286 $out->addHTML( $this->getFilterPanel() );
287
288 $link = $this->getLinkRenderer()->makeLink(
289 $this->target,
290 null,
291 [],
292 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
293 );
294
295 $msg = $this->msg( 'linkshere' )
296 ->params( $this->target->getPrefixedText() )
297 ->rawParams( $link )
298 ->parseAsBlock();
299 $out->addHTML( $msg );
300
301 $prevnext = $this->getPrevNext( $prevId, $nextId );
302 $out->addHTML( $prevnext );
303 }
304 $out->addHTML( $this->listStart( $level ) );
305 foreach ( $rows as $row ) {
306 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
307
308 if ( $row->rd_from && $level < 2 ) {
309 $out->addHTML( $this->listItem( $row, $nt, $target, true ) );
310 $this->showIndirectLinks(
311 $level + 1,
312 $nt,
313 $this->getConfig()->get( 'MaxRedirectLinksRetrieved' )
314 );
315 $out->addHTML( Xml::closeElement( 'li' ) );
316 } else {
317 $out->addHTML( $this->listItem( $row, $nt, $target ) );
318 }
319 }
320
321 $out->addHTML( $this->listEnd() );
322
323 if ( $level == 0 && !$this->including() ) {
324 $out->addHTML( $prevnext );
325 }
326 }
327
328 protected function listStart( $level ) {
329 return Xml::openElement( 'ul', ( $level ? [] : [ 'id' => 'mw-whatlinkshere-list' ] ) );
330 }
331
332 protected function listItem( $row, $nt, $target, $notClose = false ) {
333 $dirmark = $this->getLanguage()->getDirMark();
334
335 # local message cache
336 static $msgcache = null;
337 if ( $msgcache === null ) {
338 static $msgs = [ 'isredirect', 'istemplate', 'semicolon-separator',
339 'whatlinkshere-links', 'isimage', 'editlink' ];
340 $msgcache = [];
341 foreach ( $msgs as $msg ) {
342 $msgcache[$msg] = $this->msg( $msg )->escaped();
343 }
344 }
345
346 if ( $row->rd_from ) {
347 $query = [ 'redirect' => 'no' ];
348 } else {
349 $query = [];
350 }
351
352 $link = $this->getLinkRenderer()->makeKnownLink(
353 $nt,
354 null,
355 $row->page_is_redirect ? [ 'class' => 'mw-redirect' ] : [],
356 $query
357 );
358
359 // Display properties (redirect or template)
360 $propsText = '';
361 $props = [];
362 if ( $row->rd_from ) {
363 $props[] = $msgcache['isredirect'];
364 }
365 if ( $row->is_template ) {
366 $props[] = $msgcache['istemplate'];
367 }
368 if ( $row->is_image ) {
369 $props[] = $msgcache['isimage'];
370 }
371
372 Hooks::run( 'WhatLinksHereProps', [ $row, $nt, $target, &$props ] );
373
374 if ( count( $props ) ) {
375 $propsText = $this->msg( 'parentheses' )
376 ->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
377 }
378
379 # Space for utilities links, with a what-links-here link provided
380 $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'], $msgcache['editlink'] );
381 $wlh = Xml::wrapClass(
382 $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(),
383 'mw-whatlinkshere-tools'
384 );
385
386 return $notClose ?
387 Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
388 Xml::tags( 'li', null, "$link $propsText $dirmark $wlh" ) . "\n";
389 }
390
391 protected function listEnd() {
392 return Xml::closeElement( 'ul' );
393 }
394
395 protected function wlhLink( Title $target, $text, $editText ) {
396 static $title = null;
397 if ( $title === null ) {
398 $title = $this->getPageTitle();
399 }
400
402
403 if ( $text !== null ) {
404 $text = new HtmlArmor( $text );
405 }
406
407 // always show a "<- Links" link
408 $links = [
409 'links' => $linkRenderer->makeKnownLink(
410 $title,
411 $text,
412 [],
413 [ 'target' => $target->getPrefixedText() ]
414 ),
415 ];
416
417 // if the page is editable, add an edit link
418 if (
419 // check user permissions
420 MediaWikiServices::getInstance()
422 ->userHasRight( $this->getUser(), 'edit' ) &&
423 // check, if the content model is editable through action=edit
424 ContentHandler::getForTitle( $target )->supportsDirectEditing()
425 ) {
426 if ( $editText !== null ) {
427 $editText = new HtmlArmor( $editText );
428 }
429
430 $links['edit'] = $linkRenderer->makeKnownLink(
431 $target,
432 $editText,
433 [],
434 [ 'action' => 'edit' ]
435 );
436 }
437
438 // build the links html
439 return $this->getLanguage()->pipeList( $links );
440 }
441
442 function makeSelfLink( $text, $query ) {
443 if ( $text !== null ) {
444 $text = new HtmlArmor( $text );
445 }
446
447 return $this->getLinkRenderer()->makeKnownLink(
448 $this->selfTitle,
449 $text,
450 [],
451 $query
452 );
453 }
454
455 function getPrevNext( $prevId, $nextId ) {
456 $currentLimit = $this->opts->getValue( 'limit' );
457 $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
458 $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
459
460 $changed = $this->opts->getChangedValues();
461 unset( $changed['target'] ); // Already in the request title
462
463 if ( $prevId != 0 ) {
464 $overrides = [ 'from' => $this->opts->getValue( 'back' ) ];
465 $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
466 }
467 if ( $nextId != 0 ) {
468 $overrides = [ 'from' => $nextId, 'back' => $prevId ];
469 $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
470 }
471
472 $limitLinks = [];
473 $lang = $this->getLanguage();
474 foreach ( $this->limits as $limit ) {
475 $prettyLimit = htmlspecialchars( $lang->formatNum( $limit ) );
476 $overrides = [ 'limit' => $limit ];
477 $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
478 }
479
480 $nums = $lang->pipeList( $limitLinks );
481
482 return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
483 }
484
485 function whatlinkshereForm() {
486 // We get nicer value from the title object
487 $this->opts->consumeValue( 'target' );
488 // Reset these for new requests
489 $this->opts->consumeValues( [ 'back', 'from' ] );
490
491 $target = $this->target ? $this->target->getPrefixedText() : '';
492 $namespace = $this->opts->consumeValue( 'namespace' );
493 $nsinvert = $this->opts->consumeValue( 'invert' );
494
495 # Build up the form
496 $f = Xml::openElement( 'form', [ 'action' => wfScript() ] );
497
498 # Values that should not be forgotten
499 $f .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
500 foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
501 $f .= Html::hidden( $name, $value );
502 }
503
504 $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
505
506 # Target input (.mw-searchInput enables suggestions)
507 $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
508 'mw-whatlinkshere-target', 40, $target, [ 'class' => 'mw-searchInput' ] );
509
510 $f .= ' ';
511
512 # Namespace selector
513 $f .= Html::namespaceSelector(
514 [
515 'selected' => $namespace,
516 'all' => '',
517 'label' => $this->msg( 'namespace' )->text(),
518 'in-user-lang' => true,
519 ], [
520 'name' => 'namespace',
521 'id' => 'namespace',
522 'class' => 'namespaceselector',
523 ]
524 );
525
526 $f .= "\u{00A0}" .
527 Xml::checkLabel(
528 $this->msg( 'invert' )->text(),
529 'invert',
530 'nsinvert',
531 $nsinvert,
532 [ 'title' => $this->msg( 'tooltip-whatlinkshere-invert' )->text() ]
533 );
534
535 $f .= ' ';
536
537 # Submit
538 $f .= Xml::submitButton( $this->msg( 'whatlinkshere-submit' )->text() );
539
540 # Close
541 $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
542
543 return $f;
544 }
545
551 function getFilterPanel() {
552 $show = $this->msg( 'show' )->escaped();
553 $hide = $this->msg( 'hide' )->escaped();
554
555 $changed = $this->opts->getChangedValues();
556 unset( $changed['target'] ); // Already in the request title
557
558 $links = [];
559 $types = [ 'hidetrans', 'hidelinks', 'hideredirs' ];
560 if ( $this->target->getNamespace() == NS_FILE ) {
561 $types[] = 'hideimages';
562 }
563
564 // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
565 // 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
566 // To be sure they will be found by grep
567 foreach ( $types as $type ) {
568 $chosen = $this->opts->getValue( $type );
569 $msg = $chosen ? $show : $hide;
570 $overrides = [ $type => !$chosen ];
571 $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
572 $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
573 }
574
575 return Xml::fieldset(
576 $this->msg( 'whatlinkshere-filters' )->text(),
577 $this->getLanguage()->pipeList( $links )
578 );
579 }
580
589 public function prefixSearchSubpages( $search, $limit, $offset ) {
590 return $this->prefixSearchString( $search, $limit, $offset );
591 }
592
593 protected function getGroupName() {
594 return 'pagetools';
595 }
596}
getPermissionManager()
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Helper class to keep track of options when mixing links and form elements.
add( $name, $default, $type=self::AUTO)
Add an option to be handled by this FormOptions instance.
setValue( $name, $value, $force=false)
Use to set the value of an option.
fetchValuesFromRequest(WebRequest $r, $optionKeys=null)
Fetch values for all options (or selected options) from the given WebRequest, making them available f...
validateIntBounds( $name, $min, $max)
const INTNULL
Integer type or null, maps to WebRequest::getIntOrNull() This is useful for the namespace selector.
getValue( $name)
Get the value for the given option name.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:28
Shortcut to construct an includable special page.
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition LinkBatch.php:34
MediaWikiServices is the service locator for the application scope of MediaWiki.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getSkin()
Shortcut to get the skin being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
prefixSearchString( $search, $limit, $offset)
Perform a regular substring search for prefixSearchSubpages.
MediaWiki Linker LinkRenderer null $linkRenderer
Implements Special:Whatlinkshere.
execute( $par)
Default execute method Checks user permissions.
wlhLink(Title $target, $text, $editText)
listItem( $row, $nt, $target, $notClose=false)
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
getFilterPanel()
Create filter panel.
showIndirectLinks( $level, $target, $limit, $from=0, $back=0)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
getPrevNext( $prevId, $nextId)
Represents a title within MediaWiki.
Definition Title.php:42
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getDBkey()
Get the main part with underscores.
Definition Title.php:1013
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1818
const NS_FILE
Definition Defines.php:75
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang