MediaWiki
REL1_20
|
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( $wgHtml5 ) { 00215 $validTypes = array_merge( $validTypes, array( 00216 'datetime', 00217 'datetime-local', 00218 'date', 00219 'month', 00220 'time', 00221 'week', 00222 'number', 00223 'range', 00224 'email', 00225 'url', 00226 'search', 00227 'tel', 00228 'color', 00229 ) ); 00230 } 00231 if ( isset( $attribs['type'] ) 00232 && !in_array( $attribs['type'], $validTypes ) ) { 00233 unset( $attribs['type'] ); 00234 } 00235 00236 if ( isset( $attribs['type'] ) && $attribs['type'] == 'search' 00237 && !$wgHtml5 ) { 00238 unset( $attribs['type'] ); 00239 } 00240 } 00241 00242 if ( !$wgHtml5 && $element == 'textarea' && isset( $attribs['maxlength'] ) ) { 00243 unset( $attribs['maxlength'] ); 00244 } 00245 00246 return "<$element" . self::expandAttributes( 00247 self::dropDefaults( $element, $attribs ) ) . '>'; 00248 } 00249 00258 public static function closeElement( $element ) { 00259 global $wgWellFormedXml; 00260 00261 $element = strtolower( $element ); 00262 00263 # Reference: 00264 # http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags 00265 if ( !$wgWellFormedXml && in_array( $element, array( 00266 'html', 00267 'head', 00268 'body', 00269 'li', 00270 'dt', 00271 'dd', 00272 'tr', 00273 'td', 00274 'th', 00275 ) ) ) { 00276 return ''; 00277 } 00278 return "</$element>"; 00279 } 00280 00298 private static function dropDefaults( $element, $attribs ) { 00299 # Don't bother doing anything if we aren't outputting HTML5; it's too 00300 # much of a pain to maintain two sets of defaults. 00301 global $wgHtml5; 00302 if ( !$wgHtml5 ) { 00303 return $attribs; 00304 } 00305 00306 # Whenever altering this array, please provide a covering test case 00307 # in HtmlTest::provideElementsWithAttributesHavingDefaultValues 00308 static $attribDefaults = array( 00309 'area' => array( 'shape' => 'rect' ), 00310 'button' => array( 00311 'formaction' => 'GET', 00312 'formenctype' => 'application/x-www-form-urlencoded', 00313 'type' => 'submit', 00314 ), 00315 'canvas' => array( 00316 'height' => '150', 00317 'width' => '300', 00318 ), 00319 'command' => array( 'type' => 'command' ), 00320 'form' => array( 00321 'action' => 'GET', 00322 'autocomplete' => 'on', 00323 'enctype' => 'application/x-www-form-urlencoded', 00324 ), 00325 'input' => array( 00326 'formaction' => 'GET', 00327 'type' => 'text', 00328 ), 00329 'keygen' => array( 'keytype' => 'rsa' ), 00330 'link' => array( 'media' => 'all' ), 00331 'menu' => array( 'type' => 'list' ), 00332 # Note: the use of text/javascript here instead of other JavaScript 00333 # MIME types follows the HTML5 spec. 00334 'script' => array( 'type' => 'text/javascript' ), 00335 'style' => array( 00336 'media' => 'all', 00337 'type' => 'text/css', 00338 ), 00339 'textarea' => array( 'wrap' => 'soft' ), 00340 ); 00341 00342 $element = strtolower( $element ); 00343 00344 foreach ( $attribs as $attrib => $value ) { 00345 $lcattrib = strtolower( $attrib ); 00346 if( is_array( $value ) ) { 00347 $value = implode( ' ', $value ); 00348 } else { 00349 $value = strval( $value ); 00350 } 00351 00352 # Simple checks using $attribDefaults 00353 if ( isset( $attribDefaults[$element][$lcattrib] ) && 00354 $attribDefaults[$element][$lcattrib] == $value ) { 00355 unset( $attribs[$attrib] ); 00356 } 00357 00358 if ( $lcattrib == 'class' && $value == '' ) { 00359 unset( $attribs[$attrib] ); 00360 } 00361 } 00362 00363 # More subtle checks 00364 if ( $element === 'link' && isset( $attribs['type'] ) 00365 && strval( $attribs['type'] ) == 'text/css' ) { 00366 unset( $attribs['type'] ); 00367 } 00368 if ( $element === 'input' ) { 00369 $type = isset( $attribs['type'] ) ? $attribs['type'] : null; 00370 $value = isset( $attribs['value'] ) ? $attribs['value'] : null; 00371 if ( $type === 'checkbox' || $type === 'radio' ) { 00372 // The default value for checkboxes and radio buttons is 'on' 00373 // not ''. By stripping value="" we break radio boxes that 00374 // actually wants empty values. 00375 if ( $value === 'on' ) { 00376 unset( $attribs['value'] ); 00377 } 00378 } elseif ( $type === 'submit' ) { 00379 // The default value for submit appears to be "Submit" but 00380 // let's not bother stripping out localized text that matches 00381 // that. 00382 } else { 00383 // The default value for nearly every other field type is '' 00384 // The 'range' and 'color' types use different defaults but 00385 // stripping a value="" does not hurt them. 00386 if ( $value === '' ) { 00387 unset( $attribs['value'] ); 00388 } 00389 } 00390 } 00391 if ( $element === 'select' && isset( $attribs['size'] ) ) { 00392 if ( in_array( 'multiple', $attribs ) 00393 || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false ) 00394 ) { 00395 # A multi-select 00396 if ( strval( $attribs['size'] ) == '4' ) { 00397 unset( $attribs['size'] ); 00398 } 00399 } else { 00400 # Single select 00401 if ( strval( $attribs['size'] ) == '1' ) { 00402 unset( $attribs['size'] ); 00403 } 00404 } 00405 } 00406 00407 return $attribs; 00408 } 00409 00449 public static function expandAttributes( $attribs ) { 00450 global $wgHtml5, $wgWellFormedXml; 00451 00452 $ret = ''; 00453 $attribs = (array)$attribs; 00454 foreach ( $attribs as $key => $value ) { 00455 if ( $value === false || is_null( $value ) ) { 00456 continue; 00457 } 00458 00459 # For boolean attributes, support array( 'foo' ) instead of 00460 # requiring array( 'foo' => 'meaningless' ). 00461 if ( is_int( $key ) 00462 && in_array( strtolower( $value ), self::$boolAttribs ) ) { 00463 $key = $value; 00464 } 00465 00466 # Not technically required in HTML5, but required in XHTML 1.0, 00467 # and we'd like consistency and better compression anyway. 00468 $key = strtolower( $key ); 00469 00470 # Here we're blacklisting some HTML5-only attributes... 00471 if ( !$wgHtml5 && in_array( $key, self::$HTMLFiveOnlyAttribs ) 00472 ) { 00473 continue; 00474 } 00475 00476 # Bug 23769: Blacklist all form validation attributes for now. Current 00477 # (June 2010) WebKit has no UI, so the form just refuses to submit 00478 # without telling the user why, which is much worse than failing 00479 # server-side validation. Opera is the only other implementation at 00480 # this time, and has ugly UI, so just kill the feature entirely until 00481 # we have at least one good implementation. 00482 00483 # As the default value of "1" for "step" rejects decimal 00484 # numbers to be entered in 'type="number"' fields, allow 00485 # the special case 'step="any"'. 00486 00487 if ( in_array( $key, array( 'max', 'min', 'pattern', 'required' ) ) || 00488 $key === 'step' && $value !== 'any' ) { 00489 continue; 00490 } 00491 00492 // http://www.w3.org/TR/html401/index/attributes.html ("space-separated") 00493 // http://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") 00494 $spaceSeparatedListAttributes = array( 00495 'class', // html4, html5 00496 'accesskey', // as of html5, multiple space-separated values allowed 00497 // html4-spec doesn't document rel= as space-separated 00498 // but has been used like that and is now documented as such 00499 // in the html5-spec. 00500 'rel', 00501 ); 00502 00503 # Specific features for attributes that allow a list of space-separated values 00504 if ( in_array( $key, $spaceSeparatedListAttributes ) ) { 00505 // Apply some normalization and remove duplicates 00506 00507 // Convert into correct array. Array can contain space-seperated 00508 // values. Implode/explode to get those into the main array as well. 00509 if ( is_array( $value ) ) { 00510 // If input wasn't an array, we can skip this step 00511 00512 $newValue = array(); 00513 foreach ( $value as $k => $v ) { 00514 if ( is_string( $v ) ) { 00515 // String values should be normal `array( 'foo' )` 00516 // Just append them 00517 if ( !isset( $value[$v] ) ) { 00518 // As a special case don't set 'foo' if a 00519 // separate 'foo' => true/false exists in the array 00520 // keys should be authoritive 00521 $newValue[] = $v; 00522 } 00523 } elseif ( $v ) { 00524 // If the value is truthy but not a string this is likely 00525 // an array( 'foo' => true ), falsy values don't add strings 00526 $newValue[] = $k; 00527 } 00528 } 00529 $value = implode( ' ', $newValue ); 00530 } 00531 $value = explode( ' ', $value ); 00532 00533 // Normalize spacing by fixing up cases where people used 00534 // more than 1 space and/or a trailing/leading space 00535 $value = array_diff( $value, array( '', ' ' ) ); 00536 00537 // Remove duplicates and create the string 00538 $value = implode( ' ', array_unique( $value ) ); 00539 } 00540 00541 # See the "Attributes" section in the HTML syntax part of HTML5, 00542 # 9.1.2.3 as of 2009-08-10. Most attributes can have quotation 00543 # marks omitted, but not all. (Although a literal " is not 00544 # permitted, we don't check for that, since it will be escaped 00545 # anyway.) 00546 # 00547 # See also research done on further characters that need to be 00548 # escaped: http://code.google.com/p/html5lib/issues/detail?id=93 00549 $badChars = "\\x00- '=<>`/\x{00a0}\x{1680}\x{180e}\x{180F}\x{2000}\x{2001}" 00550 . "\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}" 00551 . "\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}"; 00552 if ( $wgWellFormedXml || $value === '' 00553 || preg_match( "![$badChars]!u", $value ) ) { 00554 $quote = '"'; 00555 } else { 00556 $quote = ''; 00557 } 00558 00559 if ( in_array( $key, self::$boolAttribs ) ) { 00560 # In XHTML 1.0 Transitional, the value needs to be equal to the 00561 # key. In HTML5, we can leave the value empty instead. If we 00562 # don't need well-formed XML, we can omit the = entirely. 00563 if ( !$wgWellFormedXml ) { 00564 $ret .= " $key"; 00565 } elseif ( $wgHtml5 ) { 00566 $ret .= " $key=\"\""; 00567 } else { 00568 $ret .= " $key=\"$key\""; 00569 } 00570 } else { 00571 # Apparently we need to entity-encode \n, \r, \t, although the 00572 # spec doesn't mention that. Since we're doing strtr() anyway, 00573 # and we don't need <> escaped here, we may as well not call 00574 # htmlspecialchars(). 00575 # @todo FIXME: Verify that we actually need to 00576 # escape \n\r\t here, and explain why, exactly. 00577 # 00578 # We could call Sanitizer::encodeAttribute() for this, but we 00579 # don't because we're stubborn and like our marginal savings on 00580 # byte size from not having to encode unnecessary quotes. 00581 $map = array( 00582 '&' => '&', 00583 '"' => '"', 00584 "\n" => ' ', 00585 "\r" => ' ', 00586 "\t" => '	' 00587 ); 00588 if ( $wgWellFormedXml ) { 00589 # This is allowed per spec: <http://www.w3.org/TR/xml/#NT-AttValue> 00590 # But reportedly it breaks some XML tools? 00591 # @todo FIXME: Is this really true? 00592 $map['<'] = '<'; 00593 } 00594 00595 $ret .= " $key=$quote" . strtr( $value, $map ) . $quote; 00596 } 00597 } 00598 return $ret; 00599 } 00600 00610 public static function inlineScript( $contents ) { 00611 global $wgHtml5, $wgJsMimeType, $wgWellFormedXml; 00612 00613 $attrs = array(); 00614 00615 if ( !$wgHtml5 ) { 00616 $attrs['type'] = $wgJsMimeType; 00617 } 00618 00619 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00620 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00621 } 00622 00623 return self::rawElement( 'script', $attrs, $contents ); 00624 } 00625 00633 public static function linkedScript( $url ) { 00634 global $wgHtml5, $wgJsMimeType; 00635 00636 $attrs = array( 'src' => $url ); 00637 00638 if ( !$wgHtml5 ) { 00639 $attrs['type'] = $wgJsMimeType; 00640 } 00641 00642 return self::element( 'script', $attrs ); 00643 } 00644 00654 public static function inlineStyle( $contents, $media = 'all' ) { 00655 global $wgWellFormedXml; 00656 00657 if ( $wgWellFormedXml && preg_match( '/[<&]/', $contents ) ) { 00658 $contents = "/*<![CDATA[*/$contents/*]]>*/"; 00659 } 00660 00661 return self::rawElement( 'style', array( 00662 'type' => 'text/css', 00663 'media' => $media, 00664 ), $contents ); 00665 } 00666 00675 public static function linkedStyle( $url, $media = 'all' ) { 00676 return self::element( 'link', array( 00677 'rel' => 'stylesheet', 00678 'href' => $url, 00679 'type' => 'text/css', 00680 'media' => $media, 00681 ) ); 00682 } 00683 00696 public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { 00697 $attribs['type'] = $type; 00698 $attribs['value'] = $value; 00699 $attribs['name'] = $name; 00700 00701 return self::element( 'input', $attribs ); 00702 } 00703 00713 public static function hidden( $name, $value, $attribs = array() ) { 00714 return self::input( $name, $value, 'hidden', $attribs ); 00715 } 00716 00731 public static function textarea( $name, $value = '', $attribs = array() ) { 00732 global $wgHtml5; 00733 00734 $attribs['name'] = $name; 00735 00736 if ( !$wgHtml5 ) { 00737 if ( !isset( $attribs['cols'] ) ) { 00738 $attribs['cols'] = ""; 00739 } 00740 00741 if ( !isset( $attribs['rows'] ) ) { 00742 $attribs['rows'] = ""; 00743 } 00744 } 00745 00746 if (substr($value, 0, 1) == "\n") { 00747 // Workaround for bug 12130: browsers eat the initial newline 00748 // assuming that it's just for show, but they do keep the later 00749 // newlines, which we may want to preserve during editing. 00750 // Prepending a single newline 00751 $spacedValue = "\n" . $value; 00752 } else { 00753 $spacedValue = $value; 00754 } 00755 return self::element( 'textarea', $attribs, $spacedValue ); 00756 } 00771 public static function namespaceSelector( Array $params = array(), Array $selectAttribs = array() ) { 00772 global $wgContLang; 00773 00774 ksort( $selectAttribs ); 00775 00776 // Is a namespace selected? 00777 if ( isset( $params['selected'] ) ) { 00778 // If string only contains digits, convert to clean int. Selected could also 00779 // be "all" or "" etc. which needs to be left untouched. 00780 // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues 00781 // and returns false for already clean ints. Use regex instead.. 00782 if ( preg_match( '/^\d+$/', $params['selected'] ) ) { 00783 $params['selected'] = intval( $params['selected'] ); 00784 } 00785 // else: leaves it untouched for later processing 00786 } else { 00787 $params['selected'] = ''; 00788 } 00789 00790 if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { 00791 $params['exclude'] = array(); 00792 } 00793 if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { 00794 $params['disable'] = array(); 00795 } 00796 00797 // Associative array between option-values and option-labels 00798 $options = array(); 00799 00800 if ( isset( $params['all'] ) ) { 00801 // add an option that would let the user select all namespaces. 00802 // Value is provided by user, the name shown is localized for the user. 00803 $options[$params['all']] = wfMessage( 'namespacesall' )->text(); 00804 } 00805 // Add all namespaces as options (in the content langauge) 00806 $options += $wgContLang->getFormattedNamespaces(); 00807 00808 // Convert $options to HTML and filter out namespaces below 0 00809 $optionsHtml = array(); 00810 foreach ( $options as $nsId => $nsName ) { 00811 if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { 00812 continue; 00813 } 00814 if ( $nsId === 0 ) { 00815 // For other namespaces use use the namespace prefix as label, but for 00816 // main we don't use "" but the user message descripting it (e.g. "(Main)" or "(Article)") 00817 $nsName = wfMessage( 'blanknamespace' )->text(); 00818 } 00819 $optionsHtml[] = Html::element( 00820 'option', array( 00821 'disabled' => in_array( $nsId, $params['disable'] ), 00822 'value' => $nsId, 00823 'selected' => $nsId === $params['selected'], 00824 ), $nsName 00825 ); 00826 } 00827 00828 $ret = ''; 00829 if ( isset( $params['label'] ) ) { 00830 $ret .= Html::element( 00831 'label', array( 00832 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null, 00833 ), $params['label'] 00834 ) . ' '; 00835 } 00836 00837 // Wrap options in a <select> 00838 $ret .= Html::openElement( 'select', $selectAttribs ) 00839 . "\n" 00840 . implode( "\n", $optionsHtml ) 00841 . "\n" 00842 . Html::closeElement( 'select' ); 00843 00844 return $ret; 00845 } 00846 00855 public static function htmlHeader( $attribs = array() ) { 00856 $ret = ''; 00857 00858 global $wgMimeType; 00859 00860 if ( self::isXmlMimeType( $wgMimeType ) ) { 00861 $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n"; 00862 } 00863 00864 global $wgHtml5, $wgHtml5Version, $wgDocType, $wgDTD; 00865 global $wgXhtmlNamespaces, $wgXhtmlDefaultNamespace; 00866 00867 if ( $wgHtml5 ) { 00868 $ret .= "<!DOCTYPE html>\n"; 00869 00870 if ( $wgHtml5Version ) { 00871 $attribs['version'] = $wgHtml5Version; 00872 } 00873 } else { 00874 $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\" \"$wgDTD\">\n"; 00875 $attribs['xmlns'] = $wgXhtmlDefaultNamespace; 00876 00877 foreach ( $wgXhtmlNamespaces as $tag => $ns ) { 00878 $attribs["xmlns:$tag"] = $ns; 00879 } 00880 } 00881 00882 $html = Html::openElement( 'html', $attribs ); 00883 00884 if ( $html ) { 00885 $html .= "\n"; 00886 } 00887 00888 $ret .= $html; 00889 00890 return $ret; 00891 } 00892 00899 public static function isXmlMimeType( $mimetype ) { 00900 switch ( $mimetype ) { 00901 case 'text/xml': 00902 case 'application/xhtml+xml': 00903 case 'application/xml': 00904 return true; 00905 default: 00906 return false; 00907 } 00908 } 00909 00921 static function infoBox( $text, $icon, $alt, $class = false, $useStylePath = true ) { 00922 global $wgStylePath; 00923 00924 if ( $useStylePath ) { 00925 $icon = $wgStylePath.'/common/images/'.$icon; 00926 } 00927 00928 $s = Html::openElement( 'div', array( 'class' => "mw-infobox $class") ); 00929 00930 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-left' ) ). 00931 Html::element( 'img', 00932 array( 00933 'src' => $icon, 00934 'alt' => $alt, 00935 ) 00936 ). 00937 Html::closeElement( 'div' ); 00938 00939 $s .= Html::openElement( 'div', array( 'class' => 'mw-infobox-right' ) ). 00940 $text. 00941 Html::closeElement( 'div' ); 00942 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00943 00944 $s .= Html::closeElement( 'div' ); 00945 00946 $s .= Html::element( 'div', array( 'style' => 'clear: left;' ), ' ' ); 00947 00948 return $s; 00949 } 00950 }