Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.36% |
296 / 373 |
|
51.43% |
18 / 35 |
CRAP | |
0.00% |
0 / 1 |
Html | |
79.57% |
296 / 372 |
|
51.43% |
18 / 35 |
280.58 | |
0.00% |
0 / 1 |
buttonAttributes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getTextInputAttributes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
linkButton | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
submitButton | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
rawElement | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
element | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
openElement | |
97.22% |
35 / 36 |
|
0.00% |
0 / 1 |
7 | |||
closeElement | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
dropDefaults | |
98.31% |
58 / 59 |
|
0.00% |
0 / 1 |
25 | |||
expandAttributes | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
17 | |||
inlineScript | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
linkedScript | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
inlineStyle | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
linkedStyle | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
input | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
check | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
messageBox | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
3 | |||
noticeBox | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
warningBox | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
errorBox | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
successBox | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
radio | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
label | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
hidden | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
textarea | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
namespaceSelectorOptions | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
10 | |||
namespaceSelector | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
8 | |||
htmlHeader | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
isXmlMimeType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
srcSet | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
encodeJsVar | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
encodeJsCall | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
encodeJsList | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
listDropdownOptions | |
86.36% |
19 / 22 |
|
0.00% |
0 / 1 |
11.31 | |||
listDropdownOptionsOoui | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 |
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 | namespace MediaWiki\Html; |
27 | |
28 | use FormatJson; |
29 | use MediaWiki\MainConfigNames; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Request\ContentSecurityPolicy; |
32 | use UnexpectedValueException; |
33 | |
34 | /** |
35 | * This class is a collection of static functions that serve two purposes: |
36 | * |
37 | * 1) Implement any algorithms specified by HTML5, or other HTML |
38 | * specifications, in a convenient and self-contained way. |
39 | * |
40 | * 2) Allow HTML elements to be conveniently and safely generated, like the |
41 | * current Xml class but a) less confused (Xml supports HTML-specific things, |
42 | * but only sometimes!) and b) not necessarily confined to XML-compatible |
43 | * output. |
44 | * |
45 | * There are two important configuration options this class uses: |
46 | * |
47 | * $wgMimeType: If this is set to an xml MIME type then output should be |
48 | * valid XHTML5. |
49 | * |
50 | * This class is meant to be confined to utility functions that are called from |
51 | * trusted code paths. It does not do enforcement of policy like not allowing |
52 | * <a> elements. |
53 | * |
54 | * @since 1.16 |
55 | */ |
56 | class Html { |
57 | /** @var bool[] List of void elements from HTML5, section 8.1.2 as of 2016-09-19 */ |
58 | private static $voidElements = [ |
59 | 'area' => true, |
60 | 'base' => true, |
61 | 'br' => true, |
62 | 'col' => true, |
63 | 'embed' => true, |
64 | 'hr' => true, |
65 | 'img' => true, |
66 | 'input' => true, |
67 | 'keygen' => true, |
68 | 'link' => true, |
69 | 'meta' => true, |
70 | 'param' => true, |
71 | 'source' => true, |
72 | 'track' => true, |
73 | 'wbr' => true, |
74 | ]; |
75 | |
76 | /** |
77 | * Boolean attributes, which may have the value omitted entirely. Manually |
78 | * collected from the HTML5 spec as of 2011-08-12. |
79 | * @var bool[] |
80 | */ |
81 | private static $boolAttribs = [ |
82 | 'async' => true, |
83 | 'autofocus' => true, |
84 | 'autoplay' => true, |
85 | 'checked' => true, |
86 | 'controls' => true, |
87 | 'default' => true, |
88 | 'defer' => true, |
89 | 'disabled' => true, |
90 | 'formnovalidate' => true, |
91 | 'hidden' => true, |
92 | 'ismap' => true, |
93 | 'itemscope' => true, |
94 | 'loop' => true, |
95 | 'multiple' => true, |
96 | 'muted' => true, |
97 | 'novalidate' => true, |
98 | 'open' => true, |
99 | 'pubdate' => true, |
100 | 'readonly' => true, |
101 | 'required' => true, |
102 | 'reversed' => true, |
103 | 'scoped' => true, |
104 | 'seamless' => true, |
105 | 'selected' => true, |
106 | 'truespeed' => true, |
107 | 'typemustmatch' => true, |
108 | ]; |
109 | |
110 | /** |
111 | * Modifies a set of attributes meant for button elements. |
112 | * |
113 | * @param array $attrs HTML attributes in an associative array |
114 | * @param string[] $modifiers Unused |
115 | * @return array Modified attributes array |
116 | * @deprecated since 1.42 No-op |
117 | */ |
118 | public static function buttonAttributes( array $attrs, array $modifiers = [] ) { |
119 | wfDeprecated( __METHOD__, '1.42' ); |
120 | return $attrs; |
121 | } |
122 | |
123 | /** |
124 | * Modifies a set of attributes meant for text input elements. |
125 | * |
126 | * @param array $attrs An attribute array. |
127 | * @return array Modified attributes array |
128 | * @deprecated since 1.42 No-op |
129 | */ |
130 | public static function getTextInputAttributes( array $attrs ) { |
131 | wfDeprecated( __METHOD__, '1.42' ); |
132 | return $attrs; |
133 | } |
134 | |
135 | /** |
136 | * Returns an HTML link element in a string. |
137 | * |
138 | * @param string $text The text of the element. Will be escaped (not raw HTML) |
139 | * @param array $attrs Associative array of attributes, e.g., [ |
140 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
141 | * further documentation. |
142 | * @param string[] $modifiers Unused |
143 | * @return string Raw HTML |
144 | */ |
145 | public static function linkButton( $text, array $attrs, array $modifiers = [] ) { |
146 | return self::element( |
147 | 'a', |
148 | $attrs, |
149 | $text |
150 | ); |
151 | } |
152 | |
153 | /** |
154 | * Returns an HTML input element in a string. |
155 | * |
156 | * @param string $contents Plain text label for the button value |
157 | * @param array $attrs Associative array of attributes, e.g., [ |
158 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
159 | * further documentation. |
160 | * @param string[] $modifiers Unused |
161 | * @return string Raw HTML |
162 | */ |
163 | public static function submitButton( $contents, array $attrs = [], array $modifiers = [] ) { |
164 | $attrs['type'] = 'submit'; |
165 | $attrs['value'] = $contents; |
166 | return self::element( 'input', $attrs ); |
167 | } |
168 | |
169 | /** |
170 | * Returns an HTML element in a string. The major advantage here over |
171 | * manually typing out the HTML is that it will escape all attribute |
172 | * values. |
173 | * |
174 | * This is quite similar to Xml::tags(), but it implements some useful |
175 | * HTML-specific logic. For instance, there is no $allowShortTag |
176 | * parameter: the closing tag is magically omitted if $element has an empty |
177 | * content model. |
178 | * |
179 | * @param string $element The element's name, e.g., 'a' |
180 | * @param-taint $element tainted |
181 | * @param array $attribs Associative array of attributes, e.g., [ |
182 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
183 | * further documentation. |
184 | * @param-taint $attribs escapes_html |
185 | * @param string $contents The raw HTML contents of the element: *not* |
186 | * escaped! |
187 | * @param-taint $contents tainted |
188 | * @return string Raw HTML |
189 | * @return-taint escaped |
190 | */ |
191 | public static function rawElement( $element, $attribs = [], $contents = '' ) { |
192 | $start = self::openElement( $element, $attribs ); |
193 | if ( isset( self::$voidElements[$element] ) ) { |
194 | return $start; |
195 | } else { |
196 | return $start . $contents . self::closeElement( $element ); |
197 | } |
198 | } |
199 | |
200 | /** |
201 | * Identical to rawElement(), but HTML-escapes $contents (like |
202 | * Xml::element()). |
203 | * |
204 | * @param string $element Name of the element, e.g., 'a' |
205 | * @param-taint $element tainted |
206 | * @param array $attribs Associative array of attributes, e.g., [ |
207 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
208 | * further documentation. |
209 | * @param-taint $attribs escapes_html |
210 | * @param string $contents |
211 | * @param-taint $contents escapes_html |
212 | * |
213 | * @return string |
214 | * @return-taint escaped |
215 | */ |
216 | public static function element( $element, $attribs = [], $contents = '' ) { |
217 | return self::rawElement( |
218 | $element, |
219 | $attribs, |
220 | strtr( $contents ?? '', [ |
221 | // There's no point in escaping quotes, >, etc. in the contents of |
222 | // elements. |
223 | '&' => '&', |
224 | '<' => '<', |
225 | ] ) |
226 | ); |
227 | } |
228 | |
229 | /** |
230 | * Identical to rawElement(), but has no third parameter and omits the end |
231 | * tag (and the self-closing '/' in XML mode for empty elements). |
232 | * |
233 | * @param string $element Name of the element, e.g., 'a' |
234 | * @param array $attribs Associative array of attributes, e.g., [ |
235 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
236 | * further documentation. |
237 | * |
238 | * @return string |
239 | */ |
240 | public static function openElement( $element, $attribs = [] ) { |
241 | $attribs = (array)$attribs; |
242 | // This is not required in HTML5, but let's do it anyway, for |
243 | // consistency and better compression. |
244 | $element = strtolower( $element ); |
245 | |
246 | // Some people were abusing this by passing things like |
247 | // 'h1 id="foo" to $element, which we don't want. |
248 | if ( str_contains( $element, ' ' ) ) { |
249 | wfWarn( __METHOD__ . " given element name with space '$element'" ); |
250 | } |
251 | |
252 | // Remove invalid input types |
253 | if ( $element == 'input' ) { |
254 | $validTypes = [ |
255 | 'hidden' => true, |
256 | 'text' => true, |
257 | 'password' => true, |
258 | 'checkbox' => true, |
259 | 'radio' => true, |
260 | 'file' => true, |
261 | 'submit' => true, |
262 | 'image' => true, |
263 | 'reset' => true, |
264 | 'button' => true, |
265 | |
266 | // HTML input types |
267 | 'datetime' => true, |
268 | 'datetime-local' => true, |
269 | 'date' => true, |
270 | 'month' => true, |
271 | 'time' => true, |
272 | 'week' => true, |
273 | 'number' => true, |
274 | 'range' => true, |
275 | 'email' => true, |
276 | 'url' => true, |
277 | 'search' => true, |
278 | 'tel' => true, |
279 | 'color' => true, |
280 | ]; |
281 | if ( isset( $attribs['type'] ) && !isset( $validTypes[$attribs['type']] ) ) { |
282 | unset( $attribs['type'] ); |
283 | } |
284 | } |
285 | |
286 | // According to standard the default type for <button> elements is "submit". |
287 | // Depending on compatibility mode IE might use "button", instead. |
288 | // We enforce the standard "submit". |
289 | if ( $element == 'button' && !isset( $attribs['type'] ) ) { |
290 | $attribs['type'] = 'submit'; |
291 | } |
292 | |
293 | return "<$element" . self::expandAttributes( |
294 | self::dropDefaults( $element, $attribs ) ) . '>'; |
295 | } |
296 | |
297 | /** |
298 | * Returns "</$element>" |
299 | * |
300 | * @since 1.17 |
301 | * @param string $element Name of the element, e.g., 'a' |
302 | * @return string A closing tag |
303 | */ |
304 | public static function closeElement( $element ) { |
305 | $element = strtolower( $element ); |
306 | |
307 | return "</$element>"; |
308 | } |
309 | |
310 | /** |
311 | * Given an element name and an associative array of element attributes, |
312 | * return an array that is functionally identical to the input array, but |
313 | * possibly smaller. In particular, attributes might be stripped if they |
314 | * are given their default values. |
315 | * |
316 | * This method is not guaranteed to remove all redundant attributes, only |
317 | * some common ones and some others selected arbitrarily at random. It |
318 | * only guarantees that the output array should be functionally identical |
319 | * to the input array (currently per the HTML 5 draft as of 2009-09-06). |
320 | * |
321 | * @param string $element Name of the element, e.g., 'a' |
322 | * @param array $attribs Associative array of attributes, e.g., [ |
323 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
324 | * further documentation. |
325 | * @return array An array of attributes functionally identical to $attribs |
326 | */ |
327 | private static function dropDefaults( $element, array $attribs ) { |
328 | // Whenever altering this array, please provide a covering test case |
329 | // in HtmlTest::provideElementsWithAttributesHavingDefaultValues |
330 | static $attribDefaults = [ |
331 | 'area' => [ 'shape' => 'rect' ], |
332 | 'button' => [ |
333 | 'formaction' => 'GET', |
334 | 'formenctype' => 'application/x-www-form-urlencoded', |
335 | ], |
336 | 'canvas' => [ |
337 | 'height' => '150', |
338 | 'width' => '300', |
339 | ], |
340 | 'form' => [ |
341 | 'action' => 'GET', |
342 | 'autocomplete' => 'on', |
343 | 'enctype' => 'application/x-www-form-urlencoded', |
344 | ], |
345 | 'input' => [ |
346 | 'formaction' => 'GET', |
347 | 'type' => 'text', |
348 | ], |
349 | 'keygen' => [ 'keytype' => 'rsa' ], |
350 | 'link' => [ 'media' => 'all' ], |
351 | 'menu' => [ 'type' => 'list' ], |
352 | 'script' => [ 'type' => 'text/javascript' ], |
353 | 'style' => [ |
354 | 'media' => 'all', |
355 | 'type' => 'text/css', |
356 | ], |
357 | 'textarea' => [ 'wrap' => 'soft' ], |
358 | ]; |
359 | |
360 | foreach ( $attribs as $attrib => $value ) { |
361 | if ( $attrib === 'class' ) { |
362 | if ( $value === '' || $value === [] || $value === [ '' ] ) { |
363 | unset( $attribs[$attrib] ); |
364 | } |
365 | } elseif ( isset( $attribDefaults[$element][$attrib] ) ) { |
366 | if ( is_array( $value ) ) { |
367 | $value = implode( ' ', $value ); |
368 | } else { |
369 | $value = strval( $value ); |
370 | } |
371 | if ( $attribDefaults[$element][$attrib] == $value ) { |
372 | unset( $attribs[$attrib] ); |
373 | } |
374 | } |
375 | } |
376 | |
377 | // More subtle checks |
378 | if ( $element === 'link' |
379 | && isset( $attribs['type'] ) && strval( $attribs['type'] ) == 'text/css' |
380 | ) { |
381 | unset( $attribs['type'] ); |
382 | } |
383 | if ( $element === 'input' ) { |
384 | $type = $attribs['type'] ?? null; |
385 | $value = $attribs['value'] ?? null; |
386 | if ( $type === 'checkbox' || $type === 'radio' ) { |
387 | // The default value for checkboxes and radio buttons is 'on' |
388 | // not ''. By stripping value="" we break radio boxes that |
389 | // actually wants empty values. |
390 | if ( $value === 'on' ) { |
391 | unset( $attribs['value'] ); |
392 | } |
393 | } elseif ( $type === 'submit' ) { |
394 | // The default value for submit appears to be "Submit" but |
395 | // let's not bother stripping out localized text that matches |
396 | // that. |
397 | } else { |
398 | // The default value for nearly every other field type is '' |
399 | // The 'range' and 'color' types use different defaults but |
400 | // stripping a value="" does not hurt them. |
401 | if ( $value === '' ) { |
402 | unset( $attribs['value'] ); |
403 | } |
404 | } |
405 | } |
406 | if ( $element === 'select' && isset( $attribs['size'] ) ) { |
407 | if ( in_array( 'multiple', $attribs ) |
408 | || ( isset( $attribs['multiple'] ) && $attribs['multiple'] !== false ) |
409 | ) { |
410 | // A multi-select |
411 | if ( strval( $attribs['size'] ) == '4' ) { |
412 | unset( $attribs['size'] ); |
413 | } |
414 | } else { |
415 | // Single select |
416 | if ( strval( $attribs['size'] ) == '1' ) { |
417 | unset( $attribs['size'] ); |
418 | } |
419 | } |
420 | } |
421 | |
422 | return $attribs; |
423 | } |
424 | |
425 | /** |
426 | * Given an associative array of element attributes, generate a string |
427 | * to stick after the element name in HTML output. Like [ 'href' => |
428 | * 'https://www.mediawiki.org/' ] becomes something like |
429 | * ' href="https://www.mediawiki.org"'. Again, this is like |
430 | * Xml::expandAttributes(), but it implements some HTML-specific logic. |
431 | * |
432 | * Attributes that can contain space-separated lists ('class', 'accesskey' and 'rel') array |
433 | * values are allowed as well, which will automagically be normalized |
434 | * and converted to a space-separated string. In addition to a numerical |
435 | * array, the attribute value may also be an associative array. See the |
436 | * example below for how that works. |
437 | * |
438 | * @par Numerical array |
439 | * @code |
440 | * Html::element( 'em', [ |
441 | * 'class' => [ 'foo', 'bar' ] |
442 | * ] ); |
443 | * // gives '<em class="foo bar"></em>' |
444 | * @endcode |
445 | * |
446 | * @par Associative array |
447 | * @code |
448 | * Html::element( 'em', [ |
449 | * 'class' => [ 'foo', 'bar', 'foo' => false, 'quux' => true ] |
450 | * ] ); |
451 | * // gives '<em class="bar quux"></em>' |
452 | * @endcode |
453 | * |
454 | * @param array $attribs Associative array of attributes, e.g., [ |
455 | * 'href' => 'https://www.mediawiki.org/' ]. Values will be HTML-escaped. |
456 | * A value of false or null means to omit the attribute. For boolean attributes, |
457 | * you can omit the key, e.g., [ 'checked' ] instead of |
458 | * [ 'checked' => 'checked' ] or such. |
459 | * |
460 | * @return string HTML fragment that goes between element name and '>' |
461 | * (starting with a space if at least one attribute is output) |
462 | */ |
463 | public static function expandAttributes( array $attribs ) { |
464 | $ret = ''; |
465 | foreach ( $attribs as $key => $value ) { |
466 | // Support intuitive [ 'checked' => true/false ] form |
467 | if ( $value === false || $value === null ) { |
468 | continue; |
469 | } |
470 | |
471 | // For boolean attributes, support [ 'foo' ] instead of |
472 | // requiring [ 'foo' => 'meaningless' ]. |
473 | if ( is_int( $key ) && isset( self::$boolAttribs[strtolower( $value )] ) ) { |
474 | $key = $value; |
475 | } |
476 | |
477 | // Not technically required in HTML5 but we'd like consistency |
478 | // and better compression anyway. |
479 | $key = strtolower( $key ); |
480 | |
481 | // https://www.w3.org/TR/html401/index/attributes.html ("space-separated") |
482 | // https://www.w3.org/TR/html5/index.html#attributes-1 ("space-separated") |
483 | $spaceSeparatedListAttributes = [ |
484 | 'class' => true, // html4, html5 |
485 | 'accesskey' => true, // as of html5, multiple space-separated values allowed |
486 | // html4-spec doesn't document rel= as space-separated |
487 | // but has been used like that and is now documented as such |
488 | // in the html5-spec. |
489 | 'rel' => true, |
490 | ]; |
491 | |
492 | // Specific features for attributes that allow a list of space-separated values |
493 | if ( isset( $spaceSeparatedListAttributes[$key] ) ) { |
494 | // Apply some normalization and remove duplicates |
495 | |
496 | // Convert into correct array. Array can contain space-separated |
497 | // values. Implode/explode to get those into the main array as well. |
498 | if ( is_array( $value ) ) { |
499 | // If input wasn't an array, we can skip this step |
500 | $arrayValue = []; |
501 | foreach ( $value as $k => $v ) { |
502 | if ( is_string( $v ) ) { |
503 | // String values should be normal `[ 'foo' ]` |
504 | // Just append them |
505 | if ( !isset( $value[$v] ) ) { |
506 | // As a special case don't set 'foo' if a |
507 | // separate 'foo' => true/false exists in the array |
508 | // keys should be authoritative |
509 | foreach ( explode( ' ', $v ) as $part ) { |
510 | // Normalize spacing by fixing up cases where people used |
511 | // more than 1 space and/or a trailing/leading space |
512 | if ( $part !== '' && $part !== ' ' ) { |
513 | $arrayValue[] = $part; |
514 | } |
515 | } |
516 | } |
517 | } elseif ( $v ) { |
518 | // If the value is truthy but not a string this is likely |
519 | // an [ 'foo' => true ], falsy values don't add strings |
520 | $arrayValue[] = $k; |
521 | } |
522 | } |
523 | } else { |
524 | $arrayValue = explode( ' ', $value ); |
525 | // Normalize spacing by fixing up cases where people used |
526 | // more than 1 space and/or a trailing/leading space |
527 | $arrayValue = array_diff( $arrayValue, [ '', ' ' ] ); |
528 | } |
529 | |
530 | // Remove duplicates and create the string |
531 | $value = implode( ' ', array_unique( $arrayValue ) ); |
532 | |
533 | // Optimization: Skip below boolAttribs check and jump straight |
534 | // to its `else` block. The current $spaceSeparatedListAttributes |
535 | // block is mutually exclusive with $boolAttribs. |
536 | // phpcs:ignore Generic.PHP.DiscourageGoto |
537 | goto not_bool; // NOSONAR |
538 | } elseif ( is_array( $value ) ) { |
539 | throw new UnexpectedValueException( "HTML attribute $key can not contain a list of values" ); |
540 | } |
541 | |
542 | if ( isset( self::$boolAttribs[$key] ) ) { |
543 | $ret .= " $key=\"\""; |
544 | } else { |
545 | // phpcs:ignore Generic.PHP.DiscourageGoto |
546 | not_bool: |
547 | // Inlined from Sanitizer::encodeAttribute() for improved performance |
548 | $encValue = htmlspecialchars( $value, ENT_QUOTES ); |
549 | // Whitespace is normalized during attribute decoding, |
550 | // so if we've been passed non-spaces we must encode them |
551 | // ahead of time or they won't be preserved. |
552 | $encValue = strtr( $encValue, [ |
553 | "\n" => ' ', |
554 | "\r" => ' ', |
555 | "\t" => '	', |
556 | ] ); |
557 | $ret .= " $key=\"$encValue\""; |
558 | } |
559 | } |
560 | return $ret; |
561 | } |
562 | |
563 | /** |
564 | * Output an HTML script tag with the given contents. |
565 | * |
566 | * It is unsupported for the contents to contain the sequence `<script` or `</script` |
567 | * (case-insensitive). This ensures the script can be terminated easily and consistently. |
568 | * It is the responsibility of the caller to avoid such character sequence by escaping |
569 | * or avoiding it. If found at run-time, the contents are replaced with a comment, and |
570 | * a warning is logged server-side. |
571 | * |
572 | * @param string $contents JavaScript |
573 | * @param string|null $nonce Unused |
574 | * @return string Raw HTML |
575 | */ |
576 | public static function inlineScript( $contents, $nonce = null ) { |
577 | if ( preg_match( '/<\/?script/i', $contents ) ) { |
578 | wfLogWarning( __METHOD__ . ': Illegal character sequence found in inline script.' ); |
579 | $contents = '/* ERROR: Invalid script */'; |
580 | } |
581 | |
582 | return self::rawElement( 'script', [], $contents ); |
583 | } |
584 | |
585 | /** |
586 | * Output a "<script>" tag linking to the given URL, e.g., |
587 | * "<script src=foo.js></script>". |
588 | * |
589 | * @param string $url |
590 | * @param string|null $nonce Nonce for CSP header, from OutputPage->getCSP()->getNonce() |
591 | * @return string Raw HTML |
592 | */ |
593 | public static function linkedScript( $url, $nonce = null ) { |
594 | $attrs = [ 'src' => $url ]; |
595 | if ( $nonce !== null ) { |
596 | $attrs['nonce'] = $nonce; |
597 | } elseif ( ContentSecurityPolicy::isNonceRequired( MediaWikiServices::getInstance()->getMainConfig() ) ) { |
598 | wfWarn( "no nonce set on script. CSP will break it" ); |
599 | } |
600 | |
601 | return self::element( 'script', $attrs ); |
602 | } |
603 | |
604 | /** |
605 | * Output a "<style>" tag with the given contents for the given media type |
606 | * (if any). TODO: do some useful escaping as well, like if $contents |
607 | * contains literal "</style>" (admittedly unlikely). |
608 | * |
609 | * @param string $contents CSS |
610 | * @param string $media A media type string, like 'screen' |
611 | * @param array $attribs (since 1.31) Associative array of attributes, e.g., [ |
612 | * 'href' => 'https://www.mediawiki.org/' ]. See expandAttributes() for |
613 | * further documentation. |
614 | * @return string Raw HTML |
615 | */ |
616 | public static function inlineStyle( $contents, $media = 'all', $attribs = [] ) { |
617 | // Don't escape '>' since that is used |
618 | // as direct child selector. |
619 | // Remember, in css, there is no "x" for hexadecimal escapes, and |
620 | // the space immediately after an escape sequence is swallowed. |
621 | $contents = strtr( $contents, [ |
622 | '<' => '\3C ', |
623 | // CDATA end tag for good measure, but the main security |
624 | // is from escaping the '<'. |
625 | ']]>' => '\5D\5D\3E ' |
626 | ] ); |
627 | |
628 | if ( preg_match( '/[<&]/', $contents ) ) { |
629 | $contents = "/*<![CDATA[*/$contents/*]]>*/"; |
630 | } |
631 | |
632 | return self::rawElement( 'style', [ |
633 | 'media' => $media, |
634 | ] + $attribs, $contents ); |
635 | } |
636 | |
637 | /** |
638 | * Output a "<link rel=stylesheet>" linking to the given URL for the given |
639 | * media type (if any). |
640 | * |
641 | * @param string $url |
642 | * @param string $media A media type string, like 'screen' |
643 | * @return string Raw HTML |
644 | */ |
645 | public static function linkedStyle( $url, $media = 'all' ) { |
646 | return self::element( 'link', [ |
647 | 'rel' => 'stylesheet', |
648 | 'href' => $url, |
649 | 'media' => $media, |
650 | ] ); |
651 | } |
652 | |
653 | /** |
654 | * Convenience function to produce an `<input>` element. This supports the |
655 | * new HTML5 input types and attributes. |
656 | * |
657 | * @param string $name Name attribute |
658 | * @param string $value Value attribute |
659 | * @param string $type Type attribute |
660 | * @param array $attribs Associative array of miscellaneous extra |
661 | * attributes, passed to Html::element() |
662 | * @return string Raw HTML |
663 | */ |
664 | public static function input( $name, $value = '', $type = 'text', array $attribs = [] ) { |
665 | $attribs['type'] = $type; |
666 | $attribs['value'] = $value; |
667 | $attribs['name'] = $name; |
668 | return self::element( 'input', $attribs ); |
669 | } |
670 | |
671 | /** |
672 | * Convenience function to produce a checkbox (input element with type=checkbox) |
673 | * |
674 | * @param string $name Name attribute |
675 | * @param bool $checked Whether the checkbox is checked or not |
676 | * @param array $attribs Array of additional attributes |
677 | * @return string Raw HTML |
678 | */ |
679 | public static function check( $name, $checked = false, array $attribs = [] ) { |
680 | if ( isset( $attribs['value'] ) ) { |
681 | $value = $attribs['value']; |
682 | unset( $attribs['value'] ); |
683 | } else { |
684 | $value = 1; |
685 | } |
686 | |
687 | if ( $checked ) { |
688 | $attribs[] = 'checked'; |
689 | } |
690 | |
691 | return self::input( $name, $value, 'checkbox', $attribs ); |
692 | } |
693 | |
694 | /** |
695 | * Return the HTML for a message box. |
696 | * @since 1.31 |
697 | * @param string $html of contents of box |
698 | * @param string|array $className corresponding to box |
699 | * @param string $heading (optional) |
700 | * @param string $iconClassName (optional) corresponding to box icon |
701 | * @return string of HTML representing a box. |
702 | */ |
703 | private static function messageBox( $html, $className, $heading = '', $iconClassName = '' ) { |
704 | if ( $heading !== '' ) { |
705 | $html = self::element( 'h2', [], $heading ) . $html; |
706 | } |
707 | $coreClasses = [ |
708 | 'mw-message-box', |
709 | 'cdx-message', |
710 | 'cdx-message--block' |
711 | ]; |
712 | if ( is_array( $className ) ) { |
713 | $className = array_merge( |
714 | $coreClasses, |
715 | $className |
716 | ); |
717 | } else { |
718 | $className .= ' ' . implode( ' ', $coreClasses ); |
719 | } |
720 | return self::rawElement( 'div', [ 'class' => $className ], |
721 | self::element( 'span', [ 'class' => [ |
722 | 'cdx-message__icon', |
723 | $iconClassName |
724 | ] ] ) . |
725 | self::rawElement( 'div', [ |
726 | 'class' => 'cdx-message__content' |
727 | ], $html ) |
728 | ); |
729 | } |
730 | |
731 | /** |
732 | * Return the HTML for a notice message box. |
733 | * @since 1.38 |
734 | * @param string $html of contents of notice |
735 | * @param string|array $className corresponding to notice |
736 | * @param string $heading (optional) |
737 | * @param string|array $iconClassName (optional) corresponding to notice icon |
738 | * @return string of HTML representing the notice |
739 | */ |
740 | public static function noticeBox( $html, $className, $heading = '', $iconClassName = '' ) { |
741 | return self::messageBox( $html, [ |
742 | 'mw-message-box-notice', |
743 | 'cdx-message--notice', |
744 | $className |
745 | ], $heading, $iconClassName ); |
746 | } |
747 | |
748 | /** |
749 | * Return a warning box. |
750 | * @since 1.31 |
751 | * @since 1.34 $className optional parameter added |
752 | * @param string $html of contents of box |
753 | * @param string $className (optional) corresponding to box |
754 | * @return string of HTML representing a warning box. |
755 | */ |
756 | public static function warningBox( $html, $className = '' ) { |
757 | return self::messageBox( $html, [ |
758 | 'mw-message-box-warning', |
759 | 'cdx-message--warning', $className ] ); |
760 | } |
761 | |
762 | /** |
763 | * Return an error box. |
764 | * @since 1.31 |
765 | * @since 1.34 $className optional parameter added |
766 | * @param string $html of contents of error box |
767 | * @param string $heading (optional) |
768 | * @param string $className (optional) corresponding to box |
769 | * @return string of HTML representing an error box. |
770 | */ |
771 | public static function errorBox( $html, $heading = '', $className = '' ) { |
772 | return self::messageBox( $html, [ |
773 | 'mw-message-box-error', |
774 | 'cdx-message--error', $className ], $heading ); |
775 | } |
776 | |
777 | /** |
778 | * Return a success box. |
779 | * @since 1.31 |
780 | * @since 1.34 $className optional parameter added |
781 | * @param string $html of contents of box |
782 | * @param string $className (optional) corresponding to box |
783 | * @return string of HTML representing a success box. |
784 | */ |
785 | public static function successBox( $html, $className = '' ) { |
786 | return self::messageBox( $html, [ |
787 | 'mw-message-box-success', |
788 | 'cdx-message--success', $className ] ); |
789 | } |
790 | |
791 | /** |
792 | * Convenience function to produce a radio button (input element with type=radio) |
793 | * |
794 | * @param string $name Name attribute |
795 | * @param bool $checked Whether the radio button is checked or not |
796 | * @param array $attribs Array of additional attributes |
797 | * @return string Raw HTML |
798 | */ |
799 | public static function radio( $name, $checked = false, array $attribs = [] ) { |
800 | if ( isset( $attribs['value'] ) ) { |
801 | $value = $attribs['value']; |
802 | unset( $attribs['value'] ); |
803 | } else { |
804 | $value = 1; |
805 | } |
806 | |
807 | if ( $checked ) { |
808 | $attribs[] = 'checked'; |
809 | } |
810 | |
811 | return self::input( $name, $value, 'radio', $attribs ); |
812 | } |
813 | |
814 | /** |
815 | * Convenience function for generating a label for inputs. |
816 | * |
817 | * @param string $label Contents of the label |
818 | * @param string $id ID of the element being labeled |
819 | * @param array $attribs Additional attributes |
820 | * @return string Raw HTML |
821 | */ |
822 | public static function label( $label, $id, array $attribs = [] ) { |
823 | $attribs += [ |
824 | 'for' => $id, |
825 | ]; |
826 | return self::element( 'label', $attribs, $label ); |
827 | } |
828 | |
829 | /** |
830 | * Convenience function to produce an input element with type=hidden |
831 | * |
832 | * @param string $name Name attribute |
833 | * @param mixed $value Value attribute |
834 | * @param array $attribs Associative array of miscellaneous extra |
835 | * attributes, passed to Html::element() |
836 | * @return string Raw HTML |
837 | */ |
838 | public static function hidden( $name, $value, array $attribs = [] ) { |
839 | return self::input( $name, $value, 'hidden', $attribs ); |
840 | } |
841 | |
842 | /** |
843 | * Convenience function to produce a <textarea> element. |
844 | * |
845 | * This supports leaving out the cols= and rows= which Xml requires and are |
846 | * required by HTML4/XHTML but not required by HTML5. |
847 | * |
848 | * @param string $name Name attribute |
849 | * @param string $value Value attribute |
850 | * @param array $attribs Associative array of miscellaneous extra |
851 | * attributes, passed to Html::element() |
852 | * @return string Raw HTML |
853 | */ |
854 | public static function textarea( $name, $value = '', array $attribs = [] ) { |
855 | $attribs['name'] = $name; |
856 | |
857 | if ( substr( $value, 0, 1 ) == "\n" ) { |
858 | // Workaround for T14130: browsers eat the initial newline |
859 | // assuming that it's just for show, but they do keep the later |
860 | // newlines, which we may want to preserve during editing. |
861 | // Prepending a single newline |
862 | $spacedValue = "\n" . $value; |
863 | } else { |
864 | $spacedValue = $value; |
865 | } |
866 | return self::element( 'textarea', $attribs, $spacedValue ); |
867 | } |
868 | |
869 | /** |
870 | * Helper for Html::namespaceSelector(). |
871 | * @param array $params See Html::namespaceSelector() |
872 | * @return array |
873 | */ |
874 | public static function namespaceSelectorOptions( array $params = [] ) { |
875 | if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { |
876 | $params['exclude'] = []; |
877 | } |
878 | |
879 | if ( $params['in-user-lang'] ?? false ) { |
880 | global $wgLang; |
881 | $lang = $wgLang; |
882 | } else { |
883 | $lang = MediaWikiServices::getInstance()->getContentLanguage(); |
884 | } |
885 | |
886 | $optionsOut = []; |
887 | if ( isset( $params['all'] ) ) { |
888 | // add an option that would let the user select all namespaces. |
889 | // Value is provided by user, the name shown is localized for the user. |
890 | $optionsOut[$params['all']] = wfMessage( 'namespacesall' )->text(); |
891 | } |
892 | // Add all namespaces as options |
893 | $options = $lang->getFormattedNamespaces(); |
894 | // Filter out namespaces below 0 and massage labels |
895 | foreach ( $options as $nsId => $nsName ) { |
896 | if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { |
897 | continue; |
898 | } |
899 | if ( $nsId === NS_MAIN ) { |
900 | // For other namespaces use the namespace prefix as label, but for |
901 | // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") |
902 | $nsName = wfMessage( 'blanknamespace' )->text(); |
903 | } elseif ( is_int( $nsId ) ) { |
904 | $converter = MediaWikiServices::getInstance()->getLanguageConverterFactory() |
905 | ->getLanguageConverter( $lang ); |
906 | $nsName = $converter->convertNamespace( $nsId ); |
907 | } |
908 | $optionsOut[$nsId] = $nsName; |
909 | } |
910 | |
911 | return $optionsOut; |
912 | } |
913 | |
914 | /** |
915 | * Build a drop-down box for selecting a namespace |
916 | * |
917 | * @param array $params Params to set. |
918 | * - selected: [optional] Id of namespace which should be pre-selected |
919 | * - all: [optional] Value of item for "all namespaces". If null or unset, |
920 | * no "<option>" is generated to select all namespaces. |
921 | * - label: text for label to add before the field. |
922 | * - exclude: [optional] Array of namespace ids to exclude. |
923 | * - disable: [optional] Array of namespace ids for which the option should |
924 | * be disabled in the selector. |
925 | * @param array $selectAttribs HTML attributes for the generated select element. |
926 | * - id: [optional], default: 'namespace'. |
927 | * - name: [optional], default: 'namespace'. |
928 | * @return string HTML code to select a namespace. |
929 | */ |
930 | public static function namespaceSelector( |
931 | array $params = [], |
932 | array $selectAttribs = [] |
933 | ) { |
934 | ksort( $selectAttribs ); |
935 | |
936 | // Is a namespace selected? |
937 | if ( isset( $params['selected'] ) ) { |
938 | // If string only contains digits, convert to clean int. Selected could also |
939 | // be "all" or "" etc. which needs to be left untouched. |
940 | if ( !is_int( $params['selected'] ) && ctype_digit( (string)$params['selected'] ) ) { |
941 | $params['selected'] = (int)$params['selected']; |
942 | } |
943 | // else: leaves it untouched for later processing |
944 | } else { |
945 | $params['selected'] = ''; |
946 | } |
947 | |
948 | if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { |
949 | $params['disable'] = []; |
950 | } |
951 | |
952 | // Associative array between option-values and option-labels |
953 | $options = self::namespaceSelectorOptions( $params ); |
954 | |
955 | // Convert $options to HTML |
956 | $optionsHtml = []; |
957 | foreach ( $options as $nsId => $nsName ) { |
958 | $optionsHtml[] = self::element( |
959 | 'option', |
960 | [ |
961 | 'disabled' => in_array( $nsId, $params['disable'] ), |
962 | 'value' => $nsId, |
963 | 'selected' => $nsId === $params['selected'], |
964 | ], |
965 | $nsName |
966 | ); |
967 | } |
968 | |
969 | $selectAttribs['id'] ??= 'namespace'; |
970 | $selectAttribs['name'] ??= 'namespace'; |
971 | |
972 | $ret = ''; |
973 | if ( isset( $params['label'] ) ) { |
974 | $ret .= self::element( |
975 | 'label', [ |
976 | 'for' => $selectAttribs['id'], |
977 | ], $params['label'] |
978 | ) . "\u{00A0}"; |
979 | } |
980 | |
981 | // Wrap options in a <select> |
982 | $ret .= self::openElement( 'select', $selectAttribs ) |
983 | . "\n" |
984 | . implode( "\n", $optionsHtml ) |
985 | . "\n" |
986 | . self::closeElement( 'select' ); |
987 | |
988 | return $ret; |
989 | } |
990 | |
991 | /** |
992 | * Constructs the opening html-tag with necessary doctypes depending on |
993 | * global variables. |
994 | * |
995 | * @param array $attribs Associative array of miscellaneous extra |
996 | * attributes, passed to Html::element() of html tag. |
997 | * @return string Raw HTML |
998 | */ |
999 | public static function htmlHeader( array $attribs = [] ) { |
1000 | $ret = ''; |
1001 | $mainConfig = MediaWikiServices::getInstance()->getMainConfig(); |
1002 | $html5Version = $mainConfig->get( MainConfigNames::Html5Version ); |
1003 | $mimeType = $mainConfig->get( MainConfigNames::MimeType ); |
1004 | $xhtmlNamespaces = $mainConfig->get( MainConfigNames::XhtmlNamespaces ); |
1005 | |
1006 | $isXHTML = self::isXmlMimeType( $mimeType ); |
1007 | |
1008 | if ( $isXHTML ) { // XHTML5 |
1009 | // XML MIME-typed markup should have an xml header. |
1010 | // However a DOCTYPE is not needed. |
1011 | $ret .= "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"; |
1012 | |
1013 | // Add the standard xmlns |
1014 | $attribs['xmlns'] = 'http://www.w3.org/1999/xhtml'; |
1015 | |
1016 | // And support custom namespaces |
1017 | foreach ( $xhtmlNamespaces as $tag => $ns ) { |
1018 | $attribs["xmlns:$tag"] = $ns; |
1019 | } |
1020 | } else { // HTML5 |
1021 | $ret .= "<!DOCTYPE html>\n"; |
1022 | } |
1023 | |
1024 | if ( $html5Version ) { |
1025 | $attribs['version'] = $html5Version; |
1026 | } |
1027 | |
1028 | $ret .= self::openElement( 'html', $attribs ); |
1029 | |
1030 | return $ret; |
1031 | } |
1032 | |
1033 | /** |
1034 | * Determines if the given MIME type is xml. |
1035 | * |
1036 | * @param string $mimetype |
1037 | * @return bool |
1038 | */ |
1039 | public static function isXmlMimeType( $mimetype ) { |
1040 | # https://html.spec.whatwg.org/multipage/infrastructure.html#xml-mime-type |
1041 | # * text/xml |
1042 | # * application/xml |
1043 | # * Any MIME type with a subtype ending in +xml (this implicitly includes application/xhtml+xml) |
1044 | return (bool)preg_match( '!^(text|application)/xml$|^.+/.+\+xml$!', $mimetype ); |
1045 | } |
1046 | |
1047 | /** |
1048 | * Generate a srcset attribute value. |
1049 | * |
1050 | * Generates a srcset attribute value from an array mapping pixel densities |
1051 | * to URLs. A trailing 'x' in pixel density values is optional. |
1052 | * |
1053 | * @note srcset width and height values are not supported. |
1054 | * |
1055 | * @see https://html.spec.whatwg.org/#attr-img-srcset |
1056 | * |
1057 | * @par Example: |
1058 | * @code |
1059 | * Html::srcSet( [ |
1060 | * '1x' => 'standard.jpeg', |
1061 | * '1.5x' => 'large.jpeg', |
1062 | * '3x' => 'extra-large.jpeg', |
1063 | * ] ); |
1064 | * // gives 'standard.jpeg 1x, large.jpeg 1.5x, extra-large.jpeg 2x' |
1065 | * @endcode |
1066 | * |
1067 | * @param string[] $urls |
1068 | * @return string |
1069 | */ |
1070 | public static function srcSet( array $urls ) { |
1071 | $candidates = []; |
1072 | foreach ( $urls as $density => $url ) { |
1073 | // Cast density to float to strip 'x', then back to string to serve |
1074 | // as array index. |
1075 | $density = (string)(float)$density; |
1076 | $candidates[$density] = $url; |
1077 | } |
1078 | |
1079 | // Remove duplicates that are the same as a smaller value |
1080 | ksort( $candidates, SORT_NUMERIC ); |
1081 | $candidates = array_unique( $candidates ); |
1082 | |
1083 | // Append density info to the url |
1084 | foreach ( $candidates as $density => $url ) { |
1085 | $candidates[$density] = $url . ' ' . $density . 'x'; |
1086 | } |
1087 | |
1088 | return implode( ", ", $candidates ); |
1089 | } |
1090 | |
1091 | /** |
1092 | * Encode a variable of arbitrary type to JavaScript. |
1093 | * If the value is an HtmlJsCode object, pass through the object's value verbatim. |
1094 | * |
1095 | * @note Only use this function for generating JavaScript code. If generating output |
1096 | * for a proper JSON parser, just call FormatJson::encode() directly. |
1097 | * |
1098 | * @since 1.41 (previously on {@link Xml}) |
1099 | * @param mixed $value The value being encoded. Can be any type except a resource. |
1100 | * @param-taint $value escapes_html |
1101 | * @param bool $pretty If true, add non-significant whitespace to improve readability. |
1102 | * @return string|false String if successful; false upon failure |
1103 | * @return-taint none |
1104 | */ |
1105 | public static function encodeJsVar( $value, $pretty = false ) { |
1106 | if ( $value instanceof HtmlJsCode ) { |
1107 | return $value->value; |
1108 | } |
1109 | return FormatJson::encode( $value, $pretty, FormatJson::UTF8_OK ); |
1110 | } |
1111 | |
1112 | /** |
1113 | * Create a call to a JavaScript function. The supplied arguments will be |
1114 | * encoded using Html::encodeJsVar(). |
1115 | * |
1116 | * @since 1.41 (previously on {@link Xml} since 1.17) |
1117 | * @param string $name The name of the function to call, or a JavaScript expression |
1118 | * which evaluates to a function object which is called. |
1119 | * @param-taint $name tainted |
1120 | * @param array $args The arguments to pass to the function. |
1121 | * @param-taint $args escapes_html |
1122 | * @param bool $pretty If true, add non-significant whitespace to improve readability. |
1123 | * @return string|false String if successful; false upon failure |
1124 | * @return-taint none |
1125 | */ |
1126 | public static function encodeJsCall( $name, $args, $pretty = false ) { |
1127 | $encodedArgs = self::encodeJsList( $args, $pretty ); |
1128 | if ( $encodedArgs === false ) { |
1129 | return false; |
1130 | } |
1131 | return "$name($encodedArgs);"; |
1132 | } |
1133 | |
1134 | /** |
1135 | * Encode a JavaScript comma-separated list. The supplied items will be encoded using |
1136 | * Html::encodeJsVar(). |
1137 | * |
1138 | * @since 1.41. |
1139 | * @param array $args The elements of the list. |
1140 | * @param bool $pretty If true, add non-significant whitespace to improve readability. |
1141 | * @return false|string String if successful; false upon failure |
1142 | */ |
1143 | public static function encodeJsList( $args, $pretty = false ) { |
1144 | foreach ( $args as &$arg ) { |
1145 | $arg = self::encodeJsVar( $arg, $pretty ); |
1146 | if ( $arg === false ) { |
1147 | return false; |
1148 | } |
1149 | } |
1150 | if ( $pretty ) { |
1151 | return ' ' . implode( ', ', $args ) . ' '; |
1152 | } else { |
1153 | return implode( ',', $args ); |
1154 | } |
1155 | } |
1156 | |
1157 | /** |
1158 | * Build options for a drop-down box from a textual list. |
1159 | * |
1160 | * The result of this function can be passed to XmlSelect::addOptions() |
1161 | * (to render a plain `<select>` dropdown box) or to Html::listDropdownOptionsOoui() |
1162 | * and then OOUI\DropdownInputWidget() (to render a pretty one). |
1163 | * |
1164 | * @param string $list Correctly formatted text (newline delimited) to be |
1165 | * used to generate the options. |
1166 | * @param array $params Extra parameters: |
1167 | * - string $params['other'] If set, add an option with this as text and a value of 'other' |
1168 | * @return array Array keys are textual labels, values are internal values |
1169 | */ |
1170 | public static function listDropdownOptions( $list, $params = [] ) { |
1171 | $options = []; |
1172 | |
1173 | if ( isset( $params['other'] ) ) { |
1174 | $options[ $params['other'] ] = 'other'; |
1175 | } |
1176 | |
1177 | $optgroup = false; |
1178 | foreach ( explode( "\n", $list ) as $option ) { |
1179 | $value = trim( $option ); |
1180 | if ( $value == '' ) { |
1181 | continue; |
1182 | } |
1183 | if ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) { |
1184 | # A new group is starting... |
1185 | $value = trim( substr( $value, 1 ) ); |
1186 | if ( $value !== '' && |
1187 | // Do not use the value for 'other' as option group - T251351 |
1188 | ( !isset( $params['other'] ) || $value !== $params['other'] ) |
1189 | ) { |
1190 | $optgroup = $value; |
1191 | } else { |
1192 | $optgroup = false; |
1193 | } |
1194 | } elseif ( substr( $value, 0, 2 ) == '**' ) { |
1195 | # groupmember |
1196 | $opt = trim( substr( $value, 2 ) ); |
1197 | if ( $optgroup === false ) { |
1198 | $options[$opt] = $opt; |
1199 | } else { |
1200 | $options[$optgroup][$opt] = $opt; |
1201 | } |
1202 | } else { |
1203 | # groupless reason list |
1204 | $optgroup = false; |
1205 | $options[$option] = $option; |
1206 | } |
1207 | } |
1208 | |
1209 | return $options; |
1210 | } |
1211 | |
1212 | /** |
1213 | * Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc. |
1214 | * |
1215 | * TODO Find a better home for this function. |
1216 | * |
1217 | * @param array $options Options, as returned e.g. by Html::listDropdownOptions() |
1218 | * @return array |
1219 | */ |
1220 | public static function listDropdownOptionsOoui( $options ) { |
1221 | $optionsOoui = []; |
1222 | |
1223 | foreach ( $options as $text => $value ) { |
1224 | if ( is_array( $value ) ) { |
1225 | $optionsOoui[] = [ 'optgroup' => (string)$text ]; |
1226 | foreach ( $value as $text2 => $value2 ) { |
1227 | $optionsOoui[] = [ 'data' => (string)$value2, 'label' => (string)$text2 ]; |
1228 | } |
1229 | } else { |
1230 | $optionsOoui[] = [ 'data' => (string)$value, 'label' => (string)$text ]; |
1231 | } |
1232 | } |
1233 | |
1234 | return $optionsOoui; |
1235 | } |
1236 | } |
1237 | |
1238 | /** @deprecated class alias since 1.40 */ |
1239 | class_alias( Html::class, 'Html' ); |