Completed
Branch master (13ece3)
by
unknown
22:07
created

Linker   F

Complexity

Total Complexity 368

Size/Duplication

Total Lines 2384
Duplicated Lines 0.46 %

Coupling/Cohesion

Components 4
Dependencies 27

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 11
loc 2384
rs 0.5217
wmc 368
lcom 4
cbo 27

63 Methods

Rating   Name   Duplication   Size   Complexity  
A getInterwikiLinkAttributes() 0 13 1
A getInternalLinkAttributes() 0 7 1
A getInternalLinkAttributesObj() 0 8 2
A getLinkAttributesInternal() 0 14 3
B getLinkColour() 0 13 6
C link() 0 63 13
A linkKnown() 0 6 1
C linkUrl() 0 28 8
C linkAttribs() 0 56 13
A linkText() 0 13 4
A makeSelfLinkObj() 0 12 3
A fnamePart() 0 9 2
A makeExternalImage() 0 16 3
F makeImageLink() 3 135 44
C getImageLinkMTOParams() 0 24 9
A makeThumbLinkObj() 0 16 3
F makeThumbLink2() 3 117 30
C processResponsiveImages() 0 22 11
D makeBrokenImageLinkObj() 0 33 9
A getUploadUrl() 0 16 4
A makeMediaLinkObj() 0 4 1
B makeMediaLinkFile() 0 30 5
A specialLink() 0 7 2
A getInvalidTitleDescription() 0 15 3
A normaliseSpecialPage() 0 12 3
D makeExternalLink() 0 41 10
A userLink() 0 18 4
F userToolLinks() 0 47 14
A userToolLinksRedContribs() 0 3 1
A userTalkLink() 0 5 1
A blockLink() 0 5 1
A emailLink() 0 5 1
B revUserLink() 0 14 5
B revUserTools() 0 16 5
A formatComment() 0 14 1
C formatAutocomments() 0 74 7
D formatLinksInComment() 0 94 16
A makeCommentLink() 0 19 3
C normalizeSubpageLink() 5 72 14
A commentBlock() 0 14 3
B revComment() 0 17 6
A formatRevisionSize() 0 9 2
A tocIndent() 0 3 1
A tocUnindent() 0 3 2
A tocLine() 0 10 2
A tocLineEnd() 0 3 1
A tocList() 0 9 1
A generateTOC() 0 21 4
A makeHeadline() 0 12 2
A splitTrail() 0 13 3
B generateRollback() 0 23 5
C getRollbackEditCount() 0 50 11
C buildRollbackLink() 0 68 11
D formatTemplates() 0 85 15
A formatHiddenCategories() 0 19 3
A formatSize() 0 4 1
C titleAttrib() 0 29 7
B accesskey() 0 22 5
B getRevDeleteLink() 0 30 6
A revDeleteLink() 0 12 3
A revDeleteLinkDisabled() 0 6 2
A tooltipAndAccesskeyAttribs() 0 16 3
A tooltip() 0 12 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Linker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Linker, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Methods to make links and related items.
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
 */
22
use MediaWiki\Linker\LinkTarget;
23
24
/**
25
 * Some internal bits split of from Skin.php. These functions are used
26
 * for primarily page content: links, embedded images, table of contents. Links
27
 * are also used in the skin.
28
 *
29
 * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
30
 *
31
 * @ingroup Skins
32
 */
33
class Linker {
34
	/**
35
	 * Flags for userToolLinks()
36
	 */
37
	const TOOL_LINKS_NOBLOCK = 1;
38
	const TOOL_LINKS_EMAIL = 2;
39
40
	/**
41
	 * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
42
	 *
43
	 * @since 1.16.3
44
	 * @deprecated since 1.25
45
	 *
46
	 * @param string $title The title text for the link, URL-encoded (???) but
47
	 *   not HTML-escaped
48
	 * @param string $unused Unused
49
	 * @param string $class The contents of the class attribute; if an empty
50
	 *   string is passed, which is the default value, defaults to 'external'.
51
	 * @return string
52
	 */
53
	static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
54
		global $wgContLang;
55
56
		wfDeprecated( __METHOD__, '1.25' );
57
58
		# @todo FIXME: We have a whole bunch of handling here that doesn't happen in
59
		# getExternalLinkAttributes, why?
60
		$title = urldecode( $title );
61
		$title = $wgContLang->checkTitleEncoding( $title );
62
		$title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
63
64
		return self::getLinkAttributesInternal( $title, $class );
0 ignored issues
show
Deprecated Code introduced by
The method Linker::getLinkAttributesInternal() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
65
	}
66
67
	/**
68
	 * Get the appropriate HTML attributes to add to the "a" element of an internal link.
69
	 *
70
	 * @since 1.16.3
71
	 * @deprecated since 1.25
72
	 *
73
	 * @param string $title The title text for the link, URL-encoded (???) but
74
	 *   not HTML-escaped
75
	 * @param string $unused Unused
76
	 * @param string $class The contents of the class attribute, default none
77
	 * @return string
78
	 */
79
	static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
80
		wfDeprecated( __METHOD__, '1.25' );
81
82
		$title = urldecode( $title );
83
		$title = strtr( $title, '_', ' ' );
84
		return self::getLinkAttributesInternal( $title, $class );
0 ignored issues
show
Deprecated Code introduced by
The method Linker::getLinkAttributesInternal() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
85
	}
86
87
	/**
88
	 * Get the appropriate HTML attributes to add to the "a" element of an internal
89
	 * link, given the Title object for the page we want to link to.
90
	 *
91
	 * @since 1.16.3
92
	 * @deprecated since 1.25
93
	 *
94
	 * @param Title $nt
95
	 * @param string $unused Unused
96
	 * @param string $class The contents of the class attribute, default none
97
	 * @param string|bool $title Optional (unescaped) string to use in the title
98
	 *   attribute; if false, default to the name of the page we're linking to
99
	 * @return string
100
	 */
101
	static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
102
		wfDeprecated( __METHOD__, '1.25' );
103
104
		if ( $title === false ) {
105
			$title = $nt->getPrefixedText();
106
		}
107
		return self::getLinkAttributesInternal( $title, $class );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 101 can also be of type boolean; however, Linker::getLinkAttributesInternal() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Deprecated Code introduced by
The method Linker::getLinkAttributesInternal() has been deprecated with message: since 1.25

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
108
	}
109
110
	/**
111
	 * Common code for getLinkAttributesX functions
112
	 *
113
	 * @since 1.16.3
114
	 * @deprecated since 1.25
115
	 *
116
	 * @param string $title
117
	 * @param string $class
118
	 *
119
	 * @return string
120
	 */
121
	private static function getLinkAttributesInternal( $title, $class ) {
122
		wfDeprecated( __METHOD__, '1.25' );
123
124
		$title = htmlspecialchars( $title );
125
		$class = htmlspecialchars( $class );
126
		$r = '';
127
		if ( $class != '' ) {
128
			$r .= " class=\"$class\"";
129
		}
130
		if ( $title != '' ) {
131
			$r .= " title=\"$title\"";
132
		}
133
		return $r;
134
	}
135
136
	/**
137
	 * Return the CSS colour of a known link
138
	 *
139
	 * @since 1.16.3
140
	 * @param Title $t
141
	 * @param int $threshold User defined threshold
142
	 * @return string CSS class
143
	 */
144
	public static function getLinkColour( $t, $threshold ) {
145
		$colour = '';
146
		if ( $t->isRedirect() ) {
147
			# Page is a redirect
148
			$colour = 'mw-redirect';
149
		} elseif ( $threshold > 0 && $t->isContentPage() &&
150
			$t->exists() && $t->getLength() < $threshold
151
		) {
152
			# Page is a stub
153
			$colour = 'stub';
154
		}
155
		return $colour;
156
	}
157
158
	/**
159
	 * This function returns an HTML link to the given target.  It serves a few
160
	 * purposes:
161
	 *   1) If $target is a Title, the correct URL to link to will be figured
162
	 *      out automatically.
163
	 *   2) It automatically adds the usual classes for various types of link
164
	 *      targets: "new" for red links, "stub" for short articles, etc.
165
	 *   3) It escapes all attribute values safely so there's no risk of XSS.
166
	 *   4) It provides a default tooltip if the target is a Title (the page
167
	 *      name of the target).
168
	 * link() replaces the old functions in the makeLink() family.
169
	 *
170
	 * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
171
	 *
172
	 * @param Title $target Can currently only be a Title, but this may
173
	 *   change to support Images, literal URLs, etc.
174
	 * @param string $html The HTML contents of the <a> element, i.e.,
175
	 *   the link text.  This is raw HTML and will not be escaped.  If null,
176
	 *   defaults to the prefixed text of the Title; or if the Title is just a
177
	 *   fragment, the contents of the fragment.
178
	 * @param array $customAttribs A key => value array of extra HTML attributes,
179
	 *   such as title and class.  (href is ignored.)  Classes will be
180
	 *   merged with the default classes, while other attributes will replace
181
	 *   default attributes.  All passed attribute values will be HTML-escaped.
182
	 *   A false attribute value means to suppress that attribute.
183
	 * @param array $query The query string to append to the URL
184
	 *   you're linking to, in key => value array form.  Query keys and values
185
	 *   will be URL-encoded.
186
	 * @param string|array $options String or array of strings:
187
	 *     'known': Page is known to exist, so don't check if it does.
188
	 *     'broken': Page is known not to exist, so don't check if it does.
189
	 *     'noclasses': Don't add any classes automatically (includes "new",
190
	 *       "stub", "mw-redirect", "extiw").  Only use the class attribute
191
	 *       provided, if any, so you get a simple blue link with no funny i-
192
	 *       cons.
193
	 *     'forcearticlepath': Use the article path always, even with a querystring.
194
	 *       Has compatibility issues on some setups, so avoid wherever possible.
195
	 *     'http': Force a full URL with http:// as the scheme.
196
	 *     'https': Force a full URL with https:// as the scheme.
197
	 *     'stubThreshold' => (int): Stub threshold to use when determining link classes.
198
	 * @return string HTML <a> attribute
199
	 */
200
	public static function link(
201
		$target, $html = null, $customAttribs = [], $query = [], $options = []
202
	) {
203
		if ( !$target instanceof Title ) {
204
			wfWarn( __METHOD__ . ': Requires $target to be a Title object.', 2 );
205
			return "<!-- ERROR -->$html";
206
		}
207
208
		if ( is_string( $query ) ) {
209
			// some functions withing core using this still hand over query strings
210
			wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
211
			$query = wfCgiToArray( $query );
212
		}
213
		$options = (array)$options;
214
215
		$dummy = new DummyLinker; // dummy linker instance for bc on the hooks
216
217
		$ret = null;
218
		if ( !Hooks::run( 'LinkBegin',
219
			[ $dummy, $target, &$html, &$customAttribs, &$query, &$options, &$ret ] )
220
		) {
221
			return $ret;
222
		}
223
224
		# Normalize the Title if it's a special page
225
		$target = self::normaliseSpecialPage( $target );
226
227
		# If we don't know whether the page exists, let's find out.
228
		if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) {
229
			if ( $target->isKnown() ) {
230
				$options[] = 'known';
231
			} else {
232
				$options[] = 'broken';
233
			}
234
		}
235
236
		$oldquery = [];
237
		if ( in_array( "forcearticlepath", $options, true ) && $query ) {
238
			$oldquery = $query;
239
			$query = [];
240
		}
241
242
		# Note: we want the href attribute first, for prettiness.
243
		$attribs = [ 'href' => self::linkUrl( $target, $query, $options ) ];
244
		if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) {
245
			$attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
246
		}
247
248
		$attribs = array_merge(
249
			$attribs,
250
			self::linkAttribs( $target, $customAttribs, $options )
0 ignored issues
show
Compatibility introduced by
$target of type object<MediaWiki\Linker\LinkTarget> is not a sub-type of object<Title>. It seems like you assume a concrete implementation of the interface MediaWiki\Linker\LinkTarget to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
251
		);
252
		if ( is_null( $html ) ) {
253
			$html = self::linkText( $target );
0 ignored issues
show
Compatibility introduced by
$target of type object<MediaWiki\Linker\LinkTarget> is not a sub-type of object<Title>. It seems like you assume a concrete implementation of the interface MediaWiki\Linker\LinkTarget to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
254
		}
255
256
		$ret = null;
257
		if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) {
258
			$ret = Html::rawElement( 'a', $attribs, $html );
259
		}
260
261
		return $ret;
262
	}
263
264
	/**
265
	 * Identical to link(), except $options defaults to 'known'.
266
	 * @since 1.16.3
267
	 * @see Linker::link
268
	 * @return string
269
	 */
270
	public static function linkKnown(
271
		$target, $html = null, $customAttribs = [],
272
		$query = [], $options = [ 'known', 'noclasses' ]
273
	) {
274
		return self::link( $target, $html, $customAttribs, $query, $options );
275
	}
276
277
	/**
278
	 * Returns the Url used to link to a Title
279
	 *
280
	 * @param LinkTarget $target
281
	 * @param array $query Query parameters
282
	 * @param array $options
283
	 * @return string
284
	 */
285
	private static function linkUrl( LinkTarget $target, $query, $options ) {
286
		# We don't want to include fragments for broken links, because they
287
		# generally make no sense.
288
		if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) {
289
			$target = $target->createFragmentTarget( '' );
290
		}
291
292
		# If it's a broken link, add the appropriate query pieces, unless
293
		# there's already an action specified, or unless 'edit' makes no sense
294
		# (i.e., for a nonexistent special page).
295
		if ( in_array( 'broken', $options, true ) && empty( $query['action'] )
296
			&& $target->getNamespace() !== NS_SPECIAL ) {
297
			$query['action'] = 'edit';
298
			$query['redlink'] = '1';
299
		}
300
301
		if ( in_array( 'http', $options, true ) ) {
302
			$proto = PROTO_HTTP;
303
		} elseif ( in_array( 'https', $options, true ) ) {
304
			$proto = PROTO_HTTPS;
305
		} else {
306
			$proto = PROTO_RELATIVE;
307
		}
308
309
		$title = Title::newFromLinkTarget( $target );
310
		$ret = $title->getLinkURL( $query, false, $proto );
311
		return $ret;
312
	}
313
314
	/**
315
	 * Returns the array of attributes used when linking to the Title $target
316
	 *
317
	 * @param Title $target
318
	 * @param array $attribs
319
	 * @param array $options
320
	 *
321
	 * @return array
322
	 */
323
	private static function linkAttribs( $target, $attribs, $options ) {
324
		global $wgUser;
325
		$defaults = [];
326
327
		if ( !in_array( 'noclasses', $options, true ) ) {
328
			# Now build the classes.
329
			$classes = [];
330
331
			if ( in_array( 'broken', $options, true ) ) {
332
				$classes[] = 'new';
333
			}
334
335
			if ( $target->isExternal() ) {
336
				$classes[] = 'extiw';
337
			}
338
339
			if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387)
340
				$colour = self::getLinkColour(
341
					$target,
342
					isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold()
343
				);
344
				if ( $colour !== '' ) {
345
					$classes[] = $colour; # mw-redirect or stub
346
				}
347
			}
348
			if ( $classes != [] ) {
349
				$defaults['class'] = implode( ' ', $classes );
350
			}
351
		}
352
353
		# Get a default title attribute.
354
		if ( $target->getPrefixedText() == '' ) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
355
			# A link like [[#Foo]].  This used to mean an empty title
356
			# attribute, but that's silly.  Just don't output a title.
357
		} elseif ( in_array( 'known', $options, true ) ) {
358
			$defaults['title'] = $target->getPrefixedText();
359
		} else {
360
			// This ends up in parser cache!
361
			$defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )
362
				->inContentLanguage()
363
				->text();
364
		}
365
366
		# Finally, merge the custom attribs with the default ones, and iterate
367
		# over that, deleting all "false" attributes.
368
		$ret = [];
369
		$merged = Sanitizer::mergeAttributes( $defaults, $attribs );
370
		foreach ( $merged as $key => $val ) {
371
			# A false value suppresses the attribute, and we don't want the
372
			# href attribute to be overridden.
373
			if ( $key != 'href' && $val !== false ) {
374
				$ret[$key] = $val;
375
			}
376
		}
377
		return $ret;
378
	}
379
380
	/**
381
	 * Default text of the links to the Title $target
382
	 *
383
	 * @param Title $target
384
	 *
385
	 * @return string
386
	 */
387
	private static function linkText( $target ) {
388
		if ( !$target instanceof Title ) {
389
			wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
390
			return '';
391
		}
392
		// If the target is just a fragment, with no title, we return the fragment
393
		// text.  Otherwise, we return the title text itself.
394
		if ( $target->getPrefixedText() === '' && $target->hasFragment() ) {
395
			return htmlspecialchars( $target->getFragment() );
396
		}
397
398
		return htmlspecialchars( $target->getPrefixedText() );
399
	}
400
401
	/**
402
	 * Make appropriate markup for a link to the current article. This is
403
	 * currently rendered as the bold link text. The calling sequence is the
404
	 * same as the other make*LinkObj static functions, despite $query not
405
	 * being used.
406
	 *
407
	 * @since 1.16.3
408
	 * @param Title $nt
409
	 * @param string $html [optional]
410
	 * @param string $query [optional]
411
	 * @param string $trail [optional]
412
	 * @param string $prefix [optional]
413
	 *
414
	 * @return string
415
	 */
416
	public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
417
		$ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}";
418
		if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) {
419
			return $ret;
420
		}
421
422
		if ( $html == '' ) {
423
			$html = htmlspecialchars( $nt->getPrefixedText() );
424
		}
425
		list( $inside, $trail ) = self::splitTrail( $trail );
426
		return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}";
427
	}
428
429
	/**
430
	 * Get a message saying that an invalid title was encountered.
431
	 * This should be called after a method like Title::makeTitleSafe() returned
432
	 * a value indicating that the title object is invalid.
433
	 *
434
	 * @param IContextSource $context Context to use to get the messages
435
	 * @param int $namespace Namespace number
436
	 * @param string $title Text of the title, without the namespace part
437
	 * @return string
438
	 */
439
	public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
440
		global $wgContLang;
441
442
		// First we check whether the namespace exists or not.
443
		if ( MWNamespace::exists( $namespace ) ) {
444
			if ( $namespace == NS_MAIN ) {
445
				$name = $context->msg( 'blanknamespace' )->text();
446
			} else {
447
				$name = $wgContLang->getFormattedNsText( $namespace );
448
			}
449
			return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
450
		} else {
451
			return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
452
		}
453
	}
454
455
	/**
456
	 * @since 1.16.3
457
	 * @param LinkTarget $target
458
	 * @return LinkTarget|Title You will get back the same type you passed in, or a Title object
459
	 */
460
	public static function normaliseSpecialPage( LinkTarget $target ) {
461
		if ( $target->getNamespace() == NS_SPECIAL ) {
462
			list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $target->getDBkey() );
463
			if ( !$name ) {
464
				return $target;
465
			}
466
			$ret = SpecialPage::getTitleFor( $name, $subpage, $target->getFragment() );
467
			return $ret;
468
		} else {
469
			return $target;
470
		}
471
	}
472
473
	/**
474
	 * Returns the filename part of an url.
475
	 * Used as alternative text for external images.
476
	 *
477
	 * @param string $url
478
	 *
479
	 * @return string
480
	 */
481
	private static function fnamePart( $url ) {
482
		$basename = strrchr( $url, '/' );
483
		if ( false === $basename ) {
484
			$basename = $url;
485
		} else {
486
			$basename = substr( $basename, 1 );
487
		}
488
		return $basename;
489
	}
490
491
	/**
492
	 * Return the code for images which were added via external links,
493
	 * via Parser::maybeMakeExternalImage().
494
	 *
495
	 * @since 1.16.3
496
	 * @param string $url
497
	 * @param string $alt
498
	 *
499
	 * @return string
500
	 */
501
	public static function makeExternalImage( $url, $alt = '' ) {
502
		if ( $alt == '' ) {
503
			$alt = self::fnamePart( $url );
504
		}
505
		$img = '';
506
		$success = Hooks::run( 'LinkerMakeExternalImage', [ &$url, &$alt, &$img ] );
507
		if ( !$success ) {
508
			wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
509
				. "with url {$url} and alt text {$alt} to {$img}\n", true );
510
			return $img;
511
		}
512
		return Html::element( 'img',
513
			[
514
				'src' => $url,
515
				'alt' => $alt ] );
516
	}
517
518
	/**
519
	 * Given parameters derived from [[Image:Foo|options...]], generate the
520
	 * HTML that that syntax inserts in the page.
521
	 *
522
	 * @param Parser $parser
523
	 * @param Title $title Title object of the file (not the currently viewed page)
524
	 * @param File $file File object, or false if it doesn't exist
525
	 * @param array $frameParams Associative array of parameters external to the media handler.
526
	 *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
527
	 *     will often be false.
528
	 *          thumbnail       If present, downscale and frame
529
	 *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
530
	 *          framed          Shows image in original size in a frame
531
	 *          frameless       Downscale but don't frame
532
	 *          upright         If present, tweak default sizes for portrait orientation
533
	 *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
534
	 *          border          If present, show a border around the image
535
	 *          align           Horizontal alignment (left, right, center, none)
536
	 *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
537
	 *                          bottom, text-bottom)
538
	 *          alt             Alternate text for image (i.e. alt attribute). Plain text.
539
	 *          class           HTML for image classes. Plain text.
540
	 *          caption         HTML for image caption.
541
	 *          link-url        URL to link to
542
	 *          link-title      Title object to link to
543
	 *          link-target     Value for the target attribute, only with link-url
544
	 *          no-link         Boolean, suppress description link
545
	 *
546
	 * @param array $handlerParams Associative array of media handler parameters, to be passed
547
	 *       to transform(). Typical keys are "width" and "page".
548
	 * @param string|bool $time Timestamp of the file, set as false for current
549
	 * @param string $query Query params for desc url
550
	 * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
551
	 * @since 1.20
552
	 * @return string HTML for an image, with links, wrappers, etc.
553
	 */
554
	public static function makeImageLink( Parser $parser, Title $title,
555
		$file, $frameParams = [], $handlerParams = [], $time = false,
556
		$query = "", $widthOption = null
557
	) {
558
		$res = null;
559
		$dummy = new DummyLinker;
560
		if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
561
			&$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
562
			return $res;
563
		}
564
565
		if ( $file && !$file->allowInlineDisplay() ) {
566
			wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
567
			return self::link( $title );
568
		}
569
570
		// Shortcuts
571
		$fp =& $frameParams;
572
		$hp =& $handlerParams;
573
574
		// Clean up parameters
575
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
576
		if ( !isset( $fp['align'] ) ) {
577
			$fp['align'] = '';
578
		}
579
		if ( !isset( $fp['alt'] ) ) {
580
			$fp['alt'] = '';
581
		}
582
		if ( !isset( $fp['title'] ) ) {
583
			$fp['title'] = '';
584
		}
585
		if ( !isset( $fp['class'] ) ) {
586
			$fp['class'] = '';
587
		}
588
589
		$prefix = $postfix = '';
590
591
		if ( 'center' == $fp['align'] ) {
592
			$prefix = '<div class="center">';
593
			$postfix = '</div>';
594
			$fp['align'] = 'none';
595
		}
596
		if ( $file && !isset( $hp['width'] ) ) {
597
			if ( isset( $hp['height'] ) && $file->isVectorized() ) {
598
				// If its a vector image, and user only specifies height
599
				// we don't want it to be limited by its "normal" width.
600
				global $wgSVGMaxSize;
601
				$hp['width'] = $wgSVGMaxSize;
602
			} else {
603
				$hp['width'] = $file->getWidth( $page );
604
			}
605
606
			if ( isset( $fp['thumbnail'] )
607
				|| isset( $fp['manualthumb'] )
608
				|| isset( $fp['framed'] )
609
				|| isset( $fp['frameless'] )
610
				|| !$hp['width']
611
			) {
612
				global $wgThumbLimits, $wgThumbUpright;
613
614
				if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
615
					$widthOption = User::getDefaultOption( 'thumbsize' );
616
				}
617
618
				// Reduce width for upright images when parameter 'upright' is used
619
				if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
620
					$fp['upright'] = $wgThumbUpright;
621
				}
622
623
				// For caching health: If width scaled down due to upright
624
				// parameter, round to full __0 pixel to avoid the creation of a
625
				// lot of odd thumbs.
626
				$prefWidth = isset( $fp['upright'] ) ?
627
					round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) :
628
					$wgThumbLimits[$widthOption];
629
630
				// Use width which is smaller: real image width or user preference width
631
				// Unless image is scalable vector.
632
				if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 ||
633
						$prefWidth < $hp['width'] || $file->isVectorized() ) ) {
634
					$hp['width'] = $prefWidth;
635
				}
636
			}
637
		}
638
639
		if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
640
			# Create a thumbnail. Alignment depends on the writing direction of
641
			# the page content language (right-aligned for LTR languages,
642
			# left-aligned for RTL languages)
643
			# If a thumbnail width has not been provided, it is set
644
			# to the default user option as specified in Language*.php
645
			if ( $fp['align'] == '' ) {
646
				$fp['align'] = $parser->getTargetLanguage()->alignEnd();
647
			}
648
			return $prefix . self::makeThumbLink2( $title, $file, $fp, $hp, $time, $query ) . $postfix;
0 ignored issues
show
Bug introduced by
It seems like $time defined by parameter $time on line 555 can also be of type string; however, Linker::makeThumbLink2() does only seem to accept boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
649
		}
650
651
		if ( $file && isset( $fp['frameless'] ) ) {
652
			$srcWidth = $file->getWidth( $page );
653
			# For "frameless" option: do not present an image bigger than the
654
			# source (for bitmap-style images). This is the same behavior as the
655
			# "thumb" option does it already.
656 View Code Duplication
			if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
657
				$hp['width'] = $srcWidth;
658
			}
659
		}
660
661
		if ( $file && isset( $hp['width'] ) ) {
662
			# Create a resized image, without the additional thumbnail features
663
			$thumb = $file->transform( $hp );
664
		} else {
665
			$thumb = false;
666
		}
667
668
		if ( !$thumb ) {
669
			$s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
670
		} else {
671
			self::processResponsiveImages( $file, $thumb, $hp );
672
			$params = [
673
				'alt' => $fp['alt'],
674
				'title' => $fp['title'],
675
				'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
676
				'img-class' => $fp['class'] ];
677
			if ( isset( $fp['border'] ) ) {
678
				$params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
679
			}
680
			$params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
681
682
			$s = $thumb->toHtml( $params );
683
		}
684
		if ( $fp['align'] != '' ) {
685
			$s = "<div class=\"float{$fp['align']}\">{$s}</div>";
686
		}
687
		return str_replace( "\n", ' ', $prefix . $s . $postfix );
688
	}
689
690
	/**
691
	 * Get the link parameters for MediaTransformOutput::toHtml() from given
692
	 * frame parameters supplied by the Parser.
693
	 * @param array $frameParams The frame parameters
694
	 * @param string $query An optional query string to add to description page links
695
	 * @param Parser|null $parser
696
	 * @return array
697
	 */
698
	private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
699
		$mtoParams = [];
700
		if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
701
			$mtoParams['custom-url-link'] = $frameParams['link-url'];
702
			if ( isset( $frameParams['link-target'] ) ) {
703
				$mtoParams['custom-target-link'] = $frameParams['link-target'];
704
			}
705
			if ( $parser ) {
706
				$extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
707
				foreach ( $extLinkAttrs as $name => $val ) {
708
					// Currently could include 'rel' and 'target'
709
					$mtoParams['parser-extlink-' . $name] = $val;
710
				}
711
			}
712
		} elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
713
			$mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
714
		} elseif ( !empty( $frameParams['no-link'] ) ) {
0 ignored issues
show
Unused Code introduced by
This elseif statement is empty, and could be removed.

This check looks for the bodies of elseif statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These elseif bodies can be removed. If you have an empty elseif but statements in the else branch, consider inverting the condition.

Loading history...
715
			// No link
716
		} else {
717
			$mtoParams['desc-link'] = true;
718
			$mtoParams['desc-query'] = $query;
719
		}
720
		return $mtoParams;
721
	}
722
723
	/**
724
	 * Make HTML for a thumbnail including image, border and caption
725
	 * @param Title $title
726
	 * @param File|bool $file File object or false if it doesn't exist
727
	 * @param string $label
728
	 * @param string $alt
729
	 * @param string $align
730
	 * @param array $params
731
	 * @param bool $framed
732
	 * @param string $manualthumb
733
	 * @return string
734
	 */
735
	public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
736
		$align = 'right', $params = [], $framed = false, $manualthumb = ""
737
	) {
738
		$frameParams = [
739
			'alt' => $alt,
740
			'caption' => $label,
741
			'align' => $align
742
		];
743
		if ( $framed ) {
744
			$frameParams['framed'] = true;
745
		}
746
		if ( $manualthumb ) {
747
			$frameParams['manualthumb'] = $manualthumb;
748
		}
749
		return self::makeThumbLink2( $title, $file, $frameParams, $params );
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 735 can also be of type boolean; however, Linker::makeThumbLink2() does only seem to accept object<File>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
750
	}
751
752
	/**
753
	 * @param Title $title
754
	 * @param File $file
755
	 * @param array $frameParams
756
	 * @param array $handlerParams
757
	 * @param bool $time
758
	 * @param string $query
759
	 * @return string
760
	 */
761
	public static function makeThumbLink2( Title $title, $file, $frameParams = [],
762
		$handlerParams = [], $time = false, $query = ""
763
	) {
764
		$exists = $file && $file->exists();
765
766
		# Shortcuts
767
		$fp =& $frameParams;
768
		$hp =& $handlerParams;
769
770
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
771
		if ( !isset( $fp['align'] ) ) {
772
			$fp['align'] = 'right';
773
		}
774
		if ( !isset( $fp['alt'] ) ) {
775
			$fp['alt'] = '';
776
		}
777
		if ( !isset( $fp['title'] ) ) {
778
			$fp['title'] = '';
779
		}
780
		if ( !isset( $fp['caption'] ) ) {
781
			$fp['caption'] = '';
782
		}
783
784
		if ( empty( $hp['width'] ) ) {
785
			// Reduce width for upright images when parameter 'upright' is used
786
			$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
787
		}
788
		$thumb = false;
789
		$noscale = false;
790
		$manualthumb = false;
791
792
		if ( !$exists ) {
793
			$outerWidth = $hp['width'] + 2;
794
		} else {
795
			if ( isset( $fp['manualthumb'] ) ) {
796
				# Use manually specified thumbnail
797
				$manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
798
				if ( $manual_title ) {
799
					$manual_img = wfFindFile( $manual_title );
800
					if ( $manual_img ) {
801
						$thumb = $manual_img->getUnscaledThumb( $hp );
802
						$manualthumb = true;
803
					} else {
804
						$exists = false;
805
					}
806
				}
807
			} elseif ( isset( $fp['framed'] ) ) {
808
				// Use image dimensions, don't scale
809
				$thumb = $file->getUnscaledThumb( $hp );
810
				$noscale = true;
811
			} else {
812
				# Do not present an image bigger than the source, for bitmap-style images
813
				# This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
814
				$srcWidth = $file->getWidth( $page );
815 View Code Duplication
				if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
816
					$hp['width'] = $srcWidth;
817
				}
818
				$thumb = $file->transform( $hp );
819
			}
820
821
			if ( $thumb ) {
822
				$outerWidth = $thumb->getWidth() + 2;
823
			} else {
824
				$outerWidth = $hp['width'] + 2;
825
			}
826
		}
827
828
		# ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
829
		# So we don't need to pass it here in $query. However, the URL for the
830
		# zoom icon still needs it, so we make a unique query for it. See bug 14771
831
		$url = $title->getLocalURL( $query );
832
		if ( $page ) {
833
			$url = wfAppendQuery( $url, [ 'page' => $page ] );
834
		}
835
		if ( $manualthumb
836
			&& !isset( $fp['link-title'] )
837
			&& !isset( $fp['link-url'] )
838
			&& !isset( $fp['no-link'] ) ) {
839
			$fp['link-url'] = $url;
840
		}
841
842
		$s = "<div class=\"thumb t{$fp['align']}\">"
843
			. "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
844
845
		if ( !$exists ) {
846
			$s .= self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
847
			$zoomIcon = '';
848
		} elseif ( !$thumb ) {
849
			$s .= wfMessage( 'thumbnail_error', '' )->escaped();
850
			$zoomIcon = '';
851
		} else {
852
			if ( !$noscale && !$manualthumb ) {
853
				self::processResponsiveImages( $file, $thumb, $hp );
854
			}
855
			$params = [
856
				'alt' => $fp['alt'],
857
				'title' => $fp['title'],
858
				'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== ''
859
					? $fp['class'] . ' '
860
					: '' ) . 'thumbimage'
861
			];
862
			$params = self::getImageLinkMTOParams( $fp, $query ) + $params;
863
			$s .= $thumb->toHtml( $params );
864
			if ( isset( $fp['framed'] ) ) {
865
				$zoomIcon = "";
866
			} else {
867
				$zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
868
					Html::rawElement( 'a', [
869
						'href' => $url,
870
						'class' => 'internal',
871
						'title' => wfMessage( 'thumbnail-more' )->text() ],
872
						"" ) );
873
			}
874
		}
875
		$s .= '  <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>";
876
		return str_replace( "\n", ' ', $s );
877
	}
878
879
	/**
880
	 * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
881
	 * applicable.
882
	 *
883
	 * @param File $file
884
	 * @param MediaTransformOutput $thumb
885
	 * @param array $hp Image parameters
886
	 */
887
	public static function processResponsiveImages( $file, $thumb, $hp ) {
888
		global $wgResponsiveImages;
889
		if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
890
			$hp15 = $hp;
891
			$hp15['width'] = round( $hp['width'] * 1.5 );
892
			$hp20 = $hp;
893
			$hp20['width'] = $hp['width'] * 2;
894
			if ( isset( $hp['height'] ) ) {
895
				$hp15['height'] = round( $hp['height'] * 1.5 );
896
				$hp20['height'] = $hp['height'] * 2;
897
			}
898
899
			$thumb15 = $file->transform( $hp15 );
900
			$thumb20 = $file->transform( $hp20 );
901
			if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
902
				$thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
903
			}
904
			if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
905
				$thumb->responsiveUrls['2'] = $thumb20->getUrl();
906
			}
907
		}
908
	}
909
910
	/**
911
	 * Make a "broken" link to an image
912
	 *
913
	 * @since 1.16.3
914
	 * @param Title $title
915
	 * @param string $label Link label (plain text)
916
	 * @param string $query Query string
917
	 * @param string $unused1 Unused parameter kept for b/c
918
	 * @param string $unused2 Unused parameter kept for b/c
919
	 * @param bool $time A file of a certain timestamp was requested
920
	 * @return string
921
	 */
922
	public static function makeBrokenImageLinkObj( $title, $label = '',
923
		$query = '', $unused1 = '', $unused2 = '', $time = false
924
	) {
925
		if ( !$title instanceof Title ) {
926
			wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
927
			return "<!-- ERROR -->" . htmlspecialchars( $label );
928
		}
929
930
		global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
931
		if ( $label == '' ) {
932
			$label = $title->getPrefixedText();
933
		}
934
		$encLabel = htmlspecialchars( $label );
935
		$currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
936
937
		if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
938
			&& !$currentExists
939
		) {
940
			$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
941
942
			if ( $redir ) {
943
				return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) );
944
			}
945
946
			$href = self::getUploadUrl( $title, $query );
947
948
			return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
949
				htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
950
				$encLabel . '</a>';
951
		}
952
953
		return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) );
954
	}
955
956
	/**
957
	 * Get the URL to upload a certain file
958
	 *
959
	 * @since 1.16.3
960
	 * @param Title $destFile Title object of the file to upload
961
	 * @param string $query Urlencoded query string to prepend
962
	 * @return string Urlencoded URL
963
	 */
964
	protected static function getUploadUrl( $destFile, $query = '' ) {
965
		global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
966
		$q = 'wpDestFile=' . $destFile->getPartialURL();
967
		if ( $query != '' ) {
968
			$q .= '&' . $query;
969
		}
970
971
		if ( $wgUploadMissingFileUrl ) {
972
			return wfAppendQuery( $wgUploadMissingFileUrl, $q );
973
		} elseif ( $wgUploadNavigationUrl ) {
974
			return wfAppendQuery( $wgUploadNavigationUrl, $q );
975
		} else {
976
			$upload = SpecialPage::getTitleFor( 'Upload' );
977
			return $upload->getLocalURL( $q );
978
		}
979
	}
980
981
	/**
982
	 * Create a direct link to a given uploaded file.
983
	 *
984
	 * @since 1.16.3
985
	 * @param Title $title
986
	 * @param string $html Pre-sanitized HTML
987
	 * @param string $time MW timestamp of file creation time
988
	 * @return string HTML
989
	 */
990
	public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
991
		$img = wfFindFile( $title, [ 'time' => $time ] );
992
		return self::makeMediaLinkFile( $title, $img, $html );
993
	}
994
995
	/**
996
	 * Create a direct link to a given uploaded file.
997
	 * This will make a broken link if $file is false.
998
	 *
999
	 * @since 1.16.3
1000
	 * @param Title $title
1001
	 * @param File|bool $file File object or false
1002
	 * @param string $html Pre-sanitized HTML
1003
	 * @return string HTML
1004
	 *
1005
	 * @todo Handle invalid or missing images better.
1006
	 */
1007
	public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
1008
		if ( $file && $file->exists() ) {
1009
			$url = $file->getUrl();
0 ignored issues
show
Bug introduced by
It seems like $file is not always an object, but can also be of type boolean. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1010
			$class = 'internal';
1011
		} else {
1012
			$url = self::getUploadUrl( $title );
1013
			$class = 'new';
1014
		}
1015
1016
		$alt = $title->getText();
1017
		if ( $html == '' ) {
1018
			$html = $alt;
1019
		}
1020
1021
		$ret = '';
1022
		$attribs = [
1023
			'href' => $url,
1024
			'class' => $class,
1025
			'title' => $alt
1026
		];
1027
1028
		if ( !Hooks::run( 'LinkerMakeMediaLinkFile',
1029
			[ $title, $file, &$html, &$attribs, &$ret ] ) ) {
1030
			wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
1031
				. "with url {$url} and text {$html} to {$ret}\n", true );
1032
			return $ret;
1033
		}
1034
1035
		return Html::rawElement( 'a', $attribs, $html );
1036
	}
1037
1038
	/**
1039
	 * Make a link to a special page given its name and, optionally,
1040
	 * a message key from the link text.
1041
	 * Usage example: Linker::specialLink( 'Recentchanges' )
1042
	 *
1043
	 * @since 1.16.3
1044
	 * @param string $name
1045
	 * @param string $key
1046
	 * @return string
1047
	 */
1048
	public static function specialLink( $name, $key = '' ) {
1049
		if ( $key == '' ) {
1050
			$key = strtolower( $name );
1051
		}
1052
1053
		return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
1054
	}
1055
1056
	/**
1057
	 * Make an external link
1058
	 * @since 1.16.3. $title added in 1.21
1059
	 * @param string $url URL to link to
1060
	 * @param string $text Text of link
1061
	 * @param bool $escape Do we escape the link text?
1062
	 * @param string $linktype Type of external link. Gets added to the classes
1063
	 * @param array $attribs Array of extra attributes to <a>
1064
	 * @param Title|null $title Title object used for title specific link attributes
1065
	 * @return string
1066
	 */
1067
	public static function makeExternalLink( $url, $text, $escape = true,
1068
		$linktype = '', $attribs = [], $title = null
1069
	) {
1070
		global $wgTitle;
1071
		$class = "external";
1072
		if ( $linktype ) {
1073
			$class .= " $linktype";
1074
		}
1075
		if ( isset( $attribs['class'] ) && $attribs['class'] ) {
1076
			$class .= " {$attribs['class']}";
1077
		}
1078
		$attribs['class'] = $class;
1079
1080
		if ( $escape ) {
1081
			$text = htmlspecialchars( $text );
1082
		}
1083
1084
		if ( !$title ) {
1085
			$title = $wgTitle;
1086
		}
1087
		$newRel = Parser::getExternalLinkRel( $url, $title );
1088
		if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) {
1089
			$attribs['rel'] = $newRel;
1090
		} elseif( $newRel !== '' ) {
1091
			// Merge the rel attributes.
1092
			$newRels = explode( ' ', $newRel );
1093
			$oldRels = explode( ' ', $attribs['rel'] );
1094
			$combined = array_unique( array_merge( $newRels, $oldRels ) );
1095
			$attribs['rel'] = implode( ' ', $combined );
1096
		}
1097
		$link = '';
1098
		$success = Hooks::run( 'LinkerMakeExternalLink',
1099
			[ &$url, &$text, &$link, &$attribs, $linktype ] );
1100
		if ( !$success ) {
1101
			wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
1102
				. "with url {$url} and text {$text} to {$link}\n", true );
1103
			return $link;
1104
		}
1105
		$attribs['href'] = $url;
1106
		return Html::rawElement( 'a', $attribs, $text );
1107
	}
1108
1109
	/**
1110
	 * Make user link (or user contributions for unregistered users)
1111
	 * @param int $userId User id in database.
1112
	 * @param string $userName User name in database.
1113
	 * @param string $altUserName Text to display instead of the user name (optional)
1114
	 * @return string HTML fragment
1115
	 * @since 1.16.3. $altUserName was added in 1.19.
1116
	 */
1117
	public static function userLink( $userId, $userName, $altUserName = false ) {
1118
		$classes = 'mw-userlink';
1119
		if ( $userId == 0 ) {
1120
			$page = SpecialPage::getTitleFor( 'Contributions', $userName );
1121
			if ( $altUserName === false ) {
1122
				$altUserName = IP::prettifyIP( $userName );
1123
			}
1124
			$classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
1125
		} else {
1126
			$page = Title::makeTitle( NS_USER, $userName );
1127
		}
1128
1129
		return self::link(
1130
			$page,
1131
			htmlspecialchars( $altUserName !== false ? $altUserName : $userName ),
1132
			[ 'class' => $classes ]
1133
		);
1134
	}
1135
1136
	/**
1137
	 * Generate standard user tool links (talk, contributions, block link, etc.)
1138
	 *
1139
	 * @since 1.16.3
1140
	 * @param int $userId User identifier
1141
	 * @param string $userText User name or IP address
1142
	 * @param bool $redContribsWhenNoEdits Should the contributions link be
1143
	 *   red if the user has no edits?
1144
	 * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1145
	 *   and Linker::TOOL_LINKS_EMAIL).
1146
	 * @param int $edits User edit count (optional, for performance)
1147
	 * @return string HTML fragment
1148
	 */
1149
	public static function userToolLinks(
1150
		$userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1151
	) {
1152
		global $wgUser, $wgDisableAnonTalk, $wgLang;
1153
		$talkable = !( $wgDisableAnonTalk && 0 == $userId );
1154
		$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1155
		$addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1156
1157
		$items = [];
1158
		if ( $talkable ) {
1159
			$items[] = self::userTalkLink( $userId, $userText );
1160
		}
1161
		if ( $userId ) {
1162
			// check if the user has an edit
1163
			$attribs = [];
1164
			if ( $redContribsWhenNoEdits ) {
1165
				if ( intval( $edits ) === 0 && $edits !== 0 ) {
1166
					$user = User::newFromId( $userId );
1167
					$edits = $user->getEditCount();
1168
				}
1169
				if ( $edits === 0 ) {
1170
					$attribs['class'] = 'new';
1171
				}
1172
			}
1173
			$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1174
1175
			$items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1176
		}
1177
		if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
1178
			$items[] = self::blockLink( $userId, $userText );
1179
		}
1180
1181
		if ( $addEmailLink && $wgUser->canSendEmail() ) {
1182
			$items[] = self::emailLink( $userId, $userText );
1183
		}
1184
1185
		Hooks::run( 'UserToolLinksEdit', [ $userId, $userText, &$items ] );
1186
1187
		if ( $items ) {
1188
			return wfMessage( 'word-separator' )->escaped()
1189
				. '<span class="mw-usertoollinks">'
1190
				. wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1191
				. '</span>';
1192
		} else {
1193
			return '';
1194
		}
1195
	}
1196
1197
	/**
1198
	 * Alias for userToolLinks( $userId, $userText, true );
1199
	 * @since 1.16.3
1200
	 * @param int $userId User identifier
1201
	 * @param string $userText User name or IP address
1202
	 * @param int $edits User edit count (optional, for performance)
1203
	 * @return string
1204
	 */
1205
	public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
1206
		return self::userToolLinks( $userId, $userText, true, 0, $edits );
1207
	}
1208
1209
	/**
1210
	 * @since 1.16.3
1211
	 * @param int $userId User id in database.
1212
	 * @param string $userText User name in database.
1213
	 * @return string HTML fragment with user talk link
1214
	 */
1215
	public static function userTalkLink( $userId, $userText ) {
1216
		$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
1217
		$userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
1218
		return $userTalkLink;
1219
	}
1220
1221
	/**
1222
	 * @since 1.16.3
1223
	 * @param int $userId Userid
1224
	 * @param string $userText User name in database.
1225
	 * @return string HTML fragment with block link
1226
	 */
1227
	public static function blockLink( $userId, $userText ) {
1228
		$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1229
		$blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
1230
		return $blockLink;
1231
	}
1232
1233
	/**
1234
	 * @param int $userId Userid
1235
	 * @param string $userText User name in database.
1236
	 * @return string HTML fragment with e-mail user link
1237
	 */
1238
	public static function emailLink( $userId, $userText ) {
1239
		$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1240
		$emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
1241
		return $emailLink;
1242
	}
1243
1244
	/**
1245
	 * Generate a user link if the current user is allowed to view it
1246
	 * @since 1.16.3
1247
	 * @param Revision $rev
1248
	 * @param bool $isPublic Show only if all users can see it
1249
	 * @return string HTML fragment
1250
	 */
1251
	public static function revUserLink( $rev, $isPublic = false ) {
1252
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1253
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1254
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1255
			$link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
1256
				$rev->getUserText( Revision::FOR_THIS_USER ) );
1257
		} else {
1258
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1259
		}
1260
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1261
			return '<span class="history-deleted">' . $link . '</span>';
1262
		}
1263
		return $link;
1264
	}
1265
1266
	/**
1267
	 * Generate a user tool link cluster if the current user is allowed to view it
1268
	 * @since 1.16.3
1269
	 * @param Revision $rev
1270
	 * @param bool $isPublic Show only if all users can see it
1271
	 * @return string HTML
1272
	 */
1273
	public static function revUserTools( $rev, $isPublic = false ) {
1274
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1275
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1276
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1277
			$userId = $rev->getUser( Revision::FOR_THIS_USER );
1278
			$userText = $rev->getUserText( Revision::FOR_THIS_USER );
1279
			$link = self::userLink( $userId, $userText )
1280
				. self::userToolLinks( $userId, $userText );
1281
		} else {
1282
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1283
		}
1284
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1285
			return ' <span class="history-deleted">' . $link . '</span>';
1286
		}
1287
		return $link;
1288
	}
1289
1290
	/**
1291
	 * This function is called by all recent changes variants, by the page history,
1292
	 * and by the user contributions list. It is responsible for formatting edit
1293
	 * summaries. It escapes any HTML in the summary, but adds some CSS to format
1294
	 * auto-generated comments (from section editing) and formats [[wikilinks]].
1295
	 *
1296
	 * @author Erik Moeller <[email protected]>
1297
	 * @since 1.16.3. $wikiId added in 1.26
1298
	 *
1299
	 * Note: there's not always a title to pass to this function.
1300
	 * Since you can't set a default parameter for a reference, I've turned it
1301
	 * temporarily to a value pass. Should be adjusted further. --brion
1302
	 *
1303
	 * @param string $comment
1304
	 * @param Title|null $title Title object (to generate link to the section in autocomment)
1305
	 *  or null
1306
	 * @param bool $local Whether section links should refer to local page
1307
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1308
	 *  For use with external changes.
1309
	 *
1310
	 * @return mixed|string
1311
	 */
1312
	public static function formatComment(
1313
		$comment, $title = null, $local = false, $wikiId = null
1314
	) {
1315
		# Sanitize text a bit:
1316
		$comment = str_replace( "\n", " ", $comment );
1317
		# Allow HTML entities (for bug 13815)
1318
		$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
1319
1320
		# Render autocomments and make links:
1321
		$comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
1322
		$comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
1323
1324
		return $comment;
1325
	}
1326
1327
	/**
1328
	 * Converts autogenerated comments in edit summaries into section links.
1329
	 *
1330
	 * The pattern for autogen comments is / * foo * /, which makes for
1331
	 * some nasty regex.
1332
	 * We look for all comments, match any text before and after the comment,
1333
	 * add a separator where needed and format the comment itself with CSS
1334
	 * Called by Linker::formatComment.
1335
	 *
1336
	 * @param string $comment Comment text
1337
	 * @param Title|null $title An optional title object used to links to sections
1338
	 * @param bool $local Whether section links should refer to local page
1339
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1340
	 *  as used by WikiMap.
1341
	 *
1342
	 * @return string Formatted comment (wikitext)
1343
	 */
1344
	private static function formatAutocomments(
1345
		$comment, $title = null, $local = false, $wikiId = null
1346
	) {
1347
		// @todo $append here is something of a hack to preserve the status
1348
		// quo. Someone who knows more about bidi and such should decide
1349
		// (1) what sane rendering even *is* for an LTR edit summary on an RTL
1350
		// wiki, both when autocomments exist and when they don't, and
1351
		// (2) what markup will make that actually happen.
1352
		$append = '';
1353
		$comment = preg_replace_callback(
1354
			// To detect the presence of content before or after the
1355
			// auto-comment, we use capturing groups inside optional zero-width
1356
			// assertions. But older versions of PCRE can't directly make
1357
			// zero-width assertions optional, so wrap them in a non-capturing
1358
			// group.
1359
			'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
1360
			function ( $match ) use ( $title, $local, $wikiId, &$append ) {
1361
				global $wgLang;
1362
1363
				// Ensure all match positions are defined
1364
				$match += [ '', '', '', '' ];
1365
1366
				$pre = $match[1] !== '';
1367
				$auto = $match[2];
1368
				$post = $match[3] !== '';
1369
				$comment = null;
1370
1371
				Hooks::run(
1372
					'FormatAutocomments',
1373
					[ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
1374
				);
1375
1376
				if ( $comment === null ) {
1377
					$link = '';
1378
					if ( $title ) {
1379
						$section = $auto;
1380
						# Remove links that a user may have manually put in the autosummary
1381
						# This could be improved by copying as much of Parser::stripSectionName as desired.
1382
						$section = str_replace( '[[:', '', $section );
1383
						$section = str_replace( '[[', '', $section );
1384
						$section = str_replace( ']]', '', $section );
1385
1386
						$section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
1387
						if ( $local ) {
1388
							$sectionTitle = Title::newFromText( '#' . $section );
1389
						} else {
1390
							$sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
1391
								$title->getDBkey(), $section );
1392
						}
1393
						if ( $sectionTitle ) {
1394
							$link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
1395
						} else {
1396
							$link = '';
1397
						}
1398
					}
1399
					if ( $pre ) {
1400
						# written summary $presep autocomment (summary /* section */)
1401
						$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
1402
					}
1403
					if ( $post ) {
1404
						# autocomment $postsep written summary (/* section */ summary)
1405
						$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
1406
					}
1407
					$auto = '<span class="autocomment">' . $auto . '</span>';
1408
					$comment = $pre . $link . $wgLang->getDirMark()
1409
						. '<span dir="auto">' . $auto;
1410
					$append .= '</span>';
1411
				}
1412
				return $comment;
1413
			},
1414
			$comment
1415
		);
1416
		return $comment . $append;
1417
	}
1418
1419
	/**
1420
	 * Formats wiki links and media links in text; all other wiki formatting
1421
	 * is ignored
1422
	 *
1423
	 * @since 1.16.3. $wikiId added in 1.26
1424
	 * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
1425
	 *
1426
	 * @param string $comment Text to format links in. WARNING! Since the output of this
1427
	 *	function is html, $comment must be sanitized for use as html. You probably want
1428
	 *	to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
1429
	 *	this function.
1430
	 * @param Title|null $title An optional title object used to links to sections
1431
	 * @param bool $local Whether section links should refer to local page
1432
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1433
	 *  as used by WikiMap.
1434
	 *
1435
	 * @return string
1436
	 */
1437
	public static function formatLinksInComment(
1438
		$comment, $title = null, $local = false, $wikiId = null
1439
	) {
1440
		return preg_replace_callback(
1441
			'/
1442
				\[\[
1443
				:? # ignore optional leading colon
1444
				([^\]|]+) # 1. link target; page names cannot include ] or |
1445
				(?:\|
1446
					# 2. a pipe-separated substring; only the last is captured
1447
					# Stop matching at | and ]] without relying on backtracking.
1448
					((?:]?[^\]|])*+)
1449
				)*
1450
				\]\]
1451
				([^[]*) # 3. link trail (the text up until the next link)
1452
			/x',
1453
			function ( $match ) use ( $title, $local, $wikiId ) {
1454
				global $wgContLang;
1455
1456
				$medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
1457
				$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
1458
1459
				$comment = $match[0];
1460
1461
				# fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
1462
				if ( strpos( $match[1], '%' ) !== false ) {
1463
					$match[1] = strtr(
1464
						rawurldecode( $match[1] ),
1465
						[ '<' => '&lt;', '>' => '&gt;' ]
1466
					);
1467
				}
1468
1469
				# Handle link renaming [[foo|text]] will show link as "text"
1470
				if ( $match[2] != "" ) {
1471
					$text = $match[2];
1472
				} else {
1473
					$text = $match[1];
1474
				}
1475
				$submatch = [];
1476
				$thelink = null;
1477
				if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
1478
					# Media link; trail not supported.
1479
					$linkRegexp = '/\[\[(.*?)\]\]/';
1480
					$title = Title::makeTitleSafe( NS_FILE, $submatch[1] );
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $title, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
1481
					if ( $title ) {
1482
						$thelink = Linker::makeMediaLinkObj( $title, $text );
1483
					}
1484
				} else {
1485
					# Other kind of link
1486
					# Make sure its target is non-empty
1487
					if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
1488
						$match[1] = substr( $match[1], 1 );
1489
					}
1490
					if ( $match[1] !== false && $match[1] !== '' ) {
1491
						if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
1492
							$trail = $submatch[1];
1493
						} else {
1494
							$trail = "";
1495
						}
1496
						$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
1497
						list( $inside, $trail ) = Linker::splitTrail( $trail );
1498
1499
						$linkText = $text;
1500
						$linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 1438 can be null; however, Linker::normalizeSubpageLink() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
1501
1502
						$target = Title::newFromText( $linkTarget );
1503
						if ( $target ) {
1504
							if ( $target->getText() == '' && !$target->isExternal()
1505
								&& !$local && $title
1506
							) {
1507
								$newTarget = clone $title;
1508
								$newTarget->setFragment( '#' . $target->getFragment() );
1509
								$target = $newTarget;
1510
							}
1511
1512
							$thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
1513
						}
1514
					}
1515
				}
1516
				if ( $thelink ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $thelink of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1517
					// If the link is still valid, go ahead and replace it in!
1518
					$comment = preg_replace(
1519
						$linkRegexp,
0 ignored issues
show
Bug introduced by
The variable $linkRegexp 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...
1520
						StringUtils::escapeRegexReplacement( $thelink ),
1521
						$comment,
1522
						1
1523
					);
1524
				}
1525
1526
				return $comment;
1527
			},
1528
			$comment
1529
		);
1530
	}
1531
1532
	/**
1533
	 * Generates a link to the given Title
1534
	 *
1535
	 * @note This is only public for technical reasons. It's not intended for use outside Linker.
1536
	 *
1537
	 * @param Title $title
1538
	 * @param string $text
1539
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1540
	 *  as used by WikiMap.
1541
	 * @param string|string[] $options See the $options parameter in Linker::link.
1542
	 *
1543
	 * @return string HTML link
1544
	 */
1545
	public static function makeCommentLink(
1546
		Title $title, $text, $wikiId = null, $options = []
1547
	) {
1548
		if ( $wikiId !== null && !$title->isExternal() ) {
1549
			$link = Linker::makeExternalLink(
1550
				WikiMap::getForeignURL(
0 ignored issues
show
Security Bug introduced by
It seems like \WikiMap::getForeignURL(... $title->getFragment()) targeting WikiMap::getForeignURL() can also be of type false; however, Linker::makeExternalLink() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1551
					$wikiId,
1552
					$title->getPrefixedText(),
1553
					$title->getFragment()
1554
				),
1555
				$text,
1556
				/* escape = */ false // Already escaped
1557
			);
1558
		} else {
1559
			$link = Linker::link( $title, $text, [], [], $options );
1560
		}
1561
1562
		return $link;
1563
	}
1564
1565
	/**
1566
	 * @param Title $contextTitle
1567
	 * @param string $target
1568
	 * @param string $text
1569
	 * @return string
1570
	 */
1571
	public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1572
		# Valid link forms:
1573
		# Foobar -- normal
1574
		# :Foobar -- override special treatment of prefix (images, language links)
1575
		# /Foobar -- convert to CurrentPage/Foobar
1576
		# /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1577
		# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1578
		# ../Foobar -- convert to CurrentPage/Foobar,
1579
		#              (from CurrentPage/CurrentSubPage)
1580
		# ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1581
		#              (from CurrentPage/CurrentSubPage)
1582
1583
		$ret = $target; # default return value is no change
1584
1585
		# Some namespaces don't allow subpages,
1586
		# so only perform processing if subpages are allowed
1587
		if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
1588
			$hash = strpos( $target, '#' );
1589
			if ( $hash !== false ) {
1590
				$suffix = substr( $target, $hash );
1591
				$target = substr( $target, 0, $hash );
1592
			} else {
1593
				$suffix = '';
1594
			}
1595
			# bug 7425
1596
			$target = trim( $target );
1597
			# Look at the first character
1598
			if ( $target != '' && $target[0] === '/' ) {
1599
				# / at end means we don't want the slash to be shown
1600
				$m = [];
1601
				$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1602 View Code Duplication
				if ( $trailingSlashes ) {
1603
					$noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1604
				} else {
1605
					$noslash = substr( $target, 1 );
1606
				}
1607
1608
				$ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
1609
				if ( $text === '' ) {
1610
					$text = $target . $suffix;
1611
				} # this might be changed for ugliness reasons
1612
			} else {
1613
				# check for .. subpage backlinks
1614
				$dotdotcount = 0;
1615
				$nodotdot = $target;
1616
				while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
1617
					++$dotdotcount;
1618
					$nodotdot = substr( $nodotdot, 3 );
1619
				}
1620
				if ( $dotdotcount > 0 ) {
1621
					$exploded = explode( '/', $contextTitle->getPrefixedText() );
1622
					if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1623
						$ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1624
						# / at the end means don't show full path
1625
						if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1626
							$nodotdot = rtrim( $nodotdot, '/' );
1627
							if ( $text === '' ) {
1628
								$text = $nodotdot . $suffix;
1629
							}
1630
						}
1631
						$nodotdot = trim( $nodotdot );
1632
						if ( $nodotdot != '' ) {
1633
							$ret .= '/' . $nodotdot;
1634
						}
1635
						$ret .= $suffix;
1636
					}
1637
				}
1638
			}
1639
		}
1640
1641
		return $ret;
1642
	}
1643
1644
	/**
1645
	 * Wrap a comment in standard punctuation and formatting if
1646
	 * it's non-empty, otherwise return empty string.
1647
	 *
1648
	 * @since 1.16.3. $wikiId added in 1.26
1649
	 * @param string $comment
1650
	 * @param Title|null $title Title object (to generate link to section in autocomment) or null
1651
	 * @param bool $local Whether section links should refer to local page
1652
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1653
	 *  For use with external changes.
1654
	 *
1655
	 * @return string
1656
	 */
1657
	public static function commentBlock(
1658
		$comment, $title = null, $local = false, $wikiId = null
1659
	) {
1660
		// '*' used to be the comment inserted by the software way back
1661
		// in antiquity in case none was provided, here for backwards
1662
		// compatibility, acc. to brion -ævar
1663
		if ( $comment == '' || $comment == '*' ) {
1664
			return '';
1665
		} else {
1666
			$formatted = self::formatComment( $comment, $title, $local, $wikiId );
1667
			$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
1668
			return " <span class=\"comment\">$formatted</span>";
1669
		}
1670
	}
1671
1672
	/**
1673
	 * Wrap and format the given revision's comment block, if the current
1674
	 * user is allowed to view it.
1675
	 *
1676
	 * @since 1.16.3
1677
	 * @param Revision $rev
1678
	 * @param bool $local Whether section links should refer to local page
1679
	 * @param bool $isPublic Show only if all users can see it
1680
	 * @return string HTML fragment
1681
	 */
1682
	public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
1683
		if ( $rev->getComment( Revision::RAW ) == "" ) {
1684
			return "";
1685
		}
1686
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
1687
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1688
		} elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
1689
			$block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
1690
				$rev->getTitle(), $local );
1691
		} else {
1692
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1693
		}
1694
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
1695
			return " <span class=\"history-deleted\">$block</span>";
1696
		}
1697
		return $block;
1698
	}
1699
1700
	/**
1701
	 * @since 1.16.3
1702
	 * @param int $size
1703
	 * @return string
1704
	 */
1705
	public static function formatRevisionSize( $size ) {
1706
		if ( $size == 0 ) {
1707
			$stxt = wfMessage( 'historyempty' )->escaped();
1708
		} else {
1709
			$stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1710
			$stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
1711
		}
1712
		return "<span class=\"history-size\">$stxt</span>";
1713
	}
1714
1715
	/**
1716
	 * Add another level to the Table of Contents
1717
	 *
1718
	 * @since 1.16.3
1719
	 * @return string
1720
	 */
1721
	public static function tocIndent() {
1722
		return "\n<ul>";
1723
	}
1724
1725
	/**
1726
	 * Finish one or more sublevels on the Table of Contents
1727
	 *
1728
	 * @since 1.16.3
1729
	 * @param int $level
1730
	 * @return string
1731
	 */
1732
	public static function tocUnindent( $level ) {
1733
		return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1734
	}
1735
1736
	/**
1737
	 * parameter level defines if we are on an indentation level
1738
	 *
1739
	 * @since 1.16.3
1740
	 * @param string $anchor
1741
	 * @param string $tocline
1742
	 * @param string $tocnumber
1743
	 * @param string $level
1744
	 * @param string|bool $sectionIndex
1745
	 * @return string
1746
	 */
1747
	public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1748
		$classes = "toclevel-$level";
1749
		if ( $sectionIndex !== false ) {
1750
			$classes .= " tocsection-$sectionIndex";
1751
		}
1752
		return "\n<li class=\"$classes\"><a href=\"#" .
1753
			$anchor . '"><span class="tocnumber">' .
1754
			$tocnumber . '</span> <span class="toctext">' .
1755
			$tocline . '</span></a>';
1756
	}
1757
1758
	/**
1759
	 * End a Table Of Contents line.
1760
	 * tocUnindent() will be used instead if we're ending a line below
1761
	 * the new level.
1762
	 * @since 1.16.3
1763
	 * @return string
1764
	 */
1765
	public static function tocLineEnd() {
1766
		return "</li>\n";
1767
	}
1768
1769
	/**
1770
	 * Wraps the TOC in a table and provides the hide/collapse javascript.
1771
	 *
1772
	 * @since 1.16.3
1773
	 * @param string $toc Html of the Table Of Contents
1774
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1775
	 * @return string Full html of the TOC
1776
	 */
1777
	public static function tocList( $toc, $lang = false ) {
1778
		$lang = wfGetLangObj( $lang );
1779
		$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1780
1781
		return '<div id="toc" class="toc">'
1782
			. '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
1783
			. $toc
1784
			. "</ul>\n</div>\n";
1785
	}
1786
1787
	/**
1788
	 * Generate a table of contents from a section tree.
1789
	 *
1790
	 * @since 1.16.3. $lang added in 1.17
1791
	 * @param array $tree Return value of ParserOutput::getSections()
1792
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1793
	 * @return string HTML fragment
1794
	 */
1795
	public static function generateTOC( $tree, $lang = false ) {
1796
		$toc = '';
1797
		$lastLevel = 0;
1798
		foreach ( $tree as $section ) {
1799
			if ( $section['toclevel'] > $lastLevel ) {
1800
				$toc .= self::tocIndent();
1801
			} elseif ( $section['toclevel'] < $lastLevel ) {
1802
				$toc .= self::tocUnindent(
1803
					$lastLevel - $section['toclevel'] );
1804
			} else {
1805
				$toc .= self::tocLineEnd();
1806
			}
1807
1808
			$toc .= self::tocLine( $section['anchor'],
1809
				$section['line'], $section['number'],
1810
				$section['toclevel'], $section['index'] );
1811
			$lastLevel = $section['toclevel'];
1812
		}
1813
		$toc .= self::tocLineEnd();
1814
		return self::tocList( $toc, $lang );
1815
	}
1816
1817
	/**
1818
	 * Create a headline for content
1819
	 *
1820
	 * @since 1.16.3
1821
	 * @param int $level The level of the headline (1-6)
1822
	 * @param string $attribs Any attributes for the headline, starting with
1823
	 *   a space and ending with '>'
1824
	 *   This *must* be at least '>' for no attribs
1825
	 * @param string $anchor The anchor to give the headline (the bit after the #)
1826
	 * @param string $html Html for the text of the header
1827
	 * @param string $link HTML to add for the section edit link
1828
	 * @param bool|string $legacyAnchor A second, optional anchor to give for
1829
	 *   backward compatibility (false to omit)
1830
	 *
1831
	 * @return string HTML headline
1832
	 */
1833
	public static function makeHeadline( $level, $attribs, $anchor, $html,
1834
		$link, $legacyAnchor = false
1835
	) {
1836
		$ret = "<h$level$attribs"
1837
			. "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
1838
			. $link
1839
			. "</h$level>";
1840
		if ( $legacyAnchor !== false ) {
1841
			$ret = "<div id=\"$legacyAnchor\"></div>$ret";
1842
		}
1843
		return $ret;
1844
	}
1845
1846
	/**
1847
	 * Split a link trail, return the "inside" portion and the remainder of the trail
1848
	 * as a two-element array
1849
	 * @param string $trail
1850
	 * @return array
1851
	 */
1852
	static function splitTrail( $trail ) {
1853
		global $wgContLang;
1854
		$regex = $wgContLang->linkTrail();
1855
		$inside = '';
1856
		if ( $trail !== '' ) {
1857
			$m = [];
1858
			if ( preg_match( $regex, $trail, $m ) ) {
1859
				$inside = $m[1];
1860
				$trail = $m[2];
1861
			}
1862
		}
1863
		return [ $inside, $trail ];
1864
	}
1865
1866
	/**
1867
	 * Generate a rollback link for a given revision.  Currently it's the
1868
	 * caller's responsibility to ensure that the revision is the top one. If
1869
	 * it's not, of course, the user will get an error message.
1870
	 *
1871
	 * If the calling page is called with the parameter &bot=1, all rollback
1872
	 * links also get that parameter. It causes the edit itself and the rollback
1873
	 * to be marked as "bot" edits. Bot edits are hidden by default from recent
1874
	 * changes, so this allows sysops to combat a busy vandal without bothering
1875
	 * other users.
1876
	 *
1877
	 * If the option verify is set this function will return the link only in case the
1878
	 * revision can be reverted. Please note that due to performance limitations
1879
	 * it might be assumed that a user isn't the only contributor of a page while
1880
	 * (s)he is, which will lead to useless rollback links. Furthermore this wont
1881
	 * work if $wgShowRollbackEditCount is disabled, so this can only function
1882
	 * as an additional check.
1883
	 *
1884
	 * If the option noBrackets is set the rollback link wont be enclosed in []
1885
	 *
1886
	 * @since 1.16.3. $context added in 1.20. $options added in 1.21
1887
	 *
1888
	 * @param Revision $rev
1889
	 * @param IContextSource $context Context to use or null for the main context.
1890
	 * @param array $options
1891
	 * @return string
1892
	 */
1893
	public static function generateRollback( $rev, IContextSource $context = null,
1894
		$options = [ 'verify' ]
1895
	) {
1896
		if ( $context === null ) {
1897
			$context = RequestContext::getMain();
1898
		}
1899
1900
		$editCount = false;
1901
		if ( in_array( 'verify', $options, true ) ) {
1902
			$editCount = self::getRollbackEditCount( $rev, true );
1903
			if ( $editCount === false ) {
1904
				return '';
1905
			}
1906
		}
1907
1908
		$inner = self::buildRollbackLink( $rev, $context, $editCount );
0 ignored issues
show
Bug introduced by
It seems like $editCount defined by self::getRollbackEditCount($rev, true) on line 1902 can also be of type null; however, Linker::buildRollbackLink() does only seem to accept false|integer, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1909
1910
		if ( !in_array( 'noBrackets', $options, true ) ) {
1911
			$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1912
		}
1913
1914
		return '<span class="mw-rollback-link">' . $inner . '</span>';
1915
	}
1916
1917
	/**
1918
	 * This function will return the number of revisions which a rollback
1919
	 * would revert and, if $verify is set it will verify that a revision
1920
	 * can be reverted (that the user isn't the only contributor and the
1921
	 * revision we might rollback to isn't deleted). These checks can only
1922
	 * function as an additional check as this function only checks against
1923
	 * the last $wgShowRollbackEditCount edits.
1924
	 *
1925
	 * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
1926
	 * is set and the user is the only contributor of the page.
1927
	 *
1928
	 * @param Revision $rev
1929
	 * @param bool $verify Try to verify that this revision can really be rolled back
1930
	 * @return int|bool|null
1931
	 */
1932
	public static function getRollbackEditCount( $rev, $verify ) {
1933
		global $wgShowRollbackEditCount;
1934
		if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
1935
			// Nothing has happened, indicate this by returning 'null'
1936
			return null;
1937
		}
1938
1939
		$dbr = wfGetDB( DB_SLAVE );
1940
1941
		// Up to the value of $wgShowRollbackEditCount revisions are counted
1942
		$res = $dbr->select(
1943
			'revision',
1944
			[ 'rev_user_text', 'rev_deleted' ],
1945
			// $rev->getPage() returns null sometimes
1946
			[ 'rev_page' => $rev->getTitle()->getArticleID() ],
1947
			__METHOD__,
1948
			[
1949
				'USE INDEX' => [ 'revision' => 'page_timestamp' ],
1950
				'ORDER BY' => 'rev_timestamp DESC',
1951
				'LIMIT' => $wgShowRollbackEditCount + 1
1952
			]
1953
		);
1954
1955
		$editCount = 0;
1956
		$moreRevs = false;
1957
		foreach ( $res as $row ) {
0 ignored issues
show
Bug introduced by
The expression $res of type object<ResultWrapper>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1958
			if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) {
1959
				if ( $verify &&
1960
					( $row->rev_deleted & Revision::DELETED_TEXT
1961
						|| $row->rev_deleted & Revision::DELETED_USER
1962
				) ) {
1963
					// If the user or the text of the revision we might rollback
1964
					// to is deleted in some way we can't rollback. Similar to
1965
					// the sanity checks in WikiPage::commitRollback.
1966
					return false;
1967
				}
1968
				$moreRevs = true;
1969
				break;
1970
			}
1971
			$editCount++;
1972
		}
1973
1974
		if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
1975
			// We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1976
			// and there weren't any other revisions. That means that the current user is the only
1977
			// editor, so we can't rollback
1978
			return false;
1979
		}
1980
		return $editCount;
1981
	}
1982
1983
	/**
1984
	 * Build a raw rollback link, useful for collections of "tool" links
1985
	 *
1986
	 * @since 1.16.3. $context added in 1.20. $editCount added in 1.21
1987
	 * @param Revision $rev
1988
	 * @param IContextSource|null $context Context to use or null for the main context.
1989
	 * @param int $editCount Number of edits that would be reverted
1990
	 * @return string HTML fragment
1991
	 */
1992
	public static function buildRollbackLink( $rev, IContextSource $context = null,
1993
		$editCount = false
1994
	) {
1995
		global $wgShowRollbackEditCount, $wgMiserMode;
1996
1997
		// To config which pages are affected by miser mode
1998
		$disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1999
2000
		if ( $context === null ) {
2001
			$context = RequestContext::getMain();
2002
		}
2003
2004
		$title = $rev->getTitle();
2005
		$query = [
2006
			'action' => 'rollback',
2007
			'from' => $rev->getUserText(),
2008
			'token' => $context->getUser()->getEditToken( [
2009
				$title->getPrefixedText(),
2010
				$rev->getUserText()
2011
			] ),
2012
		];
2013
		if ( $context->getRequest()->getBool( 'bot' ) ) {
2014
			$query['bot'] = '1';
2015
			$query['hidediff'] = '1'; // bug 15999
2016
		}
2017
2018
		$disableRollbackEditCount = false;
2019
		if ( $wgMiserMode ) {
2020
			foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
2021
				if ( $context->getTitle()->isSpecial( $specialPage ) ) {
2022
					$disableRollbackEditCount = true;
2023
					break;
2024
				}
2025
			}
2026
		}
2027
2028
		if ( !$disableRollbackEditCount
2029
			&& is_int( $wgShowRollbackEditCount )
2030
			&& $wgShowRollbackEditCount > 0
2031
		) {
2032
			if ( !is_numeric( $editCount ) ) {
2033
				$editCount = self::getRollbackEditCount( $rev, false );
2034
			}
2035
2036
			if ( $editCount > $wgShowRollbackEditCount ) {
2037
				$editCount_output = $context->msg( 'rollbacklinkcount-morethan' )
2038
					->numParams( $wgShowRollbackEditCount )->parse();
2039
			} else {
2040
				$editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
2041
			}
2042
2043
			return self::link(
2044
				$title,
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 2004 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2045
				$editCount_output,
2046
				[ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
2047
				$query,
2048
				[ 'known', 'noclasses' ]
2049
			);
2050
		} else {
2051
			return self::link(
2052
				$title,
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 2004 can be null; however, Linker::link() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
2053
				$context->msg( 'rollbacklink' )->escaped(),
2054
				[ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
2055
				$query,
2056
				[ 'known', 'noclasses' ]
2057
			);
2058
		}
2059
	}
2060
2061
	/**
2062
	 * Returns HTML for the "templates used on this page" list.
2063
	 *
2064
	 * Make an HTML list of templates, and then add a "More..." link at
2065
	 * the bottom. If $more is null, do not add a "More..." link. If $more
2066
	 * is a Title, make a link to that title and use it. If $more is a string,
2067
	 * directly paste it in as the link (escaping needs to be done manually).
2068
	 * Finally, if $more is a Message, call toString().
2069
	 *
2070
	 * @since 1.16.3. $more added in 1.21
2071
	 * @param Title[] $templates Array of templates
2072
	 * @param bool $preview Whether this is for a preview
2073
	 * @param bool $section Whether this is for a section edit
2074
	 * @param Title|Message|string|null $more An escaped link for "More..." of the templates
2075
	 * @return string HTML output
2076
	 */
2077
	public static function formatTemplates( $templates, $preview = false,
2078
		$section = false, $more = null
2079
	) {
2080
		global $wgLang;
2081
2082
		$outText = '';
2083
		if ( count( $templates ) > 0 ) {
2084
			# Do a batch existence check
2085
			$batch = new LinkBatch;
2086
			foreach ( $templates as $title ) {
2087
				$batch->addObj( $title );
2088
			}
2089
			$batch->execute();
2090
2091
			# Construct the HTML
2092
			$outText = '<div class="mw-templatesUsedExplanation">';
2093
			if ( $preview ) {
2094
				$outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
2095
					->parseAsBlock();
2096
			} elseif ( $section ) {
2097
				$outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
2098
					->parseAsBlock();
2099
			} else {
2100
				$outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
2101
					->parseAsBlock();
2102
			}
2103
			$outText .= "</div><ul>\n";
2104
2105
			usort( $templates, 'Title::compare' );
2106
			foreach ( $templates as $titleObj ) {
2107
				$protected = '';
2108
				$restrictions = $titleObj->getRestrictions( 'edit' );
2109
				if ( $restrictions ) {
2110
					// Check backwards-compatible messages
2111
					$msg = null;
2112
					if ( $restrictions === [ 'sysop' ] ) {
2113
						$msg = wfMessage( 'template-protected' );
2114
					} elseif ( $restrictions === [ 'autoconfirmed' ] ) {
2115
						$msg = wfMessage( 'template-semiprotected' );
2116
					}
2117
					if ( $msg && !$msg->isDisabled() ) {
2118
						$protected = $msg->parse();
2119
					} else {
2120
						// Construct the message from restriction-level-*
2121
						// e.g. restriction-level-sysop, restriction-level-autoconfirmed
2122
						$msgs = [];
2123
						foreach ( $restrictions as $r ) {
2124
							$msgs[] = wfMessage( "restriction-level-$r" )->parse();
2125
						}
2126
						$protected = wfMessage( 'parentheses' )
2127
							->rawParams( $wgLang->commaList( $msgs ) )->escaped();
2128
					}
2129
				}
2130
				if ( $titleObj->quickUserCan( 'edit' ) ) {
2131
					$editLink = self::link(
2132
						$titleObj,
2133
						wfMessage( 'editlink' )->escaped(),
2134
						[],
2135
						[ 'action' => 'edit' ]
2136
					);
2137
				} else {
2138
					$editLink = self::link(
2139
						$titleObj,
2140
						wfMessage( 'viewsourcelink' )->escaped(),
2141
						[],
2142
						[ 'action' => 'edit' ]
2143
					);
2144
				}
2145
				$outText .= '<li>' . self::link( $titleObj )
2146
					. wfMessage( 'word-separator' )->escaped()
2147
					. wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
2148
					. wfMessage( 'word-separator' )->escaped()
2149
					. $protected . '</li>';
2150
			}
2151
2152
			if ( $more instanceof Title ) {
2153
				$outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
2154
			} elseif ( $more ) {
2155
				$outText .= "<li>$more</li>";
2156
			}
2157
2158
			$outText .= '</ul>';
2159
		}
2160
		return $outText;
2161
	}
2162
2163
	/**
2164
	 * Returns HTML for the "hidden categories on this page" list.
2165
	 *
2166
	 * @since 1.16.3
2167
	 * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
2168
	 *   or similar
2169
	 * @return string HTML output
2170
	 */
2171
	public static function formatHiddenCategories( $hiddencats ) {
2172
2173
		$outText = '';
2174
		if ( count( $hiddencats ) > 0 ) {
2175
			# Construct the HTML
2176
			$outText = '<div class="mw-hiddenCategoriesExplanation">';
2177
			$outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2178
			$outText .= "</div><ul>\n";
2179
2180
			foreach ( $hiddencats as $titleObj ) {
2181
				# If it's hidden, it must exist - no need to check with a LinkBatch
2182
				$outText .= '<li>'
2183
					. self::link( $titleObj, null, [], [], 'known' )
2184
					. "</li>\n";
2185
			}
2186
			$outText .= '</ul>';
2187
		}
2188
		return $outText;
2189
	}
2190
2191
	/**
2192
	 * Format a size in bytes for output, using an appropriate
2193
	 * unit (B, KB, MB or GB) according to the magnitude in question
2194
	 *
2195
	 * @since 1.16.3
2196
	 * @param int $size Size to format
2197
	 * @return string
2198
	 */
2199
	public static function formatSize( $size ) {
2200
		global $wgLang;
2201
		return htmlspecialchars( $wgLang->formatSize( $size ) );
2202
	}
2203
2204
	/**
2205
	 * Given the id of an interface element, constructs the appropriate title
2206
	 * attribute from the system messages.  (Note, this is usually the id but
2207
	 * isn't always, because sometimes the accesskey needs to go on a different
2208
	 * element than the id, for reverse-compatibility, etc.)
2209
	 *
2210
	 * @since 1.16.3 $msgParams added in 1.27
2211
	 * @param string $name Id of the element, minus prefixes.
2212
	 * @param string|null $options Null or the string 'withaccess' to add an access-
2213
	 *   key hint
2214
	 * @param array $msgParams Parameters to pass to the message
2215
	 *
2216
	 * @return string Contents of the title attribute (which you must HTML-
2217
	 *   escape), or false for no title attribute
2218
	 */
2219
	public static function titleAttrib( $name, $options = null, array $msgParams = [] ) {
2220
		$message = wfMessage( "tooltip-$name", $msgParams );
2221
		if ( !$message->exists() ) {
2222
			$tooltip = false;
2223
		} else {
2224
			$tooltip = $message->text();
2225
			# Compatibility: formerly some tooltips had [alt-.] hardcoded
2226
			$tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
2227
			# Message equal to '-' means suppress it.
2228
			if ( $tooltip == '-' ) {
2229
				$tooltip = false;
2230
			}
2231
		}
2232
2233
		if ( $options == 'withaccess' ) {
2234
			$accesskey = self::accesskey( $name );
2235
			if ( $accesskey !== false ) {
2236
				// Should be build the same as in jquery.accessKeyLabel.js
2237
				if ( $tooltip === false || $tooltip === '' ) {
2238
					$tooltip = wfMessage( 'brackets', $accesskey )->text();
2239
				} else {
2240
					$tooltip .= wfMessage( 'word-separator' )->text();
2241
					$tooltip .= wfMessage( 'brackets', $accesskey )->text();
2242
				}
2243
			}
2244
		}
2245
2246
		return $tooltip;
2247
	}
2248
2249
	public static $accesskeycache;
2250
2251
	/**
2252
	 * Given the id of an interface element, constructs the appropriate
2253
	 * accesskey attribute from the system messages.  (Note, this is usually
2254
	 * the id but isn't always, because sometimes the accesskey needs to go on
2255
	 * a different element than the id, for reverse-compatibility, etc.)
2256
	 *
2257
	 * @since 1.16.3
2258
	 * @param string $name Id of the element, minus prefixes.
2259
	 * @return string Contents of the accesskey attribute (which you must HTML-
2260
	 *   escape), or false for no accesskey attribute
2261
	 */
2262
	public static function accesskey( $name ) {
2263
		if ( isset( self::$accesskeycache[$name] ) ) {
2264
			return self::$accesskeycache[$name];
2265
		}
2266
2267
		$message = wfMessage( "accesskey-$name" );
2268
2269
		if ( !$message->exists() ) {
2270
			$accesskey = false;
2271
		} else {
2272
			$accesskey = $message->plain();
2273
			if ( $accesskey === '' || $accesskey === '-' ) {
2274
				# @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
2275
				# attribute, but this is broken for accesskey: that might be a useful
2276
				# value.
2277
				$accesskey = false;
2278
			}
2279
		}
2280
2281
		self::$accesskeycache[$name] = $accesskey;
2282
		return self::$accesskeycache[$name];
2283
	}
2284
2285
	/**
2286
	 * Get a revision-deletion link, or disabled link, or nothing, depending
2287
	 * on user permissions & the settings on the revision.
2288
	 *
2289
	 * Will use forward-compatible revision ID in the Special:RevDelete link
2290
	 * if possible, otherwise the timestamp-based ID which may break after
2291
	 * undeletion.
2292
	 *
2293
	 * @param User $user
2294
	 * @param Revision $rev
2295
	 * @param Title $title
2296
	 * @return string HTML fragment
2297
	 */
2298
	public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
2299
		$canHide = $user->isAllowed( 'deleterevision' );
2300
		if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
2301
			return '';
2302
		}
2303
2304
		if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
2305
			return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2306
		} else {
2307
			if ( $rev->getId() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $rev->getId() of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
2308
				// RevDelete links using revision ID are stable across
2309
				// page deletion and undeletion; use when possible.
2310
				$query = [
2311
					'type' => 'revision',
2312
					'target' => $title->getPrefixedDBkey(),
2313
					'ids' => $rev->getId()
2314
				];
2315
			} else {
2316
				// Older deleted entries didn't save a revision ID.
2317
				// We have to refer to these by timestamp, ick!
2318
				$query = [
2319
					'type' => 'archive',
2320
					'target' => $title->getPrefixedDBkey(),
2321
					'ids' => $rev->getTimestamp()
2322
				];
2323
			}
2324
			return Linker::revDeleteLink( $query,
2325
				$rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
2326
		}
2327
	}
2328
2329
	/**
2330
	 * Creates a (show/hide) link for deleting revisions/log entries
2331
	 *
2332
	 * @param array $query Query parameters to be passed to link()
2333
	 * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
2334
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2335
	 *
2336
	 * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
2337
	 * span to allow for customization of appearance with CSS
2338
	 */
2339
	public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2340
		$sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2341
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2342
		$html = wfMessage( $msgKey )->escaped();
2343
		$tag = $restricted ? 'strong' : 'span';
2344
		$link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2345
		return Xml::tags(
2346
			$tag,
2347
			[ 'class' => 'mw-revdelundel-link' ],
2348
			wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2349
		);
2350
	}
2351
2352
	/**
2353
	 * Creates a dead (show/hide) link for deleting revisions/log entries
2354
	 *
2355
	 * @since 1.16.3
2356
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2357
	 *
2358
	 * @return string HTML text wrapped in a span to allow for customization
2359
	 * of appearance with CSS
2360
	 */
2361
	public static function revDeleteLinkDisabled( $delete = true ) {
2362
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2363
		$html = wfMessage( $msgKey )->escaped();
2364
		$htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2365
		return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2366
	}
2367
2368
	/* Deprecated methods */
2369
2370
	/**
2371
	 * Returns the attributes for the tooltip and access key.
2372
	 *
2373
	 * @since 1.16.3. $msgParams introduced in 1.27
2374
	 * @param string $name
2375
	 * @param array $msgParams Params for constructing the message
2376
	 *
2377
	 * @return array
2378
	 */
2379
	public static function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) {
2380
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2381
		# no attribute" instead of "output '' as value for attribute", this
2382
		# would be three lines.
2383
		$attribs = [
2384
			'title' => self::titleAttrib( $name, 'withaccess', $msgParams ),
2385
			'accesskey' => self::accesskey( $name )
2386
		];
2387
		if ( $attribs['title'] === false ) {
2388
			unset( $attribs['title'] );
2389
		}
2390
		if ( $attribs['accesskey'] === false ) {
2391
			unset( $attribs['accesskey'] );
2392
		}
2393
		return $attribs;
2394
	}
2395
2396
	/**
2397
	 * Returns raw bits of HTML, use titleAttrib()
2398
	 * @since 1.16.3
2399
	 * @param string $name
2400
	 * @param array|null $options
2401
	 * @return null|string
2402
	 */
2403
	public static function tooltip( $name, $options = null ) {
2404
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2405
		# no attribute" instead of "output '' as value for attribute", this
2406
		# would be two lines.
2407
		$tooltip = self::titleAttrib( $name, $options );
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 2403 can also be of type array; however, Linker::titleAttrib() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
2408
		if ( $tooltip === false ) {
2409
			return '';
2410
		}
2411
		return Xml::expandAttributes( [
2412
			'title' => $tooltip
2413
		] );
2414
	}
2415
2416
}
2417
2418