Html.php 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060
  1. <?php
  2. /**
  3. * Collection of methods to generate HTML content
  4. *
  5. * Copyright © 2009 Aryeh Gregor
  6. * https://www.mediawiki.org/
  7. *
  8. * This program is free software; you can redistribute it and/or modify
  9. * it under the terms of the GNU General Public License as published by
  10. * the Free Software Foundation; either version 2 of the License, or
  11. * (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License along
  19. * with this program; if not, write to the Free Software Foundation, Inc.,
  20. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  21. * http://www.gnu.org/copyleft/gpl.html
  22. *
  23. * @file
  24. */
  25. /**
  26. * This class is a collection of static functions that serve two purposes:
  27. *
  28. * 1) Implement any algorithms specified by HTML5, or other HTML
  29. * specifications, in a convenient and self-contained way.
  30. *
  31. * 2) Allow HTML elements to be conveniently and safely generated, like the
  32. * current Xml class but a) less confused (Xml supports HTML-specific things,
  33. * but only sometimes!) and b) not necessarily confined to XML-compatible
  34. * output.
  35. *
  36. * There are two important configuration options this class uses:
  37. *
  38. * $wgMimeType: If this is set to an xml MIME type then output should be
  39. * valid XHTML5.
  40. *
  41. * This class is meant to be confined to utility functions that are called from
  42. * trusted code paths. It does not do enforcement of policy like not allowing
  43. * <a> elements.
  44. *
  45. * @since 1.16
  46. */
  47. class Html {
  48. // List of void elements from HTML5, section 8.1.2 as of 2016-09-19
  49. private static $voidElements = [
  50. 'area',
  51. 'base',
  52. 'br',
  53. 'col',
  54. 'embed',
  55. 'hr',
  56. 'img',
  57. 'input',
  58. 'keygen',
  59. 'link',
  60. 'meta',
  61. 'param',
  62. 'source',
  63. 'track',
  64. 'wbr',
  65. ];
  66. // Boolean attributes, which may have the value omitted entirely. Manually
  67. // collected from the HTML5 spec as of 2011-08-12.
  68. private static $boolAttribs = [
  69. 'async',
  70. 'autofocus',
  71. 'autoplay',
  72. 'checked',
  73. 'controls',
  74. 'default',
  75. 'defer',
  76. 'disabled',
  77. 'formnovalidate',
  78. 'hidden',
  79. 'ismap',
  80. 'itemscope',
  81. 'loop',
  82. 'multiple',
  83. 'muted',
  84. 'novalidate',
  85. 'open',
  86. 'pubdate',
  87. 'readonly',
  88. 'required',
  89. 'reversed',
  90. 'scoped',
  91. 'seamless',
  92. 'selected',
  93. 'truespeed',
  94. 'typemustmatch',
  95. // HTML5 Microdata
  96. 'itemscope',
  97. ];
  98. /**
  99. * Modifies a set of attributes meant for button elements
  100. * and apply a set of default attributes when $wgUseMediaWikiUIEverywhere enabled.
  101. * @param array $attrs HTML attributes in an associative array
  102. * @param string[] $modifiers classes to add to the button
  103. * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
  104. * @return array $attrs A modified attribute array
  105. */
  106. public static function buttonAttributes( array $attrs, array $modifiers = [] ) {
  107. global $wgUseMediaWikiUIEverywhere;
  108. if ( $wgUseMediaWikiUIEverywhere ) {
  109. if ( isset( $attrs['class'] ) ) {
  110. if ( is_array( $attrs['class'] ) ) {
  111. $attrs['class'][] = 'mw-ui-button';
  112. $attrs['class'] = array_merge( $attrs['class'], $modifiers );
  113. // ensure compatibility with Xml
  114. $attrs['class'] = implode( ' ', $attrs['class'] );
  115. } else {
  116. $attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers );
  117. }
  118. } else {
  119. // ensure compatibility with Xml
  120. $attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers );
  121. }
  122. }
  123. return $attrs;
  124. }
  125. /**
  126. * Modifies a set of attributes meant for text input elements
  127. * and apply a set of default attributes.
  128. * Removes size attribute when $wgUseMediaWikiUIEverywhere enabled.
  129. * @param array $attrs An attribute array.
  130. * @return array $attrs A modified attribute array
  131. */
  132. public static function getTextInputAttributes( array $attrs ) {
  133. global $wgUseMediaWikiUIEverywhere;
  134. if ( $wgUseMediaWikiUIEverywhere ) {
  135. if ( isset( $attrs['class'] ) ) {
  136. if ( is_array( $attrs['class'] ) ) {
  137. $attrs['class'][] = 'mw-ui-input';
  138. } else {
  139. $attrs['class'] .= ' mw-ui-input';
  140. }
  141. } else {
  142. $attrs['class'] = 'mw-ui-input';
  143. }
  144. }
  145. return $attrs;
  146. }
  147. /**
  148. * Returns an HTML link element in a string styled as a button
  149. * (when $wgUseMediaWikiUIEverywhere is enabled).
  150. *
  151. * @param string $contents The raw HTML contents of the element: *not*
  152. * escaped!
  153. * @param array $attrs Associative array of attributes, e.g., [
  154. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  155. * further documentation.
  156. * @param string[] $modifiers classes to add to the button
  157. * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
  158. * @return string Raw HTML
  159. */
  160. public static function linkButton( $contents, array $attrs, array $modifiers = [] ) {
  161. return self::element( 'a',
  162. self::buttonAttributes( $attrs, $modifiers ),
  163. $contents
  164. );
  165. }
  166. /**
  167. * Returns an HTML link element in a string styled as a button
  168. * (when $wgUseMediaWikiUIEverywhere is enabled).
  169. *
  170. * @param string $contents The raw HTML contents of the element: *not*
  171. * escaped!
  172. * @param array $attrs Associative array of attributes, e.g., [
  173. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  174. * further documentation.
  175. * @param string[] $modifiers classes to add to the button
  176. * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers
  177. * @return string Raw HTML
  178. */
  179. public static function submitButton( $contents, array $attrs, array $modifiers = [] ) {
  180. $attrs['type'] = 'submit';
  181. $attrs['value'] = $contents;
  182. return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) );
  183. }
  184. /**
  185. * Returns an HTML element in a string. The major advantage here over
  186. * manually typing out the HTML is that it will escape all attribute
  187. * values.
  188. *
  189. * This is quite similar to Xml::tags(), but it implements some useful
  190. * HTML-specific logic. For instance, there is no $allowShortTag
  191. * parameter: the closing tag is magically omitted if $element has an empty
  192. * content model.
  193. *
  194. * @param string $element The element's name, e.g., 'a'
  195. * @param array $attribs Associative array of attributes, e.g., [
  196. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  197. * further documentation.
  198. * @param string $contents The raw HTML contents of the element: *not*
  199. * escaped!
  200. * @return string Raw HTML
  201. */
  202. public static function rawElement( $element, $attribs = [], $contents = '' ) {
  203. $start = self::openElement( $element, $attribs );
  204. if ( in_array( $element, self::$voidElements ) ) {
  205. // Silly XML.
  206. return substr( $start, 0, -1 ) . '/>';
  207. } else {
  208. return "$start$contents" . self::closeElement( $element );
  209. }
  210. }
  211. /**
  212. * Identical to rawElement(), but HTML-escapes $contents (like
  213. * Xml::element()).
  214. *
  215. * @param string $element Name of the element, e.g., 'a'
  216. * @param array $attribs Associative array of attributes, e.g., [
  217. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  218. * further documentation.
  219. * @param string $contents
  220. *
  221. * @return string
  222. */
  223. public static function element( $element, $attribs = [], $contents = '' ) {
  224. return self::rawElement( $element, $attribs, strtr( $contents, [
  225. // There's no point in escaping quotes, >, etc. in the contents of
  226. // elements.
  227. '&' => '&amp;',
  228. '<' => '&lt;'
  229. ] ) );
  230. }
  231. /**
  232. * Identical to rawElement(), but has no third parameter and omits the end
  233. * tag (and the self-closing '/' in XML mode for empty elements).
  234. *
  235. * @param string $element Name of the element, e.g., 'a'
  236. * @param array $attribs Associative array of attributes, e.g., [
  237. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  238. * further documentation.
  239. *
  240. * @return string
  241. */
  242. public static function openElement( $element, $attribs = [] ) {
  243. $attribs = (array)$attribs;
  244. // This is not required in HTML5, but let's do it anyway, for
  245. // consistency and better compression.
  246. $element = strtolower( $element );
  247. // Remove invalid input types
  248. if ( $element == 'input' ) {
  249. $validTypes = [
  250. 'hidden',
  251. 'text',
  252. 'password',
  253. 'checkbox',
  254. 'radio',
  255. 'file',
  256. 'submit',
  257. 'image',
  258. 'reset',
  259. 'button',
  260. // HTML input types
  261. 'datetime',
  262. 'datetime-local',
  263. 'date',
  264. 'month',
  265. 'time',
  266. 'week',
  267. 'number',
  268. 'range',
  269. 'email',
  270. 'url',
  271. 'search',
  272. 'tel',
  273. 'color',
  274. ];
  275. if ( isset( $attribs['type'] ) && !in_array( $attribs['type'], $validTypes ) ) {
  276. unset( $attribs['type'] );
  277. }
  278. }
  279. // According to standard the default type for <button> elements is "submit".
  280. // Depending on compatibility mode IE might use "button", instead.
  281. // We enforce the standard "submit".
  282. if ( $element == 'button' && !isset( $attribs['type'] ) ) {
  283. $attribs['type'] = 'submit';
  284. }
  285. return "<$element" . self::expandAttributes(
  286. self::dropDefaults( $element, $attribs ) ) . '>';
  287. }
  288. /**
  289. * Returns "</$element>"
  290. *
  291. * @since 1.17
  292. * @param string $element Name of the element, e.g., 'a'
  293. * @return string A closing tag
  294. */
  295. public static function closeElement( $element ) {
  296. $element = strtolower( $element );
  297. return "</$element>";
  298. }
  299. /**
  300. * Given an element name and an associative array of element attributes,
  301. * return an array that is functionally identical to the input array, but
  302. * possibly smaller. In particular, attributes might be stripped if they
  303. * are given their default values.
  304. *
  305. * This method is not guaranteed to remove all redundant attributes, only
  306. * some common ones and some others selected arbitrarily at random. It
  307. * only guarantees that the output array should be functionally identical
  308. * to the input array (currently per the HTML 5 draft as of 2009-09-06).
  309. *
  310. * @param string $element Name of the element, e.g., 'a'
  311. * @param array $attribs Associative array of attributes, e.g., [
  312. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  313. * further documentation.
  314. * @return array An array of attributes functionally identical to $attribs
  315. */
  316. private static function dropDefaults( $element, array $attribs ) {
  317. // Whenever altering this array, please provide a covering test case
  318. // in HtmlTest::provideElementsWithAttributesHavingDefaultValues
  319. static $attribDefaults = [
  320. 'area' => [ 'shape' => 'rect' ],
  321. 'button' => [
  322. 'formaction' => 'GET',
  323. 'formenctype' => 'application/x-www-form-urlencoded',
  324. ],
  325. 'canvas' => [
  326. 'height' => '150',
  327. 'width' => '300',
  328. ],
  329. 'form' => [
  330. 'action' => 'GET',
  331. 'autocomplete' => 'on',
  332. 'enctype' => 'application/x-www-form-urlencoded',
  333. ],
  334. 'input' => [
  335. 'formaction' => 'GET',
  336. 'type' => 'text',
  337. ],
  338. 'keygen' => [ 'keytype' => 'rsa' ],
  339. 'link' => [ 'media' => 'all' ],
  340. 'menu' => [ 'type' => 'list' ],
  341. 'script' => [ 'type' => 'text/javascript' ],
  342. 'style' => [
  343. 'media' => 'all',
  344. 'type' => 'text/css',
  345. ],
  346. 'textarea' => [ 'wrap' => 'soft' ],
  347. ];
  348. $element = strtolower( $element );
  349. foreach ( $attribs as $attrib => $value ) {
  350. $lcattrib = strtolower( $attrib );
  351. if ( is_array( $value ) ) {
  352. $value = implode( ' ', $value );
  353. } else {
  354. $value = strval( $value );
  355. }
  356. // Simple checks using $attribDefaults
  357. if ( isset( $attribDefaults[$element][$lcattrib] )
  358. && $attribDefaults[$element][$lcattrib] == $value
  359. ) {
  360. unset( $attribs[$attrib] );
  361. }
  362. if ( $lcattrib == 'class' && $value == '' ) {
  363. unset( $attribs[$attrib] );
  364. }
  365. }
  366. // More subtle checks
  367. if ( $element === 'link'
  368. && isset( $attribs['type'] ) && strval( $attribs['type'] ) == 'text/css'
  369. ) {
  370. unset( $attribs['type'] );
  371. }
  372. if ( $element === 'input' ) {
  373. $type = isset( $attribs['type'] ) ? $attribs['type'] : null;
  374. $value = isset( $attribs['value'] ) ? $attribs['value'] : null;
  375. if ( $type === 'checkbox' || $type === 'radio' ) {
  376. // The default value for checkboxes and radio buttons is 'on'
  377. // not ''. By stripping value="" we break radio boxes that
  378. // actually wants empty values.
  379. if ( $value === 'on' ) {
  380. unset( $attribs['value'] );
  381. }
  382. } elseif ( $type === 'submit' ) {
  383. // The default value for submit appears to be "Submit" but
  384. // let's not bother stripping out localized text that matches
  385. // that.
  386. } else {
  387. // The default value for nearly every other field type is ''
  388. // The 'range' and 'color' types use different defaults but
  389. // stripping a value="" does not hurt them.
  390. if ( $value === '' ) {
  391. unset( $attribs['value'] );
  392. }
  393. }
  394. }
  395. if ( $element === 'select' && isset( $attribs['size'] ) ) {
  396. if ( in_array( 'multiple', $attribs )
  397. || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false )
  398. ) {
  399. // A multi-select
  400. if ( strval( $attribs['size'] ) == '4' ) {
  401. unset( $attribs['size'] );
  402. }
  403. } else {
  404. // Single select
  405. if ( strval( $attribs['size'] ) == '1' ) {
  406. unset( $attribs['size'] );
  407. }
  408. }
  409. }
  410. return $attribs;
  411. }
  412. /**
  413. * Given an associative array of element attributes, generate a string
  414. * to stick after the element name in HTML output. Like [ 'href' =>
  415. * 'https://www.mediawiki.org/' ] becomes something like
  416. * ' href="https://www.mediawiki.org"'. Again, this is like
  417. * Xml::expandAttributes(), but it implements some HTML-specific logic.
  418. *
  419. * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array
  420. * values are allowed as well, which will automagically be normalized
  421. * and converted to a space-separated string. In addition to a numerical
  422. * array, the attribute value may also be an associative array. See the
  423. * example below for how that works.
  424. *
  425. * @par Numerical array
  426. * @code
  427. * Html::element( 'em', [
  428. * 'class' => [ 'foo', 'bar' ]
  429. * ] );
  430. * // gives '<em class="foo bar"></em>'
  431. * @endcode
  432. *
  433. * @par Associative array
  434. * @code
  435. * Html::element( 'em', [
  436. * 'class' => [ 'foo', 'bar', 'foo' => false, 'quux' => true ]
  437. * ] );
  438. * // gives '<em class="bar quux"></em>'
  439. * @endcode
  440. *
  441. * @param array $attribs Associative array of attributes, e.g., [
  442. * 'href' => 'https://www.mediawiki.org/' ]. Values will be HTML-escaped.
  443. * A value of false or null means to omit the attribute. For boolean attributes,
  444. * you can omit the key, e.g., [ 'checked' ] instead of
  445. * [ 'checked' => 'checked' ] or such.
  446. *
  447. * @throws MWException If an attribute that doesn't allow lists is set to an array
  448. * @return string HTML fragment that goes between element name and '>'
  449. * (starting with a space if at least one attribute is output)
  450. */
  451. public static function expandAttributes( array $attribs ) {
  452. $ret = '';
  453. foreach ( $attribs as $key => $value ) {
  454. // Support intuitive [ 'checked' => true/false ] form
  455. if ( $value === false || is_null( $value ) ) {
  456. continue;
  457. }
  458. // For boolean attributes, support [ 'foo' ] instead of
  459. // requiring [ 'foo' => 'meaningless' ].
  460. if ( is_int( $key ) && in_array( strtolower( $value ), self::$boolAttribs ) ) {
  461. $key = $value;
  462. }
  463. // Not technically required in HTML5 but we'd like consistency
  464. // and better compression anyway.
  465. $key = strtolower( $key );
  466. // https://www.w3.org/TR/html401/index/attributes.html ("space-separated")
  467. // https://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated")
  468. $spaceSeparatedListAttributes = [
  469. 'class', // html4, html5
  470. 'accesskey', // as of html5, multiple space-separated values allowed
  471. // html4-spec doesn't document rel= as space-separated
  472. // but has been used like that and is now documented as such
  473. // in the html5-spec.
  474. 'rel',
  475. ];
  476. // Specific features for attributes that allow a list of space-separated values
  477. if ( in_array( $key, $spaceSeparatedListAttributes ) ) {
  478. // Apply some normalization and remove duplicates
  479. // Convert into correct array. Array can contain space-separated
  480. // values. Implode/explode to get those into the main array as well.
  481. if ( is_array( $value ) ) {
  482. // If input wasn't an array, we can skip this step
  483. $newValue = [];
  484. foreach ( $value as $k => $v ) {
  485. if ( is_string( $v ) ) {
  486. // String values should be normal `array( 'foo' )`
  487. // Just append them
  488. if ( !isset( $value[$v] ) ) {
  489. // As a special case don't set 'foo' if a
  490. // separate 'foo' => true/false exists in the array
  491. // keys should be authoritative
  492. $newValue[] = $v;
  493. }
  494. } elseif ( $v ) {
  495. // If the value is truthy but not a string this is likely
  496. // an [ 'foo' => true ], falsy values don't add strings
  497. $newValue[] = $k;
  498. }
  499. }
  500. $value = implode( ' ', $newValue );
  501. }
  502. $value = explode( ' ', $value );
  503. // Normalize spacing by fixing up cases where people used
  504. // more than 1 space and/or a trailing/leading space
  505. $value = array_diff( $value, [ '', ' ' ] );
  506. // Remove duplicates and create the string
  507. $value = implode( ' ', array_unique( $value ) );
  508. } elseif ( is_array( $value ) ) {
  509. throw new MWException( "HTML attribute $key can not contain a list of values" );
  510. }
  511. $quote = '"';
  512. if ( in_array( $key, self::$boolAttribs ) ) {
  513. $ret .= " $key=\"\"";
  514. } else {
  515. $ret .= " $key=$quote" . Sanitizer::encodeAttribute( $value ) . $quote;
  516. }
  517. }
  518. return $ret;
  519. }
  520. /**
  521. * Output a "<script>" tag with the given contents.
  522. *
  523. * @todo do some useful escaping as well, like if $contents contains
  524. * literal "</script>" or (for XML) literal "]]>".
  525. *
  526. * @param string $contents JavaScript
  527. * @return string Raw HTML
  528. */
  529. public static function inlineScript( $contents ) {
  530. $attrs = [];
  531. if ( preg_match( '/[<&]/', $contents ) ) {
  532. $contents = "/*<![CDATA[*/$contents/*]]>*/";
  533. }
  534. return self::rawElement( 'script', $attrs, $contents );
  535. }
  536. /**
  537. * Output a "<script>" tag linking to the given URL, e.g.,
  538. * "<script src=foo.js></script>".
  539. *
  540. * @param string $url
  541. * @return string Raw HTML
  542. */
  543. public static function linkedScript( $url ) {
  544. $attrs = [ 'src' => $url ];
  545. return self::element( 'script', $attrs );
  546. }
  547. /**
  548. * Output a "<style>" tag with the given contents for the given media type
  549. * (if any). TODO: do some useful escaping as well, like if $contents
  550. * contains literal "</style>" (admittedly unlikely).
  551. *
  552. * @param string $contents CSS
  553. * @param string $media A media type string, like 'screen'
  554. * @param array $attribs (since 1.31) Associative array of attributes, e.g., [
  555. * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for
  556. * further documentation.
  557. * @return string Raw HTML
  558. */
  559. public static function inlineStyle( $contents, $media = 'all', $attribs = [] ) {
  560. // Don't escape '>' since that is used
  561. // as direct child selector.
  562. // Remember, in css, there is no "x" for hexadecimal escapes, and
  563. // the space immediately after an escape sequence is swallowed.
  564. $contents = strtr( $contents, [
  565. '<' => '\3C ',
  566. // CDATA end tag for good measure, but the main security
  567. // is from escaping the '<'.
  568. ']]>' => '\5D\5D\3E '
  569. ] );
  570. if ( preg_match( '/[<&]/', $contents ) ) {
  571. $contents = "/*<![CDATA[*/$contents/*]]>*/";
  572. }
  573. return self::rawElement( 'style', [
  574. 'media' => $media,
  575. ] + $attribs, $contents );
  576. }
  577. /**
  578. * Output a "<link rel=stylesheet>" linking to the given URL for the given
  579. * media type (if any).
  580. *
  581. * @param string $url
  582. * @param string $media A media type string, like 'screen'
  583. * @return string Raw HTML
  584. */
  585. public static function linkedStyle( $url, $media = 'all' ) {
  586. return self::element( 'link', [
  587. 'rel' => 'stylesheet',
  588. 'href' => $url,
  589. 'media' => $media,
  590. ] );
  591. }
  592. /**
  593. * Convenience function to produce an "<input>" element. This supports the
  594. * new HTML5 input types and attributes.
  595. *
  596. * @param string $name Name attribute
  597. * @param string $value Value attribute
  598. * @param string $type Type attribute
  599. * @param array $attribs Associative array of miscellaneous extra
  600. * attributes, passed to Html::element()
  601. * @return string Raw HTML
  602. */
  603. public static function input( $name, $value = '', $type = 'text', array $attribs = [] ) {
  604. $attribs['type'] = $type;
  605. $attribs['value'] = $value;
  606. $attribs['name'] = $name;
  607. if ( in_array( $type, [ 'text', 'search', 'email', 'password', 'number' ] ) ) {
  608. $attribs = self::getTextInputAttributes( $attribs );
  609. }
  610. if ( in_array( $type, [ 'button', 'reset', 'submit' ] ) ) {
  611. $attribs = self::buttonAttributes( $attribs );
  612. }
  613. return self::element( 'input', $attribs );
  614. }
  615. /**
  616. * Convenience function to produce a checkbox (input element with type=checkbox)
  617. *
  618. * @param string $name Name attribute
  619. * @param bool $checked Whether the checkbox is checked or not
  620. * @param array $attribs Array of additional attributes
  621. * @return string Raw HTML
  622. */
  623. public static function check( $name, $checked = false, array $attribs = [] ) {
  624. if ( isset( $attribs['value'] ) ) {
  625. $value = $attribs['value'];
  626. unset( $attribs['value'] );
  627. } else {
  628. $value = 1;
  629. }
  630. if ( $checked ) {
  631. $attribs[] = 'checked';
  632. }
  633. return self::input( $name, $value, 'checkbox', $attribs );
  634. }
  635. /**
  636. * Return the HTML for a message box.
  637. * @since 1.31
  638. * @param string $html of contents of box
  639. * @param string $className corresponding to box
  640. * @param string $heading (optional)
  641. * @return string of HTML representing a box.
  642. */
  643. private static function messageBox( $html, $className, $heading = '' ) {
  644. if ( $heading ) {
  645. $html = self::element( 'h2', [], $heading ) . $html;
  646. }
  647. return self::rawElement( 'div', [ 'class' => $className ], $html );
  648. }
  649. /**
  650. * Return a warning box.
  651. * @since 1.31
  652. * @param string $html of contents of box
  653. * @return string of HTML representing a warning box.
  654. */
  655. public static function warningBox( $html ) {
  656. return self::messageBox( $html, 'warningbox' );
  657. }
  658. /**
  659. * Return an error box.
  660. * @since 1.31
  661. * @param string $html of contents of error box
  662. * @param string $heading (optional)
  663. * @return string of HTML representing an error box.
  664. */
  665. public static function errorBox( $html, $heading = '' ) {
  666. return self::messageBox( $html, 'errorbox', $heading );
  667. }
  668. /**
  669. * Return a success box.
  670. * @since 1.31
  671. * @param string $html of contents of box
  672. * @return string of HTML representing a success box.
  673. */
  674. public static function successBox( $html ) {
  675. return self::messageBox( $html, 'successbox' );
  676. }
  677. /**
  678. * Convenience function to produce a radio button (input element with type=radio)
  679. *
  680. * @param string $name Name attribute
  681. * @param bool $checked Whether the radio button is checked or not
  682. * @param array $attribs Array of additional attributes
  683. * @return string Raw HTML
  684. */
  685. public static function radio( $name, $checked = false, array $attribs = [] ) {
  686. if ( isset( $attribs['value'] ) ) {
  687. $value = $attribs['value'];
  688. unset( $attribs['value'] );
  689. } else {
  690. $value = 1;
  691. }
  692. if ( $checked ) {
  693. $attribs[] = 'checked';
  694. }
  695. return self::input( $name, $value, 'radio', $attribs );
  696. }
  697. /**
  698. * Convenience function for generating a label for inputs.
  699. *
  700. * @param string $label Contents of the label
  701. * @param string $id ID of the element being labeled
  702. * @param array $attribs Additional attributes
  703. * @return string Raw HTML
  704. */
  705. public static function label( $label, $id, array $attribs = [] ) {
  706. $attribs += [
  707. 'for' => $id
  708. ];
  709. return self::element( 'label', $attribs, $label );
  710. }
  711. /**
  712. * Convenience function to produce an input element with type=hidden
  713. *
  714. * @param string $name Name attribute
  715. * @param string $value Value attribute
  716. * @param array $attribs Associative array of miscellaneous extra
  717. * attributes, passed to Html::element()
  718. * @return string Raw HTML
  719. */
  720. public static function hidden( $name, $value, array $attribs = [] ) {
  721. return self::input( $name, $value, 'hidden', $attribs );
  722. }
  723. /**
  724. * Convenience function to produce a <textarea> element.
  725. *
  726. * This supports leaving out the cols= and rows= which Xml requires and are
  727. * required by HTML4/XHTML but not required by HTML5.
  728. *
  729. * @param string $name Name attribute
  730. * @param string $value Value attribute
  731. * @param array $attribs Associative array of miscellaneous extra
  732. * attributes, passed to Html::element()
  733. * @return string Raw HTML
  734. */
  735. public static function textarea( $name, $value = '', array $attribs = [] ) {
  736. $attribs['name'] = $name;
  737. if ( substr( $value, 0, 1 ) == "\n" ) {
  738. // Workaround for T14130: browsers eat the initial newline
  739. // assuming that it's just for show, but they do keep the later
  740. // newlines, which we may want to preserve during editing.
  741. // Prepending a single newline
  742. $spacedValue = "\n" . $value;
  743. } else {
  744. $spacedValue = $value;
  745. }
  746. return self::element( 'textarea', self::getTextInputAttributes( $attribs ), $spacedValue );
  747. }
  748. /**
  749. * Helper for Html::namespaceSelector().
  750. * @param array $params See Html::namespaceSelector()
  751. * @return array
  752. */
  753. public static function namespaceSelectorOptions( array $params = [] ) {
  754. global $wgContLang;
  755. $options = [];
  756. if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) {
  757. $params['exclude'] = [];
  758. }
  759. if ( isset( $params['all'] ) ) {
  760. // add an option that would let the user select all namespaces.
  761. // Value is provided by user, the name shown is localized for the user.
  762. $options[$params['all']] = wfMessage( 'namespacesall' )->text();
  763. }
  764. // Add all namespaces as options (in the content language)
  765. $options += $wgContLang->getFormattedNamespaces();
  766. $optionsOut = [];
  767. // Filter out namespaces below 0 and massage labels
  768. foreach ( $options as $nsId => $nsName ) {
  769. if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) {
  770. continue;
  771. }
  772. if ( $nsId === NS_MAIN ) {
  773. // For other namespaces use the namespace prefix as label, but for
  774. // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)")
  775. $nsName = wfMessage( 'blanknamespace' )->text();
  776. } elseif ( is_int( $nsId ) ) {
  777. $nsName = $wgContLang->convertNamespace( $nsId );
  778. }
  779. $optionsOut[$nsId] = $nsName;
  780. }
  781. return $optionsOut;
  782. }
  783. /**
  784. * Build a drop-down box for selecting a namespace
  785. *
  786. * @param array $params Params to set.
  787. * - selected: [optional] Id of namespace which should be pre-selected
  788. * - all: [optional] Value of item for "all namespaces". If null or unset,
  789. * no "<option>" is generated to select all namespaces.
  790. * - label: text for label to add before the field.
  791. * - exclude: [optional] Array of namespace ids to exclude.
  792. * - disable: [optional] Array of namespace ids for which the option should
  793. * be disabled in the selector.
  794. * @param array $selectAttribs HTML attributes for the generated select element.
  795. * - id: [optional], default: 'namespace'.
  796. * - name: [optional], default: 'namespace'.
  797. * @return string HTML code to select a namespace.
  798. */
  799. public static function namespaceSelector( array $params = [],
  800. array $selectAttribs = []
  801. ) {
  802. ksort( $selectAttribs );
  803. // Is a namespace selected?
  804. if ( isset( $params['selected'] ) ) {
  805. // If string only contains digits, convert to clean int. Selected could also
  806. // be "all" or "" etc. which needs to be left untouched.
  807. // PHP is_numeric() has issues with large strings, PHP ctype_digit has other issues
  808. // and returns false for already clean ints. Use regex instead..
  809. if ( preg_match( '/^\d+$/', $params['selected'] ) ) {
  810. $params['selected'] = intval( $params['selected'] );
  811. }
  812. // else: leaves it untouched for later processing
  813. } else {
  814. $params['selected'] = '';
  815. }
  816. if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) {
  817. $params['disable'] = [];
  818. }
  819. // Associative array between option-values and option-labels
  820. $options = self::namespaceSelectorOptions( $params );
  821. // Convert $options to HTML
  822. $optionsHtml = [];
  823. foreach ( $options as $nsId => $nsName ) {
  824. $optionsHtml[] = self::element(
  825. 'option', [
  826. 'disabled' => in_array( $nsId, $params['disable'] ),
  827. 'value' => $nsId,
  828. 'selected' => $nsId === $params['selected'],
  829. ], $nsName
  830. );
  831. }
  832. if ( !array_key_exists( 'id', $selectAttribs ) ) {
  833. $selectAttribs['id'] = 'namespace';
  834. }
  835. if ( !array_key_exists( 'name', $selectAttribs ) ) {
  836. $selectAttribs['name'] = 'namespace';
  837. }
  838. $ret = '';
  839. if ( isset( $params['label'] ) ) {
  840. $ret .= self::element(
  841. 'label', [
  842. 'for' => isset( $selectAttribs['id'] ) ? $selectAttribs['id'] : null,
  843. ], $params['label']
  844. ) . '&#160;';
  845. }
  846. // Wrap options in a <select>
  847. $ret .= self::openElement( 'select', $selectAttribs )
  848. . "\n"
  849. . implode( "\n", $optionsHtml )
  850. . "\n"
  851. . self::closeElement( 'select' );
  852. return $ret;
  853. }
  854. /**
  855. * Constructs the opening html-tag with necessary doctypes depending on
  856. * global variables.
  857. *
  858. * @param array $attribs Associative array of miscellaneous extra
  859. * attributes, passed to Html::element() of html tag.
  860. * @return string Raw HTML
  861. */
  862. public static function htmlHeader( array $attribs = [] ) {
  863. $ret = '';
  864. global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces;
  865. $isXHTML = self::isXmlMimeType( $wgMimeType );
  866. if ( $isXHTML ) { // XHTML5
  867. // XML MIME-typed markup should have an xml header.
  868. // However a DOCTYPE is not needed.
  869. $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?" . ">\n";
  870. // Add the standard xmlns
  871. $attribs['xmlns'] = 'http://www.w3.org/1999/xhtml';
  872. // And support custom namespaces
  873. foreach ( $wgXhtmlNamespaces as $tag => $ns ) {
  874. $attribs["xmlns:$tag"] = $ns;
  875. }
  876. } else { // HTML5
  877. // DOCTYPE
  878. $ret .= "<!DOCTYPE html>\n";
  879. }
  880. if ( $wgHtml5Version ) {
  881. $attribs['version'] = $wgHtml5Version;
  882. }
  883. $ret .= self::openElement( 'html', $attribs );
  884. return $ret;
  885. }
  886. /**
  887. * Determines if the given MIME type is xml.
  888. *
  889. * @param string $mimetype MIME type
  890. * @return bool
  891. */
  892. public static function isXmlMimeType( $mimetype ) {
  893. # https://html.spec.whatwg.org/multipage/infrastructure.html#xml-mime-type
  894. # * text/xml
  895. # * application/xml
  896. # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml)
  897. return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype );
  898. }
  899. /**
  900. * Get HTML for an info box with an icon.
  901. *
  902. * @param string $text Wikitext, get this with wfMessage()->plain()
  903. * @param string $icon Path to icon file (used as 'src' attribute)
  904. * @param string $alt Alternate text for the icon
  905. * @param string $class Additional class name to add to the wrapper div
  906. *
  907. * @return string
  908. */
  909. static function infoBox( $text, $icon, $alt, $class = '' ) {
  910. $s = self::openElement( 'div', [ 'class' => "mw-infobox $class" ] );
  911. $s .= self::openElement( 'div', [ 'class' => 'mw-infobox-left' ] ) .
  912. self::element( 'img',
  913. [
  914. 'src' => $icon,
  915. 'alt' => $alt,
  916. ]
  917. ) .
  918. self::closeElement( 'div' );
  919. $s .= self::openElement( 'div', [ 'class' => 'mw-infobox-right' ] ) .
  920. $text .
  921. self::closeElement( 'div' );
  922. $s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
  923. $s .= self::closeElement( 'div' );
  924. $s .= self::element( 'div', [ 'style' => 'clear: left;' ], ' ' );
  925. return $s;
  926. }
  927. /**
  928. * Generate a srcset attribute value.
  929. *
  930. * Generates a srcset attribute value from an array mapping pixel densities
  931. * to URLs. A trailing 'x' in pixel density values is optional.
  932. *
  933. * @note srcset width and height values are not supported.
  934. *
  935. * @see https://html.spec.whatwg.org/#attr-img-srcset
  936. *
  937. * @par Example:
  938. * @code
  939. * Html::srcSet( [
  940. * '1x' => 'standard.jpeg',
  941. * '1.5x' => 'large.jpeg',
  942. * '3x' => 'extra-large.jpeg',
  943. * ] );
  944. * // gives 'standard.jpeg 1x, large.jpeg 1.5x, extra-large.jpeg 2x'
  945. * @endcode
  946. *
  947. * @param string[] $urls
  948. * @return string
  949. */
  950. static function srcSet( array $urls ) {
  951. $candidates = [];
  952. foreach ( $urls as $density => $url ) {
  953. // Cast density to float to strip 'x', then back to string to serve
  954. // as array index.
  955. $density = (string)(float)$density;
  956. $candidates[$density] = $url;
  957. }
  958. // Remove duplicates that are the same as a smaller value
  959. ksort( $candidates, SORT_NUMERIC );
  960. $candidates = array_unique( $candidates );
  961. // Append density info to the url
  962. foreach ( $candidates as $density => $url ) {
  963. $candidates[$density] = $url . ' ' . $density . 'x';
  964. }
  965. return implode( ", ", $candidates );
  966. }
  967. }