Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/specials/SpecialWhatlinkshere.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Implements Special:Whatlinkshere
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @todo Use some variant of Pager or something; the pagination here is lousy.
22
 */
23
24
/**
25
 * Implements Special:Whatlinkshere
26
 *
27
 * @ingroup SpecialPage
28
 */
29
class SpecialWhatLinksHere extends IncludableSpecialPage {
30
	/** @var FormOptions */
31
	protected $opts;
32
33
	protected $selfTitle;
34
35
	/** @var Title */
36
	protected $target;
37
38
	protected $limits = [ 20, 50, 100, 250, 500 ];
39
40
	public function __construct() {
41
		parent::__construct( 'Whatlinkshere' );
42
	}
43
44
	function execute( $par ) {
45
		$out = $this->getOutput();
46
47
		$this->setHeaders();
48
		$this->outputHeader();
49
		$this->addHelpLink( 'Help:What links here' );
50
51
		$opts = new FormOptions();
52
53
		$opts->add( 'target', '' );
54
		$opts->add( 'namespace', '', FormOptions::INTNULL );
55
		$opts->add( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
56
		$opts->add( 'from', 0 );
57
		$opts->add( 'back', 0 );
58
		$opts->add( 'hideredirs', false );
59
		$opts->add( 'hidetrans', false );
60
		$opts->add( 'hidelinks', false );
61
		$opts->add( 'hideimages', false );
62
		$opts->add( 'invert', false );
63
64
		$opts->fetchValuesFromRequest( $this->getRequest() );
65
		$opts->validateIntBounds( 'limit', 0, 5000 );
66
67
		// Give precedence to subpage syntax
68
		if ( $par !== null ) {
69
			$opts->setValue( 'target', $par );
70
		}
71
72
		// Bind to member variable
73
		$this->opts = $opts;
74
75
		$this->target = Title::newFromText( $opts->getValue( 'target' ) );
76
		if ( !$this->target ) {
77
			if ( !$this->including() ) {
78
				$out->addHTML( $this->whatlinkshereForm() );
79
			}
80
81
			return;
82
		}
83
84
		$this->getSkin()->setRelevantTitle( $this->target );
85
86
		$this->selfTitle = $this->getPageTitle( $this->target->getPrefixedDBkey() );
87
88
		$out->setPageTitle( $this->msg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
89
		$out->addBacklinkSubtitle( $this->target );
90
		$this->showIndirectLinks(
91
			0,
92
			$this->target,
93
			$opts->getValue( 'limit' ),
94
			$opts->getValue( 'from' ),
95
			$opts->getValue( 'back' )
96
		);
97
	}
98
99
	/**
100
	 * @param int $level Recursion level
101
	 * @param Title $target Target title
102
	 * @param int $limit Number of entries to display
103
	 * @param int $from Display from this article ID (default: 0)
104
	 * @param int $back Display from this article ID at backwards scrolling (default: 0)
105
	 */
106
	function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
107
		$out = $this->getOutput();
108
		$dbr = wfGetDB( DB_REPLICA );
109
110
		$hidelinks = $this->opts->getValue( 'hidelinks' );
111
		$hideredirs = $this->opts->getValue( 'hideredirs' );
112
		$hidetrans = $this->opts->getValue( 'hidetrans' );
113
		$hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
114
115
		$fetchlinks = ( !$hidelinks || !$hideredirs );
116
117
		// Build query conds in concert for all three tables...
118
		$conds['pagelinks'] = [
0 ignored issues
show
Coding Style Comprehensibility introduced by
$conds was never initialized. Although not strictly required by PHP, it is generally a good practice to add $conds = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
119
			'pl_namespace' => $target->getNamespace(),
120
			'pl_title' => $target->getDBkey(),
121
		];
122
		$conds['templatelinks'] = [
123
			'tl_namespace' => $target->getNamespace(),
124
			'tl_title' => $target->getDBkey(),
125
		];
126
		$conds['imagelinks'] = [
127
			'il_to' => $target->getDBkey(),
128
		];
129
130
		$namespace = $this->opts->getValue( 'namespace' );
131
		$invert = $this->opts->getValue( 'invert' );
132
		$nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace );
133 View Code Duplication
		if ( is_int( $namespace ) ) {
134
			$conds['pagelinks'][] = "pl_from_namespace $nsComparison";
135
			$conds['templatelinks'][] = "tl_from_namespace $nsComparison";
136
			$conds['imagelinks'][] = "il_from_namespace $nsComparison";
137
		}
138
139 View Code Duplication
		if ( $from ) {
140
			$conds['templatelinks'][] = "tl_from >= $from";
141
			$conds['pagelinks'][] = "pl_from >= $from";
142
			$conds['imagelinks'][] = "il_from >= $from";
143
		}
144
145
		if ( $hideredirs ) {
146
			$conds['pagelinks']['rd_from'] = null;
147
		} elseif ( $hidelinks ) {
148
			$conds['pagelinks'][] = 'rd_from is NOT NULL';
149
		}
150
151
		$queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use (
152
			$conds, $target, $limit
153
		) {
154
			// Read an extra row as an at-end check
155
			$queryLimit = $limit + 1;
156
			$on = [
157
				"rd_from = $fromCol",
158
				'rd_title' => $target->getDBkey(),
159
				'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
160
			];
161
			$on['rd_namespace'] = $target->getNamespace();
162
			// Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
163
			$subQuery = $dbr->selectSQLText(
164
				[ $table, 'redirect', 'page' ],
165
				[ $fromCol, 'rd_from' ],
166
				$conds[$table],
167
				__CLASS__ . '::showIndirectLinks',
168
				// Force JOIN order per T106682 to avoid large filesorts
169
				[ 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ],
170
				[
171
					'page' => [ 'INNER JOIN', "$fromCol = page_id" ],
172
					'redirect' => [ 'LEFT JOIN', $on ]
173
				]
174
			);
175
			return $dbr->select(
176
				[ 'page', 'temp_backlink_range' => "($subQuery)" ],
177
				[ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ],
178
				[],
179
				__CLASS__ . '::showIndirectLinks',
180
				[ 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ],
181
				[ 'page' => [ 'INNER JOIN', "$fromCol = page_id" ] ]
182
			);
183
		};
184
185
		if ( $fetchlinks ) {
186
			$plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
187
		}
188
189
		if ( !$hidetrans ) {
190
			$tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
191
		}
192
193
		if ( !$hideimages ) {
194
			$ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
195
		}
196
197
		if ( ( !$fetchlinks || !$plRes->numRows() )
0 ignored issues
show
The variable $plRes does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
198
			&& ( $hidetrans || !$tlRes->numRows() )
0 ignored issues
show
The variable $tlRes does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
199
			&& ( $hideimages || !$ilRes->numRows() )
0 ignored issues
show
The variable $ilRes does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
200
		) {
201
			if ( 0 == $level ) {
202
				if ( !$this->including() ) {
203
					$out->addHTML( $this->whatlinkshereForm() );
204
205
					// Show filters only if there are links
206
					if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
207
						$out->addHTML( $this->getFilterPanel() );
208
					}
209
					$errMsg = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
210
					$out->addWikiMsg( $errMsg, $this->target->getPrefixedText() );
211
					$out->setStatusCode( 404 );
212
				}
213
			}
214
215
			return;
216
		}
217
218
		// Read the rows into an array and remove duplicates
219
		// templatelinks comes second so that the templatelinks row overwrites the
220
		// pagelinks row, so we get (inclusion) rather than nothing
221
		if ( $fetchlinks ) {
222
			foreach ( $plRes as $row ) {
223
				$row->is_template = 0;
224
				$row->is_image = 0;
225
				$rows[$row->page_id] = $row;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$rows was never initialized. Although not strictly required by PHP, it is generally a good practice to add $rows = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
226
			}
227
		}
228
		if ( !$hidetrans ) {
229
			foreach ( $tlRes as $row ) {
230
				$row->is_template = 1;
231
				$row->is_image = 0;
232
				$rows[$row->page_id] = $row;
0 ignored issues
show
The variable $rows does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
233
			}
234
		}
235
		if ( !$hideimages ) {
236
			foreach ( $ilRes as $row ) {
237
				$row->is_template = 0;
238
				$row->is_image = 1;
239
				$rows[$row->page_id] = $row;
240
			}
241
		}
242
243
		// Sort by key and then change the keys to 0-based indices
244
		ksort( $rows );
245
		$rows = array_values( $rows );
246
247
		$numRows = count( $rows );
248
249
		// Work out the start and end IDs, for prev/next links
250
		if ( $numRows > $limit ) {
251
			// More rows available after these ones
252
			// Get the ID from the last row in the result set
253
			$nextId = $rows[$limit]->page_id;
254
			// Remove undisplayed rows
255
			$rows = array_slice( $rows, 0, $limit );
256
		} else {
257
			// No more rows after
258
			$nextId = false;
259
		}
260
		$prevId = $from;
261
262
		// use LinkBatch to make sure, that all required data (associated with Titles)
263
		// is loaded in one query
264
		$lb = new LinkBatch();
265
		foreach ( $rows as $row ) {
266
			$lb->add( $row->page_namespace, $row->page_title );
267
		}
268
		$lb->execute();
269
270
		if ( $level == 0 ) {
271
			if ( !$this->including() ) {
272
				$out->addHTML( $this->whatlinkshereForm() );
273
				$out->addHTML( $this->getFilterPanel() );
274
				$out->addWikiMsg( 'linkshere', $this->target->getPrefixedText() );
275
276
				$prevnext = $this->getPrevNext( $prevId, $nextId );
277
				$out->addHTML( $prevnext );
278
			}
279
		}
280
		$out->addHTML( $this->listStart( $level ) );
281
		foreach ( $rows as $row ) {
282
			$nt = Title::makeTitle( $row->page_namespace, $row->page_title );
283
284
			if ( $row->rd_from && $level < 2 ) {
285
				$out->addHTML( $this->listItem( $row, $nt, $target, true ) );
286
				$this->showIndirectLinks(
287
					$level + 1,
288
					$nt,
289
					$this->getConfig()->get( 'MaxRedirectLinksRetrieved' )
290
				);
291
				$out->addHTML( Xml::closeElement( 'li' ) );
292
			} else {
293
				$out->addHTML( $this->listItem( $row, $nt, $target ) );
294
			}
295
		}
296
297
		$out->addHTML( $this->listEnd() );
298
299
		if ( $level == 0 ) {
300
			if ( !$this->including() ) {
301
				$out->addHTML( $prevnext );
0 ignored issues
show
The variable $prevnext does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
302
			}
303
		}
304
	}
305
306
	protected function listStart( $level ) {
307
		return Xml::openElement( 'ul', ( $level ? [] : [ 'id' => 'mw-whatlinkshere-list' ] ) );
308
	}
309
310
	protected function listItem( $row, $nt, $target, $notClose = false ) {
311
		$dirmark = $this->getLanguage()->getDirMark();
312
313
		# local message cache
314
		static $msgcache = null;
315
		if ( $msgcache === null ) {
316
			static $msgs = [ 'isredirect', 'istemplate', 'semicolon-separator',
317
				'whatlinkshere-links', 'isimage', 'editlink' ];
318
			$msgcache = [];
319
			foreach ( $msgs as $msg ) {
320
				$msgcache[$msg] = $this->msg( $msg )->escaped();
321
			}
322
		}
323
324
		if ( $row->rd_from ) {
325
			$query = [ 'redirect' => 'no' ];
326
		} else {
327
			$query = [];
328
		}
329
330
		$link = Linker::linkKnown(
331
			$nt,
332
			null,
333
			$row->page_is_redirect ? [ 'class' => 'mw-redirect' ] : [],
334
			$query
335
		);
336
337
		// Display properties (redirect or template)
338
		$propsText = '';
339
		$props = [];
340
		if ( $row->rd_from ) {
341
			$props[] = $msgcache['isredirect'];
342
		}
343
		if ( $row->is_template ) {
344
			$props[] = $msgcache['istemplate'];
345
		}
346
		if ( $row->is_image ) {
347
			$props[] = $msgcache['isimage'];
348
		}
349
350
		Hooks::run( 'WhatLinksHereProps', [ $row, $nt, $target, &$props ] );
351
352
		if ( count( $props ) ) {
353
			$propsText = $this->msg( 'parentheses' )
354
				->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
355
		}
356
357
		# Space for utilities links, with a what-links-here link provided
358
		$wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'], $msgcache['editlink'] );
359
		$wlh = Xml::wrapClass(
360
			$this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(),
361
			'mw-whatlinkshere-tools'
362
		);
363
364
		return $notClose ?
365
			Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
366
			Xml::tags( 'li', null, "$link $propsText $dirmark $wlh" ) . "\n";
367
	}
368
369
	protected function listEnd() {
370
		return Xml::closeElement( 'ul' );
371
	}
372
373
	protected function wlhLink( Title $target, $text, $editText ) {
374
		static $title = null;
375
		if ( $title === null ) {
376
			$title = $this->getPageTitle();
377
		}
378
379
		// always show a "<- Links" link
380
		$links = [
381
			'links' => Linker::linkKnown(
382
				$title,
383
				$text,
384
				[],
385
				[ 'target' => $target->getPrefixedText() ]
386
			),
387
		];
388
389
		// if the page is editable, add an edit link
390 View Code Duplication
		if (
391
			// check user permissions
392
			$this->getUser()->isAllowed( 'edit' ) &&
393
			// check, if the content model is editable through action=edit
394
			ContentHandler::getForTitle( $target )->supportsDirectEditing()
395
		) {
396
			$links['edit'] = Linker::linkKnown(
397
				$target,
398
				$editText,
399
				[],
400
				[ 'action' => 'edit' ]
401
			);
402
		}
403
404
		// build the links html
405
		return $this->getLanguage()->pipeList( $links );
406
	}
407
408
	function makeSelfLink( $text, $query ) {
409
		return Linker::linkKnown(
410
			$this->selfTitle,
411
			$text,
412
			[],
413
			$query
414
		);
415
	}
416
417
	function getPrevNext( $prevId, $nextId ) {
418
		$currentLimit = $this->opts->getValue( 'limit' );
419
		$prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
420
		$next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
421
422
		$changed = $this->opts->getChangedValues();
423
		unset( $changed['target'] ); // Already in the request title
424
425 View Code Duplication
		if ( 0 != $prevId ) {
426
			$overrides = [ 'from' => $this->opts->getValue( 'back' ) ];
427
			$prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
428
		}
429 View Code Duplication
		if ( 0 != $nextId ) {
430
			$overrides = [ 'from' => $nextId, 'back' => $prevId ];
431
			$next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
432
		}
433
434
		$limitLinks = [];
435
		$lang = $this->getLanguage();
436
		foreach ( $this->limits as $limit ) {
437
			$prettyLimit = htmlspecialchars( $lang->formatNum( $limit ) );
438
			$overrides = [ 'limit' => $limit ];
439
			$limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
440
		}
441
442
		$nums = $lang->pipeList( $limitLinks );
443
444
		return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
445
	}
446
447
	function whatlinkshereForm() {
448
		// We get nicer value from the title object
449
		$this->opts->consumeValue( 'target' );
450
		// Reset these for new requests
451
		$this->opts->consumeValues( [ 'back', 'from' ] );
452
453
		$target = $this->target ? $this->target->getPrefixedText() : '';
454
		$namespace = $this->opts->consumeValue( 'namespace' );
455
		$nsinvert = $this->opts->consumeValue( 'invert' );
456
457
		# Build up the form
458
		$f = Xml::openElement( 'form', [ 'action' => wfScript() ] );
459
460
		# Values that should not be forgotten
461
		$f .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
462
		foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
463
			$f .= Html::hidden( $name, $value );
464
		}
465
466
		$f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
467
468
		# Target input (.mw-searchInput enables suggestions)
469
		$f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
470
			'mw-whatlinkshere-target', 40, $target, [ 'class' => 'mw-searchInput' ] );
471
472
		$f .= ' ';
473
474
		# Namespace selector
475
		$f .= Html::namespaceSelector(
476
			[
477
				'selected' => $namespace,
478
				'all' => '',
479
				'label' => $this->msg( 'namespace' )->text()
480
			], [
481
				'name' => 'namespace',
482
				'id' => 'namespace',
483
				'class' => 'namespaceselector',
484
			]
485
		);
486
487
		$f .= '&#160;' .
488
			Xml::checkLabel(
489
				$this->msg( 'invert' )->text(),
490
				'invert',
491
				'nsinvert',
492
				$nsinvert,
493
				[ 'title' => $this->msg( 'tooltip-whatlinkshere-invert' )->text() ]
494
			);
495
496
		$f .= ' ';
497
498
		# Submit
499
		$f .= Xml::submitButton( $this->msg( 'whatlinkshere-submit' )->text() );
500
501
		# Close
502
		$f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
503
504
		return $f;
505
	}
506
507
	/**
508
	 * Create filter panel
509
	 *
510
	 * @return string HTML fieldset and filter panel with the show/hide links
511
	 */
512
	function getFilterPanel() {
513
		$show = $this->msg( 'show' )->escaped();
514
		$hide = $this->msg( 'hide' )->escaped();
515
516
		$changed = $this->opts->getChangedValues();
517
		unset( $changed['target'] ); // Already in the request title
518
519
		$links = [];
520
		$types = [ 'hidetrans', 'hidelinks', 'hideredirs' ];
521
		if ( $this->target->getNamespace() == NS_FILE ) {
522
			$types[] = 'hideimages';
523
		}
524
525
		// Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
526
		// 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
527
		// To be sure they will be found by grep
528
		foreach ( $types as $type ) {
529
			$chosen = $this->opts->getValue( $type );
530
			$msg = $chosen ? $show : $hide;
531
			$overrides = [ $type => !$chosen ];
532
			$links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
533
				$this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
534
		}
535
536
		return Xml::fieldset(
537
			$this->msg( 'whatlinkshere-filters' )->text(),
538
			$this->getLanguage()->pipeList( $links )
539
		);
540
	}
541
542
	/**
543
	 * Return an array of subpages beginning with $search that this special page will accept.
544
	 *
545
	 * @param string $search Prefix to search for
546
	 * @param int $limit Maximum number of results to return (usually 10)
547
	 * @param int $offset Number of results to skip (usually 0)
548
	 * @return string[] Matching subpages
549
	 */
550
	public function prefixSearchSubpages( $search, $limit, $offset ) {
551
		return $this->prefixSearchString( $search, $limit, $offset );
552
	}
553
554
	protected function getGroupName() {
555
		return 'pagetools';
556
	}
557
}
558