MediaWiki fundraising/REL1_35
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 public 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 private 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 // For historical reasons `pagelinks` always contains an entry for the redirect target.
119 // So we only need to query `redirect` if `pagelinks` isn't being queried.
120 $fetchredirs = $hidelinks && !$hideredirs;
121
122 // Build query conds in concert for all four tables...
123 $conds = [];
124 $conds['redirect'] = [
125 'rd_namespace' => $target->getNamespace(),
126 'rd_title' => $target->getDBkey(),
127 ];
128 $conds['pagelinks'] = [
129 'pl_namespace' => $target->getNamespace(),
130 'pl_title' => $target->getDBkey(),
131 ];
132 $conds['templatelinks'] = [
133 'tl_namespace' => $target->getNamespace(),
134 'tl_title' => $target->getDBkey(),
135 ];
136 $conds['imagelinks'] = [
137 'il_to' => $target->getDBkey(),
138 ];
139
140 $namespace = $this->opts->getValue( 'namespace' );
141 $invert = $this->opts->getValue( 'invert' );
142 $nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace );
143 if ( is_int( $namespace ) ) {
144 $conds['redirect'][] = "page_namespace $nsComparison";
145 $conds['pagelinks'][] = "pl_from_namespace $nsComparison";
146 $conds['templatelinks'][] = "tl_from_namespace $nsComparison";
147 $conds['imagelinks'][] = "il_from_namespace $nsComparison";
148 }
149
150 if ( $from ) {
151 $conds['redirect'][] = "rd_from >= $from";
152 $conds['templatelinks'][] = "tl_from >= $from";
153 $conds['pagelinks'][] = "pl_from >= $from";
154 $conds['imagelinks'][] = "il_from >= $from";
155 }
156
157 if ( $hideredirs ) {
158 // For historical reasons `pagelinks` always contains an entry for the redirect target.
159 // So we hide that link when $hideredirs is set. There's unfortunately no way to tell when a
160 // redirect's content also links to the target.
161 $conds['pagelinks']['rd_from'] = null;
162 }
163
164 $queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use (
165 $conds, $target, $limit
166 ) {
167 // Read an extra row as an at-end check
168 $queryLimit = $limit + 1;
169 $on = [
170 "rd_from = $fromCol",
171 'rd_title' => $target->getDBkey(),
172 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
173 ];
174 $on['rd_namespace'] = $target->getNamespace();
175 // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
176 $subQuery = $dbr->newSelectQueryBuilder()
177 ->table( $table )
178 ->fields( [ $fromCol, 'rd_from', 'rd_fragment' ] )
179 ->conds( $conds[$table] )
180 ->orderBy( $fromCol )
181 ->limit( 2 * $queryLimit )
182 ->leftJoin( 'redirect', 'redirect', $on );
183
184 return $dbr->newSelectQueryBuilder()
185 ->table( $subQuery, 'temp_backlink_range' )
186 ->join( 'page', 'page', "$fromCol = page_id" )
187 ->fields( [ 'page_id', 'page_namespace', 'page_title',
188 'rd_from', 'rd_fragment', 'page_is_redirect' ] )
189 ->orderBy( 'page_id' )
190 ->limit( $queryLimit )
191 ->caller( __CLASS__ . '::showIndirectLinks' )
192 ->fetchResultSet();
193 };
194
195 if ( $fetchredirs ) {
196 $rdRes = $dbr->select(
197 [ 'redirect', 'page' ],
198 [ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'rd_fragment', 'page_is_redirect' ],
199 $conds['redirect'],
200 __METHOD__,
201 [ 'ORDER BY' => 'rd_from', 'LIMIT' => $limit + 1 ],
202 [ 'page' => [ 'JOIN', 'rd_from = page_id' ] ]
203 );
204 }
205
206 if ( !$hidelinks ) {
207 $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
208 }
209
210 if ( !$hidetrans ) {
211 $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
212 }
213
214 if ( !$hideimages ) {
215 $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
216 }
217
218 if ( ( !$fetchredirs || !$rdRes->numRows() )
219 && ( $hidelinks || !$plRes->numRows() )
220 && ( $hidetrans || !$tlRes->numRows() )
221 && ( $hideimages || !$ilRes->numRows() )
222 ) {
223 if ( $level == 0 && !$this->including() ) {
224 $out->addHTML( $this->whatlinkshereForm() );
225
226 // Show filters only if there are links
227 if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
228 $out->addHTML( $this->getFilterPanel() );
229 }
230 $msgKey = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
231 $link = $this->getLinkRenderer()->makeLink(
232 $this->target,
233 null,
234 [],
235 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
236 );
237
238 $errMsg = $this->msg( $msgKey )
239 ->params( $this->target->getPrefixedText() )
240 ->rawParams( $link )
241 ->parseAsBlock();
242 $out->addHTML( $errMsg );
243 $out->setStatusCode( 404 );
244 }
245
246 return;
247 }
248
249 // Read the rows into an array and remove duplicates
250 // templatelinks comes third so that the templatelinks row overwrites the
251 // pagelinks/redirect row, so we get (inclusion) rather than nothing
252 $rows = [];
253 if ( $fetchredirs ) {
254 foreach ( $rdRes as $row ) {
255 $row->is_template = 0;
256 $row->is_image = 0;
257 $rows[$row->page_id] = $row;
258 }
259 }
260 if ( !$hidelinks ) {
261 foreach ( $plRes as $row ) {
262 $row->is_template = 0;
263 $row->is_image = 0;
264 $rows[$row->page_id] = $row;
265 }
266 }
267 if ( !$hidetrans ) {
268 foreach ( $tlRes as $row ) {
269 $row->is_template = 1;
270 $row->is_image = 0;
271 $rows[$row->page_id] = $row;
272 }
273 }
274 if ( !$hideimages ) {
275 foreach ( $ilRes as $row ) {
276 $row->is_template = 0;
277 $row->is_image = 1;
278 $rows[$row->page_id] = $row;
279 }
280 }
281
282 // Sort by key and then change the keys to 0-based indices
283 ksort( $rows );
284 $rows = array_values( $rows );
285
286 $numRows = count( $rows );
287
288 // Work out the start and end IDs, for prev/next links
289 if ( $numRows > $limit ) {
290 // More rows available after these ones
291 // Get the ID from the last row in the result set
292 $nextId = $rows[$limit]->page_id;
293 // Remove undisplayed rows
294 $rows = array_slice( $rows, 0, $limit );
295 } else {
296 // No more rows after
297 $nextId = false;
298 }
299 $prevId = $from;
300
301 // use LinkBatch to make sure, that all required data (associated with Titles)
302 // is loaded in one query
303 $lb = new LinkBatch();
304 foreach ( $rows as $row ) {
305 $lb->add( $row->page_namespace, $row->page_title );
306 }
307 $lb->execute();
308
309 if ( $level == 0 && !$this->including() ) {
310 $out->addHTML( $this->whatlinkshereForm() );
311 $out->addHTML( $this->getFilterPanel() );
312
313 $link = $this->getLinkRenderer()->makeLink(
314 $this->target,
315 null,
316 [],
317 $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
318 );
319
320 $msg = $this->msg( 'linkshere' )
321 ->params( $this->target->getPrefixedText() )
322 ->rawParams( $link )
323 ->parseAsBlock();
324 $out->addHTML( $msg );
325
326 $out->addWikiMsg( 'whatlinkshere-count', Message::numParam( count( $rows ) ) );
327
328 $prevnext = $this->getPrevNext( $prevId, $nextId );
329 $out->addHTML( $prevnext );
330 }
331 $out->addHTML( $this->listStart( $level ) );
332 foreach ( $rows as $row ) {
333 $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
334
335 if ( $row->rd_from && $level < 2 ) {
336 $out->addHTML( $this->listItem( $row, $nt, $target, true ) );
337 $this->showIndirectLinks(
338 $level + 1,
339 $nt,
340 $this->getConfig()->get( 'MaxRedirectLinksRetrieved' )
341 );
342 $out->addHTML( Xml::closeElement( 'li' ) );
343 } else {
344 $out->addHTML( $this->listItem( $row, $nt, $target ) );
345 }
346 }
347
348 $out->addHTML( $this->listEnd() );
349
350 if ( $level == 0 && !$this->including() ) {
351 $out->addHTML( $prevnext );
352 }
353 }
354
355 protected function listStart( $level ) {
356 return Xml::openElement( 'ul', ( $level ? [] : [ 'id' => 'mw-whatlinkshere-list' ] ) );
357 }
358
359 protected function listItem( $row, $nt, $target, $notClose = false ) {
360 $dirmark = $this->getLanguage()->getDirMark();
361
362 # local message cache
363 static $msgcache = null;
364 if ( $msgcache === null ) {
365 static $msgs = [ 'isredirect', 'istemplate', 'semicolon-separator',
366 'whatlinkshere-links', 'isimage', 'editlink' ];
367 $msgcache = [];
368 foreach ( $msgs as $msg ) {
369 $msgcache[$msg] = $this->msg( $msg )->escaped();
370 }
371 }
372
373 if ( $row->rd_from ) {
374 $query = [ 'redirect' => 'no' ];
375 } else {
376 $query = [];
377 }
378
379 $link = $this->getLinkRenderer()->makeKnownLink(
380 $nt,
381 null,
382 $row->page_is_redirect ? [ 'class' => 'mw-redirect' ] : [],
383 $query
384 );
385
386 // Display properties (redirect or template)
387 $propsText = '';
388 $props = [];
389 if ( (string)$row->rd_fragment !== '' ) {
390 $props[] = $this->msg( 'whatlinkshere-sectionredir' )
391 ->rawParams( $this->getLinkRenderer()->makeLink(
392 $target->createFragmentTarget( $row->rd_fragment ),
393 $row->rd_fragment
394 ) )->escaped();
395 } elseif ( $row->rd_from ) {
396 $props[] = $msgcache['isredirect'];
397 }
398 if ( $row->is_template ) {
399 $props[] = $msgcache['istemplate'];
400 }
401 if ( $row->is_image ) {
402 $props[] = $msgcache['isimage'];
403 }
404
405 $this->getHookRunner()->onWhatLinksHereProps( $row, $nt, $target, $props );
406
407 if ( count( $props ) ) {
408 $propsText = $this->msg( 'parentheses' )
409 ->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
410 }
411
412 # Space for utilities links, with a what-links-here link provided
413 $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'], $msgcache['editlink'] );
414 $wlh = Xml::wrapClass(
415 $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(),
416 'mw-whatlinkshere-tools'
417 );
418
419 return $notClose ?
420 Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
421 Xml::tags( 'li', null, "$link $propsText $dirmark $wlh" ) . "\n";
422 }
423
424 protected function listEnd() {
425 return Xml::closeElement( 'ul' );
426 }
427
428 protected function wlhLink( Title $target, $text, $editText ) {
429 static $title = null;
430 if ( $title === null ) {
431 $title = $this->getPageTitle();
432 }
433
435
436 if ( $text !== null ) {
437 $text = new HtmlArmor( $text );
438 }
439
440 // always show a "<- Links" link
441 $links = [
442 'links' => $linkRenderer->makeKnownLink(
443 $title,
444 $text,
445 [],
446 [ 'target' => $target->getPrefixedText() ]
447 ),
448 ];
449
450 // if the page is editable, add an edit link
451 if (
452 // check user permissions
453 MediaWikiServices::getInstance()
455 ->userHasRight( $this->getUser(), 'edit' ) &&
456 // check, if the content model is editable through action=edit
457 MediaWikiServices::getInstance()
458 ->getContentHandlerFactory()
459 ->getContentHandler( $target->getContentModel() )
460 ->supportsDirectEditing()
461 ) {
462 if ( $editText !== null ) {
463 $editText = new HtmlArmor( $editText );
464 }
465
466 $links['edit'] = $linkRenderer->makeKnownLink(
467 $target,
468 $editText,
469 [],
470 [ 'action' => 'edit' ]
471 );
472 }
473
474 // build the links html
475 return $this->getLanguage()->pipeList( $links );
476 }
477
478 private function makeSelfLink( $text, $query ) {
479 if ( $text !== null ) {
480 $text = new HtmlArmor( $text );
481 }
482
483 return $this->getLinkRenderer()->makeKnownLink(
484 $this->selfTitle,
485 $text,
486 [],
487 $query
488 );
489 }
490
491 private function getPrevNext( $prevId, $nextId ) {
492 $currentLimit = $this->opts->getValue( 'limit' );
493 $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
494 $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
495
496 $changed = $this->opts->getChangedValues();
497 unset( $changed['target'] ); // Already in the request title
498
499 if ( $prevId != 0 ) {
500 $overrides = [ 'from' => $this->opts->getValue( 'back' ) ];
501 $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
502 }
503 if ( $nextId != 0 ) {
504 $overrides = [ 'from' => $nextId, 'back' => $prevId ];
505 $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
506 }
507
508 $limitLinks = [];
509 $lang = $this->getLanguage();
510 foreach ( $this->limits as $limit ) {
511 $prettyLimit = htmlspecialchars( $lang->formatNum( $limit ) );
512 $overrides = [ 'limit' => $limit ];
513 $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
514 }
515
516 $nums = $lang->pipeList( $limitLinks );
517
518 return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
519 }
520
521 private function whatlinkshereForm() {
522 // We get nicer value from the title object
523 $this->opts->consumeValue( 'target' );
524 // Reset these for new requests
525 $this->opts->consumeValues( [ 'back', 'from' ] );
526
527 $target = $this->target ? $this->target->getPrefixedText() : '';
528 $namespace = $this->opts->consumeValue( 'namespace' );
529 $nsinvert = $this->opts->consumeValue( 'invert' );
530
531 # Build up the form
532 $f = Xml::openElement( 'form', [ 'action' => wfScript() ] );
533
534 # Values that should not be forgotten
535 $f .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
536 foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
537 $f .= Html::hidden( $name, $value );
538 }
539
540 $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
541
542 # Target input (.mw-searchInput enables suggestions)
543 $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
544 'mw-whatlinkshere-target', 40, $target, [ 'class' => 'mw-searchInput' ] );
545
546 $f .= ' ';
547
548 # Namespace selector
549 $f .= Html::namespaceSelector(
550 [
551 'selected' => $namespace,
552 'all' => '',
553 'label' => $this->msg( 'namespace' )->text(),
554 'in-user-lang' => true,
555 ], [
556 'name' => 'namespace',
557 'id' => 'namespace',
558 'class' => 'namespaceselector',
559 ]
560 );
561
562 $f .= "\u{00A0}" .
563 Xml::checkLabel(
564 $this->msg( 'invert' )->text(),
565 'invert',
566 'nsinvert',
567 $nsinvert,
568 [ 'title' => $this->msg( 'tooltip-whatlinkshere-invert' )->text() ]
569 );
570
571 $f .= ' ';
572
573 # Submit
574 $f .= Xml::submitButton( $this->msg( 'whatlinkshere-submit' )->text() );
575
576 # Close
577 $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
578
579 return $f;
580 }
581
587 private function getFilterPanel() {
588 $show = $this->msg( 'show' )->escaped();
589 $hide = $this->msg( 'hide' )->escaped();
590
591 $changed = $this->opts->getChangedValues();
592 unset( $changed['target'] ); // Already in the request title
593
594 $links = [];
595 $types = [ 'hidetrans', 'hidelinks', 'hideredirs' ];
596 if ( $this->target->getNamespace() == NS_FILE ) {
597 $types[] = 'hideimages';
598 }
599
600 // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
601 // 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
602 // To be sure they will be found by grep
603 foreach ( $types as $type ) {
604 $chosen = $this->opts->getValue( $type );
605 $msg = $chosen ? $show : $hide;
606 $overrides = [ $type => !$chosen ];
607 $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
608 $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
609 }
610
611 return Xml::fieldset(
612 $this->msg( 'whatlinkshere-filters' )->text(),
613 $this->getLanguage()->pipeList( $links )
614 );
615 }
616
625 public function prefixSearchSubpages( $search, $limit, $offset ) {
626 return $this->prefixSearchString( $search, $limit, $offset );
627 }
628
629 protected function getGroupName() {
630 return 'pagetools';
631 }
632}
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)
getValue( $name)
Get the value for the given option name.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
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:35
MediaWikiServices is the service locator for the application scope of MediaWiki.
static numParam( $num)
Definition Message.php:1064
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:1041
createFragmentTarget( $fragment)
Creates a new Title for a different fragment of the same page.
Definition Title.php:1805
getDBkey()
Get the main part with underscores.
Definition Title.php:1032
getContentModel( $flags=0)
Get the page's content model id, see the CONTENT_MODEL_XXX constants.
Definition Title.php:1053
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1859
const NS_FILE
Definition Defines.php:76
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