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