Completed
Branch master (3f14ba)
by
unknown
24:06
created

Linker::getLinkColour()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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

This check looks at variables that 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...
1123
		} else {
1124
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1125
		}
1126
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1127
			return '<span class="history-deleted">' . $link . '</span>';
1128
		}
1129
		return $link;
1130
	}
1131
1132
	/**
1133
	 * Generate a user tool link cluster if the current user is allowed to view it
1134
	 * @since 1.16.3
1135
	 * @param Revision $rev
1136
	 * @param bool $isPublic Show only if all users can see it
1137
	 * @return string HTML
1138
	 */
1139
	public static function revUserTools( $rev, $isPublic = false ) {
1140
		if ( $rev->isDeleted( Revision::DELETED_USER ) && $isPublic ) {
1141
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1142
		} elseif ( $rev->userCan( Revision::DELETED_USER ) ) {
1143
			$userId = $rev->getUser( Revision::FOR_THIS_USER );
1144
			$userText = $rev->getUserText( Revision::FOR_THIS_USER );
1145
			$link = self::userLink( $userId, $userText )
0 ignored issues
show
Bug introduced by
It seems like $userText defined by $rev->getUserText(\Revision::FOR_THIS_USER) on line 1144 can also be of type boolean; however, Linker::userLink() does only seem to accept string, 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...
1146
				. self::userToolLinks( $userId, $userText );
0 ignored issues
show
Bug introduced by
It seems like $userText defined by $rev->getUserText(\Revision::FOR_THIS_USER) on line 1144 can also be of type boolean; however, Linker::userToolLinks() does only seem to accept string, 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...
1147
		} else {
1148
			$link = wfMessage( 'rev-deleted-user' )->escaped();
1149
		}
1150
		if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
1151
			return ' <span class="history-deleted">' . $link . '</span>';
1152
		}
1153
		return $link;
1154
	}
1155
1156
	/**
1157
	 * This function is called by all recent changes variants, by the page history,
1158
	 * and by the user contributions list. It is responsible for formatting edit
1159
	 * summaries. It escapes any HTML in the summary, but adds some CSS to format
1160
	 * auto-generated comments (from section editing) and formats [[wikilinks]].
1161
	 *
1162
	 * @author Erik Moeller <[email protected]>
1163
	 * @since 1.16.3. $wikiId added in 1.26
1164
	 *
1165
	 * Note: there's not always a title to pass to this function.
1166
	 * Since you can't set a default parameter for a reference, I've turned it
1167
	 * temporarily to a value pass. Should be adjusted further. --brion
1168
	 *
1169
	 * @param string $comment
1170
	 * @param Title|null $title Title object (to generate link to the section in autocomment)
1171
	 *  or null
1172
	 * @param bool $local Whether section links should refer to local page
1173
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1174
	 *  For use with external changes.
1175
	 *
1176
	 * @return mixed|string
1177
	 */
1178
	public static function formatComment(
1179
		$comment, $title = null, $local = false, $wikiId = null
1180
	) {
1181
		# Sanitize text a bit:
1182
		$comment = str_replace( "\n", " ", $comment );
1183
		# Allow HTML entities (for bug 13815)
1184
		$comment = Sanitizer::escapeHtmlAllowEntities( $comment );
1185
1186
		# Render autocomments and make links:
1187
		$comment = self::formatAutocomments( $comment, $title, $local, $wikiId );
1188
		$comment = self::formatLinksInComment( $comment, $title, $local, $wikiId );
1189
1190
		return $comment;
1191
	}
1192
1193
	/**
1194
	 * Converts autogenerated comments in edit summaries into section links.
1195
	 *
1196
	 * The pattern for autogen comments is / * foo * /, which makes for
1197
	 * some nasty regex.
1198
	 * We look for all comments, match any text before and after the comment,
1199
	 * add a separator where needed and format the comment itself with CSS
1200
	 * Called by Linker::formatComment.
1201
	 *
1202
	 * @param string $comment Comment text
1203
	 * @param Title|null $title An optional title object used to links to sections
1204
	 * @param bool $local Whether section links should refer to local page
1205
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1206
	 *  as used by WikiMap.
1207
	 *
1208
	 * @return string Formatted comment (wikitext)
1209
	 */
1210
	private static function formatAutocomments(
1211
		$comment, $title = null, $local = false, $wikiId = null
1212
	) {
1213
		// @todo $append here is something of a hack to preserve the status
1214
		// quo. Someone who knows more about bidi and such should decide
1215
		// (1) what sane rendering even *is* for an LTR edit summary on an RTL
1216
		// wiki, both when autocomments exist and when they don't, and
1217
		// (2) what markup will make that actually happen.
1218
		$append = '';
1219
		$comment = preg_replace_callback(
1220
			// To detect the presence of content before or after the
1221
			// auto-comment, we use capturing groups inside optional zero-width
1222
			// assertions. But older versions of PCRE can't directly make
1223
			// zero-width assertions optional, so wrap them in a non-capturing
1224
			// group.
1225
			'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
1226
			function ( $match ) use ( $title, $local, $wikiId, &$append ) {
1227
				global $wgLang;
1228
1229
				// Ensure all match positions are defined
1230
				$match += [ '', '', '', '' ];
1231
1232
				$pre = $match[1] !== '';
1233
				$auto = $match[2];
1234
				$post = $match[3] !== '';
1235
				$comment = null;
1236
1237
				Hooks::run(
1238
					'FormatAutocomments',
1239
					[ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
1240
				);
1241
1242
				if ( $comment === null ) {
1243
					$link = '';
1244
					if ( $title ) {
1245
						$section = $auto;
1246
						# Remove links that a user may have manually put in the autosummary
1247
						# This could be improved by copying as much of Parser::stripSectionName as desired.
1248
						$section = str_replace( '[[:', '', $section );
1249
						$section = str_replace( '[[', '', $section );
1250
						$section = str_replace( ']]', '', $section );
1251
1252
						$section = Sanitizer::normalizeSectionNameWhitespace( $section ); # bug 22784
1253
						if ( $local ) {
1254
							$sectionTitle = Title::newFromText( '#' . $section );
1255
						} else {
1256
							$sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
1257
								$title->getDBkey(), $section );
1258
						}
1259
						if ( $sectionTitle ) {
1260
							$link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' );
1261
						} else {
1262
							$link = '';
1263
						}
1264
					}
1265
					if ( $pre ) {
1266
						# written summary $presep autocomment (summary /* section */)
1267
						$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
1268
					}
1269
					if ( $post ) {
1270
						# autocomment $postsep written summary (/* section */ summary)
1271
						$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
1272
					}
1273
					$auto = '<span class="autocomment">' . $auto . '</span>';
1274
					$comment = $pre . $link . $wgLang->getDirMark()
1275
						. '<span dir="auto">' . $auto;
1276
					$append .= '</span>';
1277
				}
1278
				return $comment;
1279
			},
1280
			$comment
1281
		);
1282
		return $comment . $append;
1283
	}
1284
1285
	/**
1286
	 * Formats wiki links and media links in text; all other wiki formatting
1287
	 * is ignored
1288
	 *
1289
	 * @since 1.16.3. $wikiId added in 1.26
1290
	 * @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
1291
	 *
1292
	 * @param string $comment Text to format links in. WARNING! Since the output of this
1293
	 *	function is html, $comment must be sanitized for use as html. You probably want
1294
	 *	to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
1295
	 *	this function.
1296
	 * @param Title|null $title An optional title object used to links to sections
1297
	 * @param bool $local Whether section links should refer to local page
1298
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1299
	 *  as used by WikiMap.
1300
	 *
1301
	 * @return string
1302
	 */
1303
	public static function formatLinksInComment(
1304
		$comment, $title = null, $local = false, $wikiId = null
1305
	) {
1306
		return preg_replace_callback(
1307
			'/
1308
				\[\[
1309
				:? # ignore optional leading colon
1310
				([^\]|]+) # 1. link target; page names cannot include ] or |
1311
				(?:\|
1312
					# 2. a pipe-separated substring; only the last is captured
1313
					# Stop matching at | and ]] without relying on backtracking.
1314
					((?:]?[^\]|])*+)
1315
				)*
1316
				\]\]
1317
				([^[]*) # 3. link trail (the text up until the next link)
1318
			/x',
1319
			function ( $match ) use ( $title, $local, $wikiId ) {
1320
				global $wgContLang;
1321
1322
				$medians = '(?:' . preg_quote( MWNamespace::getCanonicalName( NS_MEDIA ), '/' ) . '|';
1323
				$medians .= preg_quote( $wgContLang->getNsText( NS_MEDIA ), '/' ) . '):';
1324
1325
				$comment = $match[0];
1326
1327
				# fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
1328
				if ( strpos( $match[1], '%' ) !== false ) {
1329
					$match[1] = strtr(
1330
						rawurldecode( $match[1] ),
1331
						[ '<' => '&lt;', '>' => '&gt;' ]
1332
					);
1333
				}
1334
1335
				# Handle link renaming [[foo|text]] will show link as "text"
1336
				if ( $match[2] != "" ) {
1337
					$text = $match[2];
1338
				} else {
1339
					$text = $match[1];
1340
				}
1341
				$submatch = [];
1342
				$thelink = null;
1343
				if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
1344
					# Media link; trail not supported.
1345
					$linkRegexp = '/\[\[(.*?)\]\]/';
1346
					$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...
1347
					if ( $title ) {
1348
						$thelink = Linker::makeMediaLinkObj( $title, $text );
1349
					}
1350
				} else {
1351
					# Other kind of link
1352
					# Make sure its target is non-empty
1353
					if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
1354
						$match[1] = substr( $match[1], 1 );
1355
					}
1356
					if ( $match[1] !== false && $match[1] !== '' ) {
1357
						if ( preg_match( $wgContLang->linkTrail(), $match[3], $submatch ) ) {
1358
							$trail = $submatch[1];
1359
						} else {
1360
							$trail = "";
1361
						}
1362
						$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
1363
						list( $inside, $trail ) = Linker::splitTrail( $trail );
1364
1365
						$linkText = $text;
1366
						$linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
0 ignored issues
show
Bug introduced by
It seems like $title defined by parameter $title on line 1304 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...
1367
1368
						$target = Title::newFromText( $linkTarget );
1369
						if ( $target ) {
1370
							if ( $target->getText() == '' && !$target->isExternal()
1371
								&& !$local && $title
1372
							) {
1373
								$newTarget = clone $title;
1374
								$newTarget->setFragment( '#' . $target->getFragment() );
1375
								$target = $newTarget;
1376
							}
1377
1378
							$thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
1379
						}
1380
					}
1381
				}
1382
				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...
1383
					// If the link is still valid, go ahead and replace it in!
1384
					$comment = preg_replace(
1385
						$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...
1386
						StringUtils::escapeRegexReplacement( $thelink ),
1387
						$comment,
1388
						1
1389
					);
1390
				}
1391
1392
				return $comment;
1393
			},
1394
			$comment
1395
		);
1396
	}
1397
1398
	/**
1399
	 * Generates a link to the given Title
1400
	 *
1401
	 * @note This is only public for technical reasons. It's not intended for use outside Linker.
1402
	 *
1403
	 * @param Title $title
1404
	 * @param string $text
1405
	 * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
1406
	 *  as used by WikiMap.
1407
	 * @param string|string[] $options See the $options parameter in Linker::link.
1408
	 *
1409
	 * @return string HTML link
1410
	 */
1411
	public static function makeCommentLink(
1412
		Title $title, $text, $wikiId = null, $options = []
1413
	) {
1414
		if ( $wikiId !== null && !$title->isExternal() ) {
1415
			$link = Linker::makeExternalLink(
1416
				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...
1417
					$wikiId,
1418
					$title->getPrefixedText(),
1419
					$title->getFragment()
1420
				),
1421
				$text,
1422
				/* escape = */ false // Already escaped
1423
			);
1424
		} else {
1425
			$link = Linker::link( $title, $text, [], [], $options );
1426
		}
1427
1428
		return $link;
1429
	}
1430
1431
	/**
1432
	 * @param Title $contextTitle
1433
	 * @param string $target
1434
	 * @param string $text
1435
	 * @return string
1436
	 */
1437
	public static function normalizeSubpageLink( $contextTitle, $target, &$text ) {
1438
		# Valid link forms:
1439
		# Foobar -- normal
1440
		# :Foobar -- override special treatment of prefix (images, language links)
1441
		# /Foobar -- convert to CurrentPage/Foobar
1442
		# /Foobar/ -- convert to CurrentPage/Foobar, strip the initial and final / from text
1443
		# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
1444
		# ../Foobar -- convert to CurrentPage/Foobar,
1445
		#              (from CurrentPage/CurrentSubPage)
1446
		# ../Foobar/ -- convert to CurrentPage/Foobar, use 'Foobar' as text
1447
		#              (from CurrentPage/CurrentSubPage)
1448
1449
		$ret = $target; # default return value is no change
1450
1451
		# Some namespaces don't allow subpages,
1452
		# so only perform processing if subpages are allowed
1453
		if ( $contextTitle && MWNamespace::hasSubpages( $contextTitle->getNamespace() ) ) {
1454
			$hash = strpos( $target, '#' );
1455
			if ( $hash !== false ) {
1456
				$suffix = substr( $target, $hash );
1457
				$target = substr( $target, 0, $hash );
1458
			} else {
1459
				$suffix = '';
1460
			}
1461
			# bug 7425
1462
			$target = trim( $target );
1463
			# Look at the first character
1464
			if ( $target != '' && $target[0] === '/' ) {
1465
				# / at end means we don't want the slash to be shown
1466
				$m = [];
1467
				$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
1468 View Code Duplication
				if ( $trailingSlashes ) {
1469
					$noslash = $target = substr( $target, 1, -strlen( $m[0][0] ) );
1470
				} else {
1471
					$noslash = substr( $target, 1 );
1472
				}
1473
1474
				$ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
1475
				if ( $text === '' ) {
1476
					$text = $target . $suffix;
1477
				} # this might be changed for ugliness reasons
1478
			} else {
1479
				# check for .. subpage backlinks
1480
				$dotdotcount = 0;
1481
				$nodotdot = $target;
1482
				while ( strncmp( $nodotdot, "../", 3 ) == 0 ) {
1483
					++$dotdotcount;
1484
					$nodotdot = substr( $nodotdot, 3 );
1485
				}
1486
				if ( $dotdotcount > 0 ) {
1487
					$exploded = explode( '/', $contextTitle->getPrefixedText() );
1488
					if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
1489
						$ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
1490
						# / at the end means don't show full path
1491
						if ( substr( $nodotdot, -1, 1 ) === '/' ) {
1492
							$nodotdot = rtrim( $nodotdot, '/' );
1493
							if ( $text === '' ) {
1494
								$text = $nodotdot . $suffix;
1495
							}
1496
						}
1497
						$nodotdot = trim( $nodotdot );
1498
						if ( $nodotdot != '' ) {
1499
							$ret .= '/' . $nodotdot;
1500
						}
1501
						$ret .= $suffix;
1502
					}
1503
				}
1504
			}
1505
		}
1506
1507
		return $ret;
1508
	}
1509
1510
	/**
1511
	 * Wrap a comment in standard punctuation and formatting if
1512
	 * it's non-empty, otherwise return empty string.
1513
	 *
1514
	 * @since 1.16.3. $wikiId added in 1.26
1515
	 * @param string $comment
1516
	 * @param Title|null $title Title object (to generate link to section in autocomment) or null
1517
	 * @param bool $local Whether section links should refer to local page
1518
	 * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
1519
	 *  For use with external changes.
1520
	 *
1521
	 * @return string
1522
	 */
1523
	public static function commentBlock(
1524
		$comment, $title = null, $local = false, $wikiId = null
1525
	) {
1526
		// '*' used to be the comment inserted by the software way back
1527
		// in antiquity in case none was provided, here for backwards
1528
		// compatibility, acc. to brion -ævar
1529
		if ( $comment == '' || $comment == '*' ) {
1530
			return '';
1531
		} else {
1532
			$formatted = self::formatComment( $comment, $title, $local, $wikiId );
1533
			$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
1534
			return " <span class=\"comment\">$formatted</span>";
1535
		}
1536
	}
1537
1538
	/**
1539
	 * Wrap and format the given revision's comment block, if the current
1540
	 * user is allowed to view it.
1541
	 *
1542
	 * @since 1.16.3
1543
	 * @param Revision $rev
1544
	 * @param bool $local Whether section links should refer to local page
1545
	 * @param bool $isPublic Show only if all users can see it
1546
	 * @return string HTML fragment
1547
	 */
1548
	public static function revComment( Revision $rev, $local = false, $isPublic = false ) {
1549
		if ( $rev->getComment( Revision::RAW ) == "" ) {
1550
			return "";
1551
		}
1552
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) && $isPublic ) {
1553
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1554
		} elseif ( $rev->userCan( Revision::DELETED_COMMENT ) ) {
1555
			$block = self::commentBlock( $rev->getComment( Revision::FOR_THIS_USER ),
1556
				$rev->getTitle(), $local );
1557
		} else {
1558
			$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
1559
		}
1560
		if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
1561
			return " <span class=\"history-deleted\">$block</span>";
1562
		}
1563
		return $block;
1564
	}
1565
1566
	/**
1567
	 * @since 1.16.3
1568
	 * @param int $size
1569
	 * @return string
1570
	 */
1571
	public static function formatRevisionSize( $size ) {
1572
		if ( $size == 0 ) {
1573
			$stxt = wfMessage( 'historyempty' )->escaped();
1574
		} else {
1575
			$stxt = wfMessage( 'nbytes' )->numParams( $size )->escaped();
1576
			$stxt = wfMessage( 'parentheses' )->rawParams( $stxt )->escaped();
1577
		}
1578
		return "<span class=\"history-size\">$stxt</span>";
1579
	}
1580
1581
	/**
1582
	 * Add another level to the Table of Contents
1583
	 *
1584
	 * @since 1.16.3
1585
	 * @return string
1586
	 */
1587
	public static function tocIndent() {
1588
		return "\n<ul>";
1589
	}
1590
1591
	/**
1592
	 * Finish one or more sublevels on the Table of Contents
1593
	 *
1594
	 * @since 1.16.3
1595
	 * @param int $level
1596
	 * @return string
1597
	 */
1598
	public static function tocUnindent( $level ) {
1599
		return "</li>\n" . str_repeat( "</ul>\n</li>\n", $level > 0 ? $level : 0 );
1600
	}
1601
1602
	/**
1603
	 * parameter level defines if we are on an indentation level
1604
	 *
1605
	 * @since 1.16.3
1606
	 * @param string $anchor
1607
	 * @param string $tocline
1608
	 * @param string $tocnumber
1609
	 * @param string $level
1610
	 * @param string|bool $sectionIndex
1611
	 * @return string
1612
	 */
1613
	public static function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
1614
		$classes = "toclevel-$level";
1615
		if ( $sectionIndex !== false ) {
1616
			$classes .= " tocsection-$sectionIndex";
1617
		}
1618
		return "\n<li class=\"$classes\"><a href=\"#" .
1619
			$anchor . '"><span class="tocnumber">' .
1620
			$tocnumber . '</span> <span class="toctext">' .
1621
			$tocline . '</span></a>';
1622
	}
1623
1624
	/**
1625
	 * End a Table Of Contents line.
1626
	 * tocUnindent() will be used instead if we're ending a line below
1627
	 * the new level.
1628
	 * @since 1.16.3
1629
	 * @return string
1630
	 */
1631
	public static function tocLineEnd() {
1632
		return "</li>\n";
1633
	}
1634
1635
	/**
1636
	 * Wraps the TOC in a table and provides the hide/collapse javascript.
1637
	 *
1638
	 * @since 1.16.3
1639
	 * @param string $toc Html of the Table Of Contents
1640
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1641
	 * @return string Full html of the TOC
1642
	 */
1643
	public static function tocList( $toc, $lang = false ) {
1644
		$lang = wfGetLangObj( $lang );
1645
		$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
1646
1647
		return '<div id="toc" class="toc">'
1648
			. '<div id="toctitle"><h2>' . $title . "</h2></div>\n"
1649
			. $toc
1650
			. "</ul>\n</div>\n";
1651
	}
1652
1653
	/**
1654
	 * Generate a table of contents from a section tree.
1655
	 *
1656
	 * @since 1.16.3. $lang added in 1.17
1657
	 * @param array $tree Return value of ParserOutput::getSections()
1658
	 * @param string|Language|bool $lang Language for the toc title, defaults to user language
1659
	 * @return string HTML fragment
1660
	 */
1661
	public static function generateTOC( $tree, $lang = false ) {
1662
		$toc = '';
1663
		$lastLevel = 0;
1664
		foreach ( $tree as $section ) {
1665
			if ( $section['toclevel'] > $lastLevel ) {
1666
				$toc .= self::tocIndent();
1667
			} elseif ( $section['toclevel'] < $lastLevel ) {
1668
				$toc .= self::tocUnindent(
1669
					$lastLevel - $section['toclevel'] );
1670
			} else {
1671
				$toc .= self::tocLineEnd();
1672
			}
1673
1674
			$toc .= self::tocLine( $section['anchor'],
1675
				$section['line'], $section['number'],
1676
				$section['toclevel'], $section['index'] );
1677
			$lastLevel = $section['toclevel'];
1678
		}
1679
		$toc .= self::tocLineEnd();
1680
		return self::tocList( $toc, $lang );
1681
	}
1682
1683
	/**
1684
	 * Create a headline for content
1685
	 *
1686
	 * @since 1.16.3
1687
	 * @param int $level The level of the headline (1-6)
1688
	 * @param string $attribs Any attributes for the headline, starting with
1689
	 *   a space and ending with '>'
1690
	 *   This *must* be at least '>' for no attribs
1691
	 * @param string $anchor The anchor to give the headline (the bit after the #)
1692
	 * @param string $html Html for the text of the header
1693
	 * @param string $link HTML to add for the section edit link
1694
	 * @param bool|string $legacyAnchor A second, optional anchor to give for
1695
	 *   backward compatibility (false to omit)
1696
	 *
1697
	 * @return string HTML headline
1698
	 */
1699
	public static function makeHeadline( $level, $attribs, $anchor, $html,
1700
		$link, $legacyAnchor = false
1701
	) {
1702
		$ret = "<h$level$attribs"
1703
			. "<span class=\"mw-headline\" id=\"$anchor\">$html</span>"
1704
			. $link
1705
			. "</h$level>";
1706
		if ( $legacyAnchor !== false ) {
1707
			$ret = "<div id=\"$legacyAnchor\"></div>$ret";
1708
		}
1709
		return $ret;
1710
	}
1711
1712
	/**
1713
	 * Split a link trail, return the "inside" portion and the remainder of the trail
1714
	 * as a two-element array
1715
	 * @param string $trail
1716
	 * @return array
1717
	 */
1718
	static function splitTrail( $trail ) {
1719
		global $wgContLang;
1720
		$regex = $wgContLang->linkTrail();
1721
		$inside = '';
1722
		if ( $trail !== '' ) {
1723
			$m = [];
1724
			if ( preg_match( $regex, $trail, $m ) ) {
1725
				$inside = $m[1];
1726
				$trail = $m[2];
1727
			}
1728
		}
1729
		return [ $inside, $trail ];
1730
	}
1731
1732
	/**
1733
	 * Generate a rollback link for a given revision.  Currently it's the
1734
	 * caller's responsibility to ensure that the revision is the top one. If
1735
	 * it's not, of course, the user will get an error message.
1736
	 *
1737
	 * If the calling page is called with the parameter &bot=1, all rollback
1738
	 * links also get that parameter. It causes the edit itself and the rollback
1739
	 * to be marked as "bot" edits. Bot edits are hidden by default from recent
1740
	 * changes, so this allows sysops to combat a busy vandal without bothering
1741
	 * other users.
1742
	 *
1743
	 * If the option verify is set this function will return the link only in case the
1744
	 * revision can be reverted. Please note that due to performance limitations
1745
	 * it might be assumed that a user isn't the only contributor of a page while
1746
	 * (s)he is, which will lead to useless rollback links. Furthermore this wont
1747
	 * work if $wgShowRollbackEditCount is disabled, so this can only function
1748
	 * as an additional check.
1749
	 *
1750
	 * If the option noBrackets is set the rollback link wont be enclosed in "[]".
1751
	 *
1752
	 * @since 1.16.3. $context added in 1.20. $options added in 1.21
1753
	 *
1754
	 * @param Revision $rev
1755
	 * @param IContextSource $context Context to use or null for the main context.
1756
	 * @param array $options
1757
	 * @return string
1758
	 */
1759
	public static function generateRollback( $rev, IContextSource $context = null,
1760
		$options = [ 'verify' ]
1761
	) {
1762
		if ( $context === null ) {
1763
			$context = RequestContext::getMain();
1764
		}
1765
1766
		$editCount = false;
1767
		if ( in_array( 'verify', $options, true ) ) {
1768
			$editCount = self::getRollbackEditCount( $rev, true );
1769
			if ( $editCount === false ) {
1770
				return '';
1771
			}
1772
		}
1773
1774
		$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 1768 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...
1775
1776
		if ( !in_array( 'noBrackets', $options, true ) ) {
1777
			$inner = $context->msg( 'brackets' )->rawParams( $inner )->escaped();
1778
		}
1779
1780
		return '<span class="mw-rollback-link">' . $inner . '</span>';
1781
	}
1782
1783
	/**
1784
	 * This function will return the number of revisions which a rollback
1785
	 * would revert and, if $verify is set it will verify that a revision
1786
	 * can be reverted (that the user isn't the only contributor and the
1787
	 * revision we might rollback to isn't deleted). These checks can only
1788
	 * function as an additional check as this function only checks against
1789
	 * the last $wgShowRollbackEditCount edits.
1790
	 *
1791
	 * Returns null if $wgShowRollbackEditCount is disabled or false if $verify
1792
	 * is set and the user is the only contributor of the page.
1793
	 *
1794
	 * @param Revision $rev
1795
	 * @param bool $verify Try to verify that this revision can really be rolled back
1796
	 * @return int|bool|null
1797
	 */
1798
	public static function getRollbackEditCount( $rev, $verify ) {
1799
		global $wgShowRollbackEditCount;
1800
		if ( !is_int( $wgShowRollbackEditCount ) || !$wgShowRollbackEditCount > 0 ) {
1801
			// Nothing has happened, indicate this by returning 'null'
1802
			return null;
1803
		}
1804
1805
		$dbr = wfGetDB( DB_SLAVE );
1806
1807
		// Up to the value of $wgShowRollbackEditCount revisions are counted
1808
		$res = $dbr->select(
1809
			'revision',
1810
			[ 'rev_user_text', 'rev_deleted' ],
1811
			// $rev->getPage() returns null sometimes
1812
			[ 'rev_page' => $rev->getTitle()->getArticleID() ],
1813
			__METHOD__,
1814
			[
1815
				'USE INDEX' => [ 'revision' => 'page_timestamp' ],
1816
				'ORDER BY' => 'rev_timestamp DESC',
1817
				'LIMIT' => $wgShowRollbackEditCount + 1
1818
			]
1819
		);
1820
1821
		$editCount = 0;
1822
		$moreRevs = false;
1823
		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...
1824
			if ( $rev->getUserText( Revision::RAW ) != $row->rev_user_text ) {
1825
				if ( $verify &&
1826
					( $row->rev_deleted & Revision::DELETED_TEXT
1827
						|| $row->rev_deleted & Revision::DELETED_USER
1828
				) ) {
1829
					// If the user or the text of the revision we might rollback
1830
					// to is deleted in some way we can't rollback. Similar to
1831
					// the sanity checks in WikiPage::commitRollback.
1832
					return false;
1833
				}
1834
				$moreRevs = true;
1835
				break;
1836
			}
1837
			$editCount++;
1838
		}
1839
1840
		if ( $verify && $editCount <= $wgShowRollbackEditCount && !$moreRevs ) {
1841
			// We didn't find at least $wgShowRollbackEditCount revisions made by the current user
1842
			// and there weren't any other revisions. That means that the current user is the only
1843
			// editor, so we can't rollback
1844
			return false;
1845
		}
1846
		return $editCount;
1847
	}
1848
1849
	/**
1850
	 * Build a raw rollback link, useful for collections of "tool" links
1851
	 *
1852
	 * @since 1.16.3. $context added in 1.20. $editCount added in 1.21
1853
	 * @param Revision $rev
1854
	 * @param IContextSource|null $context Context to use or null for the main context.
1855
	 * @param int $editCount Number of edits that would be reverted
1856
	 * @return string HTML fragment
1857
	 */
1858
	public static function buildRollbackLink( $rev, IContextSource $context = null,
1859
		$editCount = false
1860
	) {
1861
		global $wgShowRollbackEditCount, $wgMiserMode;
1862
1863
		// To config which pages are affected by miser mode
1864
		$disableRollbackEditCountSpecialPage = [ 'Recentchanges', 'Watchlist' ];
1865
1866
		if ( $context === null ) {
1867
			$context = RequestContext::getMain();
1868
		}
1869
1870
		$title = $rev->getTitle();
1871
		$query = [
1872
			'action' => 'rollback',
1873
			'from' => $rev->getUserText(),
1874
			'token' => $context->getUser()->getEditToken( 'rollback' ),
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 1870 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 1870 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