MediaWiki  1.27.1
Html.php
Go to the documentation of this file.
1 <?php
48 class Html {
49  // List of void elements from HTML5, section 8.1.2 as of 2011-08-12
50  private static $voidElements = [
51  'area',
52  'base',
53  'br',
54  'col',
55  'command',
56  'embed',
57  'hr',
58  'img',
59  'input',
60  'keygen',
61  'link',
62  'meta',
63  'param',
64  'source',
65  'track',
66  'wbr',
67  ];
68 
69  // Boolean attributes, which may have the value omitted entirely. Manually
70  // collected from the HTML5 spec as of 2011-08-12.
71  private static $boolAttribs = [
72  'async',
73  'autofocus',
74  'autoplay',
75  'checked',
76  'controls',
77  'default',
78  'defer',
79  'disabled',
80  'formnovalidate',
81  'hidden',
82  'ismap',
83  'itemscope',
84  'loop',
85  'multiple',
86  'muted',
87  'novalidate',
88  'open',
89  'pubdate',
90  'readonly',
91  'required',
92  'reversed',
93  'scoped',
94  'seamless',
95  'selected',
96  'truespeed',
97  'typemustmatch',
98  // HTML5 Microdata
99  'itemscope',
100  ];
101 
110  public static function buttonAttributes( array $attrs, array $modifiers = [] ) {
112  if ( $wgUseMediaWikiUIEverywhere ) {
113  if ( isset( $attrs['class'] ) ) {
114  if ( is_array( $attrs['class'] ) ) {
115  $attrs['class'][] = 'mw-ui-button';
116  $attrs['class'] = array_merge( $attrs['class'], $modifiers );
117  // ensure compatibility with Xml
118  $attrs['class'] = implode( ' ', $attrs['class'] );
119  } else {
120  $attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers );
121  }
122  } else {
123  // ensure compatibility with Xml
124  $attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers );
125  }
126  }
127  return $attrs;
128  }
129 
137  public static function getTextInputAttributes( array $attrs ) {
139  if ( $wgUseMediaWikiUIEverywhere ) {
140  if ( isset( $attrs['class'] ) ) {
141  if ( is_array( $attrs['class'] ) ) {
142  $attrs['class'][] = 'mw-ui-input';
143  } else {
144  $attrs['class'] .= ' mw-ui-input';
145  }
146  } else {
147  $attrs['class'] = 'mw-ui-input';
148  }
149  }
150  return $attrs;
151  }
152 
166  public static function linkButton( $contents, array $attrs, array $modifiers = [] ) {
167  return self::element( 'a',
168  self::buttonAttributes( $attrs, $modifiers ),
169  $contents
170  );
171  }
172 
186  public static function submitButton( $contents, array $attrs, array $modifiers = [] ) {
187  $attrs['type'] = 'submit';
188  $attrs['value'] = $contents;
189  return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) );
190  }
191 
210  public static function rawElement( $element, $attribs = [], $contents = '' ) {
211  $start = self::openElement( $element, $attribs );
212  if ( in_array( $element, self::$voidElements ) ) {
213  // Silly XML.
214  return substr( $start, 0, -1 ) . '/>';
215  } else {
216  return "$start$contents" . self::closeElement( $element );
217  }
218  }
219 
230  public static function element( $element, $attribs = [], $contents = '' ) {
231  return self::rawElement( $element, $attribs, strtr( $contents, [
232  // There's no point in escaping quotes, >, etc. in the contents of
233  // elements.
234  '&' => '&amp;',
235  '<' => '&lt;'
236  ] ) );
237  }
238 
248  public static function openElement( $element, $attribs = [] ) {
250  // This is not required in HTML5, but let's do it anyway, for
251  // consistency and better compression.
252  $element = strtolower( $element );
253 
254  // Remove invalid input types
255  if ( $element == 'input' ) {
256  $validTypes = [
257  'hidden',
258  'text',
259  'password',
260  'checkbox',
261  'radio',
262  'file',
263  'submit',
264  'image',
265  'reset',
266  'button',
267 
268  // HTML input types
269  'datetime',
270  'datetime-local',
271  'date',
272  'month',
273  'time',
274  'week',
275  'number',
276  'range',
277  'email',
278  'url',
279  'search',
280  'tel',
281  'color',
282  ];
283  if ( isset( $attribs['type'] ) && !in_array( $attribs['type'], $validTypes ) ) {
284  unset( $attribs['type'] );
285  }
286  }
287 
288  // According to standard the default type for <button> elements is "submit".
289  // Depending on compatibility mode IE might use "button", instead.
290  // We enforce the standard "submit".
291  if ( $element == 'button' && !isset( $attribs['type'] ) ) {
292  $attribs['type'] = 'submit';
293  }
294 
295  return "<$element" . self::expandAttributes(
296  self::dropDefaults( $element, $attribs ) ) . '>';
297  }
298 
306  public static function closeElement( $element ) {
307  $element = strtolower( $element );
308 
309  return "</$element>";
310  }
311 
329  private static function dropDefaults( $element, array $attribs ) {
330  // Whenever altering this array, please provide a covering test case
331  // in HtmlTest::provideElementsWithAttributesHavingDefaultValues
332  static $attribDefaults = [
333  'area' => [ 'shape' => 'rect' ],
334  'button' => [
335  'formaction' => 'GET',
336  'formenctype' => 'application/x-www-form-urlencoded',
337  ],
338  'canvas' => [
339  'height' => '150',
340  'width' => '300',
341  ],
342  'command' => [ 'type' => 'command' ],
343  'form' => [
344  'action' => 'GET',
345  'autocomplete' => 'on',
346  'enctype' => 'application/x-www-form-urlencoded',
347  ],
348  'input' => [
349  'formaction' => 'GET',
350  'type' => 'text',
351  ],
352  'keygen' => [ 'keytype' => 'rsa' ],
353  'link' => [ 'media' => 'all' ],
354  'menu' => [ 'type' => 'list' ],
355  'script' => [ 'type' => 'text/javascript' ],
356  'style' => [
357  'media' => 'all',
358  'type' => 'text/css',
359  ],
360  'textarea' => [ 'wrap' => 'soft' ],
361  ];
362 
363  $element = strtolower( $element );
364 
365  foreach ( $attribs as $attrib => $value ) {
366  $lcattrib = strtolower( $attrib );
367  if ( is_array( $value ) ) {
368  $value = implode( ' ', $value );
369  } else {
370  $value = strval( $value );
371  }
372 
373  // Simple checks using $attribDefaults
374  if ( isset( $attribDefaults[$element][$lcattrib] )
375  && $attribDefaults[$element][$lcattrib] == $value
376  ) {
377  unset( $attribs[$attrib] );
378  }
379 
380  if ( $lcattrib == 'class' && $value == '' ) {
381  unset( $attribs[$attrib] );
382  }
383  }
384 
385  // More subtle checks
386  if ( $element === 'link'
387  && isset( $attribs['type'] ) && strval( $attribs['type'] ) == 'text/css'
388  ) {
389  unset( $attribs['type'] );
390  }
391  if ( $element === 'input' ) {
392  $type = isset( $attribs['type'] ) ? $attribs['type'] : null;
393  $value = isset( $attribs['value'] ) ? $attribs['value'] : null;
394  if ( $type === 'checkbox' || $type === 'radio' ) {
395  // The default value for checkboxes and radio buttons is 'on'
396  // not ''. By stripping value="" we break radio boxes that
397  // actually wants empty values.
398  if ( $value === 'on' ) {
399  unset( $attribs['value'] );
400  }
401  } elseif ( $type === 'submit' ) {
402  // The default value for submit appears to be "Submit" but
403  // let's not bother stripping out localized text that matches
404  // that.
405  } else {
406  // The default value for nearly every other field type is ''
407  // The 'range' and 'color' types use different defaults but
408  // stripping a value="" does not hurt them.
409  if ( $value === '' ) {
410  unset( $attribs['value'] );
411  }
412  }
413  }
414  if ( $element === 'select' && isset( $attribs['size'] ) ) {
415  if ( in_array( 'multiple', $attribs )
416  || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
417  ) {
418  // A multi-select
419  if ( strval( $attribs['size'] ) == '4' ) {
420  unset( $attribs['size'] );
421  }
422  } else {
423  // Single select
424  if ( strval( $attribs['size'] ) == '1' ) {
425  unset( $attribs['size'] );
426  }
427  }
428  }
429 
430  return $attribs;
431  }
432 
472  public static function expandAttributes( array $attribs ) {
473  $ret = '';
474  foreach ( $attribs as $key => $value ) {
475  // Support intuitive array( 'checked' => true/false ) form
476  if ( $value === false || is_null( $value ) ) {
477  continue;
478  }
479 
480  // For boolean attributes, support array( 'foo' ) instead of
481  // requiring array( 'foo' => 'meaningless' ).
482  if ( is_int( $key ) && in_array( strtolower( $value ), self::$boolAttribs ) ) {
483  $key = $value;
484  }
485 
486  // Not technically required in HTML5 but we'd like consistency
487  // and better compression anyway.
488  $key = strtolower( $key );
489 
490  // Bug 23769: Blacklist all form validation attributes for now. Current
491  // (June 2010) WebKit has no UI, so the form just refuses to submit
492  // without telling the user why, which is much worse than failing
493  // server-side validation. Opera is the only other implementation at
494  // this time, and has ugly UI, so just kill the feature entirely until
495  // we have at least one good implementation.
496 
497  // As the default value of "1" for "step" rejects decimal
498  // numbers to be entered in 'type="number"' fields, allow
499  // the special case 'step="any"'.
500 
501  if ( in_array( $key, [ 'max', 'min', 'pattern', 'required' ] )
502  || $key === 'step' && $value !== 'any' ) {
503  continue;
504  }
505 
506  // http://www.w3.org/TR/html401/index/attributes.html ("space-separated")
507  // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated")
508  $spaceSeparatedListAttributes = [
509  'class', // html4, html5
510  'accesskey', // as of html5, multiple space-separated values allowed
511  // html4-spec doesn't document rel= as space-separated
512  // but has been used like that and is now documented as such
513  // in the html5-spec.
514  'rel',
515  ];
516 
517  // Specific features for attributes that allow a list of space-separated values
518  if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
519  // Apply some normalization and remove duplicates
520 
521  // Convert into correct array. Array can contain space-separated
522  // values. Implode/explode to get those into the main array as well.
523  if ( is_array( $value ) ) {
524  // If input wasn't an array, we can skip this step
525  $newValue = [];
526  foreach ( $value as $k => $v ) {
527  if ( is_string( $v ) ) {
528  // String values should be normal `array( 'foo' )`
529  // Just append them
530  if ( !isset( $value[$v] ) ) {
531  // As a special case don't set 'foo' if a
532  // separate 'foo' => true/false exists in the array
533  // keys should be authoritative
534  $newValue[] = $v;
535  }
536  } elseif ( $v ) {
537  // If the value is truthy but not a string this is likely
538  // an array( 'foo' => true ), falsy values don't add strings
539  $newValue[] = $k;
540  }
541  }
542  $value = implode( ' ', $newValue );
543  }
544  $value = explode( ' ', $value );
545 
546  // Normalize spacing by fixing up cases where people used
547  // more than 1 space and/or a trailing/leading space
548  $value = array_diff( $value, [ '', ' ' ] );
549 
550  // Remove duplicates and create the string
551  $value = implode( ' ', array_unique( $value ) );
552  } elseif ( is_array( $value ) ) {
553  throw new MWException( "HTML attribute $key can not contain a list of values" );
554  }
555 
556  $quote = '"';
557 
558  if ( in_array( $key, self::$boolAttribs ) ) {
559  $ret .= " $key=\"\"";
560  } else {
561  // Apparently we need to entity-encode \n, \r, \t, although the
562  // spec doesn't mention that. Since we're doing strtr() anyway,
563  // we may as well not call htmlspecialchars().
564  // @todo FIXME: Verify that we actually need to
565  // escape \n\r\t here, and explain why, exactly.
566  // We could call Sanitizer::encodeAttribute() for this, but we
567  // don't because we're stubborn and like our marginal savings on
568  // byte size from not having to encode unnecessary quotes.
569  // The only difference between this transform and the one by
570  // Sanitizer::encodeAttribute() is ' is not encoded.
571  $map = [
572  '&' => '&amp;',
573  '"' => '&quot;',
574  '>' => '&gt;',
575  // '<' allegedly allowed per spec
576  // but breaks some tools if not escaped.
577  "<" => '&lt;',
578  "\n" => '&#10;',
579  "\r" => '&#13;',
580  "\t" => '&#9;'
581  ];
582  $ret .= " $key=$quote" . strtr( $value, $map ) . $quote;
583  }
584  }
585  return $ret;
586  }
587 
597  public static function inlineScript( $contents ) {
598  $attrs = [];
599 
600  if ( preg_match( '/[<&]/', $contents ) ) {
601  $contents = "/*<![CDATA[*/$contents/*]]>*/";
602  }
603 
604  return self::rawElement( 'script', $attrs, $contents );
605  }
606 
614  public static function linkedScript( $url ) {
615  $attrs = [ 'src' => $url ];
616 
617  return self::element( 'script', $attrs );
618  }
619 
628  public static function inlineStyle( $contents, $media = 'all' ) {
629  // Don't escape '>' since that is used
630  // as direct child selector.
631  // Remember, in css, there is no "x" for hexadecimal escapes, and
632  // the space immediately after an escape sequence is swallowed.
633  $contents = strtr( $contents, [
634  '<' => '\3C ',
635  // CDATA end tag for good measure, but the main security
636  // is from escaping the '<'.
637  ']]>' => '\5D\5D\3E '
638  ] );
639 
640  if ( preg_match( '/[<&]/', $contents ) ) {
641  $contents = "/*<![CDATA[*/$contents/*]]>*/";
642  }
643 
644  return self::rawElement( 'style', [
645  'media' => $media,
646  ], $contents );
647  }
648 
657  public static function linkedStyle( $url, $media = 'all' ) {
658  return self::element( 'link', [
659  'rel' => 'stylesheet',
660  'href' => $url,
661  'media' => $media,
662  ] );
663  }
664 
676  public static function input( $name, $value = '', $type = 'text', array $attribs = [] ) {
677  $attribs['type'] = $type;
678  $attribs['value'] = $value;
679  $attribs['name'] = $name;
680  if ( in_array( $type, [ 'text', 'search', 'email', 'password', 'number' ] ) ) {
681  $attribs = self::getTextInputAttributes( $attribs );
682  }
683  if ( in_array( $type, [ 'button', 'reset', 'submit' ] ) ) {
684  $attribs = self::buttonAttributes( $attribs );
685  }
686  return self::element( 'input', $attribs );
687  }
688 
697  public static function check( $name, $checked = false, array $attribs = [] ) {
698  if ( isset( $attribs['value'] ) ) {
699  $value = $attribs['value'];
700  unset( $attribs['value'] );
701  } else {
702  $value = 1;
703  }
704 
705  if ( $checked ) {
706  $attribs[] = 'checked';
707  }
708 
709  return self::input( $name, $value, 'checkbox', $attribs );
710  }
711 
720  public static function radio( $name, $checked = false, array $attribs = [] ) {
721  if ( isset( $attribs['value'] ) ) {
722  $value = $attribs['value'];
723  unset( $attribs['value'] );
724  } else {
725  $value = 1;
726  }
727 
728  if ( $checked ) {
729  $attribs[] = 'checked';
730  }
731 
732  return self::input( $name, $value, 'radio', $attribs );
733  }
734 
743  public static function label( $label, $id, array $attribs = [] ) {
744  $attribs += [
745  'for' => $id
746  ];
747  return self::element( 'label', $attribs, $label );
748  }
749 
759  public static function hidden( $name, $value, array $attribs = [] ) {
760  return self::input( $name, $value, 'hidden', $attribs );
761  }
762 
775  public static function textarea( $name, $value = '', array $attribs = [] ) {
776  $attribs['name'] = $name;
777 
778  if ( substr( $value, 0, 1 ) == "\n" ) {
779  // Workaround for bug 12130: browsers eat the initial newline
780  // assuming that it's just for show, but they do keep the later
781  // newlines, which we may want to preserve during editing.
782  // Prepending a single newline
783  $spacedValue = "\n" . $value;
784  } else {
785  $spacedValue = $value;
786  }
787  return self::element( 'textarea', self::getTextInputAttributes( $attribs ), $spacedValue );
788  }
789 
795  public static function namespaceSelectorOptions( array $params = [] ) {
797 
798  $options = [];
799 
800  if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) {
801  $params['exclude'] = [];
802  }
803 
804  if ( isset( $params['all'] ) ) {
805  // add an option that would let the user select all namespaces.
806  // Value is provided by user, the name shown is localized for the user.
807  $options[$params['all']] = wfMessage( 'namespacesall' )->text();
808  }
809  // Add all namespaces as options (in the content language)
810  $options += $wgContLang->getFormattedNamespaces();
811 
812  $optionsOut = [];
813  // Filter out namespaces below 0 and massage labels
814  foreach ( $options as $nsId => $nsName ) {
815  if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
816  continue;
817  }
818  if ( $nsId === NS_MAIN ) {
819  // For other namespaces use the namespace prefix as label, but for
820  // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)")
821  $nsName = wfMessage( 'blanknamespace' )->text();
822  } elseif ( is_int( $nsId ) ) {
823  $nsName = $wgContLang->convertNamespace( $nsId );
824  }
825  $optionsOut[$nsId] = $nsName;
826  }
827 
828  return $optionsOut;
829  }
830 
847  public static function namespaceSelector( array $params = [],
848  array $selectAttribs = []
849  ) {
850  ksort( $selectAttribs );
851 
852  // Is a namespace selected?
853  if ( isset( $params['selected'] ) ) {
854  // If string only contains digits, convert to clean int. Selected could also
855  // be "all" or "" etc. which needs to be left untouched.
856  // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues
857  // and returns false for already clean ints. Use regex instead..
858  if ( preg_match( '/^\d+$/', $params['selected'] ) ) {
859  $params['selected'] = intval( $params['selected'] );
860  }
861  // else: leaves it untouched for later processing
862  } else {
863  $params['selected'] = '';
864  }
865 
866  if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) {
867  $params['disable'] = [];
868  }
869 
870  // Associative array between option-values and option-labels
871  $options = self::namespaceSelectorOptions( $params );
872 
873  // Convert $options to HTML
874  $optionsHtml = [];
875  foreach ( $options as $nsId => $nsName ) {
876  $optionsHtml[] = self::element(
877  'option', [
878  'disabled' => in_array( $nsId, $params['disable'] ),
879  'value' => $nsId,
880  'selected' => $nsId === $params['selected'],
881  ], $nsName
882  );
883  }
884 
885  if ( !array_key_exists( 'id', $selectAttribs ) ) {
886  $selectAttribs['id'] = 'namespace';
887  }
888 
889  if ( !array_key_exists( 'name', $selectAttribs ) ) {
890  $selectAttribs['name'] = 'namespace';
891  }
892 
893  $ret = '';
894  if ( isset( $params['label'] ) ) {
895  $ret .= self::element(
896  'label', [
897  'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null,
898  ], $params['label']
899  ) . '&#160;';
900  }
901 
902  // Wrap options in a <select>
903  $ret .= self::openElement( 'select', $selectAttribs )
904  . "\n"
905  . implode( "\n", $optionsHtml )
906  . "\n"
907  . self::closeElement( 'select' );
908 
909  return $ret;
910  }
911 
920  public static function htmlHeader( array $attribs = [] ) {
921  $ret = '';
922 
924 
925  $isXHTML = self::isXmlMimeType( $wgMimeType );
926 
927  if ( $isXHTML ) { // XHTML5
928  // XML MIME-typed markup should have an xml header.
929  // However a DOCTYPE is not needed.
930  $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
931 
932  // Add the standard xmlns
933  $attribs['xmlns'] = 'http://www.w3.org/1999/xhtml';
934 
935  // And support custom namespaces
936  foreach ( $wgXhtmlNamespaces as $tag => $ns ) {
937  $attribs["xmlns:$tag"] = $ns;
938  }
939  } else { // HTML5
940  // DOCTYPE
941  $ret .= "<!DOCTYPE html>\n";
942  }
943 
944  if ( $wgHtml5Version ) {
945  $attribs['version'] = $wgHtml5Version;
946  }
947 
948  $html = self::openElement( 'html', $attribs );
949 
950  if ( $html ) {
951  $html .= "\n";
952  }
953 
954  $ret .= $html;
955 
956  return $ret;
957  }
958 
965  public static function isXmlMimeType( $mimetype ) {
966  # http://www.whatwg.org/html/infrastructure.html#xml-mime-type
967  # * text/xml
968  # * application/xml
969  # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
970  return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
971  }
972 
983  static function infoBox( $text, $icon, $alt, $class = '' ) {
984  $s = self::openElement( 'div', [ 'class' => "mw-infobox $class" ] );
985 
986  $s .= self::openElement( 'div', [ 'class' => 'mw-infobox-left' ] ) .
987  self::element( 'img',
988  [
989  'src' => $icon,
990  'alt' => $alt,
991  ]
992  ) .
993  self::closeElement( 'div' );
994 
995  $s .= self::openElement( 'div', [ 'class' => 'mw-infobox-right' ] ) .
996  $text .
997  self::closeElement( 'div' );
998  $s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
999 
1000  $s .= self::closeElement( 'div' );
1001 
1002  $s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
1003 
1004  return $s;
1005  }
1006 
1030  static function srcSet( array $urls ) {
1031  $candidates = [];
1032  foreach ( $urls as $density => $url ) {
1033  // Cast density to float to strip 'x'.
1034  $candidates[] = $url . ' ' . (float)$density . 'x';
1035  }
1036  return implode( ", ", $candidates );
1037  }
1038 }
static closeElement($element)
Returns "".
Definition: Html.php:306
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1798
static inlineScript($contents)
Output a "".
Definition: Html.php:614
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
static infoBox($text, $icon, $alt, $class= '')
Get HTML for an info box with an icon.
Definition: Html.php:983
static radio($name, $checked=false, array $attribs=[])
Convenience function to produce a radio button (input element with type=radio)
Definition: Html.php:720
static $voidElements
Definition: Html.php:50
static getTextInputAttributes(array $attrs)
Modifies a set of attributes meant for text input elements and apply a set of default attributes...
Definition: Html.php:137
static openElement($element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:248
static textarea($name, $value= '', array $attribs=[])
Convenience function to produce a