Completed
Branch master (227f0c)
by
unknown
30:54
created

Linker   F

Complexity

Total Complexity 339

Size/Duplication

Total Lines 2248
Duplicated Lines 0.49 %

Coupling/Cohesion

Components 4
Dependencies 32

Importance

Changes 7
Bugs 0 Features 5
Metric Value
c 7
b 0
f 5
dl 11
loc 2248
rs 0.5217
wmc 339
lcom 4
cbo 32

60 Methods

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

How to fix   Duplicated Code    Complexity   

Duplicated Code

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

Common duplication problems, and corresponding solutions are:

Complex Class

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

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

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

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

1
<?php
2
/**
3
 * Methods to make links and related items.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 */
22
use MediaWiki\Linker\LinkTarget;
23
use MediaWiki\MediaWikiServices;
24
25
/**
26
 * Some internal bits split of from Skin.php. These functions are used
27
 * for primarily page content: links, embedded images, table of contents. Links
28
 * are also used in the skin.
29
 *
30
 * @todo turn this into a legacy interface for HtmlPageLinkRenderer and similar services.
31
 *
32
 * @ingroup Skins
33
 */
34
class Linker {
35
	/**
36
	 * Flags for userToolLinks()
37
	 */
38
	const TOOL_LINKS_NOBLOCK = 1;
39
	const TOOL_LINKS_EMAIL = 2;
40
41
	/**
42
	 * Get the appropriate HTML attributes to add to the "a" element of an interwiki link.
43
	 *
44
	 * @since 1.16.3
45
	 * @deprecated since 1.25
46
	 *
47
	 * @param string $title The title text for the link, URL-encoded (???) but
48
	 *   not HTML-escaped
49
	 * @param string $unused Unused
50
	 * @param string $class The contents of the class attribute; if an empty
51
	 *   string is passed, which is the default value, defaults to 'external'.
52
	 * @return string
53
	 */
54
	static function getInterwikiLinkAttributes( $title, $unused = null, $class = 'external' ) {
55
		global $wgContLang;
56
57
		wfDeprecated( __METHOD__, '1.25' );
58
59
		# @todo FIXME: We have a whole bunch of handling here that doesn't happen in
60
		# getExternalLinkAttributes, why?
61
		$title = urldecode( $title );
62
		$title = $wgContLang->checkTitleEncoding( $title );
63
		$title = preg_replace( '/[\\x00-\\x1f]/', ' ', $title );
64
65
		return self::getLinkAttributesInternal( $title, $class );
0 ignored issues
show
Deprecated Code introduced by
The method Linker::getLinkAttributesInternal() has been deprecated with message: since 1.25

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

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

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

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

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

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

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

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

An additional type check may prevent trouble.

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

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

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

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