Completed
Branch master (5cbada)
by
unknown
28:59
created

Linker::userToolLinks()   F

Complexity

Conditions 14
Paths 384

Size

Total Lines 47
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 31
nc 384
nop 5
dl 0
loc 47
rs 3.7522
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Methods to make links and related items.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
use MediaWiki\Linker\LinkTarget;
23
use MediaWiki\MediaWikiServices;
24
25
/**
26
 * Some internal bits split of from Skin.php. These functions are used
27
 * for primarily page content: links, embedded images, table of contents. Links
28
 * are also used in the skin.
29
 *
30
 * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
31
 *
32
 * @ingroup Skins
33
 */
34
class Linker {
35
	/**
36
	 * Flags for userToolLinks()
37
	 */
38
	const TOOL_LINKS_NOBLOCK = 1;
39
	const TOOL_LINKS_EMAIL = 2;
40
41
	/**
42
	 * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
43
	 *
44
	 * @since 1.16.3
45
	 * @deprecated since 1.25
46
	 *
47
	 * @param string $title The title text for the link, URL-encoded (???) but
48
	 *   not HTML-escaped
49
	 * @param string $unused Unused
50
	 * @param string $class The contents of the class attribute; if an empty
51
	 *   string is passed, which is the default value, defaults to 'external'.
52
	 * @return string
53
	 */
54
	static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
55
		global $wgContLang;
56
57
		wfDeprecated( __METHOD__, '1.25' );
58
59
		# @todo FIXME: We have a whole bunch of handling here that doesn't happen in
60
		# getExternalLinkAttributes, why?
61
		$title = urldecode( $title );
62
		$title = $wgContLang->checkTitleEncoding( $title );
63
		$title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
64
65
		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...
66
	}
67
68
	/**
69
	 * Get the appropriate HTML attributes to add to the "a" element of an internal link.
70
	 *
71
	 * @since 1.16.3
72
	 * @deprecated since 1.25
73
	 *
74
	 * @param string $title The title text for the link, URL-encoded (???) but
75
	 *   not HTML-escaped
76
	 * @param string $unused Unused
77
	 * @param string $class The contents of the class attribute, default none
78
	 * @return string
79
	 */
80
	static function getInternalLinkAttributes( $title, $unused = null, $class = '' ) {
81
		wfDeprecated( __METHOD__, '1.25' );
82
83
		$title = urldecode( $title );
84
		$title = strtr( $title, '_', ' ' );
85
		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...
86
	}
87
88
	/**
89
	 * Get the appropriate HTML attributes to add to the "a" element of an internal
90
	 * link, given the Title object for the page we want to link to.
91
	 *
92
	 * @since 1.16.3
93
	 * @deprecated since 1.25
94
	 *
95
	 * @param Title $nt
96
	 * @param string $unused Unused
97
	 * @param string $class The contents of the class attribute, default none
98
	 * @param string|bool $title Optional (unescaped) string to use in the title
99
	 *   attribute; if false, default to the name of the page we're linking to
100
	 * @return string
101
	 */
102
	static function getInternalLinkAttributesObj( $nt, $unused = null, $class = '', $title = false ) {
103
		wfDeprecated( __METHOD__, '1.25' );
104
105
		if ( $title === false ) {
106
			$title = $nt->getPrefixedText();
107
		}
108
		return self::getLinkAttributesInternal( $title, $class );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 102 can also be of type boolean; however, Linker::getLinkAttributesInternal() does only seem to accept string, maybe add an additional type check?

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

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

An additional type check may prevent trouble.

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

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

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

Loading history...
109
	}
110
111
	/**
112
	 * Common code for getLinkAttributesX functions
113
	 *
114
	 * @since 1.16.3
115
	 * @deprecated since 1.25
116
	 *
117
	 * @param string $title
118
	 * @param string $class
119
	 *
120
	 * @return string
121
	 */
122
	private static function getLinkAttributesInternal( $title, $class ) {
123
		wfDeprecated( __METHOD__, '1.25' );
124
125
		$title = htmlspecialchars( $title );
126
		$class = htmlspecialchars( $class );
127
		$r = '';
128
		if ( $class != '' ) {
129
			$r .= " class=\"$class\"";
130
		}
131
		if ( $title != '' ) {
132
			$r .= " title=\"$title\"";
133
		}
134
		return $r;
135
	}
136
137
	/**
138
	 * Return the CSS colour of a known link
139
	 *
140
	 * @since 1.16.3
141
	 * @param Title $t
142
	 * @param int $threshold User defined threshold
143
	 * @return string CSS class
144
	 */
145
	public static function getLinkColour( $t, $threshold ) {
146
		$colour = '';
147
		if ( $t->isRedirect() ) {
148
			# Page is a redirect
149
			$colour = 'mw-redirect';
150
		} elseif ( $threshold > 0 && $t->isContentPage() &&
151
			$t->exists() && $t->getLength() < $threshold
152
		) {
153
			# Page is a stub
154
			$colour = 'stub';
155
		}
156
		return $colour;
157
	}
158
159
	/**
160
	 * This function returns an HTML link to the given target.  It serves a few
161
	 * purposes:
162
	 *   1) If $target is a Title, the correct URL to link to will be figured
163
	 *      out automatically.
164
	 *   2) It automatically adds the usual classes for various types of link
165
	 *      targets: "new" for red links, "stub" for short articles, etc.
166
	 *   3) It escapes all attribute values safely so there's no risk of XSS.
167
	 *   4) It provides a default tooltip if the target is a Title (the page
168
	 *      name of the target).
169
	 * link() replaces the old functions in the makeLink() family.
170
	 *
171
	 * @since 1.18 Method exists since 1.16 as non-static, made static in 1.18.
172
	 *
173
	 * @param Title $target Can currently only be a Title, but this may
174
	 *   change to support Images, literal URLs, etc.
175
	 * @param string $html The HTML contents of the <a> element, i.e.,
176
	 *   the link text.  This is raw HTML and will not be escaped.  If null,
177
	 *   defaults to the prefixed text of the Title; or if the Title is just a
178
	 *   fragment, the contents of the fragment.
179
	 * @param array $customAttribs A key => value array of extra HTML attributes,
180
	 *   such as title and class.  (href is ignored.)  Classes will be
181
	 *   merged with the default classes, while other attributes will replace
182
	 *   default attributes.  All passed attribute values will be HTML-escaped.
183
	 *   A false attribute value means to suppress that attribute.
184
	 * @param array $query The query string to append to the URL
185
	 *   you're linking to, in key => value array form.  Query keys and values
186
	 *   will be URL-encoded.
187
	 * @param string|array $options String or array of strings:
188
	 *     'known': Page is known to exist, so don't check if it does.
189
	 *     'broken': Page is known not to exist, so don't check if it does.
190
	 *     'noclasses': Don't add any classes automatically (includes "new",
191
	 *       "stub", "mw-redirect", "extiw").  Only use the class attribute
192
	 *       provided, if any, so you get a simple blue link with no funny i-
193
	 *       cons.
194
	 *     'forcearticlepath': Use the article path always, even with a querystring.
195
	 *       Has compatibility issues on some setups, so avoid wherever possible.
196
	 *     'http': Force a full URL with http:// as the scheme.
197
	 *     'https': Force a full URL with https:// as the scheme.
198
	 *     'stubThreshold' => (int): Stub threshold to use when determining link classes.
199
	 * @return string HTML <a> attribute
200
	 */
201
	public static function link(
202
		$target, $html = null, $customAttribs = [], $query = [], $options = []
203
	) {
204
		if ( !$target instanceof Title ) {
205
			wfWarn( __METHOD__ . ': Requires $target to be a Title object.', 2 );
206
			return "<!-- ERROR -->$html";
207
		}
208
209
		if ( is_string( $query ) ) {
210
			// some functions withing core using this still hand over query strings
211
			wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
212
			$query = wfCgiToArray( $query );
213
		}
214
215
		$services = MediaWikiServices::getInstance();
216
		$options = (array)$options;
217
		if ( $options ) {
218
			// Custom options, create new LinkRenderer
219
			if ( !isset( $options['stubThreshold'] ) ) {
220
				global $wgUser;
221
				$options['stubThreshold'] = $wgUser->getStubThreshold();
222
			}
223
			$linkRenderer = $services->getLinkRendererFactory()
224
				->createFromLegacyOptions( $options );
225
		} else {
226
			$linkRenderer = $services->getLinkRenderer();
227
		}
228
229
		if ( $html !== null ) {
230
			$text = new HtmlArmor( $html );
231
		} else {
232
			$text = $html; // null
233
		}
234 View Code Duplication
		if ( in_array( 'known', $options, true ) ) {
235
			return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
236
		} elseif ( in_array( 'broken', $options, true ) ) {
237
			return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
238
		} else {
239
			return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
240
		}
241
	}
242
243
	/**
244
	 * Identical to link(), except $options defaults to 'known'.
245
	 * @since 1.16.3
246
	 * @see Linker::link
247
	 * @return string
248
	 */
249
	public static function linkKnown(
250
		$target, $html = null, $customAttribs = [],
251
		$query = [], $options = [ 'known' ]
252
	) {
253
		return self::link( $target, $html, $customAttribs, $query, $options );
254
	}
255
256
	/**
257
	 * Make appropriate markup for a link to the current article. This is
258
	 * currently rendered as the bold link text. The calling sequence is the
259
	 * same as the other make*LinkObj static functions, despite $query not
260
	 * being used.
261
	 *
262
	 * @since 1.16.3
263
	 * @param Title $nt
264
	 * @param string $html [optional]
265
	 * @param string $query [optional]
266
	 * @param string $trail [optional]
267
	 * @param string $prefix [optional]
268
	 *
269
	 * @return string
270
	 */
271
	public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
272
		$ret = "<strong class=\"selflink\">{$prefix}{$html}</strong>{$trail}";
273
		if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) {
274
			return $ret;
275
		}
276
277
		if ( $html == '' ) {
278
			$html = htmlspecialchars( $nt->getPrefixedText() );
279
		}
280
		list( $inside, $trail ) = self::splitTrail( $trail );
281
		return "<strong class=\"selflink\">{$prefix}{$html}{$inside}</strong>{$trail}";
282
	}
283
284
	/**
285
	 * Get a message saying that an invalid title was encountered.
286
	 * This should be called after a method like Title::makeTitleSafe() returned
287
	 * a value indicating that the title object is invalid.
288
	 *
289
	 * @param IContextSource $context Context to use to get the messages
290
	 * @param int $namespace Namespace number
291
	 * @param string $title Text of the title, without the namespace part
292
	 * @return string
293
	 */
294
	public static function getInvalidTitleDescription( IContextSource $context, $namespace, $title ) {
295
		global $wgContLang;
296
297
		// First we check whether the namespace exists or not.
298
		if ( MWNamespace::exists( $namespace ) ) {
299
			if ( $namespace == NS_MAIN ) {
300
				$name = $context->msg( 'blanknamespace' )->text();
301
			} else {
302
				$name = $wgContLang->getFormattedNsText( $namespace );
303
			}
304
			return $context->msg( 'invalidtitle-knownnamespace', $namespace, $name, $title )->text();
305
		} else {
306
			return $context->msg( 'invalidtitle-unknownnamespace', $namespace, $title )->text();
307
		}
308
	}
309
310
	/**
311
	 * @since 1.16.3
312
	 * @param LinkTarget $target
313
	 * @return LinkTarget|Title You will get back the same type you passed in, or a Title object
314
	 */
315
	public static function normaliseSpecialPage( LinkTarget $target ) {
316
		if ( $target->getNamespace() == NS_SPECIAL ) {
317
			list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $target->getDBkey() );
318
			if ( !$name ) {
319
				return $target;
320
			}
321
			$ret = SpecialPage::getTitleFor( $name, $subpage, $target->getFragment() );
322
			return $ret;
323
		} else {
324
			return $target;
325
		}
326
	}
327
328
	/**
329
	 * Returns the filename part of an url.
330
	 * Used as alternative text for external images.
331
	 *
332
	 * @param string $url
333
	 *
334
	 * @return string
335
	 */
336
	private static function fnamePart( $url ) {
337
		$basename = strrchr( $url, '/' );
338
		if ( false === $basename ) {
339
			$basename = $url;
340
		} else {
341
			$basename = substr( $basename, 1 );
342
		}
343
		return $basename;
344
	}
345
346
	/**
347
	 * Return the code for images which were added via external links,
348
	 * via Parser::maybeMakeExternalImage().
349
	 *
350
	 * @since 1.16.3
351
	 * @param string $url
352
	 * @param string $alt
353
	 *
354
	 * @return string
355
	 */
356
	public static function makeExternalImage( $url, $alt = '' ) {
357
		if ( $alt == '' ) {
358
			$alt = self::fnamePart( $url );
359
		}
360
		$img = '';
361
		$success = Hooks::run( 'LinkerMakeExternalImage', [ &$url, &$alt, &$img ] );
362
		if ( !$success ) {
363
			wfDebug( "Hook LinkerMakeExternalImage changed the output of external image "
364
				. "with url {$url} and alt text {$alt} to {$img}\n", true );
365
			return $img;
366
		}
367
		return Html::element( 'img',
368
			[
369
				'src' => $url,
370
				'alt' => $alt ] );
371
	}
372
373
	/**
374
	 * Given parameters derived from [[Image:Foo|options...]], generate the
375
	 * HTML that that syntax inserts in the page.
376
	 *
377
	 * @param Parser $parser
378
	 * @param Title $title Title object of the file (not the currently viewed page)
379
	 * @param File $file File object, or false if it doesn't exist
380
	 * @param array $frameParams Associative array of parameters external to the media handler.
381
	 *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
382
	 *     will often be false.
383
	 *          thumbnail       If present, downscale and frame
384
	 *          manualthumb     Image name to use as a thumbnail, instead of automatic scaling
385
	 *          framed          Shows image in original size in a frame
386
	 *          frameless       Downscale but don't frame
387
	 *          upright         If present, tweak default sizes for portrait orientation
388
	 *          upright_factor  Fudge factor for "upright" tweak (default 0.75)
389
	 *          border          If present, show a border around the image
390
	 *          align           Horizontal alignment (left, right, center, none)
391
	 *          valign          Vertical alignment (baseline, sub, super, top, text-top, middle,
392
	 *                          bottom, text-bottom)
393
	 *          alt             Alternate text for image (i.e. alt attribute). Plain text.
394
	 *          class           HTML for image classes. Plain text.
395
	 *          caption         HTML for image caption.
396
	 *          link-url        URL to link to
397
	 *          link-title      Title object to link to
398
	 *          link-target     Value for the target attribute, only with link-url
399
	 *          no-link         Boolean, suppress description link
400
	 *
401
	 * @param array $handlerParams Associative array of media handler parameters, to be passed
402
	 *       to transform(). Typical keys are "width" and "page".
403
	 * @param string|bool $time Timestamp of the file, set as false for current
404
	 * @param string $query Query params for desc url
405
	 * @param int|null $widthOption Used by the parser to remember the user preference thumbnailsize
406
	 * @since 1.20
407
	 * @return string HTML for an image, with links, wrappers, etc.
408
	 */
409
	public static function makeImageLink( Parser $parser, Title $title,
410
		$file, $frameParams = [], $handlerParams = [], $time = false,
411
		$query = "", $widthOption = null
412
	) {
413
		$res = null;
414
		$dummy = new DummyLinker;
415
		if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
416
			&$file, &$frameParams, &$handlerParams, &$time, &$res ] ) ) {
417
			return $res;
418
		}
419
420
		if ( $file && !$file->allowInlineDisplay() ) {
421
			wfDebug( __METHOD__ . ': ' . $title->getPrefixedDBkey() . " does not allow inline display\n" );
422
			return self::link( $title );
423
		}
424
425
		// Shortcuts
426
		$fp =& $frameParams;
427
		$hp =& $handlerParams;
428
429
		// Clean up parameters
430
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
431
		if ( !isset( $fp['align'] ) ) {
432
			$fp['align'] = '';
433
		}
434
		if ( !isset( $fp['alt'] ) ) {
435
			$fp['alt'] = '';
436
		}
437
		if ( !isset( $fp['title'] ) ) {
438
			$fp['title'] = '';
439
		}
440
		if ( !isset( $fp['class'] ) ) {
441
			$fp['class'] = '';
442
		}
443
444
		$prefix = $postfix = '';
445
446
		if ( 'center' == $fp['align'] ) {
447
			$prefix = '<div class="center">';
448
			$postfix = '</div>';
449
			$fp['align'] = 'none';
450
		}
451
		if ( $file && !isset( $hp['width'] ) ) {
452
			if ( isset( $hp['height'] ) && $file->isVectorized() ) {
453
				// If its a vector image, and user only specifies height
454
				// we don't want it to be limited by its "normal" width.
455
				global $wgSVGMaxSize;
456
				$hp['width'] = $wgSVGMaxSize;
457
			} else {
458
				$hp['width'] = $file->getWidth( $page );
459
			}
460
461
			if ( isset( $fp['thumbnail'] )
462
				|| isset( $fp['manualthumb'] )
463
				|| isset( $fp['framed'] )
464
				|| isset( $fp['frameless'] )
465
				|| !$hp['width']
466
			) {
467
				global $wgThumbLimits, $wgThumbUpright;
468
469
				if ( $widthOption === null || !isset( $wgThumbLimits[$widthOption] ) ) {
470
					$widthOption = User::getDefaultOption( 'thumbsize' );
471
				}
472
473
				// Reduce width for upright images when parameter 'upright' is used
474
				if ( isset( $fp['upright'] ) && $fp['upright'] == 0 ) {
475
					$fp['upright'] = $wgThumbUpright;
476
				}
477
478
				// For caching health: If width scaled down due to upright
479
				// parameter, round to full __0 pixel to avoid the creation of a
480
				// lot of odd thumbs.
481
				$prefWidth = isset( $fp['upright'] ) ?
482
					round( $wgThumbLimits[$widthOption] * $fp['upright'], -1 ) :
483
					$wgThumbLimits[$widthOption];
484
485
				// Use width which is smaller: real image width or user preference width
486
				// Unless image is scalable vector.
487
				if ( !isset( $hp['height'] ) && ( $hp['width'] <= 0 ||
488
						$prefWidth < $hp['width'] || $file->isVectorized() ) ) {
489
					$hp['width'] = $prefWidth;
490
				}
491
			}
492
		}
493
494
		if ( isset( $fp['thumbnail'] ) || isset( $fp['manualthumb'] ) || isset( $fp['framed'] ) ) {
495
			# Create a thumbnail. Alignment depends on the writing direction of
496
			# the page content language (right-aligned for LTR languages,
497
			# left-aligned for RTL languages)
498
			# If a thumbnail width has not been provided, it is set
499
			# to the default user option as specified in Language*.php
500
			if ( $fp['align'] == '' ) {
501
				$fp['align'] = $parser->getTargetLanguage()->alignEnd();
502
			}
503
			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 410 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...
504
		}
505
506
		if ( $file && isset( $fp['frameless'] ) ) {
507
			$srcWidth = $file->getWidth( $page );
508
			# For "frameless" option: do not present an image bigger than the
509
			# source (for bitmap-style images). This is the same behavior as the
510
			# "thumb" option does it already.
511 View Code Duplication
			if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
512
				$hp['width'] = $srcWidth;
513
			}
514
		}
515
516
		if ( $file && isset( $hp['width'] ) ) {
517
			# Create a resized image, without the additional thumbnail features
518
			$thumb = $file->transform( $hp );
519
		} else {
520
			$thumb = false;
521
		}
522
523
		if ( !$thumb ) {
524
			$s = self::makeBrokenImageLinkObj( $title, $fp['title'], '', '', '', $time == true );
525
		} else {
526
			self::processResponsiveImages( $file, $thumb, $hp );
527
			$params = [
528
				'alt' => $fp['alt'],
529
				'title' => $fp['title'],
530
				'valign' => isset( $fp['valign'] ) ? $fp['valign'] : false,
531
				'img-class' => $fp['class'] ];
532
			if ( isset( $fp['border'] ) ) {
533
				$params['img-class'] .= ( $params['img-class'] !== '' ? ' ' : '' ) . 'thumbborder';
534
			}
535
			$params = self::getImageLinkMTOParams( $fp, $query, $parser ) + $params;
536
537
			$s = $thumb->toHtml( $params );
538
		}
539
		if ( $fp['align'] != '' ) {
540
			$s = "<div class=\"float{$fp['align']}\">{$s}</div>";
541
		}
542
		return str_replace( "\n", ' ', $prefix . $s . $postfix );
543
	}
544
545
	/**
546
	 * Get the link parameters for MediaTransformOutput::toHtml() from given
547
	 * frame parameters supplied by the Parser.
548
	 * @param array $frameParams The frame parameters
549
	 * @param string $query An optional query string to add to description page links
550
	 * @param Parser|null $parser
551
	 * @return array
552
	 */
553
	private static function getImageLinkMTOParams( $frameParams, $query = '', $parser = null ) {
554
		$mtoParams = [];
555
		if ( isset( $frameParams['link-url'] ) && $frameParams['link-url'] !== '' ) {
556
			$mtoParams['custom-url-link'] = $frameParams['link-url'];
557
			if ( isset( $frameParams['link-target'] ) ) {
558
				$mtoParams['custom-target-link'] = $frameParams['link-target'];
559
			}
560
			if ( $parser ) {
561
				$extLinkAttrs = $parser->getExternalLinkAttribs( $frameParams['link-url'] );
562
				foreach ( $extLinkAttrs as $name => $val ) {
563
					// Currently could include 'rel' and 'target'
564
					$mtoParams['parser-extlink-' . $name] = $val;
565
				}
566
			}
567
		} elseif ( isset( $frameParams['link-title'] ) && $frameParams['link-title'] !== '' ) {
568
			$mtoParams['custom-title-link'] = self::normaliseSpecialPage( $frameParams['link-title'] );
569
		} 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...
570
			// No link
571
		} else {
572
			$mtoParams['desc-link'] = true;
573
			$mtoParams['desc-query'] = $query;
574
		}
575
		return $mtoParams;
576
	}
577
578
	/**
579
	 * Make HTML for a thumbnail including image, border and caption
580
	 * @param Title $title
581
	 * @param File|bool $file File object or false if it doesn't exist
582
	 * @param string $label
583
	 * @param string $alt
584
	 * @param string $align
585
	 * @param array $params
586
	 * @param bool $framed
587
	 * @param string $manualthumb
588
	 * @return string
589
	 */
590
	public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt,
591
		$align = 'right', $params = [], $framed = false, $manualthumb = ""
592
	) {
593
		$frameParams = [
594
			'alt' => $alt,
595
			'caption' => $label,
596
			'align' => $align
597
		];
598
		if ( $framed ) {
599
			$frameParams['framed'] = true;
600
		}
601
		if ( $manualthumb ) {
602
			$frameParams['manualthumb'] = $manualthumb;
603
		}
604
		return self::makeThumbLink2( $title, $file, $frameParams, $params );
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 590 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...
605
	}
606
607
	/**
608
	 * @param Title $title
609
	 * @param File $file
610
	 * @param array $frameParams
611
	 * @param array $handlerParams
612
	 * @param bool $time
613
	 * @param string $query
614
	 * @return string
615
	 */
616
	public static function makeThumbLink2( Title $title, $file, $frameParams = [],
617
		$handlerParams = [], $time = false, $query = ""
618
	) {
619
		$exists = $file && $file->exists();
620
621
		# Shortcuts
622
		$fp =& $frameParams;
623
		$hp =& $handlerParams;
624
625
		$page = isset( $hp['page'] ) ? $hp['page'] : false;
626
		if ( !isset( $fp['align'] ) ) {
627
			$fp['align'] = 'right';
628
		}
629
		if ( !isset( $fp['alt'] ) ) {
630
			$fp['alt'] = '';
631
		}
632
		if ( !isset( $fp['title'] ) ) {
633
			$fp['title'] = '';
634
		}
635
		if ( !isset( $fp['caption'] ) ) {
636
			$fp['caption'] = '';
637
		}
638
639
		if ( empty( $hp['width'] ) ) {
640
			// Reduce width for upright images when parameter 'upright' is used
641
			$hp['width'] = isset( $fp['upright'] ) ? 130 : 180;
642
		}
643
		$thumb = false;
644
		$noscale = false;
645
		$manualthumb = false;
646
647
		if ( !$exists ) {
648
			$outerWidth = $hp['width'] + 2;
649
		} else {
650
			if ( isset( $fp['manualthumb'] ) ) {
651
				# Use manually specified thumbnail
652
				$manual_title = Title::makeTitleSafe( NS_FILE, $fp['manualthumb'] );
653
				if ( $manual_title ) {
654
					$manual_img = wfFindFile( $manual_title );
655
					if ( $manual_img ) {
656
						$thumb = $manual_img->getUnscaledThumb( $hp );
657
						$manualthumb = true;
658
					} else {
659
						$exists = false;
660
					}
661
				}
662
			} elseif ( isset( $fp['framed'] ) ) {
663
				// Use image dimensions, don't scale
664
				$thumb = $file->getUnscaledThumb( $hp );
665
				$noscale = true;
666
			} else {
667
				# Do not present an image bigger than the source, for bitmap-style images
668
				# This is a hack to maintain compatibility with arbitrary pre-1.10 behavior
669
				$srcWidth = $file->getWidth( $page );
670 View Code Duplication
				if ( $srcWidth && !$file->mustRender() && $hp['width'] > $srcWidth ) {
671
					$hp['width'] = $srcWidth;
672
				}
673
				$thumb = $file->transform( $hp );
674
			}
675
676
			if ( $thumb ) {
677
				$outerWidth = $thumb->getWidth() + 2;
678
			} else {
679
				$outerWidth = $hp['width'] + 2;
680
			}
681
		}
682
683
		# ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
684
		# So we don't need to pass it here in $query. However, the URL for the
685
		# zoom icon still needs it, so we make a unique query for it. See bug 14771
686
		$url = $title->getLocalURL( $query );
687
		if ( $page ) {
688
			$url = wfAppendQuery( $url, [ 'page' => $page ] );
689
		}
690
		if ( $manualthumb
691
			&& !isset( $fp['link-title'] )
692
			&& !isset( $fp['link-url'] )
693
			&& !isset( $fp['no-link'] ) ) {
694
			$fp['link-url'] = $url;
695
		}
696
697
		$s = "<div class=\"thumb t{$fp['align']}\">"
698
			. "<div class=\"thumbinner\" style=\"width:{$outerWidth}px;\">";
699
700
		if ( !$exists ) {
701
			$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...
702
			$zoomIcon = '';
703
		} elseif ( !$thumb ) {
704
			$s .= wfMessage( 'thumbnail_error', '' )->escaped();
705
			$zoomIcon = '';
706
		} else {
707
			if ( !$noscale && !$manualthumb ) {
708
				self::processResponsiveImages( $file, $thumb, $hp );
709
			}
710
			$params = [
711
				'alt' => $fp['alt'],
712
				'title' => $fp['title'],
713
				'img-class' => ( isset( $fp['class'] ) && $fp['class'] !== ''
714
					? $fp['class'] . ' '
715
					: '' ) . 'thumbimage'
716
			];
717
			$params = self::getImageLinkMTOParams( $fp, $query ) + $params;
718
			$s .= $thumb->toHtml( $params );
719
			if ( isset( $fp['framed'] ) ) {
720
				$zoomIcon = "";
721
			} else {
722
				$zoomIcon = Html::rawElement( 'div', [ 'class' => 'magnify' ],
723
					Html::rawElement( 'a', [
724
						'href' => $url,
725
						'class' => 'internal',
726
						'title' => wfMessage( 'thumbnail-more' )->text() ],
727
						"" ) );
728
			}
729
		}
730
		$s .= '  <div class="thumbcaption">' . $zoomIcon . $fp['caption'] . "</div></div></div>";
731
		return str_replace( "\n", ' ', $s );
732
	}
733
734
	/**
735
	 * Process responsive images: add 1.5x and 2x subimages to the thumbnail, where
736
	 * applicable.
737
	 *
738
	 * @param File $file
739
	 * @param MediaTransformOutput $thumb
740
	 * @param array $hp Image parameters
741
	 */
742
	public static function processResponsiveImages( $file, $thumb, $hp ) {
743
		global $wgResponsiveImages;
744
		if ( $wgResponsiveImages && $thumb && !$thumb->isError() ) {
745
			$hp15 = $hp;
746
			$hp15['width'] = round( $hp['width'] * 1.5 );
747
			$hp20 = $hp;
748
			$hp20['width'] = $hp['width'] * 2;
749
			if ( isset( $hp['height'] ) ) {
750
				$hp15['height'] = round( $hp['height'] * 1.5 );
751
				$hp20['height'] = $hp['height'] * 2;
752
			}
753
754
			$thumb15 = $file->transform( $hp15 );
755
			$thumb20 = $file->transform( $hp20 );
756
			if ( $thumb15 && !$thumb15->isError() && $thumb15->getUrl() !== $thumb->getUrl() ) {
757
				$thumb->responsiveUrls['1.5'] = $thumb15->getUrl();
758
			}
759
			if ( $thumb20 && !$thumb20->isError() && $thumb20->getUrl() !== $thumb->getUrl() ) {
760
				$thumb->responsiveUrls['2'] = $thumb20->getUrl();
761
			}
762
		}
763
	}
764
765
	/**
766
	 * Make a "broken" link to an image
767
	 *
768
	 * @since 1.16.3
769
	 * @param Title $title
770
	 * @param string $label Link label (plain text)
771
	 * @param string $query Query string
772
	 * @param string $unused1 Unused parameter kept for b/c
773
	 * @param string $unused2 Unused parameter kept for b/c
774
	 * @param bool $time A file of a certain timestamp was requested
775
	 * @return string
776
	 */
777
	public static function makeBrokenImageLinkObj( $title, $label = '',
778
		$query = '', $unused1 = '', $unused2 = '', $time = false
779
	) {
780
		if ( !$title instanceof Title ) {
781
			wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
782
			return "<!-- ERROR -->" . htmlspecialchars( $label );
783
		}
784
785
		global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
786
		if ( $label == '' ) {
787
			$label = $title->getPrefixedText();
788
		}
789
		$encLabel = htmlspecialchars( $label );
790
		$currentExists = $time ? ( wfFindFile( $title ) != false ) : false;
791
792
		if ( ( $wgUploadMissingFileUrl || $wgUploadNavigationUrl || $wgEnableUploads )
793
			&& !$currentExists
794
		) {
795
			$redir = RepoGroup::singleton()->getLocalRepo()->checkRedirect( $title );
796
797
			if ( $redir ) {
798
				// We already know it's a redirect, so mark it
799
				// accordingly
800
				return self::link(
801
					$title,
802
					$encLabel,
803
					[ 'class' => 'mw-redirect' ],
804
					wfCgiToArray( $query ),
805
					[ 'known', 'noclasses' ]
806
				);
807
			}
808
809
			$href = self::getUploadUrl( $title, $query );
810
811
			return '<a href="' . htmlspecialchars( $href ) . '" class="new" title="' .
812
				htmlspecialchars( $title->getPrefixedText(), ENT_QUOTES ) . '">' .
813
				$encLabel . '</a>';
814
		}
815
816
		return self::link( $title, $encLabel, [], wfCgiToArray( $query ), [ 'known', 'noclasses' ] );
817
	}
818
819
	/**
820
	 * Get the URL to upload a certain file
821
	 *
822
	 * @since 1.16.3
823
	 * @param Title $destFile Title object of the file to upload
824
	 * @param string $query Urlencoded query string to prepend
825
	 * @return string Urlencoded URL
826
	 */
827
	protected static function getUploadUrl( $destFile, $query = '' ) {
828
		global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
829
		$q = 'wpDestFile=' . $destFile->getPartialURL();
830
		if ( $query != '' ) {
831
			$q .= '&' . $query;
832
		}
833
834
		if ( $wgUploadMissingFileUrl ) {
835
			return wfAppendQuery( $wgUploadMissingFileUrl, $q );
836
		} elseif ( $wgUploadNavigationUrl ) {
837
			return wfAppendQuery( $wgUploadNavigationUrl, $q );
838
		} else {
839
			$upload = SpecialPage::getTitleFor( 'Upload' );
840
			return $upload->getLocalURL( $q );
841
		}
842
	}
843
844
	/**
845
	 * Create a direct link to a given uploaded file.
846
	 *
847
	 * @since 1.16.3
848
	 * @param Title $title
849
	 * @param string $html Pre-sanitized HTML
850
	 * @param string $time MW timestamp of file creation time
851
	 * @return string HTML
852
	 */
853
	public static function makeMediaLinkObj( $title, $html = '', $time = false ) {
854
		$img = wfFindFile( $title, [ 'time' => $time ] );
855
		return self::makeMediaLinkFile( $title, $img, $html );
856
	}
857
858
	/**
859
	 * Create a direct link to a given uploaded file.
860
	 * This will make a broken link if $file is false.
861
	 *
862
	 * @since 1.16.3
863
	 * @param Title $title
864
	 * @param File|bool $file File object or false
865
	 * @param string $html Pre-sanitized HTML
866
	 * @return string HTML
867
	 *
868
	 * @todo Handle invalid or missing images better.
869
	 */
870
	public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
871
		if ( $file && $file->exists() ) {
872
			$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...
873
			$class = 'internal';
874
		} else {
875
			$url = self::getUploadUrl( $title );
876
			$class = 'new';
877
		}
878
879
		$alt = $title->getText();
880
		if ( $html == '' ) {
881
			$html = $alt;
882
		}
883
884
		$ret = '';
885
		$attribs = [
886
			'href' => $url,
887
			'class' => $class,
888
			'title' => $alt
889
		];
890
891
		if ( !Hooks::run( 'LinkerMakeMediaLinkFile',
892
			[ $title, $file, &$html, &$attribs, &$ret ] ) ) {
893
			wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
894
				. "with url {$url} and text {$html} to {$ret}\n", true );
895
			return $ret;
896
		}
897
898
		return Html::rawElement( 'a', $attribs, $html );
899
	}
900
901
	/**
902
	 * Make a link to a special page given its name and, optionally,
903
	 * a message key from the link text.
904
	 * Usage example: Linker::specialLink( 'Recentchanges' )
905
	 *
906
	 * @since 1.16.3
907
	 * @param string $name
908
	 * @param string $key
909
	 * @return string
910
	 */
911
	public static function specialLink( $name, $key = '' ) {
912
		if ( $key == '' ) {
913
			$key = strtolower( $name );
914
		}
915
916
		return self::linkKnown( SpecialPage::getTitleFor( $name ), wfMessage( $key )->text() );
917
	}
918
919
	/**
920
	 * Make an external link
921
	 * @since 1.16.3. $title added in 1.21
922
	 * @param string $url URL to link to
923
	 * @param string $text Text of link
924
	 * @param bool $escape Do we escape the link text?
925
	 * @param string $linktype Type of external link. Gets added to the classes
926
	 * @param array $attribs Array of extra attributes to <a>
927
	 * @param Title|null $title Title object used for title specific link attributes
928
	 * @return string
929
	 */
930
	public static function makeExternalLink( $url, $text, $escape = true,
931
		$linktype = '', $attribs = [], $title = null
932
	) {
933
		global $wgTitle;
934
		$class = "external";
935
		if ( $linktype ) {
936
			$class .= " $linktype";
937
		}
938
		if ( isset( $attribs['class'] ) && $attribs['class'] ) {
939
			$class .= " {$attribs['class']}";
940
		}
941
		$attribs['class'] = $class;
942
943
		if ( $escape ) {
944
			$text = htmlspecialchars( $text );
945
		}
946
947
		if ( !$title ) {
948
			$title = $wgTitle;
949
		}
950
		$newRel = Parser::getExternalLinkRel( $url, $title );
951
		if ( !isset( $attribs['rel'] ) || $attribs['rel'] === '' ) {
952
			$attribs['rel'] = $newRel;
953
		} elseif ( $newRel !== '' ) {
954
			// Merge the rel attributes.
955
			$newRels = explode( ' ', $newRel );
956
			$oldRels = explode( ' ', $attribs['rel'] );
957
			$combined = array_unique( array_merge( $newRels, $oldRels ) );
958
			$attribs['rel'] = implode( ' ', $combined );
959
		}
960
		$link = '';
961
		$success = Hooks::run( 'LinkerMakeExternalLink',
962
			[ &$url, &$text, &$link, &$attribs, $linktype ] );
963
		if ( !$success ) {
964
			wfDebug( "Hook LinkerMakeExternalLink changed the output of link "
965
				. "with url {$url} and text {$text} to {$link}\n", true );
966
			return $link;
967
		}
968
		$attribs['href'] = $url;
969
		return Html::rawElement( 'a', $attribs, $text );
970
	}
971
972
	/**
973
	 * Make user link (or user contributions for unregistered users)
974
	 * @param int $userId User id in database.
975
	 * @param string $userName User name in database.
976
	 * @param string $altUserName Text to display instead of the user name (optional)
977
	 * @return string HTML fragment
978
	 * @since 1.16.3. $altUserName was added in 1.19.
979
	 */
980
	public static function userLink( $userId, $userName, $altUserName = false ) {
981
		$classes = 'mw-userlink';
982
		if ( $userId == 0 ) {
983
			$page = SpecialPage::getTitleFor( 'Contributions', $userName );
984
			if ( $altUserName === false ) {
985
				$altUserName = IP::prettifyIP( $userName );
986
			}
987
			$classes .= ' mw-anonuserlink'; // Separate link class for anons (bug 43179)
988
		} else {
989
			$page = Title::makeTitle( NS_USER, $userName );
990
		}
991
992
		return self::link(
993
			$page,
994
			htmlspecialchars( $altUserName !== false ? $altUserName : $userName ),
995
			[ 'class' => $classes ]
996
		);
997
	}
998
999
	/**
1000
	 * Generate standard user tool links (talk, contributions, block link, etc.)
1001
	 *
1002
	 * @since 1.16.3
1003
	 * @param int $userId User identifier
1004
	 * @param string $userText User name or IP address
1005
	 * @param bool $redContribsWhenNoEdits Should the contributions link be
1006
	 *   red if the user has no edits?
1007
	 * @param int $flags Customisation flags (e.g. Linker::TOOL_LINKS_NOBLOCK
1008
	 *   and Linker::TOOL_LINKS_EMAIL).
1009
	 * @param int $edits User edit count (optional, for performance)
1010
	 * @return string HTML fragment
1011
	 */
1012
	public static function userToolLinks(
1013
		$userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits = null
1014
	) {
1015
		global $wgUser, $wgDisableAnonTalk, $wgLang;
1016
		$talkable = !( $wgDisableAnonTalk && 0 == $userId );
1017
		$blockable = !( $flags & self::TOOL_LINKS_NOBLOCK );
1018
		$addEmailLink = $flags & self::TOOL_LINKS_EMAIL && $userId;
1019
1020
		$items = [];
1021
		if ( $talkable ) {
1022
			$items[] = self::userTalkLink( $userId, $userText );
1023
		}
1024
		if ( $userId ) {
1025
			// check if the user has an edit
1026
			$attribs = [];
1027
			if ( $redContribsWhenNoEdits ) {
1028
				if ( intval( $edits ) === 0 && $edits !== 0 ) {
1029
					$user = User::newFromId( $userId );
1030
					$edits = $user->getEditCount();
1031
				}
1032
				if ( $edits === 0 ) {
1033
					$attribs['class'] = 'new';
1034
				}
1035
			}
1036
			$contribsPage = SpecialPage::getTitleFor( 'Contributions', $userText );
1037
1038
			$items[] = self::link( $contribsPage, wfMessage( 'contribslink' )->escaped(), $attribs );
1039
		}
1040
		if ( $blockable && $wgUser->isAllowed( 'block' ) ) {
1041
			$items[] = self::blockLink( $userId, $userText );
1042
		}
1043
1044
		if ( $addEmailLink && $wgUser->canSendEmail() ) {
1045
			$items[] = self::emailLink( $userId, $userText );
1046
		}
1047
1048
		Hooks::run( 'UserToolLinksEdit', [ $userId, $userText, &$items ] );
1049
1050
		if ( $items ) {
1051
			return wfMessage( 'word-separator' )->escaped()
1052
				. '<span class="mw-usertoollinks">'
1053
				. wfMessage( 'parentheses' )->rawParams( $wgLang->pipeList( $items ) )->escaped()
1054
				. '</span>';
1055
		} else {
1056
			return '';
1057
		}
1058
	}
1059
1060
	/**
1061
	 * Alias for userToolLinks( $userId, $userText, true );
1062
	 * @since 1.16.3
1063
	 * @param int $userId User identifier
1064
	 * @param string $userText User name or IP address
1065
	 * @param int $edits User edit count (optional, for performance)
1066
	 * @return string
1067
	 */
1068
	public static function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
1069
		return self::userToolLinks( $userId, $userText, true, 0, $edits );
1070
	}
1071
1072
	/**
1073
	 * @since 1.16.3
1074
	 * @param int $userId User id in database.
1075
	 * @param string $userText User name in database.
1076
	 * @return string HTML fragment with user talk link
1077
	 */
1078
	public static function userTalkLink( $userId, $userText ) {
1079
		$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
1080
		$userTalkLink = self::link( $userTalkPage, wfMessage( 'talkpagelinktext' )->escaped() );
1081
		return $userTalkLink;
1082
	}
1083
1084
	/**
1085
	 * @since 1.16.3
1086
	 * @param int $userId Userid
1087
	 * @param string $userText User name in database.
1088
	 * @return string HTML fragment with block link
1089
	 */
1090
	public static function blockLink( $userId, $userText ) {
1091
		$blockPage = SpecialPage::getTitleFor( 'Block', $userText );
1092
		$blockLink = self::link( $blockPage, wfMessage( 'blocklink' )->escaped() );
1093
		return $blockLink;
1094
	}
1095
1096
	/**
1097
	 * @param int $userId Userid
1098
	 * @param string $userText User name in database.
1099
	 * @return string HTML fragment with e-mail user link
1100
	 */
1101
	public static function emailLink( $userId, $userText ) {
1102
		$emailPage = SpecialPage::getTitleFor( 'Emailuser', $userText );
1103
		$emailLink = self::link( $emailPage, wfMessage( 'emaillink' )->escaped() );
1104
		return $emailLink;
1105
	}
1106
1107
	/**
1108
	 * Generate a user link if the current user is allowed to view it
1109
	 * @since 1.16.3
1110
	 * @param Revision $rev
1111
	 * @param bool $isPublic Show only if all users can see it
1112
	 * @return string HTML fragment
1113
	 */
1114
	public static function revUserLink( $rev, $isPublic = false ) {
1115
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1116
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1117
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1118
			$link = self::userLink( $rev->getUser( Revision::FOR_THIS_USER ),
1119
				$rev->getUserText( Revision::FOR_THIS_USER ) );
1120
		} else {
1121
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1122
		}
1123
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1124
			return '<span class="history-deleted">' . $link . '</span>';
1125
		}
1126
		return $link;
1127
	}
1128
1129
	/**
1130
	 * Generate a user tool link cluster if the current user is allowed to view it
1131
	 * @since 1.16.3
1132
	 * @param Revision $rev
1133
	 * @param bool $isPublic Show only if all users can see it
1134
	 * @return string HTML
1135
	 */
1136
	public static function revUserTools( $rev, $isPublic = false ) {
1137
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1138
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1139
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1140
			$userId = $rev->getUser( Revision::FOR_THIS_USER );
1141
			$userText = $rev->getUserText( Revision::FOR_THIS_USER );
1142
			$link = self::userLink( $userId, $userText )
1143
				. self::userToolLinks( $userId, $userText );
1144
		} else {
1145
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1146
		}
1147
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1148
			return ' <span class="history-deleted">' . $link . '</span>';
1149
		}
1150
		return $link;
1151
	}
1152
1153
	/**
1154
	 * This function is called by all recent changes variants, by the page history,
1155
	 * and by the user contributions list. It is responsible for formatting edit
1156
	 * summaries. It escapes any HTML in the summary, but adds some CSS to format
1157
	 * auto-generated comments (from section editing) and formats [[wikilinks]].
1158
	 *
1159
	 * @author Erik Moeller <[email protected]>
1160
	 * @since 1.16.3. $wikiId added in 1.26
1161
	 *
1162
	 * Note: there's not always a title to pass to this function.
1163
	 * Since you can't set a default parameter for a reference, I've turned it
1164
	 * temporarily to a value pass. Should be adjusted further. --brion
1165
	 *
1166
	 * @param string $comment
1167
	 * @param Title|null $title Title object (to generate link to the section in autocomment)
1168
	 *  or null
1169
	 * @param bool $local Whether section links should refer to local page
1170
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1171
	 *  For use with external changes.
1172
	 *
1173
	 * @return mixed|string
1174
	 */
1175
	public static function formatComment(
1176
		$comment, $title = null, $local = false, $wikiId = null
1177
	) {
1178
		# Sanitize text a bit:
1179
		$comment = str_replace( "\n", " ", $comment );
1180
		# Allow HTML entities (for bug 13815)
1181
		$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
1182
1183
		# Render autocomments and make links:
1184
		$comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
1185
		$comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
1186
1187
		return $comment;
1188
	}
1189
1190
	/**
1191
	 * Converts autogenerated comments in edit summaries into section links.
1192
	 *
1193
	 * The pattern for autogen comments is / * foo * /, which makes for
1194
	 * some nasty regex.
1195
	 * We look for all comments, match any text before and after the comment,
1196
	 * add a separator where needed and format the comment itself with CSS
1197
	 * Called by Linker::formatComment.
1198
	 *
1199
	 * @param string $comment Comment text
1200
	 * @param Title|null $title An optional title object used to links to sections
1201
	 * @param bool $local Whether section links should refer to local page
1202
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1203
	 *  as used by WikiMap.
1204
	 *
1205
	 * @return string Formatted comment (wikitext)
1206
	 */
1207
	private static function formatAutocomments(
1208
		$comment, $title = null, $local = false, $wikiId = null
1209
	) {
1210
		// @todo $append here is something of a hack to preserve the status
1211
		// quo. Someone who knows more about bidi and such should decide
1212
		// (1) what sane rendering even *is* for an LTR edit summary on an RTL
1213
		// wiki, both when autocomments exist and when they don't, and
1214
		// (2) what markup will make that actually happen.
1215
		$append = '';
1216
		$comment = preg_replace_callback(
1217
			// To detect the presence of content before or after the
1218
			// auto-comment, we use capturing groups inside optional zero-width
1219
			// assertions. But older versions of PCRE can't directly make
1220
			// zero-width assertions optional, so wrap them in a non-capturing
1221
			// group.
1222
			'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
1223
			function ( $match ) use ( $title, $local, $wikiId, &$append ) {
1224
				global $wgLang;
1225
1226
				// Ensure all match positions are defined
1227
				$match += [ '', '', '', '' ];
1228
1229
				$pre = $match[1] !== '';
1230
				$auto = $match[2];
1231
				$post = $match[3] !== '';
1232
				$comment = null;
1233
1234
				Hooks::run(
1235
					'FormatAutocomments',
1236
					[ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
1237
				);
1238
1239
				if ( $comment === null ) {
1240
					$link = '';
1241
					if ( $title ) {
1242
						$section = $auto;
1243
						# Remove links that a user may have manually put in the autosummary
1244
						# This could be improved by copying as much of Parser::stripSectionName as desired.
1245
						$section = str_replace( '[[:', '', $section );
1246
						$section = str_replace( '[[', '', $section );
1247
						$section = str_replace( ']]', '', $section );
1248
1249
						$section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
1250
						if ( $local ) {
1251
							$sectionTitle = Title::newFromText( '#' . $section );
1252
						} else {
1253
							$sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
1254
								$title->getDBkey(), $section );
1255
						}
1256
						if ( $sectionTitle ) {
1257
							$link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
1258
						} else {
1259
							$link = '';
1260
						}
1261
					}
1262
					if ( $pre ) {
1263
						# written summary $presep autocomment (summary /* section */)
1264
						$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
1265
					}
1266
					if ( $post ) {
1267
						# autocomment $postsep written summary (/* section */ summary)
1268
						$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
1269
					}
1270
					$auto = '<span class="autocomment">' . $auto . '</span>';
1271
					$comment = $pre . $link . $wgLang->getDirMark()
1272
						. '<span dir="auto">' . $auto;
1273
					$append .= '</span>';
1274
				}
1275
				return $comment;
1276
			},
1277
			$comment
1278
		);
1279
		return $comment . $append;
1280
	}
1281
1282
	/**
1283
	 * Formats wiki links and media links in text; all other wiki formatting
1284
	 * is ignored
1285
	 *
1286
	 * @since 1.16.3. $wikiId added in 1.26
1287
	 * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
1288
	 *
1289
	 * @param string $comment Text to format links in. WARNING! Since the output of this
1290
	 *	function is html, $comment must be sanitized for use as html. You probably want
1291
	 *	to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
1292
	 *	this function.
1293
	 * @param Title|null $title An optional title object used to links to sections
1294
	 * @param bool $local Whether section links should refer to local page
1295
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1296
	 *  as used by WikiMap.
1297
	 *
1298
	 * @return string
1299
	 */
1300
	public static function formatLinksInComment(
1301
		$comment, $title = null, $local = false, $wikiId = null
1302
	) {
1303
		return preg_replace_callback(
1304
			'/
1305
				\[\[
1306
				:? # ignore optional leading colon
1307
				([^\]|]+) # 1. link target; page names cannot include ] or |
1308
				(?:\|
1309
					# 2. a pipe-separated substring; only the last is captured
1310
					# Stop matching at | and ]] without relying on backtracking.
1311
					((?:]?[^\]|])*+)
1312
				)*
1313
				\]\]
1314
				([^[]*) # 3. link trail (the text up until the next link)
1315
			/x',
1316
			function ( $match ) use ( $title, $local, $wikiId ) {
1317
				global $wgContLang;
1318
1319
				$medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
1320
				$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
1321
1322
				$comment = $match[0];
1323
1324
				# fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
1325
				if ( strpos( $match[1], '%' ) !== false ) {
1326
					$match[1] = strtr(
1327
						rawurldecode( $match[1] ),
1328
						[ '<' => '&lt;', '>' => '&gt;' ]
1329
					);
1330
				}
1331
1332
				# Handle link renaming [[foo|text]] will show link as "text"
1333
				if ( $match[2] != "" ) {
1334
					$text = $match[2];
1335
				} else {
1336
					$text = $match[1];
1337
				}
1338
				$submatch = [];
1339
				$thelink = null;
1340
				if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
1341
					# Media link; trail not supported.
1342
					$linkRegexp = '/\[\[(.*?)\]\]/';
1343
					$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...
1344
					if ( $title ) {
1345
						$thelink = Linker::makeMediaLinkObj( $title, $text );
1346
					}
1347
				} else {
1348
					# Other kind of link
1349
					# Make sure its target is non-empty
1350
					if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
1351
						$match[1] = substr( $match[1], 1 );
1352
					}
1353
					if ( $match[1] !== false && $match[1] !== '' ) {
1354
						if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
1355
							$trail = $submatch[1];
1356
						} else {
1357
							$trail = "";
1358
						}
1359
						$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
1360
						list( $inside, $trail ) = Linker::splitTrail( $trail );
1361
1362
						$linkText = $text;
1363
						$linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 1301 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...
1364
1365
						$target = Title::newFromText( $linkTarget );
1366
						if ( $target ) {
1367
							if ( $target->getText() == '' && !$target->isExternal()
1368
								&& !$local && $title
1369
							) {
1370
								$newTarget = clone $title;
1371
								$newTarget->setFragment( '#' . $target->getFragment() );
1372
								$target = $newTarget;
1373
							}
1374
1375
							$thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
1376
						}
1377
					}
1378
				}
1379
				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...
1380
					// If the link is still valid, go ahead and replace it in!
1381
					$comment = preg_replace(
1382
						$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...
1383
						StringUtils::escapeRegexReplacement( $thelink ),
1384
						$comment,
1385
						1
1386
					);
1387
				}
1388
1389
				return $comment;
1390
			},
1391
			$comment
1392
		);
1393
	}
1394
1395
	/**
1396
	 * Generates a link to the given Title
1397
	 *
1398
	 * @note This is only public for technical reasons. It's not intended for use outside Linker.
1399
	 *
1400
	 * @param Title $title
1401
	 * @param string $text
1402
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1403
	 *  as used by WikiMap.
1404
	 * @param string|string[] $options See the $options parameter in Linker::link.
1405
	 *
1406
	 * @return string HTML link
1407
	 */
1408
	public static function makeCommentLink(
1409
		Title $title, $text, $wikiId = null, $options = []
1410
	) {
1411
		if ( $wikiId !== null && !$title->isExternal() ) {
1412
			$link = Linker::makeExternalLink(
1413
				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...
1414
					$wikiId,
1415
					$title->getPrefixedText(),
1416
					$title->getFragment()
1417
				),
1418
				$text,
1419
				/* escape = */ false // Already escaped
1420
			);
1421
		} else {
1422
			$link = Linker::link( $title, $text, [], [], $options );
1423
		}
1424
1425
		return $link;
1426
	}
1427
1428
	/**
1429
	 * @param Title $contextTitle
1430
	 * @param string $target
1431
	 * @param string $text
1432
	 * @return string
1433
	 */
1434
	public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1435
		# Valid link forms:
1436
		# Foobar -- normal
1437
		# :Foobar -- override special treatment of prefix (images, language links)
1438
		# /Foobar -- convert to CurrentPage/Foobar
1439
		# /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1440
		# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1441
		# ../Foobar -- convert to CurrentPage/Foobar,
1442
		#              (from CurrentPage/CurrentSubPage)
1443
		# ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1444
		#              (from CurrentPage/CurrentSubPage)
1445
1446
		$ret = $target; # default return value is no change
1447
1448
		# Some namespaces don't allow subpages,
1449
		# so only perform processing if subpages are allowed
1450
		if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
1451
			$hash = strpos( $target, '#' );
1452
			if ( $hash !== false ) {
1453
				$suffix = substr( $target, $hash );
1454
				$target = substr( $target, 0, $hash );
1455
			} else {
1456
				$suffix = '';
1457
			}
1458
			# bug 7425
1459
			$target = trim( $target );
1460
			# Look at the first character
1461
			if ( $target != '' && $target[0] === '/' ) {
1462
				# / at end means we don't want the slash to be shown
1463
				$m = [];
1464
				$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1465 View Code Duplication
				if ( $trailingSlashes ) {
1466
					$noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1467
				} else {
1468
					$noslash = substr( $target, 1 );
1469
				}
1470
1471
				$ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
1472
				if ( $text === '' ) {
1473
					$text = $target . $suffix;
1474
				} # this might be changed for ugliness reasons
1475
			} else {
1476
				# check for .. subpage backlinks
1477
				$dotdotcount = 0;
1478
				$nodotdot = $target;
1479
				while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
1480
					++$dotdotcount;
1481
					$nodotdot = substr( $nodotdot, 3 );
1482
				}
1483
				if ( $dotdotcount > 0 ) {
1484
					$exploded = explode( '/', $contextTitle->getPrefixedText() );
1485
					if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1486
						$ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1487
						# / at the end means don't show full path
1488
						if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1489
							$nodotdot = rtrim( $nodotdot, '/' );
1490
							if ( $text === '' ) {
1491
								$text = $nodotdot . $suffix;
1492
							}
1493
						}
1494
						$nodotdot = trim( $nodotdot );
1495
						if ( $nodotdot != '' ) {
1496
							$ret .= '/' . $nodotdot;
1497
						}
1498
						$ret .= $suffix;
1499
					}
1500
				}
1501
			}
1502
		}
1503
1504
		return $ret;
1505
	}
1506
1507
	/**
1508
	 * Wrap a comment in standard punctuation and formatting if
1509
	 * it's non-empty, otherwise return empty string.
1510
	 *
1511
	 * @since 1.16.3. $wikiId added in 1.26
1512
	 * @param string $comment
1513
	 * @param Title|null $title Title object (to generate link to section in autocomment) or null
1514
	 * @param bool $local Whether section links should refer to local page
1515
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1516
	 *  For use with external changes.
1517
	 *
1518
	 * @return string
1519
	 */
1520
	public static function commentBlock(
1521
		$comment, $title = null, $local = false, $wikiId = null
1522
	) {
1523
		// '*' used to be the comment inserted by the software way back
1524
		// in antiquity in case none was provided, here for backwards
1525
		// compatibility, acc. to brion -ævar
1526
		if ( $comment == '' || $comment == '*' ) {
1527
			return '';
1528
		} else {
1529
			$formatted = self::formatComment( $comment, $title, $local, $wikiId );
1530
			$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
1531
			return " <span class=\"comment\">$formatted</span>";
1532
		}
1533
	}
1534
1535
	/**
1536
	 * Wrap and format the given revision's comment block, if the current
1537
	 * user is allowed to view it.
1538
	 *
1539
	 * @since 1.16.3
1540
	 * @param Revision $rev
1541
	 * @param bool $local Whether section links should refer to local page
1542
	 * @param bool $isPublic Show only if all users can see it
1543
	 * @return string HTML fragment
1544
	 */
1545
	public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
1546
		if ( $rev->getComment( Revision::RAW ) == "" ) {
1547
			return "";
1548
		}
1549
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
1550
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1551
		} elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
1552
			$block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
1553
				$rev->getTitle(), $local );
1554
		} else {
1555
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1556
		}
1557
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
1558
			return " <span class=\"history-deleted\">$block</span>";
1559
		}
1560
		return $block;
1561
	}
1562
1563
	/**
1564
	 * @since 1.16.3
1565
	 * @param int $size
1566
	 * @return string
1567
	 */
1568
	public static function formatRevisionSize( $size ) {
1569
		if ( $size == 0 ) {
1570
			$stxt = wfMessage( 'historyempty' )->escaped();
1571
		} else {
1572
			$stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1573
			$stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
1574
		}
1575
		return "<span class=\"history-size\">$stxt</span>";
1576
	}
1577
1578
	/**
1579
	 * Add another level to the Table of Contents
1580
	 *
1581
	 * @since 1.16.3
1582
	 * @return string
1583
	 */
1584
	public static function tocIndent() {
1585
		return "\n<ul>";
1586
	}
1587
1588
	/**
1589
	 * Finish one or more sublevels on the Table of Contents
1590
	 *
1591
	 * @since 1.16.3
1592
	 * @param int $level
1593
	 * @return string
1594
	 */
1595
	public static function tocUnindent( $level ) {
1596
		return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1597
	}
1598
1599
	/**
1600
	 * parameter level defines if we are on an indentation level
1601
	 *
1602
	 * @since 1.16.3
1603
	 * @param string $anchor
1604
	 * @param string $tocline
1605
	 * @param string $tocnumber
1606
	 * @param string $level
1607
	 * @param string|bool $sectionIndex
1608
	 * @return string
1609
	 */
1610
	public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1611
		$classes = "toclevel-$level";
1612
		if ( $sectionIndex !== false ) {
1613
			$classes .= " tocsection-$sectionIndex";
1614
		}
1615
		return "\n<li class=\"$classes\"><a href=\"#" .
1616
			$anchor . '"><span class="tocnumber">' .
1617
			$tocnumber . '</span> <span class="toctext">' .
1618
			$tocline . '</span></a>';
1619
	}
1620
1621
	/**
1622
	 * End a Table Of Contents line.
1623
	 * tocUnindent() will be used instead if we're ending a line below
1624
	 * the new level.
1625
	 * @since 1.16.3
1626
	 * @return string
1627
	 */
1628
	public static function tocLineEnd() {
1629
		return "</li>\n";
1630
	}
1631
1632
	/**
1633
	 * Wraps the TOC in a table and provides the hide/collapse javascript.
1634
	 *
1635
	 * @since 1.16.3
1636
	 * @param string $toc Html of the Table Of Contents
1637
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1638
	 * @return string Full html of the TOC
1639
	 */
1640
	public static function tocList( $toc, $lang = false ) {
1641
		$lang = wfGetLangObj( $lang );
1642
		$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1643
1644
		return '<div id="toc" class="toc">'
1645
			. '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
1646
			. $toc
1647
			. "</ul>\n</div>\n";
1648
	}
1649
1650
	/**
1651
	 * Generate a table of contents from a section tree.
1652
	 *
1653
	 * @since 1.16.3. $lang added in 1.17
1654
	 * @param array $tree Return value of ParserOutput::getSections()
1655
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1656
	 * @return string HTML fragment
1657
	 */
1658
	public static function generateTOC( $tree, $lang = false ) {
1659
		$toc = '';
1660
		$lastLevel = 0;
1661
		foreach ( $tree as $section ) {
1662
			if ( $section['toclevel'] > $lastLevel ) {
1663
				$toc .= self::tocIndent();
1664
			} elseif ( $section['toclevel'] < $lastLevel ) {
1665
				$toc .= self::tocUnindent(
1666
					$lastLevel - $section['toclevel'] );
1667
			} else {
1668
				$toc .= self::tocLineEnd();
1669
			}
1670
1671
			$toc .= self::tocLine( $section['anchor'],
1672
				$section['line'], $section['number'],
1673
				$section['toclevel'], $section['index'] );
1674
			$lastLevel = $section['toclevel'];
1675
		}
1676
		$toc .= self::tocLineEnd();
1677
		return self::tocList( $toc, $lang );
1678
	}
1679
1680
	/**
1681
	 * Create a headline for content
1682
	 *
1683
	 * @since 1.16.3
1684
	 * @param int $level The level of the headline (1-6)
1685
	 * @param string $attribs Any attributes for the headline, starting with
1686
	 *   a space and ending with '>'
1687
	 *   This *must* be at least '>' for no attribs
1688
	 * @param string $anchor The anchor to give the headline (the bit after the #)
1689
	 * @param string $html Html for the text of the header
1690
	 * @param string $link HTML to add for the section edit link
1691
	 * @param bool|string $legacyAnchor A second, optional anchor to give for
1692
	 *   backward compatibility (false to omit)
1693
	 *
1694
	 * @return string HTML headline
1695
	 */
1696
	public static function makeHeadline( $level, $attribs, $anchor, $html,
1697
		$link, $legacyAnchor = false
1698
	) {
1699
		$ret = "<h$level$attribs"
1700
			. "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
1701
			. $link
1702
			. "</h$level>";
1703
		if ( $legacyAnchor !== false ) {
1704
			$ret = "<div id=\"$legacyAnchor\"></div>$ret";
1705
		}
1706
		return $ret;
1707
	}
1708
1709
	/**
1710
	 * Split a link trail, return the "inside" portion and the remainder of the trail
1711
	 * as a two-element array
1712
	 * @param string $trail
1713
	 * @return array
1714
	 */
1715
	static function splitTrail( $trail ) {
1716
		global $wgContLang;
1717
		$regex = $wgContLang->linkTrail();
1718
		$inside = '';
1719
		if ( $trail !== '' ) {
1720
			$m = [];
1721
			if ( preg_match( $regex, $trail, $m ) ) {
1722
				$inside = $m[1];
1723
				$trail = $m[2];
1724
			}
1725
		}
1726
		return [ $inside, $trail ];
1727
	}
1728
1729
	/**
1730
	 * Generate a rollback link for a given revision.  Currently it's the
1731
	 * caller's responsibility to ensure that the revision is the top one. If
1732
	 * it's not, of course, the user will get an error message.
1733
	 *
1734
	 * If the calling page is called with the parameter &bot=1, all rollback
1735
	 * links also get that parameter. It causes the edit itself and the rollback
1736
	 * to be marked as "bot" edits. Bot edits are hidden by default from recent
1737
	 * changes, so this allows sysops to combat a busy vandal without bothering
1738
	 * other users.
1739
	 *
1740
	 * If the option verify is set this function will return the link only in case the
1741
	 * revision can be reverted. Please note that due to performance limitations
1742
	 * it might be assumed that a user isn't the only contributor of a page while
1743
	 * (s)he is, which will lead to useless rollback links. Furthermore this wont
1744
	 * work if $wgShowRollbackEditCount is disabled, so this can only function
1745
	 * as an additional check.
1746
	 *
1747
	 * If the option noBrackets is set the rollback link wont be enclosed in "[]".
1748
	 *
1749
	 * See the "mediawiki.page.rollback" module for the client-side handling of this link.
1750
	 *
1751
	 * @since 1.16.3. $context added in 1.20. $options added in 1.21
1752
	 *
1753
	 * @param Revision $rev
1754
	 * @param IContextSource $context Context to use or null for the main context.
1755
	 * @param array $options
1756
	 * @return string
1757
	 */
1758
	public static function generateRollback( $rev, IContextSource $context = null,
1759
		$options = [ 'verify' ]
1760
	) {
1761
		if ( $context === null ) {
1762
			$context = RequestContext::getMain();
1763
		}
1764
1765
		$editCount = false;
1766
		if ( in_array( 'verify', $options, true ) ) {
1767
			$editCount = self::getRollbackEditCount( $rev, true );
1768
			if ( $editCount === false ) {
1769
				return '';
1770
			}
1771
		}
1772
1773
		$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 1767 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...
1774
1775
		if ( !in_array( 'noBrackets', $options, true ) ) {
1776
			$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1777
		}
1778
1779
		$context->getOutput()->addModules( 'mediawiki.page.rollback' );
1780
1781
		return '<span class="mw-rollback-link">' . $inner . '</span>';
1782
	}
1783
1784
	/**
1785
	 * This function will return the number of revisions which a rollback
1786
	 * would revert and, if $verify is set it will verify that a revision
1787
	 * can be reverted (that the user isn't the only contributor and the
1788
	 * revision we might rollback to isn't deleted). These checks can only
1789
	 * function as an additional check as this function only checks against
1790
	 * the last $wgShowRollbackEditCount edits.
1791
	 *
1792
	 * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
1793
	 * is set and the user is the only contributor of the page.
1794
	 *
1795
	 * @param Revision $rev
1796
	 * @param bool $verify Try to verify that this revision can really be rolled back
1797
	 * @return int|bool|null
1798
	 */
1799
	public static function getRollbackEditCount( $rev, $verify ) {
1800
		global $wgShowRollbackEditCount;
1801
		if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
1802
			// Nothing has happened, indicate this by returning 'null'
1803
			return null;
1804
		}
1805
1806
		$dbr = wfGetDB( DB_SLAVE );
1807
1808
		// Up to the value of $wgShowRollbackEditCount revisions are counted
1809
		$res = $dbr->select(
1810
			'revision',
1811
			[ 'rev_user_text', 'rev_deleted' ],
1812
			// $rev->getPage() returns null sometimes
1813
			[ 'rev_page' => $rev->getTitle()->getArticleID() ],
1814
			__METHOD__,
1815
			[
1816
				'USE INDEX' => [ 'revision' => 'page_timestamp' ],
1817
				'ORDER BY' => 'rev_timestamp DESC',
1818
				'LIMIT' => $wgShowRollbackEditCount + 1
1819
			]
1820
		);
1821
1822
		$editCount = 0;
1823
		$moreRevs = false;
1824
		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...
1825
			if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) {
1826
				if ( $verify &&
1827
					( $row->rev_deleted & Revision::DELETED_TEXT
1828
						|| $row->rev_deleted & Revision::DELETED_USER
1829
				) ) {
1830
					// If the user or the text of the revision we might rollback
1831
					// to is deleted in some way we can't rollback. Similar to
1832
					// the sanity checks in WikiPage::commitRollback.
1833
					return false;
1834
				}
1835
				$moreRevs = true;
1836
				break;
1837
			}
1838
			$editCount++;
1839
		}
1840
1841
		if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
1842
			// We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1843
			// and there weren't any other revisions. That means that the current user is the only
1844
			// editor, so we can't rollback
1845
			return false;
1846
		}
1847
		return $editCount;
1848
	}
1849
1850
	/**
1851
	 * Build a raw rollback link, useful for collections of "tool" links
1852
	 *
1853
	 * @since 1.16.3. $context added in 1.20. $editCount added in 1.21
1854
	 * @param Revision $rev
1855
	 * @param IContextSource|null $context Context to use or null for the main context.
1856
	 * @param int $editCount Number of edits that would be reverted
1857
	 * @return string HTML fragment
1858
	 */
1859
	public static function buildRollbackLink( $rev, IContextSource $context = null,
1860
		$editCount = false
1861
	) {
1862
		global $wgShowRollbackEditCount, $wgMiserMode;
1863
1864
		// To config which pages are affected by miser mode
1865
		$disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1866
1867
		if ( $context === null ) {
1868
			$context = RequestContext::getMain();
1869
		}
1870
1871
		$title = $rev->getTitle();
1872
		$query = [
1873
			'action' => 'rollback',
1874
			'from' => $rev->getUserText(),
1875
		];
1876
		$attrs = [
1877
			'data-mw' => 'interface',
1878
			'title' => $context->msg( 'tooltip-rollback' )->text(),
1879
		];
1880
		$options = [ 'known', 'noclasses' ];
1881
1882
		if ( $context->getRequest()->getBool( 'bot' ) ) {
1883
			$query['bot'] = '1';
1884
			$query['hidediff'] = '1'; // bug 15999
1885
		}
1886
1887
		$disableRollbackEditCount = false;
1888
		if ( $wgMiserMode ) {
1889
			foreach ( $disableRollbackEditCountSpecialPage as $specialPage ) {
1890
				if ( $context->getTitle()->isSpecial( $specialPage ) ) {
1891
					$disableRollbackEditCount = true;
1892
					break;
1893
				}
1894
			}
1895
		}
1896
1897
		if ( !$disableRollbackEditCount
1898
			&& is_int( $wgShowRollbackEditCount )
1899
			&& $wgShowRollbackEditCount > 0
1900
		) {
1901
			if ( !is_numeric( $editCount ) ) {
1902
				$editCount = self::getRollbackEditCount( $rev, false );
1903
			}
1904
1905
			if ( $editCount > $wgShowRollbackEditCount ) {
1906
				$html = $context->msg( 'rollbacklinkcount-morethan' )
1907
					->numParams( $wgShowRollbackEditCount )->parse();
1908
			} else {
1909
				$html = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
1910
			}
1911
1912
			return self::link( $title, $html, $attrs, $query, $options );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 1871 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...
1913
		} else {
1914
			$html = $context->msg( 'rollbacklink' )->escaped();
1915
			return self::link( $title, $html, $attrs, $query, $options );
0 ignored issues
show
Bug introduced by
It seems like $title defined by $rev->getTitle() on line 1871 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...
1916
		}
1917
	}
1918
1919
	/**
1920
	 * Returns HTML for the "templates used on this page" list.
1921
	 *
1922
	 * Make an HTML list of templates, and then add a "More..." link at
1923
	 * the bottom. If $more is null, do not add a "More..." link. If $more
1924
	 * is a Title, make a link to that title and use it. If $more is a string,
1925
	 * directly paste it in as the link (escaping needs to be done manually).
1926
	 * Finally, if $more is a Message, call toString().
1927
	 *
1928
	 * @since 1.16.3. $more added in 1.21
1929
	 * @param Title[] $templates Array of templates
1930
	 * @param bool $preview Whether this is for a preview
1931
	 * @param bool $section Whether this is for a section edit
1932
	 * @param Title|Message|string|null $more An escaped link for "More..." of the templates
1933
	 * @return string HTML output
1934
	 */
1935
	public static function formatTemplates( $templates, $preview = false,
1936
		$section = false, $more = null
1937
	) {
1938
		global $wgLang;
1939
1940
		$outText = '';
1941
		if ( count( $templates ) > 0 ) {
1942
			# Do a batch existence check
1943
			$batch = new LinkBatch;
1944
			foreach ( $templates as $title ) {
1945
				$batch->addObj( $title );
1946
			}
1947
			$batch->execute();
1948
1949
			# Construct the HTML
1950
			$outText = '<div class="mw-templatesUsedExplanation">';
1951
			if ( $preview ) {
1952
				$outText .= wfMessage( 'templatesusedpreview' )->numParams( count( $templates ) )
1953
					->parseAsBlock();
1954
			} elseif ( $section ) {
1955
				$outText .= wfMessage( 'templatesusedsection' )->numParams( count( $templates ) )
1956
					->parseAsBlock();
1957
			} else {
1958
				$outText .= wfMessage( 'templatesused' )->numParams( count( $templates ) )
1959
					->parseAsBlock();
1960
			}
1961
			$outText .= "</div><ul>\n";
1962
1963
			usort( $templates, 'Title::compare' );
1964
			foreach ( $templates as $titleObj ) {
1965
				$protected = '';
1966
				$restrictions = $titleObj->getRestrictions( 'edit' );
1967
				if ( $restrictions ) {
1968
					// Check backwards-compatible messages
1969
					$msg = null;
1970
					if ( $restrictions === [ 'sysop' ] ) {
1971
						$msg = wfMessage( 'template-protected' );
1972
					} elseif ( $restrictions === [ 'autoconfirmed' ] ) {
1973
						$msg = wfMessage( 'template-semiprotected' );
1974
					}
1975
					if ( $msg && !$msg->isDisabled() ) {
1976
						$protected = $msg->parse();
1977
					} else {
1978
						// Construct the message from restriction-level-*
1979
						// e.g. restriction-level-sysop, restriction-level-autoconfirmed
1980
						$msgs = [];
1981
						foreach ( $restrictions as $r ) {
1982
							$msgs[] = wfMessage( "restriction-level-$r" )->parse();
1983
						}
1984
						$protected = wfMessage( 'parentheses' )
1985
							->rawParams( $wgLang->commaList( $msgs ) )->escaped();
1986
					}
1987
				}
1988
				if ( $titleObj->quickUserCan( 'edit' ) ) {
1989
					$editLink = self::link(
1990
						$titleObj,
1991
						wfMessage( 'editlink' )->escaped(),
1992
						[],
1993
						[ 'action' => 'edit' ]
1994
					);
1995
				} else {
1996
					$editLink = self::link(
1997
						$titleObj,
1998
						wfMessage( 'viewsourcelink' )->escaped(),
1999
						[],
2000
						[ 'action' => 'edit' ]
2001
					);
2002
				}
2003
				$outText .= '<li>' . self::link( $titleObj )
2004
					. wfMessage( 'word-separator' )->escaped()
2005
					. wfMessage( 'parentheses' )->rawParams( $editLink )->escaped()
2006
					. wfMessage( 'word-separator' )->escaped()
2007
					. $protected . '</li>';
2008
			}
2009
2010
			if ( $more instanceof Title ) {
2011
				$outText .= '<li>' . self::link( $more, wfMessage( 'moredotdotdot' ) ) . '</li>';
2012
			} elseif ( $more ) {
2013
				$outText .= "<li>$more</li>";
2014
			}
2015
2016
			$outText .= '</ul>';
2017
		}
2018
		return $outText;
2019
	}
2020
2021
	/**
2022
	 * Returns HTML for the "hidden categories on this page" list.
2023
	 *
2024
	 * @since 1.16.3
2025
	 * @param array $hiddencats Array of hidden categories from Article::getHiddenCategories
2026
	 *   or similar
2027
	 * @return string HTML output
2028
	 */
2029
	public static function formatHiddenCategories( $hiddencats ) {
2030
2031
		$outText = '';
2032
		if ( count( $hiddencats ) > 0 ) {
2033
			# Construct the HTML
2034
			$outText = '<div class="mw-hiddenCategoriesExplanation">';
2035
			$outText .= wfMessage( 'hiddencategories' )->numParams( count( $hiddencats ) )->parseAsBlock();
2036
			$outText .= "</div><ul>\n";
2037
2038
			foreach ( $hiddencats as $titleObj ) {
2039
				# If it's hidden, it must exist - no need to check with a LinkBatch
2040
				$outText .= '<li>'
2041
					. self::link( $titleObj, null, [], [], 'known' )
2042
					. "</li>\n";
2043
			}
2044
			$outText .= '</ul>';
2045
		}
2046
		return $outText;
2047
	}
2048
2049
	/**
2050
	 * Format a size in bytes for output, using an appropriate
2051
	 * unit (B, KB, MB or GB) according to the magnitude in question
2052
	 *
2053
	 * @since 1.16.3
2054
	 * @param int $size Size to format
2055
	 * @return string
2056
	 */
2057
	public static function formatSize( $size ) {
2058
		global $wgLang;
2059
		return htmlspecialchars( $wgLang->formatSize( $size ) );
2060
	}
2061
2062
	/**
2063
	 * Given the id of an interface element, constructs the appropriate title
2064
	 * attribute from the system messages.  (Note, this is usually the id but
2065
	 * isn't always, because sometimes the accesskey needs to go on a different
2066
	 * element than the id, for reverse-compatibility, etc.)
2067
	 *
2068
	 * @since 1.16.3 $msgParams added in 1.27
2069
	 * @param string $name Id of the element, minus prefixes.
2070
	 * @param string|null $options Null or the string 'withaccess' to add an access-
2071
	 *   key hint
2072
	 * @param array $msgParams Parameters to pass to the message
2073
	 *
2074
	 * @return string Contents of the title attribute (which you must HTML-
2075
	 *   escape), or false for no title attribute
2076
	 */
2077
	public static function titleAttrib( $name, $options = null, array $msgParams = [] ) {
2078
		$message = wfMessage( "tooltip-$name", $msgParams );
2079
		if ( !$message->exists() ) {
2080
			$tooltip = false;
2081
		} else {
2082
			$tooltip = $message->text();
2083
			# Compatibility: formerly some tooltips had [alt-.] hardcoded
2084
			$tooltip = preg_replace( "/ ?\[alt-.\]$/", '', $tooltip );
2085
			# Message equal to '-' means suppress it.
2086
			if ( $tooltip == '-' ) {
2087
				$tooltip = false;
2088
			}
2089
		}
2090
2091
		if ( $options == 'withaccess' ) {
2092
			$accesskey = self::accesskey( $name );
2093
			if ( $accesskey !== false ) {
2094
				// Should be build the same as in jquery.accessKeyLabel.js
2095
				if ( $tooltip === false || $tooltip === '' ) {
2096
					$tooltip = wfMessage( 'brackets', $accesskey )->text();
2097
				} else {
2098
					$tooltip .= wfMessage( 'word-separator' )->text();
2099
					$tooltip .= wfMessage( 'brackets', $accesskey )->text();
2100
				}
2101
			}
2102
		}
2103
2104
		return $tooltip;
2105
	}
2106
2107
	public static $accesskeycache;
2108
2109
	/**
2110
	 * Given the id of an interface element, constructs the appropriate
2111
	 * accesskey attribute from the system messages.  (Note, this is usually
2112
	 * the id but isn't always, because sometimes the accesskey needs to go on
2113
	 * a different element than the id, for reverse-compatibility, etc.)
2114
	 *
2115
	 * @since 1.16.3
2116
	 * @param string $name Id of the element, minus prefixes.
2117
	 * @return string Contents of the accesskey attribute (which you must HTML-
2118
	 *   escape), or false for no accesskey attribute
2119
	 */
2120
	public static function accesskey( $name ) {
2121
		if ( isset( self::$accesskeycache[$name] ) ) {
2122
			return self::$accesskeycache[$name];
2123
		}
2124
2125
		$message = wfMessage( "accesskey-$name" );
2126
2127
		if ( !$message->exists() ) {
2128
			$accesskey = false;
2129
		} else {
2130
			$accesskey = $message->plain();
2131
			if ( $accesskey === '' || $accesskey === '-' ) {
2132
				# @todo FIXME: Per standard MW behavior, a value of '-' means to suppress the
2133
				# attribute, but this is broken for accesskey: that might be a useful
2134
				# value.
2135
				$accesskey = false;
2136
			}
2137
		}
2138
2139
		self::$accesskeycache[$name] = $accesskey;
2140
		return self::$accesskeycache[$name];
2141
	}
2142
2143
	/**
2144
	 * Get a revision-deletion link, or disabled link, or nothing, depending
2145
	 * on user permissions & the settings on the revision.
2146
	 *
2147
	 * Will use forward-compatible revision ID in the Special:RevDelete link
2148
	 * if possible, otherwise the timestamp-based ID which may break after
2149
	 * undeletion.
2150
	 *
2151
	 * @param User $user
2152
	 * @param Revision $rev
2153
	 * @param Title $title
2154
	 * @return string HTML fragment
2155
	 */
2156
	public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
2157
		$canHide = $user->isAllowed( 'deleterevision' );
2158
		if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
2159
			return '';
2160
		}
2161
2162
		if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
2163
			return Linker::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
2164
		} else {
2165
			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...
2166
				// RevDelete links using revision ID are stable across
2167
				// page deletion and undeletion; use when possible.
2168
				$query = [
2169
					'type' => 'revision',
2170
					'target' => $title->getPrefixedDBkey(),
2171
					'ids' => $rev->getId()
2172
				];
2173
			} else {
2174
				// Older deleted entries didn't save a revision ID.
2175
				// We have to refer to these by timestamp, ick!
2176
				$query = [
2177
					'type' => 'archive',
2178
					'target' => $title->getPrefixedDBkey(),
2179
					'ids' => $rev->getTimestamp()
2180
				];
2181
			}
2182
			return Linker::revDeleteLink( $query,
2183
				$rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
2184
		}
2185
	}
2186
2187
	/**
2188
	 * Creates a (show/hide) link for deleting revisions/log entries
2189
	 *
2190
	 * @param array $query Query parameters to be passed to link()
2191
	 * @param bool $restricted Set to true to use a "<strong>" instead of a "<span>"
2192
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2193
	 *
2194
	 * @return string HTML "<a>" link to Special:Revisiondelete, wrapped in a
2195
	 * span to allow for customization of appearance with CSS
2196
	 */
2197
	public static function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
2198
		$sp = SpecialPage::getTitleFor( 'Revisiondelete' );
2199
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2200
		$html = wfMessage( $msgKey )->escaped();
2201
		$tag = $restricted ? 'strong' : 'span';
2202
		$link = self::link( $sp, $html, [], $query, [ 'known', 'noclasses' ] );
2203
		return Xml::tags(
2204
			$tag,
2205
			[ 'class' => 'mw-revdelundel-link' ],
2206
			wfMessage( 'parentheses' )->rawParams( $link )->escaped()
2207
		);
2208
	}
2209
2210
	/**
2211
	 * Creates a dead (show/hide) link for deleting revisions/log entries
2212
	 *
2213
	 * @since 1.16.3
2214
	 * @param bool $delete Set to true to use (show/hide) rather than (show)
2215
	 *
2216
	 * @return string HTML text wrapped in a span to allow for customization
2217
	 * of appearance with CSS
2218
	 */
2219
	public static function revDeleteLinkDisabled( $delete = true ) {
2220
		$msgKey = $delete ? 'rev-delundel' : 'rev-showdeleted';
2221
		$html = wfMessage( $msgKey )->escaped();
2222
		$htmlParentheses = wfMessage( 'parentheses' )->rawParams( $html )->escaped();
2223
		return Xml::tags( 'span', [ 'class' => 'mw-revdelundel-link' ], $htmlParentheses );
2224
	}
2225
2226
	/* Deprecated methods */
2227
2228
	/**
2229
	 * Returns the attributes for the tooltip and access key.
2230
	 *
2231
	 * @since 1.16.3. $msgParams introduced in 1.27
2232
	 * @param string $name
2233
	 * @param array $msgParams Params for constructing the message
2234
	 *
2235
	 * @return array
2236
	 */
2237
	public static function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) {
2238
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2239
		# no attribute" instead of "output '' as value for attribute", this
2240
		# would be three lines.
2241
		$attribs = [
2242
			'title' => self::titleAttrib( $name, 'withaccess', $msgParams ),
2243
			'accesskey' => self::accesskey( $name )
2244
		];
2245
		if ( $attribs['title'] === false ) {
2246
			unset( $attribs['title'] );
2247
		}
2248
		if ( $attribs['accesskey'] === false ) {
2249
			unset( $attribs['accesskey'] );
2250
		}
2251
		return $attribs;
2252
	}
2253
2254
	/**
2255
	 * Returns raw bits of HTML, use titleAttrib()
2256
	 * @since 1.16.3
2257
	 * @param string $name
2258
	 * @param array|null $options
2259
	 * @return null|string
2260
	 */
2261
	public static function tooltip( $name, $options = null ) {
2262
		# @todo FIXME: If Sanitizer::expandAttributes() treated "false" as "output
2263
		# no attribute" instead of "output '' as value for attribute", this
2264
		# would be two lines.
2265
		$tooltip = self::titleAttrib( $name, $options );
0 ignored issues
show
Bug introduced by
It seems like $options defined by parameter $options on line 2261 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...
2266
		if ( $tooltip === false ) {
2267
			return '';
2268
		}
2269
		return Xml::expandAttributes( [
2270
			'title' => $tooltip
2271
		] );
2272
	}
2273
2274
}
2275
2276