MediaWiki
REL1_19
|
00001 <?php 00050 class Html { 00051 # List of void elements from HTML5, section 8.1.2 as of 2011-08-12 00052 private static $voidElements = array( 00053 'area', 00054 'base', 00055 'br', 00056 'col', 00057 'command', 00058 'embed', 00059 'hr', 00060 'img', 00061 'input', 00062 'keygen', 00063 'link', 00064 'meta', 00065 'param', 00066 'source', 00067 'track', 00068 'wbr', 00069 ); 00070 00071 # Boolean attributes, which may have the value omitted entirely. Manually 00072 # collected from the HTML5 spec as of 2011-08-12. 00073 private static $boolAttribs = array( 00074 'async', 00075 'autofocus', 00076 'autoplay', 00077 'checked', 00078 'controls', 00079 'default', 00080 'defer', 00081 'disabled', 00082 'formnovalidate', 00083 'hidden', 00084 'ismap', 00085 'itemscope', 00086 'loop', 00087 'multiple', 00088 'muted', 00089 'novalidate', 00090 'open', 00091 'pubdate', 00092 'readonly', 00093 'required', 00094 'reversed', 00095 'scoped', 00096 'seamless', 00097 'selected', 00098 'truespeed', 00099 'typemustmatch', 00100 # HTML5 Microdata 00101 'itemscope', 00102 ); 00103 00104 private static $HTMLFiveOnlyAttribs = array( 00105 'autocomplete', 00106 'autofocus', 00107 'max', 00108 'min', 00109 'multiple', 00110 'pattern', 00111 'placeholder', 00112 'required', 00113 'step', 00114 'spellcheck', 00115 ); 00116 00137 public static function rawElement( $element, $attribs = array(), $contents = '' ) { 00138 global $wgWellFormedXml; 00139 $start = self::openElement( $element, $attribs ); 00140 if ( in_array( $element, self::$voidElements ) ) { 00141 if ( $wgWellFormedXml ) { 00142 # Silly XML. 00143 return substr( $start, 0, -1 ) . ' />'; 00144 } 00145 return $start; 00146 } else { 00147 return "$start$contents" . self::closeElement( $element ); 00148 } 00149 } 00150 00161 public static function element( $element, $attribs = array(), $contents = '' ) { 00162 return self::rawElement( $element, $attribs, strtr( $contents, array( 00163 # There's no point in escaping quotes, >, etc. in the contents of 00164 # elements. 00165 '&' => '&', 00166 '<' => '<' 00167 ) ) ); 00168 } 00169 00179 public static function openElement( $element, $attribs = array() ) { 00180 global $wgHtml5, $wgWellFormedXml; 00181 $attribs = (array)$attribs; 00182 # This is not required in HTML5, but let's do it anyway, for 00183 # consistency and better compression. 00184 $element = strtolower( $element ); 00185 00186 # In text/html, initial <html> and <head> tags can be omitted under 00187 # pretty much any sane circumstances, if they have no attributes. See: 00188 # <http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags> 00189 if ( !$wgWellFormedXml && !$attribs 00190 && in_array( $element, array( 'html', 'head' ) ) ) { 00191 return ''; 00192 } 00193 00194 # Remove HTML5-only attributes if we aren't doing HTML5, and disable 00195 # form validation regardless (see bug 23769 and the more detailed 00196 # comment in expandAttributes()) 00197 if ( $element == 'input' ) { 00198 # Whitelist of types that don't cause validation. All except 00199 # 'search' are valid in XHTML1. 00200 $validTypes = array( 00201 'hidden', 00202 'text', 00203 'password', 00204 'checkbox', 00205 'radio', 00206 'file', 00207 'submit', 00208 'image', 00209 'reset', 00210 'button', 00211 'search', 00212 ); 00213 00214 if ( isset( $attribs['type'] ) 00215 && !in_array( $attribs['type'], $validTypes ) ) { 00216 unset( $attribs['type'] ); 00217 } 00218 00219 if ( isset( $attribs['type'] ) && $attribs['type'] == 'search' 00220 && !$wgHtml5 ) { 00221 unset( $attribs['type'] ); 00222 } 00223 } 00224 00225 if ( !$wgHtml5 && $element == 'textarea' && isset( $attribs['maxlength'] ) ) { 00226 unset( $attribs['maxlength'] ); 00227 } 00228 00229 return "<$element" . self::expandAttributes( 00230 self::dropDefaults( $element, $attribs ) ) . '>'; 00231 } 00232 00241 public static function closeElement( $element ) { 00242 global $wgWellFormedXml; 00243 00244 $element = strtolower( $element ); 00245 00246 # Reference: 00247 # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags 00248 if ( !$wgWellFormedXml && in_array( $element, array( 00249 'html', 00250 'head', 00251 'body', 00252 'li', 00253 'dt', 00254 'dd', 00255 'tr', 00256 'td', 00257 'th', 00258 ) ) ) { 00259 return ''; 00260 } 00261 return "</$element>"; 00262 } 00263 00281 private static function dropDefaults( $element, $attribs ) { 00282 # Don't bother doing anything if we aren't outputting HTML5; it's too 00283 # much of a pain to maintain two sets of defaults. 00284 global $wgHtml5; 00285 if ( !$wgHtml5 ) { 00286 return $attribs; 00287 } 00288 00289 static $attribDefaults = array( 00290 'area' => array( 'shape' => 'rect' ), 00291 'button' => array( 00292 'formaction' => 'GET', 00293 'formenctype' => 'application/x-www-form-urlencoded', 00294 'type' => 'submit', 00295 ), 00296 'canvas' => array( 00297 'height' => '150', 00298 'width' => '300', 00299 ), 00300 'command' => array( 'type' => 'command' ), 00301 'form' => array( 00302 'action' => 'GET', 00303 'autocomplete' => 'on', 00304 'enctype' => 'application/x-www-form-urlencoded', 00305 ), 00306 'input' => array( 00307 'formaction' => 'GET', 00308 'type' => 'text', 00309 'value' => '', 00310 ), 00311 'keygen' => array( 'keytype' => 'rsa' ), 00312 'link' => array( 'media' => 'all' ), 00313 'menu' => array( 'type' => 'list' ), 00314 # Note: the use of text/javascript here instead of other JavaScript 00315 # MIME types follows the HTML5 spec. 00316 'script' => array( 'type' => 'text/javascript' ), 00317 'style' => array( 00318 'media' => 'all', 00319 'type' => 'text/css', 00320 ), 00321 'textarea' => array( 'wrap' => 'soft' ), 00322 ); 00323 00324 $element = strtolower( $element ); 00325 00326 foreach ( $attribs as $attrib => $value ) { 00327 $lcattrib = strtolower( $attrib ); 00328 $value = strval( $value ); 00329 00330 # Simple checks using $attribDefaults 00331 if ( isset( $attribDefaults[$element][$lcattrib] ) && 00332 $attribDefaults[$element][$lcattrib] == $value ) { 00333 unset( $attribs[$attrib] ); 00334 } 00335 00336 if ( $lcattrib == 'class' && $value == '' ) { 00337 unset( $attribs[$attrib] ); 00338 } 00339 } 00340 00341 # More subtle checks 00342 if ( $element === 'link' && isset( $attribs['type'] ) 00343 && strval( $attribs['type'] ) == 'text/css' ) { 00344 unset( $attribs['type'] ); 00345 } 00346 if ( $element === 'select' && isset( $attribs['size'] ) ) { 00347 if ( in_array( 'multiple', $attribs ) 00348 || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false ) 00349 ) { 00350 # A multi-select 00351 if ( strval( $attribs['size'] ) == '4' ) { 00352 unset( $attribs['size'] ); 00353 } 00354 } else { 00355 # Single select 00356 if ( strval( $attribs['size'] ) == '1' ) { 00357 unset( $attribs['size'] ); 00358 } 00359 } 00360 } 00361 00362 return $attribs; 00363 } 00364 00404 public static function expandAttributes( $attribs ) { 00405 global $wgHtml5, $wgWellFormedXml; 00406 00407 $ret = ''; 00408 $attribs = (array)$attribs; 00409 foreach ( $attribs as $key => $value ) { 00410 if ( $value === false || is_null( $value ) ) { 00411 continue; 00412 } 00413 00414 # For boolean attributes, support array( 'foo' ) instead of 00415 # requiring array( 'foo' => 'meaningless' ). 00416 if ( is_int( $key ) 00417 && in_array( strtolower( $value ), self::$boolAttribs ) ) { 00418 $key = $value; 00419 } 00420 00421 # Not technically required in HTML5, but required in XHTML 1.0, 00422 # and we'd like consistency and better compression anyway. 00423 $key = strtolower( $key ); 00424 00425 # Here we're blacklisting some HTML5-only attributes... 00426 if ( !$wgHtml5 && in_array( $key, self::$HTMLFiveOnlyAttribs ) 00427 ) { 00428 continue; 00429 } 00430 00431 # Bug 23769: Blacklist all form validation attributes for now. Current 00432 # (June 2010) WebKit has no UI, so the form just refuses to submit 00433 # without telling the user why, which is much worse than failing 00434 # server-side validation. Opera is the only other implementation at 00435 # this time, and has ugly UI, so just kill the feature entirely until 00436 # we have at least one good implementation. 00437 00438 # As the default value of "1" for "step" rejects decimal 00439 # numbers to be entered in 'type="number"' fields, allow 00440 # the special case 'step="any"'. 00441 00442 if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) ) || 00443 $key === 'step' && $value !== 'any' ) { 00444 continue; 00445 } 00446 00447 // http://www.w3.org/TR/html401/index/attributes.html ("space-separated") 00448 // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") 00449 $spaceSeparatedListAttributes = array( 00450 'class', // html4, html5 00451 'accesskey', // as of html5, multiple space-separated values allowed 00452 // html4-spec doesn't document rel= as space-separated 00453 // but has been used like that and is now documented as such 00454 // in the html5-spec. 00455 'rel', 00456 ); 00457 00458 # Specific features for attributes that allow a list of space-separated values 00459 if ( in_array( $key, $spaceSeparatedListAttributes ) ) { 00460 // Apply some normalization and remove duplicates 00461 00462 // Convert into correct array. Array can contain space-seperated 00463 // values. Implode/explode to get those into the main array as well. 00464 if ( is_array( $value ) ) { 00465 // If input wasn't an array, we can skip this step 00466 00467 $newValue = array(); 00468 foreach ( $value as $k => $v ) { 00469 if ( is_string( $v ) ) { 00470 // String values should be normal `array( 'foo' )` 00471 // Just append them 00472 if ( !isset( $value[$v] ) ) { 00473 // As a special case don't set 'foo' if a 00474 // separate 'foo' => true/false exists in the array 00475 // keys should be authoritive 00476 $newValue[] = $v; 00477 } 00478 } elseif ( $v ) { 00479 // If the value is truthy but not a string this is likely 00480 // an array( 'foo' => true ), falsy values don't add strings 00481 $newValue[] = $k; 00482 } 00483 } 00484 $value = implode( ' ', $newValue ); 00485 } 00486 $value = explode( ' ', $value ); 00487 00488 // Normalize spacing by fixing up cases where people used 00489 // more than 1 space and/or a trailing/leading space 00490 $value = array_diff( $value, array( '', ' ' ) ); 00491 00492 // Remove duplicates and create the string 00493 $value = implode( ' ', array_unique( $value ) ); 00494 } 00495 00496 # See the "Attributes" section in the HTML syntax part of HTML5, 00497 # 9.1.2.3 as of 2009-08-10. Most attributes can have quotation 00498 # marks omitted, but not all. (Although a literal " is not 00499 # permitted, we don't check for that, since it will be escaped 00500 # anyway.) 00501 # 00502 # See also research done on further characters that need to be 00503 # escaped: http://code.google.com/p/html5lib/issues/detail?id=93 00504 $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}" 00505 . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}" 00506 . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}"; 00507 if ( $wgWellFormedXml || $value === '' 00508 || preg_match( "![$badChars]!u", $value ) ) { 00509 $quote = '"'; 00510 } else { 00511 $quote = ''; 00512 } 00513 00514 if ( in_array( $key, self::$boolAttribs ) ) { 00515 # In XHTML 1.0 Transitional, the value needs to be equal to the 00516 # key. In HTML5, we can leave the value empty instead. If we 00517 # don't need well-formed XML, we can omit the = entirely. 00518 if ( !$wgWellFormedXml ) { 00519 $ret .= " $key"; 00520 } elseif ( $wgHtml5 ) { 00521 $ret .= " $key=\"\""; 00522 } else { 00523 $ret .= " $key=\"$key\""; 00524 } 00525 } else { 00526 # Apparently we need to entity-encode \n, \r, \t, although the 00527 # spec doesn't mention that. Since we're doing strtr() anyway, 00528 # and we don't need <> escaped here, we may as well not call 00529 # htmlspecialchars(). 00530 # @todo FIXME: Verify that we actually need to 00531 # escape \n\r\t here, and explain why, exactly. 00532 # 00533 # We could call Sanitizer::encodeAttribute() for this, but we 00534 # don't because we're stubborn and like our marginal savings on 00535 # byte size from not having to encode unnecessary quotes. 00536 $map = array( 00537 '&' => '&', 00538 '"' => '"', 00539 "\n" => ' ', 00540 "\r" => ' ', 00541 "\t" => '	' 00542 ); 00543 if ( $wgWellFormedXml ) { 00544 # This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue> 00545 # But reportedly it breaks some XML tools? 00546 # @todo FIXME: Is this really true? 00547 $map['<'] = '<'; 00548 } 00549 00550 $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; 00551 } 00552 } 00553 return $ret; 00554 } 00555 00564 public static function inlineScript( $contents ) { 00565 global $wgHtml5, $wgJsMimeType, $wgWellFormedXml; 00566 00567 $attrs = array(); 00568 00569 if ( !$wgHtml5 ) { 00570 $attrs['type'] = $wgJsMimeType; 00571 } 00572 00573 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00574 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00575 } 00576 00577 return self::rawElement( 'script', $attrs, $contents ); 00578 } 00579 00587 public static function linkedScript( $url ) { 00588 global $wgHtml5, $wgJsMimeType; 00589 00590 $attrs = array( 'src' => $url ); 00591 00592 if ( !$wgHtml5 ) { 00593 $attrs['type'] = $wgJsMimeType; 00594 } 00595 00596 return self::element( 'script', $attrs ); 00597 } 00598 00608 public static function inlineStyle( $contents, $media = 'all' ) { 00609 global $wgWellFormedXml; 00610 00611 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00612 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00613 } 00614 00615 return self::rawElement( 'style', array( 00616 'type' => 'text/css', 00617 'media' => $media, 00618 ), $contents ); 00619 } 00620 00629 public static function linkedStyle( $url, $media = 'all' ) { 00630 return self::element( 'link', array( 00631 'rel' => 'stylesheet', 00632 'href' => $url, 00633 'type' => 'text/css', 00634 'media' => $media, 00635 ) ); 00636 } 00637 00650 public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { 00651 $attribs['type'] = $type; 00652 $attribs['value'] = $value; 00653 $attribs['name'] = $name; 00654 00655 return self::element( 'input', $attribs ); 00656 } 00657 00667 public static function hidden( $name, $value, $attribs = array() ) { 00668 return self::input( $name, $value, 'hidden', $attribs ); 00669 } 00670 00684 public static function textarea( $name, $value = '', $attribs = array() ) { 00685 global $wgHtml5; 00686 00687 $attribs['name'] = $name; 00688 00689 if ( !$wgHtml5 ) { 00690 if ( !isset( $attribs['cols'] ) ) { 00691 $attribs['cols'] = ""; 00692 } 00693 00694 if ( !isset( $attribs['rows'] ) ) { 00695 $attribs['rows'] = ""; 00696 } 00697 } 00698 00699 if (substr($value, 0, 1) == "\n") { 00700 // Workaround for bug 12130: browsers eat the initial newline 00701 // assuming that it's just for show, but they do keep the later 00702 // newlines, which we may want to preserve during editing. 00703 // Prepending a single newline 00704 $spacedValue = "\n" . $value; 00705 } else { 00706 $spacedValue = $value; 00707 } 00708 return self::element( 'textarea', $attribs, $spacedValue ); 00709 } 00722 public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) { 00723 global $wgContLang; 00724 00725 // Default 'id' & 'name' <select> attributes 00726 $selectAttribs = $selectAttribs + array( 00727 'id' => 'namespace', 00728 'name' => 'namespace', 00729 ); 00730 ksort( $selectAttribs ); 00731 00732 // Is a namespace selected? 00733 if ( isset( $params['selected'] ) ) { 00734 // If string only contains digits, convert to clean int. Selected could also 00735 // be "all" or "" etc. which needs to be left untouched. 00736 // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues 00737 // and returns false for already clean ints. Use regex instead.. 00738 if ( preg_match( '/^\d+$/', $params['selected'] ) ) { 00739 $params['selected'] = intval( $params['selected'] ); 00740 } 00741 // else: leaves it untouched for later processing 00742 } else { 00743 $params['selected'] = ''; 00744 } 00745 00746 // Array holding the <option> elements 00747 $options = array(); 00748 00749 if ( isset( $params['all'] ) ) { 00750 // add an <option> that would let the user select all namespaces. 00751 // Value is provided by user, the name shown is localized. 00752 $options[$params['all']] = wfMsg( 'namespacesall' ); 00753 } 00754 // Add defaults <option> according to content language 00755 $options += $wgContLang->getFormattedNamespaces(); 00756 00757 // Convert $options to HTML 00758 $optionsHtml = array(); 00759 foreach ( $options as $nsId => $nsName ) { 00760 if ( $nsId < NS_MAIN ) { 00761 continue; 00762 } 00763 if ( $nsId === 0 ) { 00764 $nsName = wfMsg( 'blanknamespace' ); 00765 } 00766 $optionsHtml[] = Xml::option( $nsName, $nsId, $nsId === $params['selected'] ); 00767 } 00768 00769 // Forge a <select> element and returns it 00770 $ret = ''; 00771 if ( isset( $params['label'] ) ) { 00772 $ret .= Xml::label( $params['label'], $selectAttribs['id'] ) . ' '; 00773 } 00774 $ret .= Html::openElement( 'select', $selectAttribs ) 00775 . "\n" 00776 . implode( "\n", $optionsHtml ) 00777 . "\n" 00778 . Html::closeElement( 'select' ); 00779 return $ret; 00780 } 00781 00790 public static function htmlHeader( $attribs = array() ) { 00791 $ret = ''; 00792 00793 global $wgMimeType; 00794 00795 if ( self::isXmlMimeType( $wgMimeType ) ) { 00796 $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n"; 00797 } 00798 00799 global $wgHtml5, $wgHtml5Version, $wgDocType, $wgDTD; 00800 global $wgXhtmlNamespaces, $wgXhtmlDefaultNamespace; 00801 00802 if ( $wgHtml5 ) { 00803 $ret .= "<!DOCTYPE html>\n"; 00804 00805 if ( $wgHtml5Version ) { 00806 $attribs['version'] = $wgHtml5Version; 00807 } 00808 } else { 00809 $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n"; 00810 $attribs['xmlns'] = $wgXhtmlDefaultNamespace; 00811 00812 foreach ( $wgXhtmlNamespaces as $tag => $ns ) { 00813 $attribs["xmlns:$tag"] = $ns; 00814 } 00815 } 00816 00817 $html = Html::openElement( 'html', $attribs ); 00818 00819 if ( $html ) { 00820 $html .= "\n"; 00821 } 00822 00823 $ret .= $html; 00824 00825 return $ret; 00826 } 00827 00834 public static function isXmlMimeType( $mimetype ) { 00835 switch ( $mimetype ) { 00836 case 'text/xml': 00837 case 'application/xhtml+xml': 00838 case 'application/xml': 00839 return true; 00840 default: 00841 return false; 00842 } 00843 } 00844 00856 static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = true ) { 00857 global $wgStylePath; 00858 00859 if ( $useStylePath ) { 00860 $icon = $wgStylePath.'/common/images/'.$icon; 00861 } 00862 00863 $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class") ); 00864 00865 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ). 00866 Html::element( 'img', 00867 array( 00868 'src' => $icon, 00869 'alt' => $alt, 00870 ) 00871 ). 00872 Html::closeElement( 'div' ); 00873 00874 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ). 00875 $text. 00876 Html::closeElement( 'div' ); 00877 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00878 00879 $s .= Html::closeElement( 'div' ); 00880 00881 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00882 00883 return $s; 00884 } 00885 }