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

includes/Html.php (1 issue)

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
 * Collection of methods to generate HTML content
4
 *
5
 * Copyright © 2009 Aryeh Gregor
6
 * http://www.mediawiki.org/
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License along
19
 * with this program; if not, write to the Free Software Foundation, Inc.,
20
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21
 * http://www.gnu.org/copyleft/gpl.html
22
 *
23
 * @file
24
 */
25
26
/**
27
 * This class is a collection of static functions that serve two purposes:
28
 *
29
 * 1) Implement any algorithms specified by HTML5, or other HTML
30
 * specifications, in a convenient and self-contained way.
31
 *
32
 * 2) Allow HTML elements to be conveniently and safely generated, like the
33
 * current Xml class but a) less confused (Xml supports HTML-specific things,
34
 * but only sometimes!) and b) not necessarily confined to XML-compatible
35
 * output.
36
 *
37
 * There are two important configuration options this class uses:
38
 *
39
 * $wgMimeType: If this is set to an xml MIME type then output should be
40
 *     valid XHTML5.
41
 *
42
 * This class is meant to be confined to utility functions that are called from
43
 * trusted code paths.  It does not do enforcement of policy like not allowing
44
 * <a> elements.
45
 *
46
 * @since 1.16
47
 */
48
class Html {
49
	// List of void elements from HTML5, section 8.1.2 as of 2016-09-19
50
	private static $voidElements = [
51
		'area',
52
		'base',
53
		'br',
54
		'col',
55
		'embed',
56
		'hr',
57
		'img',
58
		'input',
59
		'keygen',
60
		'link',
61
		'meta',
62
		'param',
63
		'source',
64
		'track',
65
		'wbr',
66
	];
67
68
	// Boolean attributes, which may have the value omitted entirely.  Manually
69
	// collected from the HTML5 spec as of 2011-08-12.
70
	private static $boolAttribs = [
71
		'async',
72
		'autofocus',
73
		'autoplay',
74
		'checked',
75
		'controls',
76
		'default',
77
		'defer',
78
		'disabled',
79
		'formnovalidate',
80
		'hidden',
81
		'ismap',
82
		'itemscope',
83
		'loop',
84
		'multiple',
85
		'muted',
86
		'novalidate',
87
		'open',
88
		'pubdate',
89
		'readonly',
90
		'required',
91
		'reversed',
92
		'scoped',
93
		'seamless',
94
		'selected',
95
		'truespeed',
96
		'typemustmatch',
97
		// HTML5 Microdata
98
		'itemscope',
99
	];
100
101
	/**
102
	 * Modifies a set of attributes meant for button elements
103
	 * and apply a set of default attributes when $wgUseMediaWikiUIEverywhere enabled.
104
	 * @param array $attrs HTML attributes in an associative array
105
	 * @param string[] $modifiers classes to add to the button
106
	 * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
107
	 * @return array $attrs A modified attribute array
108
	 */
109
	public static function buttonAttributes( array $attrs, array $modifiers = [] ) {
110
		global $wgUseMediaWikiUIEverywhere;
111
		if ( $wgUseMediaWikiUIEverywhere ) {
112
			if ( isset( $attrs['class'] ) ) {
113
				if ( is_array( $attrs['class'] ) ) {
114
					$attrs['class'][] = 'mw-ui-button';
115
					$attrs['class'] = array_merge( $attrs['class'], $modifiers );
116
					// ensure compatibility with Xml
117
					$attrs['class'] = implode( ' ', $attrs['class'] );
118
				} else {
119
					$attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers );
120
				}
121
			} else {
122
				// ensure compatibility with Xml
123
				$attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers );
124
			}
125
		}
126
		return $attrs;
127
	}
128
129
	/**
130
	 * Modifies a set of attributes meant for text input elements
131
	 * and apply a set of default attributes.
132
	 * Removes size attribute when $wgUseMediaWikiUIEverywhere enabled.
133
	 * @param array $attrs An attribute array.
134
	 * @return array $attrs A modified attribute array
135
	 */
136
	public static function getTextInputAttributes( array $attrs ) {
137
		global $wgUseMediaWikiUIEverywhere;
138 View Code Duplication
		if ( $wgUseMediaWikiUIEverywhere ) {
139
			if ( isset( $attrs['class'] ) ) {
140
				if ( is_array( $attrs['class'] ) ) {
141
					$attrs['class'][] = 'mw-ui-input';
142
				} else {
143
					$attrs['class'] .= ' mw-ui-input';
144
				}
145
			} else {
146
				$attrs['class'] = 'mw-ui-input';
147
			}
148
		}
149
		return $attrs;
150
	}
151
152
	/**
153
	 * Returns an HTML link element in a string styled as a button
154
	 * (when $wgUseMediaWikiUIEverywhere is enabled).
155
	 *
156
	 * @param string $contents The raw HTML contents of the element: *not*
157
	 *   escaped!
158
	 * @param array $attrs Associative array of attributes, e.g., [
159
	 *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
160
	 *   further documentation.
161
	 * @param string[] $modifiers classes to add to the button
162
	 * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
163
	 * @return string Raw HTML
164
	 */
165
	public static function linkButton( $contents, array $attrs, array $modifiers = [] ) {
166
		return self::element( 'a',
167
			self::buttonAttributes( $attrs, $modifiers ),
168
			$contents
169
		);
170
	}
171
172
	/**
173
	 * Returns an HTML link element in a string styled as a button
174
	 * (when $wgUseMediaWikiUIEverywhere is enabled).
175
	 *
176
	 * @param string $contents The raw HTML contents of the element: *not*
177
	 *   escaped!
178
	 * @param array $attrs Associative array of attributes, e.g., [
179
	 *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
180
	 *   further documentation.
181
	 * @param string[] $modifiers classes to add to the button
182
	 * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
183
	 * @return string Raw HTML
184
	 */
185
	public static function submitButton( $contents, array $attrs, array $modifiers = [] ) {
186
		$attrs['type'] = 'submit';
187
		$attrs['value'] = $contents;
188
		return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) );
189
	}
190
191
	/**
192
	 * Returns an HTML element in a string.  The major advantage here over
193
	 * manually typing out the HTML is that it will escape all attribute
194
	 * values.
195
	 *
196
	 * This is quite similar to Xml::tags(), but it implements some useful
197
	 * HTML-specific logic.  For instance, there is no $allowShortTag
198
	 * parameter: the closing tag is magically omitted if $element has an empty
199
	 * content model.
200
	 *
201
	 * @param string $element The element's name, e.g., 'a'
202
	 * @param array $attribs Associative array of attributes, e.g., [
203
	 *   'href' => 'http://www.mediawiki.org/' ]. See expandAttributes() for
204
	 *   further documentation.
205
	 * @param string $contents The raw HTML contents of the element: *not*
206
	 *   escaped!
207
	 * @return string Raw HTML
208
	 */
209
	public static function rawElement( $element, $attribs = [], $contents = '' ) {
210
		$start = self::openElement( $element, $attribs );
211
		if ( in_array( $element, self::$voidElements ) ) {
212
			// Silly XML.
213
			return substr( $start, 0, -1 ) . '/>';
214
		} else {
215
			return "$start$contents" . self::closeElement( $element );
216
		}
217
	}
218
219
	/**
220
	 * Identical to rawElement(), but HTML-escapes $contents (like
221
	 * Xml::element()).
222
	 *
223
	 * @param string $element
224
	 * @param array $attribs
225
	 * @param string $contents
226
	 *
227
	 * @return string
228
	 */
229
	public static function element( $element, $attribs = [], $contents = '' ) {
230
		return self::rawElement( $element, $attribs, strtr( $contents, [
231
			// There's no point in escaping quotes, >, etc. in the contents of
232
			// elements.
233
			'&' => '&amp;',
234
			'<' => '&lt;'
235
		] ) );
236
	}
237
238
	/**
239
	 * Identical to rawElement(), but has no third parameter and omits the end
240
	 * tag (and the self-closing '/' in XML mode for empty elements).
241
	 *
242
	 * @param string $element
243
	 * @param array $attribs
244
	 *
245
	 * @return string
246
	 */
247
	public static function openElement( $element, $attribs = [] ) {
248
		$attribs = (array)$attribs;
249
		// This is not required in HTML5, but let's do it anyway, for
250
		// consistency and better compression.
251
		$element = strtolower( $element );
252
253
		// Remove invalid input types
254
		if ( $element == 'input' ) {
255
			$validTypes = [
256
				'hidden',
257
				'text',
258
				'password',
259
				'checkbox',
260
				'radio',
261
				'file',
262
				'submit',
263
				'image',
264
				'reset',
265
				'button',
266
267
				// HTML input types
268
				'datetime',
269
				'datetime-local',
270
				'date',
271
				'month',
272
				'time',
273
				'week',
274
				'number',
275
				'range',
276
				'email',
277
				'url',
278
				'search',
279
				'tel',
280
				'color',
281
			];
282
			if ( isset( $attribs['type'] ) && !in_array( $attribs['type'], $validTypes ) ) {
283
				unset( $attribs['type'] );
284
			}
285
		}
286
287
		// According to standard the default type for <button> elements is "submit".
288
		// Depending on compatibility mode IE might use "button", instead.
289
		// We enforce the standard "submit".
290
		if ( $element == 'button' && !isset( $attribs['type'] ) ) {
291
			$attribs['type'] = 'submit';
292
		}
293
294
		return "<$element" . self::expandAttributes(
295
			self::dropDefaults( $element, $attribs ) ) . '>';
296
	}
297
298
	/**
299
	 * Returns "</$element>"
300
	 *
301
	 * @since 1.17
302
	 * @param string $element Name of the element, e.g., 'a'
303
	 * @return string A closing tag
304
	 */
305
	public static function closeElement( $element ) {
306
		$element = strtolower( $element );
307
308
		return "</$element>";
309
	}
310
311
	/**
312
	 * Given an element name and an associative array of element attributes,
313
	 * return an array that is functionally identical to the input array, but
314
	 * possibly smaller.  In particular, attributes might be stripped if they
315
	 * are given their default values.
316
	 *
317
	 * This method is not guaranteed to remove all redundant attributes, only
318
	 * some common ones and some others selected arbitrarily at random.  It
319
	 * only guarantees that the output array should be functionally identical
320
	 * to the input array (currently per the HTML 5 draft as of 2009-09-06).
321
	 *
322
	 * @param string $element Name of the element, e.g., 'a'
323
	 * @param array $attribs Associative array of attributes, e.g., [
324
	 *   'href' => 'http://www.mediawiki.org/' ].  See expandAttributes() for
325
	 *   further documentation.
326
	 * @return array An array of attributes functionally identical to $attribs
327
	 */
328
	private static function dropDefaults( $element, array $attribs ) {
329
		// Whenever altering this array, please provide a covering test case
330
		// in HtmlTest::provideElementsWithAttributesHavingDefaultValues
331
		static $attribDefaults = [
332
			'area' => [ 'shape' => 'rect' ],
333
			'button' => [
334
				'formaction' => 'GET',
335
				'formenctype' => 'application/x-www-form-urlencoded',
336
			],
337
			'canvas' => [
338
				'height' => '150',
339
				'width' => '300',
340
			],
341
			'form' => [
342
				'action' => 'GET',
343
				'autocomplete' => 'on',
344
				'enctype' => 'application/x-www-form-urlencoded',
345
			],
346
			'input' => [
347
				'formaction' => 'GET',
348
				'type' => 'text',
349
			],
350
			'keygen' => [ 'keytype' => 'rsa' ],
351
			'link' => [ 'media' => 'all' ],
352
			'menu' => [ 'type' => 'list' ],
353
			'script' => [ 'type' => 'text/javascript' ],
354
			'style' => [
355
				'media' => 'all',
356
				'type' => 'text/css',
357
			],
358
			'textarea' => [ 'wrap' => 'soft' ],
359
		];
360
361
		$element = strtolower( $element );
362
363
		foreach ( $attribs as $attrib => $value ) {
364
			$lcattrib = strtolower( $attrib );
365
			if ( is_array( $value ) ) {
366
				$value = implode( ' ', $value );
367
			} else {
368
				$value = strval( $value );
369
			}
370
371
			// Simple checks using $attribDefaults
372
			if ( isset( $attribDefaults[$element][$lcattrib] )
373
				&& $attribDefaults[$element][$lcattrib] == $value
374
			) {
375
				unset( $attribs[$attrib] );
376
			}
377
378
			if ( $lcattrib == 'class' && $value == '' ) {
379
				unset( $attribs[$attrib] );
380
			}
381
		}
382
383
		// More subtle checks
384
		if ( $element === 'link'
385
			&& isset( $attribs['type'] ) && strval( $attribs['type'] ) == 'text/css'
386
		) {
387
			unset( $attribs['type'] );
388
		}
389
		if ( $element === 'input' ) {
390
			$type = isset( $attribs['type'] ) ? $attribs['type'] : null;
391
			$value = isset( $attribs['value'] ) ? $attribs['value'] : null;
392
			if ( $type === 'checkbox' || $type === 'radio' ) {
393
				// The default value for checkboxes and radio buttons is 'on'
394
				// not ''. By stripping value="" we break radio boxes that
395
				// actually wants empty values.
396
				if ( $value === 'on' ) {
397
					unset( $attribs['value'] );
398
				}
399
			} elseif ( $type === 'submit' ) {
0 ignored issues
show
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...
400
				// The default value for submit appears to be "Submit" but
401
				// let's not bother stripping out localized text that matches
402
				// that.
403
			} else {
404
				// The default value for nearly every other field type is ''
405
				// The 'range' and 'color' types use different defaults but
406
				// stripping a value="" does not hurt them.
407
				if ( $value === '' ) {
408
					unset( $attribs['value'] );
409
				}
410
			}
411
		}
412
		if ( $element === 'select' && isset( $attribs['size'] ) ) {
413
			if ( in_array( 'multiple', $attribs )
414
				|| ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
415
			) {
416
				// A multi-select
417
				if ( strval( $attribs['size'] ) == '4' ) {
418
					unset( $attribs['size'] );
419
				}
420
			} else {
421
				// Single select
422
				if ( strval( $attribs['size'] ) == '1' ) {
423
					unset( $attribs['size'] );
424
				}
425
			}
426
		}
427
428
		return $attribs;
429
	}
430
431
	/**
432
	 * Given an associative array of element attributes, generate a string
433
	 * to stick after the element name in HTML output.  Like [ 'href' =>
434
	 * 'http://www.mediawiki.org/' ] becomes something like
435
	 * ' href="http://www.mediawiki.org"'.  Again, this is like
436
	 * Xml::expandAttributes(), but it implements some HTML-specific logic.
437
	 *
438
	 * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
439
	 * values are allowed as well, which will automagically be normalized
440
	 * and converted to a space-separated string. In addition to a numerical
441
	 * array, the attribute value may also be an associative array. See the
442
	 * example below for how that works.
443
	 *
444
	 * @par Numerical array
445
	 * @code
446
	 *     Html::element( 'em', [
447
	 *         'class' => [ 'foo', 'bar' ]
448
	 *     ] );
449
	 *     // gives '<em class="foo bar"></em>'
450
	 * @endcode
451
	 *
452
	 * @par Associative array
453
	 * @code
454
	 *     Html::element( 'em', [
455
	 *         'class' => [ 'foo', 'bar', 'foo' => false, 'quux' => true ]
456
	 *     ] );
457
	 *     // gives '<em class="bar quux"></em>'
458
	 * @endcode
459
	 *
460
	 * @param array $attribs Associative array of attributes, e.g., [
461
	 *   'href' => 'http://www.mediawiki.org/' ].  Values will be HTML-escaped.
462
	 *   A value of false means to omit the attribute.  For boolean attributes,
463
	 *   you can omit the key, e.g., [ 'checked' ] instead of
464
	 *   [ 'checked' => 'checked' ] or such.
465
	 *
466
	 * @throws MWException If an attribute that doesn't allow lists is set to an array
467
	 * @return string HTML fragment that goes between element name and '>'
468
	 *   (starting with a space if at least one attribute is output)
469
	 */
470
	public static function expandAttributes( array $attribs ) {
471
		$ret = '';
472
		foreach ( $attribs as $key => $value ) {
473
			// Support intuitive [ 'checked' => true/false ] form
474
			if ( $value === false || is_null( $value ) ) {
475
				continue;
476
			}
477
478
			// For boolean attributes, support [ 'foo' ] instead of
479
			// requiring [ 'foo' => 'meaningless' ].
480
			if ( is_int( $key ) && in_array( strtolower( $value ), self::$boolAttribs ) ) {
481
				$key = $value;
482
			}
483
484
			// Not technically required in HTML5 but we'd like consistency
485
			// and better compression anyway.
486
			$key = strtolower( $key );
487
488
			// Bug 23769: Blacklist all form validation attributes for now.  Current
489
			// (June 2010) WebKit has no UI, so the form just refuses to submit
490
			// without telling the user why, which is much worse than failing
491
			// server-side validation.  Opera is the only other implementation at
492
			// this time, and has ugly UI, so just kill the feature entirely until
493
			// we have at least one good implementation.
494
495
			// As the default value of "1" for "step" rejects decimal
496
			// numbers to be entered in 'type="number"' fields, allow
497
			// the special case 'step="any"'.
498
499
			if ( in_array( $key, [ 'max', 'min', 'pattern', 'required' ] )
500
				|| $key === 'step' && $value !== 'any' ) {
501
				continue;
502
			}
503
504
			// http://www.w3.org/TR/html401/index/attributes.html ("space-separated")
505
			// http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated")
506
			$spaceSeparatedListAttributes = [
507
				'class', // html4, html5
508
				'accesskey', // as of html5, multiple space-separated values allowed
509
				// html4-spec doesn't document rel= as space-separated
510
				// but has been used like that and is now documented as such
511
				// in the html5-spec.
512
				'rel',
513
			];
514
515
			// Specific features for attributes that allow a list of space-separated values
516
			if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
517
				// Apply some normalization and remove duplicates
518
519
				// Convert into correct array. Array can contain space-separated
520
				// values. Implode/explode to get those into the main array as well.
521
				if ( is_array( $value ) ) {
522
					// If input wasn't an array, we can skip this step
523
					$newValue = [];
524
					foreach ( $value as $k => $v ) {
525
						if ( is_string( $v ) ) {
526
							// String values should be normal `array( 'foo' )`
527
							// Just append them
528
							if ( !isset( $value[$v] ) ) {
529
								// As a special case don't set 'foo' if a
530
								// separate 'foo' => true/false exists in the array
531
								// keys should be authoritative
532
								$newValue[] = $v;
533
							}
534
						} elseif ( $v ) {
535
							// If the value is truthy but not a string this is likely
536
							// an [ 'foo' => true ], falsy values don't add strings
537
							$newValue[] = $k;
538
						}
539
					}
540
					$value = implode( ' ', $newValue );
541
				}
542
				$value = explode( ' ', $value );
543
544
				// Normalize spacing by fixing up cases where people used
545
				// more than 1 space and/or a trailing/leading space
546
				$value = array_diff( $value, [ '', ' ' ] );
547
548
				// Remove duplicates and create the string
549
				$value = implode( ' ', array_unique( $value ) );
550
			} elseif ( is_array( $value ) ) {
551
				throw new MWException( "HTML attribute $key can not contain a list of values" );
552
			}
553
554
			$quote = '"';
555
556
			if ( in_array( $key, self::$boolAttribs ) ) {
557
				$ret .= " $key=\"\"";
558
			} else {
559
				// Apparently we need to entity-encode \n, \r, \t, although the
560
				// spec doesn't mention that.  Since we're doing strtr() anyway,
561
				// we may as well not call htmlspecialchars().
562
				// @todo FIXME: Verify that we actually need to
563
				// escape \n\r\t here, and explain why, exactly.
564
				// We could call Sanitizer::encodeAttribute() for this, but we
565
				// don't because we're stubborn and like our marginal savings on
566
				// byte size from not having to encode unnecessary quotes.
567
				// The only difference between this transform and the one by
568
				// Sanitizer::encodeAttribute() is ' is not encoded.
569
				$map = [
570
					'&' => '&amp;',
571
					'"' => '&quot;',
572
					'>' => '&gt;',
573
					// '<' allegedly allowed per spec
574
					// but breaks some tools if not escaped.
575
					"<" => '&lt;',
576
					"\n" => '&#10;',
577
					"\r" => '&#13;',
578
					"\t" => '&#9;'
579
				];
580
				$ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
581
			}
582
		}
583
		return $ret;
584
	}
585
586
	/**
587
	 * Output a "<script>" tag with the given contents.
588
	 *
589
	 * @todo do some useful escaping as well, like if $contents contains
590
	 * literal "</script>" or (for XML) literal "]]>".
591
	 *
592
	 * @param string $contents JavaScript
593
	 * @return string Raw HTML
594
	 */
595
	public static function inlineScript( $contents ) {
596
		$attrs = [];
597
598
		if ( preg_match( '/[<&]/', $contents ) ) {
599
			$contents = "/*<![CDATA[*/$contents/*]]>*/";
600
		}
601
602
		return self::rawElement( 'script', $attrs, $contents );
603
	}
604
605
	/**
606
	 * Output a "<script>" tag linking to the given URL, e.g.,
607
	 * "<script src=foo.js></script>".
608
	 *
609
	 * @param string $url
610
	 * @return string Raw HTML
611
	 */
612
	public static function linkedScript( $url ) {
613
		$attrs = [ 'src' => $url ];
614
615
		return self::element( 'script', $attrs );
616
	}
617
618
	/**
619
	 * Output a "<style>" tag with the given contents for the given media type
620
	 * (if any).  TODO: do some useful escaping as well, like if $contents
621
	 * contains literal "</style>" (admittedly unlikely).
622
	 *
623
	 * @param string $contents CSS
624
	 * @param string $media A media type string, like 'screen'
625
	 * @return string Raw HTML
626
	 */
627
	public static function inlineStyle( $contents, $media = 'all' ) {
628
		// Don't escape '>' since that is used
629
		// as direct child selector.
630
		// Remember, in css, there is no "x" for hexadecimal escapes, and
631
		// the space immediately after an escape sequence is swallowed.
632
		$contents = strtr( $contents, [
633
			'<' => '\3C ',
634
			// CDATA end tag for good measure, but the main security
635
			// is from escaping the '<'.
636
			']]>' => '\5D\5D\3E '
637
		] );
638
639
		if ( preg_match( '/[<&]/', $contents ) ) {
640
			$contents = "/*<![CDATA[*/$contents/*]]>*/";
641
		}
642
643
		return self::rawElement( 'style', [
644
			'media' => $media,
645
		], $contents );
646
	}
647
648
	/**
649
	 * Output a "<link rel=stylesheet>" linking to the given URL for the given
650
	 * media type (if any).
651
	 *
652
	 * @param string $url
653
	 * @param string $media A media type string, like 'screen'
654
	 * @return string Raw HTML
655
	 */
656
	public static function linkedStyle( $url, $media = 'all' ) {
657
		return self::element( 'link', [
658
			'rel' => 'stylesheet',
659
			'href' => $url,
660
			'media' => $media,
661
		] );
662
	}
663
664
	/**
665
	 * Convenience function to produce an "<input>" element.  This supports the
666
	 * new HTML5 input types and attributes.
667
	 *
668
	 * @param string $name Name attribute
669
	 * @param string $value Value attribute
670
	 * @param string $type Type attribute
671
	 * @param array $attribs Associative array of miscellaneous extra
672
	 *   attributes, passed to Html::element()
673
	 * @return string Raw HTML
674
	 */
675
	public static function input( $name, $value = '', $type = 'text', array $attribs = [] ) {
676
		$attribs['type'] = $type;
677
		$attribs['value'] = $value;
678
		$attribs['name'] = $name;
679
		if ( in_array( $type, [ 'text', 'search', 'email', 'password', 'number' ] ) ) {
680
			$attribs = self::getTextInputAttributes( $attribs );
681
		}
682
		if ( in_array( $type, [ 'button', 'reset', 'submit' ] ) ) {
683
			$attribs = self::buttonAttributes( $attribs );
684
		}
685
		return self::element( 'input', $attribs );
686
	}
687
688
	/**
689
	 * Convenience function to produce a checkbox (input element with type=checkbox)
690
	 *
691
	 * @param string $name Name attribute
692
	 * @param bool $checked Whether the checkbox is checked or not
693
	 * @param array $attribs Array of additional attributes
694
	 * @return string Raw HTML
695
	 */
696 View Code Duplication
	public static function check( $name, $checked = false, array $attribs = [] ) {
697
		if ( isset( $attribs['value'] ) ) {
698
			$value = $attribs['value'];
699
			unset( $attribs['value'] );
700
		} else {
701
			$value = 1;
702
		}
703
704
		if ( $checked ) {
705
			$attribs[] = 'checked';
706
		}
707
708
		return self::input( $name, $value, 'checkbox', $attribs );
709
	}
710
711
	/**
712
	 * Convenience function to produce a radio button (input element with type=radio)
713
	 *
714
	 * @param string $name Name attribute
715
	 * @param bool $checked Whether the radio button is checked or not
716
	 * @param array $attribs Array of additional attributes
717
	 * @return string Raw HTML
718
	 */
719 View Code Duplication
	public static function radio( $name, $checked = false, array $attribs = [] ) {
720
		if ( isset( $attribs['value'] ) ) {
721
			$value = $attribs['value'];
722
			unset( $attribs['value'] );
723
		} else {
724
			$value = 1;
725
		}
726
727
		if ( $checked ) {
728
			$attribs[] = 'checked';
729
		}
730
731
		return self::input( $name, $value, 'radio', $attribs );
732
	}
733
734
	/**
735
	 * Convenience function for generating a label for inputs.
736
	 *
737
	 * @param string $label Contents of the label
738
	 * @param string $id ID of the element being labeled
739
	 * @param array $attribs Additional attributes
740
	 * @return string Raw HTML
741
	 */
742
	public static function label( $label, $id, array $attribs = [] ) {
743
		$attribs += [
744
			'for' => $id
745
		];
746
		return self::element( 'label', $attribs, $label );
747
	}
748
749
	/**
750
	 * Convenience function to produce an input element with type=hidden
751
	 *
752
	 * @param string $name Name attribute
753
	 * @param string $value Value attribute
754
	 * @param array $attribs Associative array of miscellaneous extra
755
	 *   attributes, passed to Html::element()
756
	 * @return string Raw HTML
757
	 */
758
	public static function hidden( $name, $value, array $attribs = [] ) {
759
		return self::input( $name, $value, 'hidden', $attribs );
760
	}
761
762
	/**
763
	 * Convenience function to produce a <textarea> element.
764
	 *
765
	 * This supports leaving out the cols= and rows= which Xml requires and are
766
	 * required by HTML4/XHTML but not required by HTML5.
767
	 *
768
	 * @param string $name Name attribute
769
	 * @param string $value Value attribute
770
	 * @param array $attribs Associative array of miscellaneous extra
771
	 *   attributes, passed to Html::element()
772
	 * @return string Raw HTML
773
	 */
774
	public static function textarea( $name, $value = '', array $attribs = [] ) {
775
		$attribs['name'] = $name;
776
777
		if ( substr( $value, 0, 1 ) == "\n" ) {
778
			// Workaround for bug 12130: browsers eat the initial newline
779
			// assuming that it's just for show, but they do keep the later
780
			// newlines, which we may want to preserve during editing.
781
			// Prepending a single newline
782
			$spacedValue = "\n" . $value;
783
		} else {
784
			$spacedValue = $value;
785
		}
786
		return self::element( 'textarea', self::getTextInputAttributes( $attribs ), $spacedValue );
787
	}
788
789
	/**
790
	 * Helper for Html::namespaceSelector().
791
	 * @param array $params See Html::namespaceSelector()
792
	 * @return array
793
	 */
794
	public static function namespaceSelectorOptions( array $params = [] ) {
795
		global $wgContLang;
796
797
		$options = [];
798
799
		if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) {
800
			$params['exclude'] = [];
801
		}
802
803
		if ( isset( $params['all'] ) ) {
804
			// add an option that would let the user select all namespaces.
805
			// Value is provided by user, the name shown is localized for the user.
806
			$options[$params['all']] = wfMessage( 'namespacesall' )->text();
807
		}
808
		// Add all namespaces as options (in the content language)
809
		$options += $wgContLang->getFormattedNamespaces();
810
811
		$optionsOut = [];
812
		// Filter out namespaces below 0 and massage labels
813
		foreach ( $options as $nsId => $nsName ) {
814
			if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
815
				continue;
816
			}
817
			if ( $nsId === NS_MAIN ) {
818
				// For other namespaces use the namespace prefix as label, but for
819
				// main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)")
820
				$nsName = wfMessage( 'blanknamespace' )->text();
821
			} elseif ( is_int( $nsId ) ) {
822
				$nsName = $wgContLang->convertNamespace( $nsId );
823
			}
824
			$optionsOut[$nsId] = $nsName;
825
		}
826
827
		return $optionsOut;
828
	}
829
830
	/**
831
	 * Build a drop-down box for selecting a namespace
832
	 *
833
	 * @param array $params Params to set.
834
	 * - selected: [optional] Id of namespace which should be pre-selected
835
	 * - all: [optional] Value of item for "all namespaces". If null or unset,
836
	 *   no "<option>" is generated to select all namespaces.
837
	 * - label: text for label to add before the field.
838
	 * - exclude: [optional] Array of namespace ids to exclude.
839
	 * - disable: [optional] Array of namespace ids for which the option should
840
	 *   be disabled in the selector.
841
	 * @param array $selectAttribs HTML attributes for the generated select element.
842
	 * - id:   [optional], default: 'namespace'.
843
	 * - name: [optional], default: 'namespace'.
844
	 * @return string HTML code to select a namespace.
845
	 */
846
	public static function namespaceSelector( array $params = [],
847
		array $selectAttribs = []
848
	) {
849
		ksort( $selectAttribs );
850
851
		// Is a namespace selected?
852
		if ( isset( $params['selected'] ) ) {
853
			// If string only contains digits, convert to clean int. Selected could also
854
			// be "all" or "" etc. which needs to be left untouched.
855
			// PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues
856
			// and returns false for already clean ints. Use regex instead..
857
			if ( preg_match( '/^\d+$/', $params['selected'] ) ) {
858
				$params['selected'] = intval( $params['selected'] );
859
			}
860
			// else: leaves it untouched for later processing
861
		} else {
862
			$params['selected'] = '';
863
		}
864
865
		if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) {
866
			$params['disable'] = [];
867
		}
868
869
		// Associative array between option-values and option-labels
870
		$options = self::namespaceSelectorOptions( $params );
871
872
		// Convert $options to HTML
873
		$optionsHtml = [];
874
		foreach ( $options as $nsId => $nsName ) {
875
			$optionsHtml[] = self::element(
876
				'option', [
877
					'disabled' => in_array( $nsId, $params['disable'] ),
878
					'value' => $nsId,
879
					'selected' => $nsId === $params['selected'],
880
				], $nsName
881
			);
882
		}
883
884
		if ( !array_key_exists( 'id', $selectAttribs ) ) {
885
			$selectAttribs['id'] = 'namespace';
886
		}
887
888
		if ( !array_key_exists( 'name', $selectAttribs ) ) {
889
			$selectAttribs['name'] = 'namespace';
890
		}
891
892
		$ret = '';
893
		if ( isset( $params['label'] ) ) {
894
			$ret .= self::element(
895
				'label', [
896
					'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null,
897
				], $params['label']
898
			) . '&#160;';
899
		}
900
901
		// Wrap options in a <select>
902
		$ret .= self::openElement( 'select', $selectAttribs )
903
			. "\n"
904
			. implode( "\n", $optionsHtml )
905
			. "\n"
906
			. self::closeElement( 'select' );
907
908
		return $ret;
909
	}
910
911
	/**
912
	 * Constructs the opening html-tag with necessary doctypes depending on
913
	 * global variables.
914
	 *
915
	 * @param array $attribs Associative array of miscellaneous extra
916
	 *   attributes, passed to Html::element() of html tag.
917
	 * @return string Raw HTML
918
	 */
919
	public static function htmlHeader( array $attribs = [] ) {
920
		$ret = '';
921
922
		global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces;
923
924
		$isXHTML = self::isXmlMimeType( $wgMimeType );
925
926
		if ( $isXHTML ) { // XHTML5
927
			// XML MIME-typed markup should have an xml header.
928
			// However a DOCTYPE is not needed.
929
			$ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
930
931
			// Add the standard xmlns
932
			$attribs['xmlns'] = 'http://www.w3.org/1999/xhtml';
933
934
			// And support custom namespaces
935
			foreach ( $wgXhtmlNamespaces as $tag => $ns ) {
936
				$attribs["xmlns:$tag"] = $ns;
937
			}
938
		} else { // HTML5
939
			// DOCTYPE
940
			$ret .= "<!DOCTYPE html>\n";
941
		}
942
943
		if ( $wgHtml5Version ) {
944
			$attribs['version'] = $wgHtml5Version;
945
		}
946
947
		$ret .= self::openElement( 'html', $attribs );
948
949
		return $ret;
950
	}
951
952
	/**
953
	 * Determines if the given MIME type is xml.
954
	 *
955
	 * @param string $mimetype MIME type
956
	 * @return bool
957
	 */
958
	public static function isXmlMimeType( $mimetype ) {
959
		# http://www.whatwg.org/html/infrastructure.html#xml-mime-type
960
		# * text/xml
961
		# * application/xml
962
		# * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
963
		return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
964
	}
965
966
	/**
967
	 * Get HTML for an info box with an icon.
968
	 *
969
	 * @param string $text Wikitext, get this with wfMessage()->plain()
970
	 * @param string $icon Path to icon file (used as 'src' attribute)
971
	 * @param string $alt Alternate text for the icon
972
	 * @param string $class Additional class name to add to the wrapper div
973
	 *
974
	 * @return string
975
	 */
976
	static function infoBox( $text, $icon, $alt, $class = '' ) {
977
		$s = self::openElement( 'div', [ 'class' => "mw-infobox $class" ] );
978
979
		$s .= self::openElement( 'div', [ 'class' => 'mw-infobox-left' ] ) .
980
				self::element( 'img',
981
					[
982
						'src' => $icon,
983
						'alt' => $alt,
984
					]
985
				) .
986
				self::closeElement( 'div' );
987
988
		$s .= self::openElement( 'div', [ 'class' => 'mw-infobox-right' ] ) .
989
				$text .
990
				self::closeElement( 'div' );
991
		$s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
992
993
		$s .= self::closeElement( 'div' );
994
995
		$s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
996
997
		return $s;
998
	}
999
1000
	/**
1001
	 * Generate a srcset attribute value.
1002
	 *
1003
	 * Generates a srcset attribute value from an array mapping pixel densities
1004
	 * to URLs. A trailing 'x' in pixel density values is optional.
1005
	 *
1006
	 * @note srcset width and height values are not supported.
1007
	 *
1008
	 * @see http://www.whatwg.org/html/embedded-content-1.html#attr-img-srcset
1009
	 *
1010
	 * @par Example:
1011
	 * @code
1012
	 *     Html::srcSet( [
1013
	 *         '1x'   => 'standard.jpeg',
1014
	 *         '1.5x' => 'large.jpeg',
1015
	 *         '3x'   => 'extra-large.jpeg',
1016
	 *     ] );
1017
	 *     // gives 'standard.jpeg 1x, large.jpeg 1.5x, extra-large.jpeg 2x'
1018
	 * @endcode
1019
	 *
1020
	 * @param string[] $urls
1021
	 * @return string
1022
	 */
1023
	static function srcSet( array $urls ) {
1024
		$candidates = [];
1025
		foreach ( $urls as $density => $url ) {
1026
			// Cast density to float to strip 'x', then back to string to serve
1027
			// as array index.
1028
			$density = (string)(float)$density;
1029
			$candidates[$density] = $url;
1030
		}
1031
1032
		// Remove duplicates that are the same as a smaller value
1033
		ksort( $candidates, SORT_NUMERIC );
1034
		$candidates = array_unique( $candidates );
1035
1036
		// Append density info to the url
1037
		foreach ( $candidates as $density => $url ) {
1038
			$candidates[$density] = $url . ' ' . $density . 'x';
1039
		}
1040
1041
		return implode( ", ", $candidates );
1042
	}
1043
}
1044