MediaWiki  REL1_19
Html.php
Go to the documentation of this file.
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                         '&' => '&amp;',
00166                         '<' => '&lt;'
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                                         '&' => '&amp;',
00538                                         '"' => '&quot;',
00539                                         "\n" => '&#10;',
00540                                         "\r" => '&#13;',
00541                                         "\t" => '&#9;'
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['<'] = '&lt;';
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'] ) . '&#160;';
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 }