Completed
Branch master (939199)
by
unknown
39:35
created

includes/Linker.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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