Completed
Branch master (efd8f8)
by
unknown
29:11
created

DummyLinker::__call()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 2
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
23
/**
24
 * Some internal bits split of from Skin.php. These functions are used
25
 * for primarily page content: links, embedded images, table of contents. Links
26
 * are also used in the skin.
27
 *
28
 * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
29
 *
30
 * @ingroup Skins
31
 */
32
class Linker {
33
	/**
34
	 * Flags for userToolLinks()
35
	 */
36
	const TOOL_LINKS_NOBLOCK = 1;
37
	const TOOL_LINKS_EMAIL = 2;
38
39
	/**
40
	 * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
41
	 *
42
	 * @deprecated since 1.25
43
	 *
44
	 * @param string $title The title text for the link, URL-encoded (???) but
45
	 *   not HTML-escaped
46
	 * @param string $unused Unused
47
	 * @param string $class The contents of the class attribute; if an empty
48
	 *   string is passed, which is the default value, defaults to 'external'.
49
	 * @return string
50
	 */
51
	static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
52
		global $wgContLang;
53
54
		wfDeprecated( __METHOD__, '1.25' );
55
56
		# @todo FIXME: We have a whole bunch of handling here that doesn't happen in
57
		# getExternalLinkAttributes, why?
58
		$title = urldecode( $title );
59
		$title = $wgContLang->checkTitleEncoding( $title );
60
		$title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
61
62
		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...
63
	}
64
65
	/**
66
	 * Get the appropriate HTML attributes to add to the "a" element of an internal link.
67
	 *
68
	 * @deprecated since 1.25
69
	 *
70
	 * @param string $title The title text for the link, URL-encoded (???) but
71
	 *   not HTML-escaped
72
	 * @param string $unused Unused
73
	 * @param string $class The contents of the class attribute, default none
74
	 * @return string
75
	 */
76
	static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
77
		wfDeprecated( __METHOD__, '1.25' );
78
79
		$title = urldecode( $title );
80
		$title = strtr( $title, '_', ' ' );
81
		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...
82
	}
83
84
	/**
85
	 * Get the appropriate HTML attributes to add to the "a" element of an internal
86
	 * link, given the Title object for the page we want to link to.
87
	 *
88
	 * @deprecated since 1.25
89
	 *
90
	 * @param Title $nt
91
	 * @param string $unused Unused
92
	 * @param string $class The contents of the class attribute, default none
93
	 * @param string|bool $title Optional (unescaped) string to use in the title
94
	 *   attribute; if false, default to the name of the page we're linking to
95
	 * @return string
96
	 */
97
	static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
98
		wfDeprecated( __METHOD__, '1.25' );
99
100
		if ( $title === false ) {
101
			$title = $nt->getPrefixedText();
102
		}
103
		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...
Bug introduced by
It seems like $title defined by parameter $title on line 97 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...
104
	}
105
106
	/**
107
	 * Common code for getLinkAttributesX functions
108
	 *
109
	 * @deprecated since 1.25
110
	 *
111
	 * @param string $title
112
	 * @param string $class
113
	 *
114
	 * @return string
115
	 */
116
	private static function getLinkAttributesInternal( $title, $class ) {
117
		wfDeprecated( __METHOD__, '1.25' );
118
119
		$title = htmlspecialchars( $title );
120
		$class = htmlspecialchars( $class );
121
		$r = '';
122
		if ( $class != '' ) {
123
			$r .= " class=\"$class\"";
124
		}
125
		if ( $title != '' ) {
126
			$r .= " title=\"$title\"";
127
		}
128
		return $r;
129
	}
130
131
	/**
132
	 * Return the CSS colour of a known link
133
	 *
134
	 * @param Title $t
135
	 * @param int $threshold User defined threshold
136
	 * @return string CSS class
137
	 */
138
	public static function getLinkColour( $t, $threshold ) {
139
		$colour = '';
140
		if ( $t->isRedirect() ) {
141
			# Page is a redirect
142
			$colour = 'mw-redirect';
143
		} elseif ( $threshold > 0 && $t->isContentPage() &&
144
			$t->exists() && $t->getLength() < $threshold
145
		) {
146
			# Page is a stub
147
			$colour = 'stub';
148
		}
149
		return $colour;
150
	}
151
152
	/**
153
	 * This function returns an HTML link to the given target.  It serves a few
154
	 * purposes:
155
	 *   1) If $target is a Title, the correct URL to link to will be figured
156
	 *      out automatically.
157
	 *   2) It automatically adds the usual classes for various types of link
158
	 *      targets: "new" for red links, "stub" for short articles, etc.
159
	 *   3) It escapes all attribute values safely so there's no risk of XSS.
160
	 *   4) It provides a default tooltip if the target is a Title (the page
161
	 *      name of the target).
162
	 * link() replaces the old functions in the makeLink() family.
163
	 *
164
	 * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
165
	 *
166
	 * @param Title $target Can currently only be a Title, but this may
167
	 *   change to support Images, literal URLs, etc.
168
	 * @param string $html The HTML contents of the <a> element, i.e.,
169
	 *   the link text.  This is raw HTML and will not be escaped.  If null,
170
	 *   defaults to the prefixed text of the Title; or if the Title is just a
171
	 *   fragment, the contents of the fragment.
172
	 * @param array $customAttribs A key => value array of extra HTML attributes,
173
	 *   such as title and class.  (href is ignored.)  Classes will be
174
	 *   merged with the default classes, while other attributes will replace
175
	 *   default attributes.  All passed attribute values will be HTML-escaped.
176
	 *   A false attribute value means to suppress that attribute.
177
	 * @param array $query The query string to append to the URL
178
	 *   you're linking to, in key => value array form.  Query keys and values
179
	 *   will be URL-encoded.
180
	 * @param string|array $options String or array of strings:
181
	 *     'known': Page is known to exist, so don't check if it does.
182
	 *     'broken': Page is known not to exist, so don't check if it does.
183
	 *     'noclasses': Don't add any classes automatically (includes "new",
184
	 *       "stub", "mw-redirect", "extiw").  Only use the class attribute
185
	 *       provided, if any, so you get a simple blue link with no funny i-
186
	 *       cons.
187
	 *     'forcearticlepath': Use the article path always, even with a querystring.
188
	 *       Has compatibility issues on some setups, so avoid wherever possible.
189
	 *     'http': Force a full URL with http:// as the scheme.
190
	 *     'https': Force a full URL with https:// as the scheme.
191
	 *     'stubThreshold' => (int): Stub threshold to use when determining link classes.
192
	 * @return string HTML <a> attribute
193
	 */
194
	public static function link(
195
		$target, $html = null, $customAttribs = [], $query = [], $options = []
196
	) {
197
		if ( !$target instanceof Title ) {
198
			wfWarn( __METHOD__ . ': Requires $target to be a Title object.', 2 );
199
			return "<!-- ERROR -->$html";
200
		}
201
202
		if ( is_string( $query ) ) {
203
			// some functions withing core using this still hand over query strings
204
			wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
205
			$query = wfCgiToArray( $query );
206
		}
207
		$options = (array)$options;
208
209
		$dummy = new DummyLinker; // dummy linker instance for bc on the hooks
210
211
		$ret = null;
212
		if ( !Hooks::run( 'LinkBegin',
213
			[ $dummy, $target, &$html, &$customAttribs, &$query, &$options, &$ret ] )
214
		) {
215
			return $ret;
216
		}
217
218
		# Normalize the Title if it's a special page
219
		$target = self::normaliseSpecialPage( $target );
220
221
		# If we don't know whether the page exists, let's find out.
222
		if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) {
223
			if ( $target->isKnown() ) {
224
				$options[] = 'known';
225
			} else {
226
				$options[] = 'broken';
227
			}
228
		}
229
230
		$oldquery = [];
231
		if ( in_array( "forcearticlepath", $options, true ) && $query ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $query of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
232
			$oldquery = $query;
233
			$query = [];
234
		}
235
236
		# Note: we want the href attribute first, for prettiness.
237
		$attribs = [ 'href' => self::linkUrl( $target, $query, $options ) ];
238
		if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oldquery of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
239
			$attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
240
		}
241
242
		$attribs = array_merge(
243
			$attribs,
244
			self::linkAttribs( $target, $customAttribs, $options )
245
		);
246
		if ( is_null( $html ) ) {
247
			$html = self::linkText( $target );
248
		}
249
250
		$ret = null;
251
		if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) {
252
			$ret = Html::rawElement( 'a', $attribs, $html );
253
		}
254
255
		return $ret;
256
	}
257
258
	/**
259
	 * Identical to link(), except $options defaults to 'known'.
260
	 * @see Linker::link
261
	 * @return string
262
	 */
263
	public static function linkKnown(
264
		$target, $html = null, $customAttribs = [],
265
		$query = [], $options = [ 'known', 'noclasses' ]
266
	) {
267
		return self::link( $target, $html, $customAttribs, $query, $options );
268
	}
269
270
	/**
271
	 * Returns the Url used to link to a Title
272
	 *
273
	 * @param Title $target
274
	 * @param array $query Query parameters
275
	 * @param array $options
276
	 * @return string
277
	 */
278
	private static function linkUrl( $target, $query, $options ) {
279
		# We don't want to include fragments for broken links, because they
280
		# generally make no sense.
281
		if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) {
282
			$target = clone $target;
283
			$target->setFragment( '' );
284
		}
285
286
		# If it's a broken link, add the appropriate query pieces, unless
287
		# there's already an action specified, or unless 'edit' makes no sense
288
		# (i.e., for a nonexistent special page).
289
		if ( in_array( 'broken', $options, true ) && empty( $query['action'] )
290
			&& !$target->isSpecialPage() ) {
291
			$query['action'] = 'edit';
292
			$query['redlink'] = '1';
293
		}
294
295
		if ( in_array( 'http', $options, true ) ) {
296
			$proto = PROTO_HTTP;
297
		} elseif ( in_array( 'https', $options, true ) ) {
298
			$proto = PROTO_HTTPS;
299
		} else {
300
			$proto = PROTO_RELATIVE;
301
		}
302
303
		$ret = $target->getLinkURL( $query, false, $proto );
304
		return $ret;
305
	}
306
307
	/**
308
	 * Returns the array of attributes used when linking to the Title $target
309
	 *
310
	 * @param Title $target
311
	 * @param array $attribs
312
	 * @param array $options
313
	 *
314
	 * @return array
315
	 */
316
	private static function linkAttribs( $target, $attribs, $options ) {
317
		global $wgUser;
318
		$defaults = [];
319
320
		if ( !in_array( 'noclasses', $options, true ) ) {
321
			# Now build the classes.
322
			$classes = [];
323
324
			if ( in_array( 'broken', $options, true ) ) {
325
				$classes[] = 'new';
326
			}
327
328
			if ( $target->isExternal() ) {
329
				$classes[] = 'extiw';
330
			}
331
332
			if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387)
333
				$colour = self::getLinkColour(
334
					$target,
335
					isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold()
336
				);
337
				if ( $colour !== '' ) {
338
					$classes[] = $colour; # mw-redirect or stub
339
				}
340
			}
341
			if ( $classes != [] ) {
342
				$defaults['class'] = implode( ' ', $classes );
343
			}
344
		}
345
346
		# Get a default title attribute.
347
		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...
348
			# A link like [[#Foo]].  This used to mean an empty title
349
			# attribute, but that's silly.  Just don't output a title.
350
		} elseif ( in_array( 'known', $options, true ) ) {
351
			$defaults['title'] = $target->getPrefixedText();
352
		} else {
353
			// This ends up in parser cache!
354
			$defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )
355
				->inContentLanguage()
356
				->text();
357
		}
358
359
		# Finally, merge the custom attribs with the default ones, and iterate
360
		# over that, deleting all "false" attributes.
361
		$ret = [];
362
		$merged = Sanitizer::mergeAttributes( $defaults, $attribs );
363
		foreach ( $merged as $key => $val ) {
364
			# A false value suppresses the attribute, and we don't want the
365
			# href attribute to be overridden.
366
			if ( $key != 'href' && $val !== false ) {
367
				$ret[$key] = $val;
368
			}
369
		}
370
		return $ret;
371
	}
372
373
	/**
374
	 * Default text of the links to the Title $target
375
	 *
376
	 * @param Title $target
377
	 *
378
	 * @return string
379
	 */
380
	private static function linkText( $target ) {
381
		if ( !$target instanceof Title ) {
382
			wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
383
			return '';
384
		}
385
		// If the target is just a fragment, with no title, we return the fragment
386
		// text.  Otherwise, we return the title text itself.
387
		if ( $target->getPrefixedText() === '' && $target->hasFragment() ) {
388
			return htmlspecialchars( $target->getFragment() );
389
		}
390
391
		return htmlspecialchars( $target->getPrefixedText() );
392
	}
393
394
	/**
395
	 * Make appropriate markup for a link to the current article. This is
396
	 * currently rendered as the bold link text. The calling sequence is the
397
	 * same as the other make*LinkObj static functions, despite $query not
398
	 * being used.
399
	 *
400
	 * @param Title $nt
401
	 * @param string $html [optional]
402
	 * @param string $query [optional]
403
	 * @param string $trail [optional]
404
	 * @param string $prefix [optional]
405
	 *
406
	 * @return string
407
	 */
408
	public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
409
		$ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}";
410
		if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) {
411
			return $ret;
412
		}
413
414
		if ( $html == '' ) {
415
			$html = htmlspecialchars( $nt->getPrefixedText() );
416
		}
417
		list( $inside, $trail ) = self::splitTrail( $trail );
418
		return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}";
419
	}
420
421
	/**
422
	 * Get a message saying that an invalid title was encountered.
423
	 * This should be called after a method like Title::makeTitleSafe() returned
424
	 * a value indicating that the title object is invalid.
425
	 *
426
	 * @param IContextSource $context Context to use to get the messages
427
	 * @param int $namespace Namespace number
428
	 * @param string $title Text of the title, without the namespace part
429
	 * @return string
430
	 */
431
	public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
432
		global $wgContLang;
433
434
		// First we check whether the namespace exists or not.
435
		if ( MWNamespace::exists( $namespace ) ) {
436
			if ( $namespace == NS_MAIN ) {
437
				$name = $context->msg( 'blanknamespace' )->text();
438
			} else {
439
				$name = $wgContLang->getFormattedNsText( $namespace );
440
			}
441
			return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
442
		} else {
443
			return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
444
		}
445
	}
446
447
	/**
448
	 * @param Title $title
449
	 * @return Title
450
	 */
451
	static function normaliseSpecialPage( Title $title ) {
452
		if ( $title->isSpecialPage() ) {
453
			list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() );
454
			if ( !$name ) {
455
				return $title;
456
			}
457
			$ret = SpecialPage::getTitleFor( $name, $subpage, $title->getFragment() );
458
			return $ret;
459
		} else {
460
			return $title;
461
		}
462
	}
463
464
	/**
465
	 * Returns the filename part of an url.
466
	 * Used as alternative text for external images.
467
	 *
468
	 * @param string $url
469
	 *
470
	 * @return string
471
	 */
472
	private static function fnamePart( $url ) {
473
		$basename = strrchr( $url, '/' );
474
		if ( false === $basename ) {
475
			$basename = $url;
476
		} else {
477
			$basename = substr( $basename, 1 );
478
		}
479
		return $basename;
480
	}
481
482
	/**
483
	 * Return the code for images which were added via external links,
484
	 * via Parser::maybeMakeExternalImage().
485
	 *
486
	 * @param string $url
487
	 * @param string $alt
488
	 *
489
	 * @return string
490
	 */
491
	public static function makeExternalImage( $url, $alt = '' ) {
492
		if ( $alt == '' ) {
493
			$alt = self::fnamePart( $url );
494
		}
495
		$img = '';
496
		$success = Hooks::run( 'LinkerMakeExternalImage', [ &$url, &$alt, &$img ] );
497
		if ( !$success ) {
498
			wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
499
				. "with url {$url} and alt text {$alt} to {$img}\n", true );
500
			return $img;
501
		}
502
		return Html::element( 'img',
503
			[
504
				'src' => $url,
505
				'alt' => $alt ] );
506
	}
507
508
	/**
509
	 * Given parameters derived from [[Image:Foo|options...]], generate the
510
	 * HTML that that syntax inserts in the page.
511
	 *
512
	 * @param Parser $parser
513
	 * @param Title $title Title object of the file (not the currently viewed page)
514
	 * @param File $file File object, or false if it doesn't exist
515
	 * @param array $frameParams Associative array of parameters external to the media handler.
516
	 *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
517
	 *     will often be false.
518
	 *          thumbnail       If present, downscale and frame
519
	 *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
520
	 *          framed          Shows image in original size in a frame
521
	 *          frameless       Downscale but don't frame
522
	 *          upright         If present, tweak default sizes for portrait orientation
523
	 *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
524
	 *          border          If present, show a border around the image
525
	 *          align           Horizontal alignment (left, right, center, none)
526
	 *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
527
	 *                          bottom, text-bottom)
528
	 *          alt             Alternate text for image (i.e. alt attribute). Plain text.
529
	 *          class           HTML for image classes. Plain text.
530
	 *          caption         HTML for image caption.
531
	 *          link-url        URL to link to
532
	 *          link-title      Title object to link to
533
	 *          link-target     Value for the target attribute, only with link-url
534
	 *          no-link         Boolean, suppress description link
535
	 *
536
	 * @param array $handlerParams Associative array of media handler parameters, to be passed
537
	 *       to transform(). Typical keys are "width" and "page".
538
	 * @param string|bool $time Timestamp of the file, set as false for current
539
	 * @param string $query Query params for desc url
540
	 * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
541
	 * @since 1.20
542
	 * @return string HTML for an image, with links, wrappers, etc.
543
	 */
544
	public static function makeImageLink( Parser $parser, Title $title,
545
		$file, $frameParams = [], $handlerParams = [], $time = false,
546
		$query = "", $widthOption = null
547
	) {
548
		$res = null;
549
		$dummy = new DummyLinker;
550
		if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
551
			&$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
552
			return $res;
553
		}
554
555
		if ( $file && !$file->allowInlineDisplay() ) {
556
			wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
557
			return self::link( $title );
558
		}
559
560
		// Shortcuts
561
		$fp =& $frameParams;
562
		$hp =& $handlerParams;
563
564
		// Clean up parameters
565
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
566
		if ( !isset( $fp['align'] ) ) {
567
			$fp['align'] = '';
568
		}
569
		if ( !isset( $fp['alt'] ) ) {
570
			$fp['alt'] = '';
571
		}
572
		if ( !isset( $fp['title'] ) ) {
573
			$fp['title'] = '';
574
		}
575
		if ( !isset( $fp['class'] ) ) {
576
			$fp['class'] = '';
577
		}
578
579
		$prefix = $postfix = '';
580
581
		if ( 'center' == $fp['align'] ) {
582
			$prefix = '<div class="center">';
583
			$postfix = '</div>';
584
			$fp['align'] = 'none';
585
		}
586
		if ( $file && !isset( $hp['width'] ) ) {
587
			if ( isset( $hp['height'] ) && $file->isVectorized() ) {
588
				// If its a vector image, and user only specifies height
589
				// we don't want it to be limited by its "normal" width.
590
				global $wgSVGMaxSize;
591
				$hp['width'] = $wgSVGMaxSize;
592
			} else {
593
				$hp['width'] = $file->getWidth( $page );
594
			}
595
596
			if ( isset( $fp['thumbnail'] )
597
				|| isset( $fp['manualthumb'] )
598
				|| isset( $fp['framed'] )
599
				|| isset( $fp['frameless'] )
600
				|| !$hp['width']
601
			) {
602
				global $wgThumbLimits, $wgThumbUpright;
603
604
				if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
605
					$widthOption = User::getDefaultOption( 'thumbsize' );
606
				}
607
608
				// Reduce width for upright images when parameter 'upright' is used
609
				if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
610
					$fp['upright'] = $wgThumbUpright;
611
				}
612
613
				// For caching health: If width scaled down due to upright
614
				// parameter, round to full __0 pixel to avoid the creation of a
615
				// lot of odd thumbs.
616
				$prefWidth = isset( $fp['upright'] ) ?
617
					round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) :
618
					$wgThumbLimits[$widthOption];
619
620
				// Use width which is smaller: real image width or user preference width
621
				// Unless image is scalable vector.
622
				if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 ||
623
						$prefWidth < $hp['width'] || $file->isVectorized() ) ) {
624
					$hp['width'] = $prefWidth;
625
				}
626
			}
627
		}
628
629
		if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
630
			# Create a thumbnail. Alignment depends on the writing direction of
631
			# the page content language (right-aligned for LTR languages,
632
			# left-aligned for RTL languages)
633
			# If a thumbnail width has not been provided, it is set
634
			# to the default user option as specified in Language*.php
635
			if ( $fp['align'] == '' ) {
636
				$fp['align'] = $parser->getTargetLanguage()->alignEnd();
637
			}
638
			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 545 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...
639
		}
640
641
		if ( $file && isset( $fp['frameless'] ) ) {
642
			$srcWidth = $file->getWidth( $page );
643
			# For "frameless" option: do not present an image bigger than the
644
			# source (for bitmap-style images). This is the same behavior as the
645
			# "thumb" option does it already.
646 View Code Duplication
			if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
647
				$hp['width'] = $srcWidth;
648
			}
649
		}
650
651
		if ( $file && isset( $hp['width'] ) ) {
652
			# Create a resized image, without the additional thumbnail features
653
			$thumb = $file->transform( $hp );
654
		} else {
655
			$thumb = false;
656
		}
657
658
		if ( !$thumb ) {
659
			$s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
660
		} else {
661
			self::processResponsiveImages( $file, $thumb, $hp );
662
			$params = [
663
				'alt' => $fp['alt'],
664
				'title' => $fp['title'],
665
				'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
666
				'img-class' => $fp['class'] ];
667
			if ( isset( $fp['border'] ) ) {
668
				$params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
669
			}
670
			$params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
671
672
			$s = $thumb->toHtml( $params );
673
		}
674
		if ( $fp['align'] != '' ) {
675
			$s = "<div class=\"float{$fp['align']}\">{$s}</div>";
676
		}
677
		return str_replace( "\n", ' ', $prefix . $s . $postfix );
678
	}
679
680
	/**
681
	 * Get the link parameters for MediaTransformOutput::toHtml() from given
682
	 * frame parameters supplied by the Parser.
683
	 * @param array $frameParams The frame parameters
684
	 * @param string $query An optional query string to add to description page links
685
	 * @param Parser|null $parser
686
	 * @return array
687
	 */
688
	private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
689
		$mtoParams = [];
690
		if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
691
			$mtoParams['custom-url-link'] = $frameParams['link-url'];
692
			if ( isset( $frameParams['link-target'] ) ) {
693
				$mtoParams['custom-target-link'] = $frameParams['link-target'];
694
			}
695
			if ( $parser ) {
696
				$extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
697
				foreach ( $extLinkAttrs as $name => $val ) {
698
					// Currently could include 'rel' and 'target'
699
					$mtoParams['parser-extlink-' . $name] = $val;
700
				}
701
			}
702
		} elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
703
			$mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
704
		} 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...
705
			// No link
706
		} else {
707
			$mtoParams['desc-link'] = true;
708
			$mtoParams['desc-query'] = $query;
709
		}
710
		return $mtoParams;
711
	}
712
713
	/**
714
	 * Make HTML for a thumbnail including image, border and caption
715
	 * @param Title $title
716
	 * @param File|bool $file File object or false if it doesn't exist
717
	 * @param string $label
718
	 * @param string $alt
719
	 * @param string $align
720
	 * @param array $params
721
	 * @param bool $framed
722
	 * @param string $manualthumb
723
	 * @return string
724
	 */
725
	public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
726
		$align = 'right', $params = [], $framed = false, $manualthumb = ""
727
	) {
728
		$frameParams = [
729
			'alt' => $alt,
730
			'caption' => $label,
731
			'align' => $align
732
		];
733
		if ( $framed ) {
734
			$frameParams['framed'] = true;
735
		}
736
		if ( $manualthumb ) {
737
			$frameParams['manualthumb'] = $manualthumb;
738
		}
739
		return self::makeThumbLink2( $title, $file, $frameParams, $params );
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 725 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...
740
	}
741
742
	/**
743
	 * @param Title $title
744
	 * @param File $file
745
	 * @param array $frameParams
746
	 * @param array $handlerParams
747
	 * @param bool $time
748
	 * @param string $query
749
	 * @return string
750
	 */
751
	public static function makeThumbLink2( Title $title, $file, $frameParams = [],
752
		$handlerParams = [], $time = false, $query = ""
753
	) {
754
		$exists = $file && $file->exists();
755
756
		# Shortcuts
757
		$fp =& $frameParams;
758
		$hp =& $handlerParams;
759
760
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
761
		if ( !isset( $fp['align'] ) ) {
762
			$fp['align'] = 'right';
763
		}
764
		if ( !isset( $fp['alt'] ) ) {
765
			$fp['alt'] = '';
766
		}
767
		if ( !isset( $fp['title'] ) ) {
768
			$fp['title'] = '';
769
		}
770
		if ( !isset( $fp['caption'] ) ) {
771
			$fp['caption'] = '';
772
		}
773
774
		if ( empty( $hp['width'] ) ) {
775
			// Reduce width for upright images when parameter 'upright' is used
776
			$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
777
		}
778
		$thumb = false;
779
		$noscale = false;
780
		$manualthumb = false;
781
782
		if ( !$exists ) {
783
			$outerWidth = $hp['width'] + 2;
784
		} else {
785
			if ( isset( $fp['manualthumb'] ) ) {
786
				# Use manually specified thumbnail
787
				$manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
788
				if ( $manual_title ) {
789
					$manual_img = wfFindFile( $manual_title );
790
					if ( $manual_img ) {
791
						$thumb = $manual_img->getUnscaledThumb( $hp );
792
						$manualthumb = true;
793
					} else {
794
						$exists = false;
795
					}
796
				}
797
			} elseif ( isset( $fp['framed'] ) ) {
798
				// Use image dimensions, don't scale
799
				$thumb = $file->getUnscaledThumb( $hp );
800
				$noscale = true;
801
			} else {
802
				# Do not present an image bigger than the source, for bitmap-style images
803
				# This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
804
				$srcWidth = $file->getWidth( $page );
805 View Code Duplication
				if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
806
					$hp['width'] = $srcWidth;
807
				}
808
				$thumb = $file->transform( $hp );
809
			}
810
811
			if ( $thumb ) {
812
				$outerWidth = $thumb->getWidth() + 2;
813
			} else {
814
				$outerWidth = $hp['width'] + 2;
815
			}
816
		}
817
818
		# ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
819
		# So we don't need to pass it here in $query. However, the URL for the
820
		# zoom icon still needs it, so we make a unique query for it. See bug 14771
821
		$url = $title->getLocalURL( $query );
822
		if ( $page ) {
823
			$url = wfAppendQuery( $url, [ 'page' => $page ] );
824
		}
825
		if ( $manualthumb
826
			&& !isset( $fp['link-title'] )
827
			&& !isset( $fp['link-url'] )
828
			&& !isset( $fp['no-link'] ) ) {
829
			$fp['link-url'] = $url;
830
		}
831
832
		$s = "<div class=\"thumb t{$fp['align']}\">"
833
			. "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
834
835
		if ( !$exists ) {
836
			$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...
837
			$zoomIcon = '';
838
		} elseif ( !$thumb ) {
839
			$s .= wfMessage( 'thumbnail_error', '' )->escaped();
840
			$zoomIcon = '';
841
		} else {
842
			if ( !$noscale && !$manualthumb ) {
843
				self::processResponsiveImages( $file, $thumb, $hp );
844
			}
845
			$params = [
846
				'alt' => $fp['alt'],
847
				'title' => $fp['title'],
848
				'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== ''
849
					? $fp['class'] . ' '
850
					: '' ) . 'thumbimage'
851
			];
852
			$params = self::getImageLinkMTOParams( $fp, $query ) + $params;
853
			$s .= $thumb->toHtml( $params );
854
			if ( isset( $fp['framed'] ) ) {
855
				$zoomIcon = "";
856
			} else {
857
				$zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
858
					Html::rawElement( 'a', [
859
						'href' => $url,
860
						'class' => 'internal',
861
						'title' => wfMessage( 'thumbnail-more' )->text() ],
862
						"" ) );
863
			}
864
		}
865
		$s .= '  <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>";
866
		return str_replace( "\n", ' ', $s );
867
	}
868
869
	/**
870
	 * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
871
	 * applicable.
872
	 *
873
	 * @param File $file
874
	 * @param MediaTransformOutput $thumb
875
	 * @param array $hp Image parameters
876
	 */
877
	public static function processResponsiveImages( $file, $thumb, $hp ) {
878
		global $wgResponsiveImages;
879
		if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
880
			$hp15 = $hp;
881
			$hp15['width'] = round( $hp['width'] * 1.5 );
882
			$hp20 = $hp;
883
			$hp20['width'] = $hp['width'] * 2;
884
			if ( isset( $hp['height'] ) ) {
885
				$hp15['height'] = round( $hp['height'] * 1.5 );
886
				$hp20['height'] = $hp['height'] * 2;
887
			}
888
889
			$thumb15 = $file->transform( $hp15 );
890
			$thumb20 = $file->transform( $hp20 );
891
			if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
892
				$thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
893
			}
894
			if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
895
				$thumb->responsiveUrls['2'] = $thumb20->getUrl();
896
			}
897
		}
898
	}
899
900
	/**
901
	 * Make a "broken" link to an image
902
	 *
903
	 * @param Title $title
904
	 * @param string $label Link label (plain text)
905
	 * @param string $query Query string
906
	 * @param string $unused1 Unused parameter kept for b/c
907
	 * @param string $unused2 Unused parameter kept for b/c
908
	 * @param bool $time A file of a certain timestamp was requested
909
	 * @return string
910
	 */
911
	public static function makeBrokenImageLinkObj( $title, $label = '',
912
		$query = '', $unused1 = '', $unused2 = '', $time = false
913
	) {
914
		if ( !$title instanceof Title ) {
915
			wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
916
			return "<!-- ERROR -->" . htmlspecialchars( $label );
917
		}
918
919
		global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
920
		if ( $label == '' ) {
921
			$label = $title->getPrefixedText();
922
		}
923
		$encLabel = htmlspecialchars( $label );
924
		$currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
925
926
		if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
927
			&& !$currentExists
928
		) {
929
			$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
930
931
			if ( $redir ) {
932
				return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) );
933
			}
934
935
			$href = self::getUploadUrl( $title, $query );
936
937
			return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
938
				htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
939
				$encLabel . '</a>';
940
		}
941
942
		return self::linkKnown( $title, $encLabel, [], wfCgiToArray( $query ) );
943
	}
944
945
	/**
946
	 * Get the URL to upload a certain file
947
	 *
948
	 * @param Title $destFile Title object of the file to upload
949
	 * @param string $query Urlencoded query string to prepend
950
	 * @return string Urlencoded URL
951
	 */
952
	protected static function getUploadUrl( $destFile, $query = '' ) {
953
		global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
954
		$q = 'wpDestFile=' . $destFile->getPartialURL();
955
		if ( $query != '' ) {
956
			$q .= '&' . $query;
957
		}
958
959
		if ( $wgUploadMissingFileUrl ) {
960
			return wfAppendQuery( $wgUploadMissingFileUrl, $q );
961
		} elseif ( $wgUploadNavigationUrl ) {
962
			return wfAppendQuery( $wgUploadNavigationUrl, $q );
963
		} else {
964
			$upload = SpecialPage::getTitleFor( 'Upload' );
965
			return $upload->getLocalURL( $q );
966
		}
967
	}
968
969
	/**
970
	 * Create a direct link to a given uploaded file.
971
	 *
972
	 * @param Title $title
973
	 * @param string $html Pre-sanitized HTML
974
	 * @param string $time MW timestamp of file creation time
975
	 * @return string HTML
976
	 */
977
	public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
978
		$img = wfFindFile( $title, [ 'time' => $time ] );
979
		return self::makeMediaLinkFile( $title, $img, $html );
980
	}
981
982
	/**
983
	 * Create a direct link to a given uploaded file.
984
	 * This will make a broken link if $file is false.
985
	 *
986
	 * @param Title $title
987
	 * @param File|bool $file File object or false
988
	 * @param string $html Pre-sanitized HTML
989
	 * @return string HTML
990
	 *
991
	 * @todo Handle invalid or missing images better.
992
	 */
993
	public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
994
		if ( $file && $file->exists() ) {
995
			$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...
996
			$class = 'internal';
997
		} else {
998
			$url = self::getUploadUrl( $title );
999
			$class = 'new';
1000
		}
1001
1002
		$alt = $title->getText();
1003
		if ( $html == '' ) {
1004
			$html = $alt;
1005
		}
1006
1007
		$ret = '';
1008
		$attribs = [
1009
			'href' => $url,
1010
			'class' => $class,
1011
			'title' => $alt
1012
		];
1013
1014
		if ( !Hooks::run( 'LinkerMakeMediaLinkFile',
1015
			[ $title, $file, &$html, &$attribs, &$ret ] ) ) {
1016
			wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
1017
				. "with url {$url} and text {$html} to {$ret}\n", true );
1018
			return $ret;
1019
		}
1020
1021
		return Html::rawElement( 'a', $attribs, $html );
1022
	}
1023
1024
	/**
1025
	 * Make a link to a special page given its name and, optionally,
1026
	 * a message key from the link text.
1027
	 * Usage example: Linker::specialLink( 'Recentchanges' )
1028
	 *
1029
	 * @param string $name
1030
	 * @param string $key
1031
	 * @return string
1032
	 */
1033
	public static function specialLink( $name, $key = '' ) {
1034
		if ( $key == '' ) {
1035
			$key = strtolower( $name );
1036
		}
1037
1038
		return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
1039
	}
1040
1041
	/**
1042
	 * Make an external link
1043
	 * @param string $url URL to link to
1044
	 * @param string $text Text of link
1045
	 * @param bool $escape Do we escape the link text?
1046
	 * @param string $linktype Type of external link. Gets added to the classes
1047
	 * @param array $attribs Array of extra attributes to <a>
1048
	 * @param Title|null $title Title object used for title specific link attributes
1049
	 * @return string
1050
	 */
1051
	public static function makeExternalLink( $url, $text, $escape = true,
1052
		$linktype = '', $attribs = [], $title = null
1053
	) {
1054
		global $wgTitle;
1055
		$class = "external";
1056
		if ( $linktype ) {
1057
			$class .= " $linktype";
1058
		}
1059
		if ( isset( $attribs['class'] ) && $attribs['class'] ) {
1060
			$class .= " {$attribs['class']}";
1061
		}
1062
		$attribs['class'] = $class;
1063
1064
		if ( $escape ) {
1065
			$text = htmlspecialchars( $text );
1066
		}
1067
1068
		if ( !$title ) {
1069
			$title = $wgTitle;
1070
		}
1071
		$attribs['rel'] = Parser::getExternalLinkRel( $url, $title );
1072
		$link = '';
1073
		$success = Hooks::run( 'LinkerMakeExternalLink',
1074
			[ &$url, &$text, &$link, &$attribs, $linktype ] );
1075
		if ( !$success ) {
1076
			wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
1077
				. "with url {$url} and text {$text} to {$link}\n", true );
1078
			return $link;
1079
		}
1080
		$attribs['href'] = $url;
1081
		return Html::rawElement( 'a', $attribs, $text );
1082
	}
1083
1084
	/**
1085
	 * Make user link (or user contributions for unregistered users)
1086
	 * @param int $userId User id in database.
1087
	 * @param string $userName User name in database.
1088
	 * @param string $altUserName Text to display instead of the user name (optional)
1089
	 * @return string HTML fragment
1090
	 * @since 1.19 Method exists for a long time. $altUserName was added in 1.19.
1091
	 */
1092
	public static function userLink( $userId, $userName, $altUserName = false ) {
1093
		$classes = 'mw-userlink';
1094
		if ( $userId == 0 ) {
1095
			$page = SpecialPage::getTitleFor( 'Contributions', $userName );
1096
			if ( $altUserName === false ) {
1097
				$altUserName = IP::prettifyIP( $userName );
1098
			}
1099
			$classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
1100
		} else {
1101
			$page = Title::makeTitle( NS_USER, $userName );
1102
		}
1103
1104
		return self::link(
1105
			$page,
1106
			htmlspecialchars( $altUserName !== false ? $altUserName : $userName ),
1107
			[ 'class' => $classes ]
1108
		);
1109
	}
1110
1111
	/**
1112
	 * Generate standard user tool links (talk, contributions, block link, etc.)
1113
	 *
1114
	 * @param int $userId User identifier
1115
	 * @param string $userText User name or IP address
1116
	 * @param bool $redContribsWhenNoEdits Should the contributions link be
1117
	 *   red if the user has no edits?
1118
	 * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1119
	 *   and Linker::TOOL_LINKS_EMAIL).
1120
	 * @param int $edits User edit count (optional, for performance)
1121
	 * @return string HTML fragment
1122
	 */
1123
	public static function userToolLinks(
1124
		$userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1125
	) {
1126
		global $wgUser, $wgDisableAnonTalk, $wgLang;
1127
		$talkable = !( $wgDisableAnonTalk && 0 == $userId );
1128
		$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1129
		$addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1130
1131
		$items = [];
1132
		if ( $talkable ) {
1133
			$items[] = self::userTalkLink( $userId, $userText );
1134
		}
1135
		if ( $userId ) {
1136
			// check if the user has an edit
1137
			$attribs = [];
1138
			if ( $redContribsWhenNoEdits ) {
1139
				if ( intval( $edits ) === 0 && $edits !== 0 ) {
1140
					$user = User::newFromId( $userId );
1141
					$edits = $user->getEditCount();
1142
				}
1143
				if ( $edits === 0 ) {
1144
					$attribs['class'] = 'new';
1145
				}
1146
			}
1147
			$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1148
1149
			$items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1150
		}
1151
		if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
1152
			$items[] = self::blockLink( $userId, $userText );
1153
		}
1154
1155
		if ( $addEmailLink && $wgUser->canSendEmail() ) {
1156
			$items[] = self::emailLink( $userId, $userText );
1157
		}
1158
1159
		Hooks::run( 'UserToolLinksEdit', [ $userId, $userText, &$items ] );
1160
1161
		if ( $items ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $items of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1162
			return wfMessage( 'word-separator' )->escaped()
1163
				. '<span class="mw-usertoollinks">'
1164
				. wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1165
				. '</span>';
1166
		} else {
1167
			return '';
1168
		}
1169
	}
1170
1171
	/**
1172
	 * Alias for userToolLinks( $userId, $userText, true );
1173
	 * @param int $userId User identifier
1174
	 * @param string $userText User name or IP address
1175
	 * @param int $edits User edit count (optional, for performance)
1176
	 * @return string
1177
	 */
1178
	public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
1179
		return self::userToolLinks( $userId, $userText, true, 0, $edits );
1180
	}
1181
1182
	/**
1183
	 * @param int $userId User id in database.
1184
	 * @param string $userText User name in database.
1185
	 * @return string HTML fragment with user talk link
1186
	 */
1187
	public static function userTalkLink( $userId, $userText ) {
1188
		$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
1189
		$userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
1190
		return $userTalkLink;
1191
	}
1192
1193
	/**
1194
	 * @param int $userId Userid
1195
	 * @param string $userText User name in database.
1196
	 * @return string HTML fragment with block link
1197
	 */
1198
	public static function blockLink( $userId, $userText ) {
1199
		$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1200
		$blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
1201
		return $blockLink;
1202
	}
1203
1204
	/**
1205
	 * @param int $userId Userid
1206
	 * @param string $userText User name in database.
1207
	 * @return string HTML fragment with e-mail user link
1208
	 */
1209
	public static function emailLink( $userId, $userText ) {
1210
		$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1211
		$emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
1212
		return $emailLink;
1213
	}
1214
1215
	/**
1216
	 * Generate a user link if the current user is allowed to view it
1217
	 * @param Revision $rev
1218
	 * @param bool $isPublic Show only if all users can see it
1219
	 * @return string HTML fragment
1220
	 */
1221
	public static function revUserLink( $rev, $isPublic = false ) {
1222
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1223
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1224
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1225
			$link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
1226
				$rev->getUserText( Revision::FOR_THIS_USER ) );
1227
		} else {
1228
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1229
		}
1230
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1231
			return '<span class="history-deleted">' . $link . '</span>';
1232
		}
1233
		return $link;
1234
	}
1235
1236
	/**
1237
	 * Generate a user tool link cluster if the current user is allowed to view it
1238
	 * @param Revision $rev
1239
	 * @param bool $isPublic Show only if all users can see it
1240
	 * @return string HTML
1241
	 */
1242
	public static function revUserTools( $rev, $isPublic = false ) {
1243
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1244
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1245
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1246
			$userId = $rev->getUser( Revision::FOR_THIS_USER );
1247
			$userText = $rev->getUserText( Revision::FOR_THIS_USER );
1248
			$link = self::userLink( $userId, $userText )
1249
				. self::userToolLinks( $userId, $userText );
1250
		} else {
1251
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1252
		}
1253
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1254
			return ' <span class="history-deleted">' . $link . '</span>';
1255
		}
1256
		return $link;
1257
	}
1258
1259
	/**
1260
	 * This function is called by all recent changes variants, by the page history,
1261
	 * and by the user contributions list. It is responsible for formatting edit
1262
	 * summaries. It escapes any HTML in the summary, but adds some CSS to format
1263
	 * auto-generated comments (from section editing) and formats [[wikilinks]].
1264
	 *
1265
	 * @author Erik Moeller <[email protected]>
1266
	 *
1267
	 * Note: there's not always a title to pass to this function.
1268
	 * Since you can't set a default parameter for a reference, I've turned it
1269
	 * temporarily to a value pass. Should be adjusted further. --brion
1270
	 *
1271
	 * @param string $comment
1272
	 * @param Title|null $title Title object (to generate link to the section in autocomment)
1273
	 *  or null
1274
	 * @param bool $local Whether section links should refer to local page
1275
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1276
	 *  For use with external changes.
1277
	 *
1278
	 * @return mixed|string
1279
	 */
1280
	public static function formatComment(
1281
		$comment, $title = null, $local = false, $wikiId = null
1282
	) {
1283
		# Sanitize text a bit:
1284
		$comment = str_replace( "\n", " ", $comment );
1285
		# Allow HTML entities (for bug 13815)
1286
		$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
1287
1288
		# Render autocomments and make links:
1289
		$comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
1290
		$comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
1291
1292
		return $comment;
1293
	}
1294
1295
	/**
1296
	 * Converts autogenerated comments in edit summaries into section links.
1297
	 *
1298
	 * The pattern for autogen comments is / * foo * /, which makes for
1299
	 * some nasty regex.
1300
	 * We look for all comments, match any text before and after the comment,
1301
	 * add a separator where needed and format the comment itself with CSS
1302
	 * Called by Linker::formatComment.
1303
	 *
1304
	 * @param string $comment Comment text
1305
	 * @param Title|null $title An optional title object used to links to sections
1306
	 * @param bool $local Whether section links should refer to local page
1307
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1308
	 *  as used by WikiMap.
1309
	 *
1310
	 * @return string Formatted comment (wikitext)
1311
	 */
1312
	private static function formatAutocomments(
1313
		$comment, $title = null, $local = false, $wikiId = null
1314
	) {
1315
		// @todo $append here is something of a hack to preserve the status
1316
		// quo. Someone who knows more about bidi and such should decide
1317
		// (1) what sane rendering even *is* for an LTR edit summary on an RTL
1318
		// wiki, both when autocomments exist and when they don't, and
1319
		// (2) what markup will make that actually happen.
1320
		$append = '';
1321
		$comment = preg_replace_callback(
1322
			// To detect the presence of content before or after the
1323
			// auto-comment, we use capturing groups inside optional zero-width
1324
			// assertions. But older versions of PCRE can't directly make
1325
			// zero-width assertions optional, so wrap them in a non-capturing
1326
			// group.
1327
			'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
1328
			function ( $match ) use ( $title, $local, $wikiId, &$append ) {
1329
				global $wgLang;
1330
1331
				// Ensure all match positions are defined
1332
				$match += [ '', '', '', '' ];
1333
1334
				$pre = $match[1] !== '';
1335
				$auto = $match[2];
1336
				$post = $match[3] !== '';
1337
				$comment = null;
1338
1339
				Hooks::run(
1340
					'FormatAutocomments',
1341
					[ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
1342
				);
1343
1344
				if ( $comment === null ) {
1345
					$link = '';
1346
					if ( $title ) {
1347
						$section = $auto;
1348
						# Remove links that a user may have manually put in the autosummary
1349
						# This could be improved by copying as much of Parser::stripSectionName as desired.
1350
						$section = str_replace( '[[:', '', $section );
1351
						$section = str_replace( '[[', '', $section );
1352
						$section = str_replace( ']]', '', $section );
1353
1354
						$section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
1355
						if ( $local ) {
1356
							$sectionTitle = Title::newFromText( '#' . $section );
1357
						} else {
1358
							$sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
1359
								$title->getDBkey(), $section );
1360
						}
1361
						if ( $sectionTitle ) {
1362
							$link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
1363
						} else {
1364
							$link = '';
1365
						}
1366
					}
1367
					if ( $pre ) {
1368
						# written summary $presep autocomment (summary /* section */)
1369
						$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
1370
					}
1371
					if ( $post ) {
1372
						# autocomment $postsep written summary (/* section */ summary)
1373
						$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
1374
					}
1375
					$auto = '<span class="autocomment">' . $auto . '</span>';
1376
					$comment = $pre . $link . $wgLang->getDirMark()
1377
						. '<span dir="auto">' . $auto;
1378
					$append .= '</span>';
1379
				}
1380
				return $comment;
1381
			},
1382
			$comment
1383
		);
1384
		return $comment . $append;
1385
	}
1386
1387
	/**
1388
	 * Formats wiki links and media links in text; all other wiki formatting
1389
	 * is ignored
1390
	 *
1391
	 * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
1392
	 * @param string $comment Text to format links in. WARNING! Since the output of this
1393
	 *	function is html, $comment must be sanitized for use as html. You probably want
1394
	 *	to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
1395
	 *	this function.
1396
	 * @param Title|null $title An optional title object used to links to sections
1397
	 * @param bool $local Whether section links should refer to local page
1398
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1399
	 *  as used by WikiMap.
1400
	 *
1401
	 * @return string
1402
	 */
1403
	public static function formatLinksInComment(
1404
		$comment, $title = null, $local = false, $wikiId = null
1405
	) {
1406
		return preg_replace_callback(
1407
			'/
1408
				\[\[
1409
				:? # ignore optional leading colon
1410
				([^\]|]+) # 1. link target; page names cannot include ] or |
1411
				(?:\|
1412
					# 2. a pipe-separated substring; only the last is captured
1413
					# Stop matching at | and ]] without relying on backtracking.
1414
					((?:]?[^\]|])*+)
1415
				)*
1416
				\]\]
1417
				([^[]*) # 3. link trail (the text up until the next link)
1418
			/x',
1419
			function ( $match ) use ( $title, $local, $wikiId ) {
1420
				global $wgContLang;
1421
1422
				$medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
1423
				$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
1424
1425
				$comment = $match[0];
1426
1427
				# fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
1428
				if ( strpos( $match[1], '%' ) !== false ) {
1429
					$match[1] = strtr(
1430
						rawurldecode( $match[1] ),
1431
						[ '<' => '&lt;', '>' => '&gt;' ]
1432
					);
1433
				}
1434
1435
				# Handle link renaming [[foo|text]] will show link as "text"
1436
				if ( $match[2] != "" ) {
1437
					$text = $match[2];
1438
				} else {
1439
					$text = $match[1];
1440
				}
1441
				$submatch = [];
1442
				$thelink = null;
1443
				if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
1444
					# Media link; trail not supported.
1445
					$linkRegexp = '/\[\[(.*?)\]\]/';
1446
					$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...
1447
					if ( $title ) {
1448
						$thelink = Linker::makeMediaLinkObj( $title, $text );
1449
					}
1450
				} else {
1451
					# Other kind of link
1452
					# Make sure its target is non-empty
1453
					if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
1454
						$match[1] = substr( $match[1], 1 );
1455
					}
1456
					if ( $match[1] !== false && $match[1] !== '' ) {
1457
						if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
1458
							$trail = $submatch[1];
1459
						} else {
1460
							$trail = "";
1461
						}
1462
						$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
1463
						list( $inside, $trail ) = Linker::splitTrail( $trail );
1464
1465
						$linkText = $text;
1466
						$linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 1404 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...
1467
1468
						$target = Title::newFromText( $linkTarget );
1469
						if ( $target ) {
1470
							if ( $target->getText() == '' && !$target->isExternal()
1471
								&& !$local && $title
1472
							) {
1473
								$newTarget = clone $title;
1474
								$newTarget->setFragment( '#' . $target->getFragment() );
1475
								$target = $newTarget;
1476
							}
1477
1478
							$thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
1479
						}
1480
					}
1481
				}
1482
				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...
1483
					// If the link is still valid, go ahead and replace it in!
1484
					$comment = preg_replace(
1485
						$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...
1486
						StringUtils::escapeRegexReplacement( $thelink ),
1487
						$comment,
1488
						1
1489
					);
1490
				}
1491
1492
				return $comment;
1493
			},
1494
			$comment
1495
		);
1496
	}
1497
1498
	/**
1499
	 * Generates a link to the given Title
1500
	 *
1501
	 * @note This is only public for technical reasons. It's not intended for use outside Linker.
1502
	 *
1503
	 * @param Title $title
1504
	 * @param string $text
1505
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1506
	 *  as used by WikiMap.
1507
	 * @param string|string[] $options See the $options parameter in Linker::link.
1508
	 *
1509
	 * @return string HTML link
1510
	 */
1511
	public static function makeCommentLink(
1512
		Title $title, $text, $wikiId = null, $options = []
1513
	) {
1514
		if ( $wikiId !== null && !$title->isExternal() ) {
1515
			$link = Linker::makeExternalLink(
1516
				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...
1517
					$wikiId,
1518
					$title->getPrefixedText(),
1519
					$title->getFragment()
1520
				),
1521
				$text,
1522
				/* escape = */ false // Already escaped
1523
			);
1524
		} else {
1525
			$link = Linker::link( $title, $text, [], [], $options );
1526
		}
1527
1528
		return $link;
1529
	}
1530
1531
	/**
1532
	 * @param Title $contextTitle
1533
	 * @param string $target
1534
	 * @param string $text
1535
	 * @return string
1536
	 */
1537
	public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1538
		# Valid link forms:
1539
		# Foobar -- normal
1540
		# :Foobar -- override special treatment of prefix (images, language links)
1541
		# /Foobar -- convert to CurrentPage/Foobar
1542
		# /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1543
		# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1544
		# ../Foobar -- convert to CurrentPage/Foobar,
1545
		#              (from CurrentPage/CurrentSubPage)
1546
		# ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1547
		#              (from CurrentPage/CurrentSubPage)
1548
1549
		$ret = $target; # default return value is no change
1550
1551
		# Some namespaces don't allow subpages,
1552
		# so only perform processing if subpages are allowed
1553
		if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
1554
			$hash = strpos( $target, '#' );
1555
			if ( $hash !== false ) {
1556
				$suffix = substr( $target, $hash );
1557
				$target = substr( $target, 0, $hash );
1558
			} else {
1559
				$suffix = '';
1560
			}
1561
			# bug 7425
1562
			$target = trim( $target );
1563
			# Look at the first character
1564
			if ( $target != '' && $target[0] === '/' ) {
1565
				# / at end means we don't want the slash to be shown
1566
				$m = [];
1567
				$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1568 View Code Duplication
				if ( $trailingSlashes ) {
1569
					$noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1570
				} else {
1571
					$noslash = substr( $target, 1 );
1572
				}
1573
1574
				$ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
1575
				if ( $text === '' ) {
1576
					$text = $target . $suffix;
1577
				} # this might be changed for ugliness reasons
1578
			} else {
1579
				# check for .. subpage backlinks
1580
				$dotdotcount = 0;
1581
				$nodotdot = $target;
1582
				while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
1583
					++$dotdotcount;
1584
					$nodotdot = substr( $nodotdot, 3 );
1585
				}
1586
				if ( $dotdotcount > 0 ) {
1587
					$exploded = explode( '/', $contextTitle->getPrefixedText() );
1588
					if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1589
						$ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1590
						# / at the end means don't show full path
1591
						if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1592
							$nodotdot = rtrim( $nodotdot, '/' );
1593
							if ( $text === '' ) {
1594
								$text = $nodotdot . $suffix;
1595
							}
1596
						}
1597
						$nodotdot = trim( $nodotdot );
1598
						if ( $nodotdot != '' ) {
1599
							$ret .= '/' . $nodotdot;
1600
						}
1601
						$ret .= $suffix;
1602
					}
1603
				}
1604
			}
1605
		}
1606
1607
		return $ret;
1608
	}
1609
1610
	/**
1611
	 * Wrap a comment in standard punctuation and formatting if
1612
	 * it's non-empty, otherwise return empty string.
1613
	 *
1614
	 * @param string $comment
1615
	 * @param Title|null $title Title object (to generate link to section in autocomment) or null
1616
	 * @param bool $local Whether section links should refer to local page
1617
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1618
	 *  For use with external changes.
1619
	 *
1620
	 * @return string
1621
	 */
1622
	public static function commentBlock(
1623
		$comment, $title = null, $local = false, $wikiId = null
1624
	) {
1625
		// '*' used to be the comment inserted by the software way back
1626
		// in antiquity in case none was provided, here for backwards
1627
		// compatibility, acc. to brion -ævar
1628
		if ( $comment == '' || $comment == '*' ) {
1629
			return '';
1630
		} else {
1631
			$formatted = self::formatComment( $comment, $title, $local, $wikiId );
1632
			$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
1633
			return " <span class=\"comment\">$formatted</span>";
1634
		}
1635
	}
1636
1637
	/**
1638
	 * Wrap and format the given revision's comment block, if the current
1639
	 * user is allowed to view it.
1640
	 *
1641
	 * @param Revision $rev
1642
	 * @param bool $local Whether section links should refer to local page
1643
	 * @param bool $isPublic Show only if all users can see it
1644
	 * @return string HTML fragment
1645
	 */
1646
	public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
1647
		if ( $rev->getComment( Revision::RAW ) == "" ) {
1648
			return "";
1649
		}
1650
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
1651
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1652
		} elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
1653
			$block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
1654
				$rev->getTitle(), $local );
1655
		} else {
1656
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1657
		}
1658
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
1659
			return " <span class=\"history-deleted\">$block</span>";
1660
		}
1661
		return $block;
1662
	}
1663
1664
	/**
1665
	 * @param int $size
1666
	 * @return string
1667
	 */
1668
	public static function formatRevisionSize( $size ) {
1669
		if ( $size == 0 ) {
1670
			$stxt = wfMessage( 'historyempty' )->escaped();
1671
		} else {
1672
			$stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1673
			$stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
1674
		}
1675
		return "<span class=\"history-size\">$stxt</span>";
1676
	}
1677
1678
	/**
1679
	 * Add another level to the Table of Contents
1680
	 *
1681
	 * @return string
1682
	 */
1683
	public static function tocIndent() {
1684
		return "\n<ul>";
1685
	}
1686
1687
	/**
1688
	 * Finish one or more sublevels on the Table of Contents
1689
	 *
1690
	 * @param int $level
1691
	 * @return string
1692
	 */
1693
	public static function tocUnindent( $level ) {
1694
		return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1695
	}
1696
1697
	/**
1698
	 * parameter level defines if we are on an indentation level
1699
	 *
1700
	 * @param string $anchor
1701
	 * @param string $tocline
1702
	 * @param string $tocnumber
1703
	 * @param string $level
1704
	 * @param string|bool $sectionIndex
1705
	 * @return string
1706
	 */
1707
	public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1708
		$classes = "toclevel-$level";
1709
		if ( $sectionIndex !== false ) {
1710
			$classes .= " tocsection-$sectionIndex";
1711
		}
1712
		return "\n<li class=\"$classes\"><a href=\"#" .
1713
			$anchor . '"><span class="tocnumber">' .
1714
			$tocnumber . '</span> <span class="toctext">' .
1715
			$tocline . '</span></a>';
1716
	}
1717
1718
	/**
1719
	 * End a Table Of Contents line.
1720
	 * tocUnindent() will be used instead if we're ending a line below
1721
	 * the new level.
1722
	 * @return string
1723
	 */
1724
	public static function tocLineEnd() {
1725
		return "</li>\n";
1726
	}
1727
1728
	/**
1729
	 * Wraps the TOC in a table and provides the hide/collapse javascript.
1730
	 *
1731
	 * @param string $toc Html of the Table Of Contents
1732
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1733
	 * @return string Full html of the TOC
1734
	 */
1735
	public static function tocList( $toc, $lang = false ) {
1736
		$lang = wfGetLangObj( $lang );
1737
		$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1738
1739
		return '<div id="toc" class="toc">'
1740
			. '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
1741
			. $toc
1742
			. "</ul>\n</div>\n";
1743
	}
1744
1745
	/**
1746
	 * Generate a table of contents from a section tree.
1747
	 *
1748
	 * @param array $tree Return value of ParserOutput::getSections()
1749
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1750
	 * @return string HTML fragment
1751
	 */
1752
	public static function generateTOC( $tree, $lang = false ) {
1753
		$toc = '';
1754
		$lastLevel = 0;
1755
		foreach ( $tree as $section ) {
1756
			if ( $section['toclevel'] > $lastLevel ) {
1757
				$toc .= self::tocIndent();
1758
			} elseif ( $section['toclevel'] < $lastLevel ) {
1759
				$toc .= self::tocUnindent(
1760
					$lastLevel - $section['toclevel'] );
1761
			} else {
1762
				$toc .= self::tocLineEnd();
1763
			}
1764
1765
			$toc .= self::tocLine( $section['anchor'],
1766
				$section['line'], $section['number'],
1767
				$section['toclevel'], $section['index'] );
1768
			$lastLevel = $section['toclevel'];
1769
		}
1770
		$toc .= self::tocLineEnd();
1771
		return self::tocList( $toc, $lang );
1772
	}
1773
1774
	/**
1775
	 * Create a headline for content
1776
	 *
1777
	 * @param int $level The level of the headline (1-6)
1778
	 * @param string $attribs Any attributes for the headline, starting with
1779
	 *   a space and ending with '>'
1780
	 *   This *must* be at least '>' for no attribs
1781
	 * @param string $anchor The anchor to give the headline (the bit after the #)
1782
	 * @param string $html Html for the text of the header
1783
	 * @param string $link HTML to add for the section edit link
1784
	 * @param bool|string $legacyAnchor A second, optional anchor to give for
1785
	 *   backward compatibility (false to omit)
1786
	 *
1787
	 * @return string HTML headline
1788
	 */
1789
	public static function makeHeadline( $level, $attribs, $anchor, $html,
1790
		$link, $legacyAnchor = false
1791
	) {
1792
		$ret = "<h$level$attribs"
1793
			. "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
1794
			. $link
1795
			. "</h$level>";
1796
		if ( $legacyAnchor !== false ) {
1797
			$ret = "<div id=\"$legacyAnchor\"></div>$ret";
1798
		}
1799
		return $ret;
1800
	}
1801
1802
	/**
1803
	 * Split a link trail, return the "inside" portion and the remainder of the trail
1804
	 * as a two-element array
1805
	 * @param string $trail
1806
	 * @return array
1807
	 */
1808
	static function splitTrail( $trail ) {
1809
		global $wgContLang;
1810
		$regex = $wgContLang->linkTrail();
1811
		$inside = '';
1812
		if ( $trail !== '' ) {
1813
			$m = [];
1814
			if ( preg_match( $regex, $trail, $m ) ) {
1815
				$inside = $m[1];
1816
				$trail = $m[2];
1817
			}
1818
		}
1819
		return [ $inside, $trail ];
1820
	}
1821
1822
	/**
1823
	 * Generate a rollback link for a given revision.  Currently it's the
1824
	 * caller's responsibility to ensure that the revision is the top one. If
1825
	 * it's not, of course, the user will get an error message.
1826
	 *
1827
	 * If the calling page is called with the parameter &bot=1, all rollback
1828
	 * links also get that parameter. It causes the edit itself and the rollback
1829
	 * to be marked as "bot" edits. Bot edits are hidden by default from recent
1830
	 * changes, so this allows sysops to combat a busy vandal without bothering
1831
	 * other users.
1832
	 *
1833
	 * If the option verify is set this function will return the link only in case the
1834
	 * revision can be reverted. Please note that due to performance limitations
1835
	 * it might be assumed that a user isn't the only contributor of a page while
1836
	 * (s)he is, which will lead to useless rollback links. Furthermore this wont
1837
	 * work if $wgShowRollbackEditCount is disabled, so this can only function
1838
	 * as an additional check.
1839
	 *
1840
	 * If the option noBrackets is set the rollback link wont be enclosed in []
1841
	 *
1842
	 * @param Revision $rev
1843
	 * @param IContextSource $context Context to use or null for the main context.
1844
	 * @param array $options
1845
	 * @return string
1846
	 */
1847
	public static function generateRollback( $rev, IContextSource $context = null,
1848
		$options = [ 'verify' ]
1849
	) {
1850
		if ( $context === null ) {
1851
			$context = RequestContext::getMain();
1852
		}
1853
1854
		$editCount = false;
1855
		if ( in_array( 'verify', $options, true ) ) {
1856
			$editCount = self::getRollbackEditCount( $rev, true );
1857
			if ( $editCount === false ) {
1858
				return '';
1859
			}
1860
		}
1861
1862
		$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 1856 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...
1863
1864
		if ( !in_array( 'noBrackets', $options, true ) ) {
1865
			$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1866
		}
1867
1868
		return '<span class="mw-rollback-link">' . $inner . '</span>';
1869
	}
1870
1871
	/**
1872
	 * This function will return the number of revisions which a rollback
1873
	 * would revert and, if $verify is set it will verify that a revision
1874
	 * can be reverted (that the user isn't the only contributor and the
1875
	 * revision we might rollback to isn't deleted). These checks can only
1876
	 * function as an additional check as this function only checks against
1877
	 * the last $wgShowRollbackEditCount edits.
1878
	 *
1879
	 * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
1880
	 * is set and the user is the only contributor of the page.
1881
	 *
1882
	 * @param Revision $rev
1883
	 * @param bool $verify Try to verify that this revision can really be rolled back
1884
	 * @return int|bool|null
1885
	 */
1886
	public static function getRollbackEditCount( $rev, $verify ) {
1887
		global $wgShowRollbackEditCount;
1888
		if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
1889
			// Nothing has happened, indicate this by returning 'null'
1890
			return null;
1891
		}
1892
1893
		$dbr = wfGetDB( DB_SLAVE );
1894
1895
		// Up to the value of $wgShowRollbackEditCount revisions are counted
1896
		$res = $dbr->select(
1897
			'revision',
1898
			[ 'rev_user_text', 'rev_deleted' ],
1899
			// $rev->getPage() returns null sometimes
1900
			[ 'rev_page' => $rev->getTitle()->getArticleID() ],
1901
			__METHOD__,
1902
			[
1903
				'USE INDEX' => [ 'revision' => 'page_timestamp' ],
1904
				'ORDER BY' => 'rev_timestamp DESC',
1905
				'LIMIT' => $wgShowRollbackEditCount + 1
1906
			]
1907
		);
1908
1909
		$editCount = 0;
1910
		$moreRevs = false;
1911
		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...
1912
			if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) {
1913
				if ( $verify &&
1914
					( $row->rev_deleted & Revision::DELETED_TEXT
1915
						|| $row->rev_deleted & Revision::DELETED_USER
1916
				) ) {
1917
					// If the user or the text of the revision we might rollback
1918
					// to is deleted in some way we can't rollback. Similar to
1919
					// the sanity checks in WikiPage::commitRollback.
1920
					return false;
1921
				}
1922
				$moreRevs = true;
1923
				break;
1924
			}
1925
			$editCount++;
1926
		}
1927
1928
		if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
1929
			// We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1930
			// and there weren't any other revisions. That means that the current user is the only
1931
			// editor, so we can't rollback
1932
			return false;
1933
		}
1934
		return $editCount;
1935
	}
1936
1937
	/**
1938
	 * Build a raw rollback link, useful for collections of "tool" links
1939
	 *
1940
	 * @param Revision $rev
1941
	 * @param IContextSource|null $context Context to use or null for the main context.
1942
	 * @param int $editCount Number of edits that would be reverted
1943
	 * @return string HTML fragment
1944
	 */
1945
	public static function buildRollbackLink( $rev, IContextSource $context = null,
1946
		$editCount = false
1947
	) {
1948
		global $wgShowRollbackEditCount, $wgMiserMode;
1949
1950
		// To config which pages are affected by miser mode
1951
		$disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1952
1953
		if ( $context === null ) {
1954
			$context = RequestContext::getMain();
1955
		}
1956
1957
		$title = $rev->getTitle();
1958
		$query = [
1959
			'action' => 'rollback',
1960
			'from' => $rev->getUserText(),
1961
			'token' => $context->getUser()->getEditToken( [
1962
				$title->getPrefixedText(),
1963
				$rev->getUserText()
1964
			] ),
1965
		];
1966
		if ( $context->getRequest()->getBool( 'bot' ) ) {
1967
			$query['bot'] = '1';
1968
			$query['hidediff'] = '1'; // bug 15999
1969
		}
1970
1971
		$disableRollbackEditCount = false;
1972
		if ( $wgMiserMode ) {
1973
			foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1974
				if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1975
					$disableRollbackEditCount = true;
1976
					break;
1977
				}
1978
			}
1979
		}
1980
1981
		if ( !$disableRollbackEditCount
1982
			&& is_int( $wgShowRollbackEditCount )
1983
			&& $wgShowRollbackEditCount > 0
1984
		) {
1985
			if ( !is_numeric( $editCount ) ) {
1986
				$editCount = self::getRollbackEditCount( $rev, false );
1987
			}
1988
1989
			if ( $editCount > $wgShowRollbackEditCount ) {
1990
				$editCount_output = $context->msg( 'rollbacklinkcount-morethan' )
1991
					->numParams( $wgShowRollbackEditCount )->parse();
1992
			} else {
1993
				$editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
1994
			}
1995
1996
			return self::link(
1997
				$title,
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 1957 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...
1998
				$editCount_output,
1999
				[ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
2000
				$query,
2001
				[ 'known', 'noclasses' ]
2002
			);
2003
		} else {
2004
			return self::link(
2005
				$title,
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 1957 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...
2006
				$context->msg( 'rollbacklink' )->escaped(),
2007
				[ 'title' => $context->msg( 'tooltip-rollback' )->text() ],
2008
				$query,
2009
				[ 'known', 'noclasses' ]
2010
			);
2011
		}
2012
	}
2013
2014
	/**
2015
	 * Returns HTML for the "templates used on this page" list.
2016
	 *
2017
	 * Make an HTML list of templates, and then add a "More..." link at
2018
	 * the bottom. If $more is null, do not add a "More..." link. If $more
2019
	 * is a Title, make a link to that title and use it. If $more is a string,
2020
	 * directly paste it in as the link (escaping needs to be done manually).
2021
	 * Finally, if $more is a Message, call toString().
2022
	 *
2023
	 * @param Title[] $templates Array of templates
2024
	 * @param bool $preview Whether this is for a preview
2025
	 * @param bool $section Whether this is for a section edit
2026
	 * @param Title|Message|string|null $more An escaped link for "More..." of the templates
2027
	 * @return string HTML output
2028
	 */
2029
	public static function formatTemplates( $templates, $preview = false,
2030
		$section = false, $more = null
2031
	) {
2032
		global $wgLang;
2033
2034
		$outText = '';
2035
		if ( count( $templates ) > 0 ) {
2036
			# Do a batch existence check
2037
			$batch = new LinkBatch;
2038
			foreach ( $templates as $title ) {
2039
				$batch->addObj( $title );
2040
			}
2041
			$batch->execute();
2042
2043
			# Construct the HTML
2044
			$outText = '<div class="mw-templatesUsedExplanation">';
2045
			if ( $preview ) {
2046
				$outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
2047
					->parseAsBlock();
2048
			} elseif ( $section ) {
2049
				$outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
2050
					->parseAsBlock();
2051
			} else {
2052
				$outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
2053
					->parseAsBlock();
2054
			}
2055
			$outText .= "</div><ul>\n";
2056
2057
			usort( $templates, 'Title::compare' );
2058
			foreach ( $templates as $titleObj ) {
2059
				$protected = '';
2060
				$restrictions = $titleObj->getRestrictions( 'edit' );
2061
				if ( $restrictions ) {
2062
					// Check backwards-compatible messages
2063
					$msg = null;
2064
					if ( $restrictions === [ 'sysop' ] ) {
2065
						$msg = wfMessage( 'template-protected' );
2066
					} elseif ( $restrictions === [ 'autoconfirmed' ] ) {
2067
						$msg = wfMessage( 'template-semiprotected' );
2068
					}
2069
					if ( $msg && !$msg->isDisabled() ) {
2070
						$protected = $msg->parse();
2071
					} else {
2072
						// Construct the message from restriction-level-*
2073
						// e.g. restriction-level-sysop, restriction-level-autoconfirmed
2074
						$msgs = [];
2075
						foreach ( $restrictions as $r ) {
2076
							$msgs[] = wfMessage( "restriction-level-$r" )->parse();
2077
						}
2078
						$protected = wfMessage( 'parentheses' )
2079
							->rawParams( $wgLang->commaList( $msgs ) )->escaped();
2080
					}
2081
				}
2082
				if ( $titleObj->quickUserCan( 'edit' ) ) {
2083
					$editLink = self::link(
2084
						$titleObj,
2085
						wfMessage( 'editlink' )->escaped(),
2086
						[],
2087
						[ 'action' => 'edit' ]
2088
					);
2089
				} else {
2090
					$editLink = self::link(
2091
						$titleObj,
2092
						wfMessage( 'viewsourcelink' )->escaped(),
2093
						[],
2094
						[ 'action' => 'edit' ]
2095
					);
2096
				}
2097
				$outText .= '<li>' . self::link( $titleObj )
2098
					. wfMessage( 'word-separator' )->escaped()
2099
					. wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
2100
					. wfMessage( 'word-separator' )->escaped()
2101
					. $protected . '</li>';
2102
			}
2103
2104
			if ( $more instanceof Title ) {
2105
				$outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
2106
			} elseif ( $more ) {
2107
				$outText .= "<li>$more</li>";
2108
			}
2109
2110
			$outText .= '</ul>';
2111
		}
2112
		return $outText;
2113
	}
2114
2115
	/**
2116
	 * Returns HTML for the "hidden categories on this page" list.
2117
	 *
2118
	 * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
2119
	 *   or similar
2120
	 * @return string HTML output
2121
	 */
2122
	public static function formatHiddenCategories( $hiddencats ) {
2123
2124
		$outText = '';
2125
		if ( count( $hiddencats ) > 0 ) {
2126
			# Construct the HTML
2127
			$outText = '<div class="mw-hiddenCategoriesExplanation">';
2128
			$outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2129
			$outText .= "</div><ul>\n";
2130
2131
			foreach ( $hiddencats as $titleObj ) {
2132
				# If it's hidden, it must exist - no need to check with a LinkBatch
2133
				$outText .= '<li>'
2134
					. self::link( $titleObj, null, [], [], 'known' )
2135
					. "</li>\n";
2136
			}
2137
			$outText .= '</ul>';
2138
		}
2139
		return $outText;
2140
	}
2141
2142
	/**
2143
	 * Format a size in bytes for output, using an appropriate
2144
	 * unit (B, KB, MB or GB) according to the magnitude in question
2145
	 *
2146
	 * @param int $size Size to format
2147
	 * @return string
2148
	 */
2149
	public static function formatSize( $size ) {
2150
		global $wgLang;
2151
		return htmlspecialchars( $wgLang->formatSize( $size ) );
2152
	}
2153
2154
	/**
2155
	 * Given the id of an interface element, constructs the appropriate title
2156
	 * attribute from the system messages.  (Note, this is usually the id but
2157
	 * isn't always, because sometimes the accesskey needs to go on a different
2158
	 * element than the id, for reverse-compatibility, etc.)
2159
	 *
2160
	 * @param string $name Id of the element, minus prefixes.
2161
	 * @param string|null $options Null or the string 'withaccess' to add an access-
2162
	 *   key hint
2163
	 * @param array $msgParams Parameters to pass to the message
2164
	 *
2165
	 * @return string Contents of the title attribute (which you must HTML-
2166
	 *   escape), or false for no title attribute
2167
	 */
2168
	public static function titleAttrib( $name, $options = null, array $msgParams = [] ) {
2169
		$message = wfMessage( "tooltip-$name", $msgParams );
2170
		if ( !$message->exists() ) {
2171
			$tooltip = false;
2172
		} else {
2173
			$tooltip = $message->text();
2174
			# Compatibility: formerly some tooltips had [alt-.] hardcoded
2175
			$tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
2176
			# Message equal to '-' means suppress it.
2177
			if ( $tooltip == '-' ) {
2178
				$tooltip = false;
2179
			}
2180
		}
2181
2182
		if ( $options == 'withaccess' ) {
2183
			$accesskey = self::accesskey( $name );
2184
			if ( $accesskey !== false ) {
2185
				// Should be build the same as in jquery.accessKeyLabel.js
2186
				if ( $tooltip === false || $tooltip === '' ) {
2187
					$tooltip = wfMessage( 'brackets', $accesskey )->text();
2188
				} else {
2189
					$tooltip .= wfMessage( 'word-separator' )->text();
2190
					$tooltip .= wfMessage( 'brackets', $accesskey )->text();
2191
				}
2192
			}
2193
		}
2194
2195
		return $tooltip;
2196
	}
2197
2198
	public static $accesskeycache;
2199
2200
	/**
2201
	 * Given the id of an interface element, constructs the appropriate
2202
	 * accesskey attribute from the system messages.  (Note, this is usually
2203
	 * the id but isn't always, because sometimes the accesskey needs to go on
2204
	 * a different element than the id, for reverse-compatibility, etc.)
2205
	 *
2206
	 * @param string $name Id of the element, minus prefixes.
2207
	 * @return string Contents of the accesskey attribute (which you must HTML-
2208
	 *   escape), or false for no accesskey attribute
2209
	 */
2210
	public static function accesskey( $name ) {
2211
		if ( isset( self::$accesskeycache[$name] ) ) {
2212
			return self::$accesskeycache[$name];
2213
		}
2214
2215
		$message = wfMessage( "accesskey-$name" );
2216
2217
		if ( !$message->exists() ) {
2218
			$accesskey = false;
2219
		} else {
2220
			$accesskey = $message->plain();
2221
			if ( $accesskey === '' || $accesskey === '-' ) {
2222
				# @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
2223
				# attribute, but this is broken for accesskey: that might be a useful
2224
				# value.
2225
				$accesskey = false;
2226
			}
2227
		}
2228
2229
		self::$accesskeycache[$name] = $accesskey;
2230
		return self::$accesskeycache[$name];
2231
	}
2232
2233
	/**
2234
	 * Get a revision-deletion link, or disabled link, or nothing, depending
2235
	 * on user permissions & the settings on the revision.
2236
	 *
2237
	 * Will use forward-compatible revision ID in the Special:RevDelete link
2238
	 * if possible, otherwise the timestamp-based ID which may break after
2239
	 * undeletion.
2240
	 *
2241
	 * @param User $user
2242
	 * @param Revision $rev
2243
	 * @param Title $title
2244
	 * @return string HTML fragment
2245
	 */
2246
	public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
2247
		$canHide = $user->isAllowed( 'deleterevision' );
2248
		if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
2249
			return '';
2250
		}
2251
2252
		if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
2253
			return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2254
		} else {
2255
			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...
2256
				// RevDelete links using revision ID are stable across
2257
				// page deletion and undeletion; use when possible.
2258
				$query = [
2259
					'type' => 'revision',
2260
					'target' => $title->getPrefixedDBkey(),
2261
					'ids' => $rev->getId()
2262
				];
2263
			} else {
2264
				// Older deleted entries didn't save a revision ID.
2265
				// We have to refer to these by timestamp, ick!
2266
				$query = [
2267
					'type' => 'archive',
2268
					'target' => $title->getPrefixedDBkey(),
2269
					'ids' => $rev->getTimestamp()
2270
				];
2271
			}
2272
			return Linker::revDeleteLink( $query,
2273
				$rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
2274
		}
2275
	}
2276
2277
	/**
2278
	 * Creates a (show/hide) link for deleting revisions/log entries
2279
	 *
2280
	 * @param array $query Query parameters to be passed to link()
2281
	 * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
2282
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2283
	 *
2284
	 * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
2285
	 * span to allow for customization of appearance with CSS
2286
	 */
2287
	public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2288
		$sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2289
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2290
		$html = wfMessage( $msgKey )->escaped();
2291
		$tag = $restricted ? 'strong' : 'span';
2292
		$link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2293
		return Xml::tags(
2294
			$tag,
2295
			[ 'class' => 'mw-revdelundel-link' ],
2296
			wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2297
		);
2298
	}
2299
2300
	/**
2301
	 * Creates a dead (show/hide) link for deleting revisions/log entries
2302
	 *
2303
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2304
	 *
2305
	 * @return string HTML text wrapped in a span to allow for customization
2306
	 * of appearance with CSS
2307
	 */
2308
	public static function revDeleteLinkDisabled( $delete = true ) {
2309
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2310
		$html = wfMessage( $msgKey )->escaped();
2311
		$htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2312
		return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2313
	}
2314
2315
	/* Deprecated methods */
2316
2317
	/**
2318
	 * Returns the attributes for the tooltip and access key.
2319
	 *
2320
	 * @param string $name
2321
	 * @param array $msgParams Params for constructing the message
2322
	 *
2323
	 * @return array
2324
	 */
2325
	public static function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) {
2326
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2327
		# no attribute" instead of "output '' as value for attribute", this
2328
		# would be three lines.
2329
		$attribs = [
2330
			'title' => self::titleAttrib( $name, 'withaccess', $msgParams ),
2331
			'accesskey' => self::accesskey( $name )
2332
		];
2333
		if ( $attribs['title'] === false ) {
2334
			unset( $attribs['title'] );
2335
		}
2336
		if ( $attribs['accesskey'] === false ) {
2337
			unset( $attribs['accesskey'] );
2338
		}
2339
		return $attribs;
2340
	}
2341
2342
	/**
2343
	 * Returns raw bits of HTML, use titleAttrib()
2344
	 * @param string $name
2345
	 * @param array|null $options
2346
	 * @return null|string
2347
	 */
2348
	public static function tooltip( $name, $options = null ) {
2349
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2350
		# no attribute" instead of "output '' as value for attribute", this
2351
		# would be two lines.
2352
		$tooltip = self::titleAttrib( $name, $options );
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 2348 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...
2353
		if ( $tooltip === false ) {
2354
			return '';
2355
		}
2356
		return Xml::expandAttributes( [
2357
			'title' => $tooltip
2358
		] );
2359
	}
2360
2361
}
2362
2363