Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
42.19% |
208 / 493 |
|
32.76% |
19 / 58 |
CRAP | |
0.00% |
0 / 1 |
HTMLFormField | |
42.28% |
208 / 492 |
|
32.76% |
19 / 58 |
10137.86 | |
0.00% |
0 / 1 |
getInputHTML | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getInputOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInputCodex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canDisplayErrors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
msg | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
hasVisibleOutput | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNearestField | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
getNearestFieldValue | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
getNearestFieldByName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateCondState | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
15 | |||
checkStateRecurse | |
100.00% |
21 / 21 |
|
100.00% |
1 / 1 |
12 | |||
parseCondState | |
62.50% |
10 / 16 |
|
0.00% |
0 / 1 |
13.27 | |||
parseCondStateForClient | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
isHidden | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isDisabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
cancelSubmit | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validate | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
11.06 | |||
filter | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
needsLabel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setShowEmptyLabel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSubmitAttempt | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
loadDataFromRequest | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
__construct | |
68.18% |
30 / 44 |
|
0.00% |
0 / 1 |
40.04 | |||
getTableRow | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
20 | |||
getDiv | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
12 | |||
getOOUI | |
62.79% |
27 / 43 |
|
0.00% |
0 / 1 |
29.19 | |||
getCodex | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
110 | |||
getClassName | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getLabelAlignOOUI | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFieldLayoutOOUI | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldInfuseOOUI | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getOOUIModules | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRaw | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getVForm | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getInline | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getHelpTextHtmlTable | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getHelpTextHtmlDiv | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getHelpTextHtmlRaw | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHelpMessages | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
5.26 | |||
getHelpText | |
37.50% |
3 / 8 |
|
0.00% |
0 / 1 |
11.10 | |||
isHelpInline | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getErrorsAndErrorClass | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
getErrorsRaw | |
33.33% |
3 / 9 |
|
0.00% |
0 / 1 |
16.67 | |||
getLabel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLabelHtml | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
90 | |||
getDefault | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTooltipAndAccessKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTooltipAndAccessKeyOOUI | |
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
3.19 | |||
getAttributes | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
5.03 | |||
lookupOptionsKeys | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
forceToStringRecursive | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getOptions | |
42.86% |
6 / 14 |
|
0.00% |
0 / 1 |
12.72 | |||
getOptionsOOUI | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
flattenOptions | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
formatErrors | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 | |||
getMessage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
skipLoadData | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
needsJSForHtml5FormValidation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\HTMLForm; |
4 | |
5 | use FormatJson; |
6 | use HtmlArmor; |
7 | use HTMLCheckField; |
8 | use HTMLFormFieldCloner; |
9 | use InvalidArgumentException; |
10 | use MediaWiki\Context\RequestContext; |
11 | use MediaWiki\Html\Html; |
12 | use MediaWiki\Linker\Linker; |
13 | use MediaWiki\Logger\LoggerFactory; |
14 | use MediaWiki\Message\Message; |
15 | use MediaWiki\Request\WebRequest; |
16 | use MediaWiki\Status\Status; |
17 | use MessageSpecifier; |
18 | use StatusValue; |
19 | |
20 | /** |
21 | * The parent class to generate form fields. Any field type should |
22 | * be a subclass of this. |
23 | * |
24 | * @stable to extend |
25 | */ |
26 | abstract class HTMLFormField { |
27 | /** @var array|array[] */ |
28 | public $mParams; |
29 | |
30 | /** @var callable(mixed,array,HTMLForm):(Status|string|bool|Message) */ |
31 | protected $mValidationCallback; |
32 | protected $mFilterCallback; |
33 | protected $mName; |
34 | protected $mDir; |
35 | protected $mLabel; # String label, as HTML. Set on construction. |
36 | protected $mID; |
37 | protected $mClass = ''; |
38 | protected $mVFormClass = ''; |
39 | protected $mHelpClass = false; |
40 | protected $mDefault; |
41 | private $mNotices; |
42 | |
43 | /** |
44 | * @var array|null|false |
45 | */ |
46 | protected $mOptions = false; |
47 | protected $mOptionsLabelsNotFromMessage = false; |
48 | /** |
49 | * @var array Array to hold params for 'hide-if' or 'disable-if' statements |
50 | */ |
51 | protected $mCondState = []; |
52 | protected $mCondStateClass = []; |
53 | |
54 | /** |
55 | * @var bool If true will generate an empty div element with no label |
56 | * @since 1.22 |
57 | */ |
58 | protected $mShowEmptyLabels = true; |
59 | |
60 | /** |
61 | * @var HTMLForm|null |
62 | */ |
63 | public $mParent; |
64 | |
65 | /** |
66 | * This function must be implemented to return the HTML to generate |
67 | * the input object itself. It should not implement the surrounding |
68 | * table cells/rows, or labels/help messages. |
69 | * |
70 | * @param mixed $value The value to set the input to; eg a default |
71 | * text for a text input. |
72 | * |
73 | * @return string Valid HTML. |
74 | */ |
75 | abstract public function getInputHTML( $value ); |
76 | |
77 | /** |
78 | * Same as getInputHTML, but returns an OOUI object. |
79 | * Defaults to false, which getOOUI will interpret as "use the HTML version" |
80 | * @stable to override |
81 | * |
82 | * @param string $value |
83 | * @return \OOUI\Widget|string|false |
84 | */ |
85 | public function getInputOOUI( $value ) { |
86 | return false; |
87 | } |
88 | |
89 | /** |
90 | * Same as getInputHTML, but for Codex. This is called by CodexHTMLForm. |
91 | * |
92 | * If not overridden, falls back to getInputHTML. |
93 | * |
94 | * @param string $value The value to set the input to |
95 | * @param bool $hasErrors Whether there are validation errors. If set to true, this method |
96 | * should apply a CSS class for the error status (e.g. cdx-text-input--status-error) |
97 | * if the component used supports that. |
98 | * @return string HTML |
99 | */ |
100 | public function getInputCodex( $value, $hasErrors ) { |
101 | // If not overridden, fall back to getInputHTML() |
102 | return $this->getInputHTML( $value ); |
103 | } |
104 | |
105 | /** |
106 | * True if this field type is able to display errors; false if validation errors need to be |
107 | * displayed in the main HTMLForm error area. |
108 | * @stable to override |
109 | * @return bool |
110 | */ |
111 | public function canDisplayErrors() { |
112 | return $this->hasVisibleOutput(); |
113 | } |
114 | |
115 | /** |
116 | * Get a translated interface message |
117 | * |
118 | * This is a wrapper around $this->mParent->msg() if $this->mParent is set |
119 | * and wfMessage() otherwise. |
120 | * |
121 | * Parameters are the same as wfMessage(). |
122 | * |
123 | * @param string|string[]|MessageSpecifier $key |
124 | * @param mixed ...$params |
125 | * @return Message |
126 | */ |
127 | public function msg( $key, ...$params ) { |
128 | if ( $this->mParent ) { |
129 | return $this->mParent->msg( $key, ...$params ); |
130 | } |
131 | return wfMessage( $key, ...$params ); |
132 | } |
133 | |
134 | /** |
135 | * If this field has a user-visible output or not. If not, |
136 | * it will not be rendered |
137 | * @stable to override |
138 | * |
139 | * @return bool |
140 | */ |
141 | public function hasVisibleOutput() { |
142 | return true; |
143 | } |
144 | |
145 | /** |
146 | * Get the field name that will be used for submission. |
147 | * |
148 | * @since 1.38 |
149 | * @return string |
150 | */ |
151 | public function getName() { |
152 | return $this->mName; |
153 | } |
154 | |
155 | /** |
156 | * Get the closest field matching a given name. |
157 | * |
158 | * It can handle array fields like the user would expect. The general |
159 | * algorithm is to look for $name as a sibling of $this, then a sibling |
160 | * of $this's parent, and so on. |
161 | * |
162 | * @param string $name |
163 | * @param bool $backCompat Whether to try striping the 'wp' prefix. |
164 | * @return HTMLFormField |
165 | */ |
166 | protected function getNearestField( $name, $backCompat = false ) { |
167 | // When the field is belong to a HTMLFormFieldCloner |
168 | $cloner = $this->mParams['cloner'] ?? null; |
169 | if ( $cloner instanceof HTMLFormFieldCloner ) { |
170 | $field = $cloner->findNearestField( $this, $name ); |
171 | if ( $field ) { |
172 | return $field; |
173 | } |
174 | } |
175 | |
176 | if ( $backCompat && str_starts_with( $name, 'wp' ) && |
177 | !$this->mParent->hasField( $name ) |
178 | ) { |
179 | // Don't break the existed use cases. |
180 | return $this->mParent->getField( substr( $name, 2 ) ); |
181 | } |
182 | return $this->mParent->getField( $name ); |
183 | } |
184 | |
185 | /** |
186 | * Fetch a field value from $alldata for the closest field matching a given |
187 | * name. |
188 | * |
189 | * @param array $alldata |
190 | * @param string $name |
191 | * @param bool $asDisplay Whether the reverting logic of HTMLCheckField |
192 | * should be ignored. |
193 | * @param bool $backCompat Whether to try striping the 'wp' prefix. |
194 | * @return mixed |
195 | */ |
196 | protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) { |
197 | $field = $this->getNearestField( $name, $backCompat ); |
198 | // When the field belongs to a HTMLFormFieldCloner |
199 | $cloner = $field->mParams['cloner'] ?? null; |
200 | if ( $cloner instanceof HTMLFormFieldCloner ) { |
201 | $value = $cloner->extractFieldData( $field, $alldata ); |
202 | } else { |
203 | // Note $alldata is an empty array when first rendering a form with a formIdentifier. |
204 | // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the |
205 | // field's default value |
206 | $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault(); |
207 | } |
208 | |
209 | // Check invert state for HTMLCheckField |
210 | if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) { |
211 | $value = !$value; |
212 | } |
213 | |
214 | return $value; |
215 | } |
216 | |
217 | /** |
218 | * Fetch a field value from $alldata for the closest field matching a given |
219 | * name. |
220 | * |
221 | * @deprecated since 1.38 Use getNearestFieldValue() instead. |
222 | * @param array $alldata |
223 | * @param string $name |
224 | * @param bool $asDisplay |
225 | * @return string |
226 | */ |
227 | protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) { |
228 | return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay ); |
229 | } |
230 | |
231 | /** |
232 | * Validate the cond-state params, the existence check of fields should |
233 | * be done later. |
234 | * |
235 | * @param array $params |
236 | */ |
237 | protected function validateCondState( $params ) { |
238 | $origParams = $params; |
239 | $op = array_shift( $params ); |
240 | |
241 | $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException { |
242 | return new InvalidArgumentException( |
243 | "Invalid hide-if or disable-if specification for $this->mName: " . |
244 | $details . " in " . var_export( $origParams, true ) |
245 | ); |
246 | }; |
247 | |
248 | switch ( $op ) { |
249 | case 'NOT': |
250 | if ( count( $params ) !== 1 ) { |
251 | throw $makeException( "NOT takes exactly one parameter" ); |
252 | } |
253 | // Fall-through intentionally |
254 | |
255 | case 'AND': |
256 | case 'OR': |
257 | case 'NAND': |
258 | case 'NOR': |
259 | foreach ( $params as $i => $p ) { |
260 | if ( !is_array( $p ) ) { |
261 | $type = gettype( $p ); |
262 | throw $makeException( "Expected array, found $type at index $i" ); |
263 | } |
264 | $this->validateCondState( $p ); |
265 | } |
266 | break; |
267 | |
268 | case '===': |
269 | case '!==': |
270 | if ( count( $params ) !== 2 ) { |
271 | throw $makeException( "$op takes exactly two parameters" ); |
272 | } |
273 | [ $name, $value ] = $params; |
274 | if ( !is_string( $name ) || !is_string( $value ) ) { |
275 | throw $makeException( "Parameters for $op must be strings" ); |
276 | } |
277 | break; |
278 | |
279 | default: |
280 | throw $makeException( "Unknown operation" ); |
281 | } |
282 | } |
283 | |
284 | /** |
285 | * Helper function for isHidden and isDisabled to handle recursive data structures. |
286 | * |
287 | * @param array $alldata |
288 | * @param array $params |
289 | * @return bool |
290 | */ |
291 | protected function checkStateRecurse( array $alldata, array $params ) { |
292 | $op = array_shift( $params ); |
293 | $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ]; |
294 | $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ]; |
295 | |
296 | switch ( $op ) { |
297 | case 'AND': |
298 | case 'OR': |
299 | case 'NAND': |
300 | case 'NOR': |
301 | foreach ( $params as $p ) { |
302 | if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) { |
303 | return !$valueRet[$op]; |
304 | } |
305 | } |
306 | return $valueRet[$op]; |
307 | |
308 | case 'NOT': |
309 | return !$this->checkStateRecurse( $alldata, $params[0] ); |
310 | |
311 | case '===': |
312 | case '!==': |
313 | [ $field, $value ] = $params; |
314 | $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true ); |
315 | switch ( $op ) { |
316 | case '===': |
317 | return ( $value === $testValue ); |
318 | case '!==': |
319 | return ( $value !== $testValue ); |
320 | } |
321 | } |
322 | } |
323 | |
324 | /** |
325 | * Parse the cond-state array to use the field name for submission, since |
326 | * the key in the form descriptor is never known in HTML. Also check for |
327 | * field existence here. |
328 | * |
329 | * @param array $params |
330 | * @return mixed[] |
331 | */ |
332 | protected function parseCondState( $params ) { |
333 | $op = array_shift( $params ); |
334 | |
335 | switch ( $op ) { |
336 | case 'AND': |
337 | case 'OR': |
338 | case 'NAND': |
339 | case 'NOR': |
340 | $ret = [ $op ]; |
341 | foreach ( $params as $p ) { |
342 | $ret[] = $this->parseCondState( $p ); |
343 | } |
344 | return $ret; |
345 | |
346 | case 'NOT': |
347 | return [ 'NOT', $this->parseCondState( $params[0] ) ]; |
348 | |
349 | case '===': |
350 | case '!==': |
351 | [ $name, $value ] = $params; |
352 | $field = $this->getNearestField( $name, true ); |
353 | return [ $op, $field->getName(), $value ]; |
354 | } |
355 | } |
356 | |
357 | /** |
358 | * Parse the cond-state array for client-side. |
359 | * |
360 | * @return array[] |
361 | */ |
362 | protected function parseCondStateForClient() { |
363 | $parsed = []; |
364 | foreach ( $this->mCondState as $type => $params ) { |
365 | $parsed[$type] = $this->parseCondState( $params ); |
366 | } |
367 | return $parsed; |
368 | } |
369 | |
370 | /** |
371 | * Test whether this field is supposed to be hidden, based on the values of |
372 | * the other form fields. |
373 | * |
374 | * @since 1.23 |
375 | * @param array $alldata The data collected from the form |
376 | * @return bool |
377 | */ |
378 | public function isHidden( $alldata ) { |
379 | return isset( $this->mCondState['hide'] ) && |
380 | $this->checkStateRecurse( $alldata, $this->mCondState['hide'] ); |
381 | } |
382 | |
383 | /** |
384 | * Test whether this field is supposed to be disabled, based on the values of |
385 | * the other form fields. |
386 | * |
387 | * @since 1.38 |
388 | * @param array $alldata The data collected from the form |
389 | * @return bool |
390 | */ |
391 | public function isDisabled( $alldata ) { |
392 | return ( $this->mParams['disabled'] ?? false ) || |
393 | $this->isHidden( $alldata ) || |
394 | ( isset( $this->mCondState['disable'] ) |
395 | && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) ); |
396 | } |
397 | |
398 | /** |
399 | * Override this function if the control can somehow trigger a form |
400 | * submission that shouldn't actually submit the HTMLForm. |
401 | * |
402 | * @stable to override |
403 | * @since 1.23 |
404 | * @param string|array $value The value the field was submitted with |
405 | * @param array $alldata The data collected from the form |
406 | * |
407 | * @return bool True to cancel the submission |
408 | */ |
409 | public function cancelSubmit( $value, $alldata ) { |
410 | return false; |
411 | } |
412 | |
413 | /** |
414 | * Override this function to add specific validation checks on the |
415 | * field input. Don't forget to call parent::validate() to ensure |
416 | * that the user-defined callback mValidationCallback is still run |
417 | * @stable to override |
418 | * |
419 | * @param mixed $value The value the field was submitted with |
420 | * @param array $alldata The data collected from the form |
421 | * |
422 | * @return bool|string|Message True on success, or String/Message error to display, or |
423 | * false to fail validation without displaying an error. |
424 | */ |
425 | public function validate( $value, $alldata ) { |
426 | if ( $this->isHidden( $alldata ) ) { |
427 | return true; |
428 | } |
429 | |
430 | if ( isset( $this->mParams['required'] ) |
431 | && $this->mParams['required'] !== false |
432 | && ( $value === '' || $value === false || $value === null ) |
433 | ) { |
434 | return $this->msg( 'htmlform-required' ); |
435 | } |
436 | |
437 | if ( !isset( $this->mValidationCallback ) ) { |
438 | return true; |
439 | } |
440 | |
441 | $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent ); |
442 | |
443 | if ( $p instanceof StatusValue ) { |
444 | $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage(); |
445 | |
446 | return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language ); |
447 | } |
448 | |
449 | return $p; |
450 | } |
451 | |
452 | /** |
453 | * @stable to override |
454 | * |
455 | * @param mixed $value |
456 | * @param mixed[] $alldata |
457 | * |
458 | * @return mixed |
459 | */ |
460 | public function filter( $value, $alldata ) { |
461 | if ( isset( $this->mFilterCallback ) ) { |
462 | $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent ); |
463 | } |
464 | |
465 | return $value; |
466 | } |
467 | |
468 | /** |
469 | * Should this field have a label, or is there no input element with the |
470 | * appropriate id for the label to point to? |
471 | * @stable to override |
472 | * |
473 | * @return bool True to output a label, false to suppress |
474 | */ |
475 | protected function needsLabel() { |
476 | return true; |
477 | } |
478 | |
479 | /** |
480 | * Tell the field whether to generate a separate label element if its label |
481 | * is blank. |
482 | * |
483 | * @since 1.22 |
484 | * |
485 | * @param bool $show Set to false to not generate a label. |
486 | * @return void |
487 | */ |
488 | public function setShowEmptyLabel( $show ) { |
489 | $this->mShowEmptyLabels = $show; |
490 | } |
491 | |
492 | /** |
493 | * Can we assume that the request is an attempt to submit a HTMLForm, as opposed to an attempt to |
494 | * just view it? This can't normally be distinguished for e.g. checkboxes. |
495 | * |
496 | * Returns true if the request was posted and has a field for a CSRF token (wpEditToken), or |
497 | * has a form identifier (wpFormIdentifier). |
498 | * |
499 | * @todo Consider moving this to HTMLForm? |
500 | * @param WebRequest $request |
501 | * @return bool |
502 | */ |
503 | protected function isSubmitAttempt( WebRequest $request ) { |
504 | // HTMLForm would add a hidden field of edit token for forms that require to be posted. |
505 | return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) ) |
506 | // The identifier matching or not has been checked in HTMLForm::prepareForm() |
507 | || $request->getCheck( 'wpFormIdentifier' ); |
508 | } |
509 | |
510 | /** |
511 | * Get the value that this input has been set to from a posted form, |
512 | * or the input's default value if it has not been set. |
513 | * @stable to override |
514 | * |
515 | * @param WebRequest $request |
516 | * @return mixed The value |
517 | */ |
518 | public function loadDataFromRequest( $request ) { |
519 | if ( $request->getCheck( $this->mName ) ) { |
520 | return $request->getText( $this->mName ); |
521 | } else { |
522 | return $this->getDefault(); |
523 | } |
524 | } |
525 | |
526 | /** |
527 | * Initialise the object |
528 | * |
529 | * @stable to call |
530 | * @param array $params Associative Array. See HTMLForm doc for syntax. |
531 | * |
532 | * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead |
533 | */ |
534 | public function __construct( $params ) { |
535 | $this->mParams = $params; |
536 | |
537 | if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) { |
538 | $this->mParent = $params['parent']; |
539 | } else { |
540 | // Normally parent is added automatically by HTMLForm::factory. |
541 | // Several field types already assume unconditionally this is always set, |
542 | // so deprecate manually creating an HTMLFormField without a parent form set. |
543 | wfDeprecatedMsg( |
544 | __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter", |
545 | "1.40" |
546 | ); |
547 | } |
548 | |
549 | # Generate the label from a message, if possible |
550 | if ( isset( $params['label-message'] ) ) { |
551 | $this->mLabel = $this->getMessage( $params['label-message'] )->parse(); |
552 | } elseif ( isset( $params['label'] ) ) { |
553 | if ( $params['label'] === ' ' || $params['label'] === "\u{00A0}" ) { |
554 | // Apparently some things set   directly and in an odd format |
555 | $this->mLabel = "\u{00A0}"; |
556 | } else { |
557 | $this->mLabel = htmlspecialchars( $params['label'] ); |
558 | } |
559 | } elseif ( isset( $params['label-raw'] ) ) { |
560 | $this->mLabel = $params['label-raw']; |
561 | } |
562 | |
563 | $this->mName = $params['name'] ?? 'wp' . $params['fieldname']; |
564 | |
565 | if ( isset( $params['dir'] ) ) { |
566 | $this->mDir = $params['dir']; |
567 | } |
568 | |
569 | $this->mID = "mw-input-{$this->mName}"; |
570 | |
571 | if ( isset( $params['default'] ) ) { |
572 | $this->mDefault = $params['default']; |
573 | } |
574 | |
575 | if ( isset( $params['id'] ) ) { |
576 | $this->mID = $params['id']; |
577 | } |
578 | |
579 | if ( isset( $params['cssclass'] ) ) { |
580 | $this->mClass = $params['cssclass']; |
581 | } |
582 | |
583 | if ( isset( $params['csshelpclass'] ) ) { |
584 | $this->mHelpClass = $params['csshelpclass']; |
585 | } |
586 | |
587 | if ( isset( $params['validation-callback'] ) ) { |
588 | $this->mValidationCallback = $params['validation-callback']; |
589 | } |
590 | |
591 | if ( isset( $params['filter-callback'] ) ) { |
592 | $this->mFilterCallback = $params['filter-callback']; |
593 | } |
594 | |
595 | if ( isset( $params['hidelabel'] ) ) { |
596 | $this->mShowEmptyLabels = false; |
597 | } |
598 | if ( isset( $params['notices'] ) ) { |
599 | $this->mNotices = $params['notices']; |
600 | } |
601 | |
602 | if ( isset( $params['hide-if'] ) && $params['hide-if'] ) { |
603 | $this->validateCondState( $params['hide-if'] ); |
604 | $this->mCondState['hide'] = $params['hide-if']; |
605 | $this->mCondStateClass[] = 'mw-htmlform-hide-if'; |
606 | } |
607 | if ( !( isset( $params['disabled'] ) && $params['disabled'] ) && |
608 | isset( $params['disable-if'] ) && $params['disable-if'] |
609 | ) { |
610 | $this->validateCondState( $params['disable-if'] ); |
611 | $this->mCondState['disable'] = $params['disable-if']; |
612 | $this->mCondStateClass[] = 'mw-htmlform-disable-if'; |
613 | } |
614 | } |
615 | |
616 | /** |
617 | * Get the complete table row for the input, including help text, |
618 | * labels, and whatever. |
619 | * @stable to override |
620 | * |
621 | * @param string $value The value to set the input to. |
622 | * |
623 | * @return string Complete HTML table row. |
624 | */ |
625 | public function getTableRow( $value ) { |
626 | [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value ); |
627 | $inputHtml = $this->getInputHTML( $value ); |
628 | $fieldType = $this->getClassName(); |
629 | $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() ); |
630 | $cellAttributes = []; |
631 | $rowAttributes = []; |
632 | $rowClasses = ''; |
633 | |
634 | if ( !empty( $this->mParams['vertical-label'] ) ) { |
635 | $cellAttributes['colspan'] = 2; |
636 | $verticalLabel = true; |
637 | } else { |
638 | $verticalLabel = false; |
639 | } |
640 | |
641 | $label = $this->getLabelHtml( $cellAttributes ); |
642 | |
643 | $field = Html::rawElement( |
644 | 'td', |
645 | [ 'class' => 'mw-input' ] + $cellAttributes, |
646 | $inputHtml . "\n$errors" |
647 | ); |
648 | |
649 | if ( $this->mCondState ) { |
650 | $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() ); |
651 | $rowClasses .= implode( ' ', $this->mCondStateClass ); |
652 | } |
653 | |
654 | if ( $verticalLabel ) { |
655 | $html = Html::rawElement( 'tr', |
656 | $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label ); |
657 | $html .= Html::rawElement( 'tr', |
658 | $rowAttributes + [ |
659 | 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" |
660 | ], |
661 | $field ); |
662 | } else { |
663 | $html = Html::rawElement( 'tr', |
664 | $rowAttributes + [ |
665 | 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses" |
666 | ], |
667 | $label . $field ); |
668 | } |
669 | |
670 | return $html . $helptext; |
671 | } |
672 | |
673 | /** |
674 | * Get the complete div for the input, including help text, |
675 | * labels, and whatever. |
676 | * @stable to override |
677 | * @since 1.20 |
678 | * |
679 | * @param string $value The value to set the input to. |
680 | * |
681 | * @return string Complete HTML table row. |
682 | */ |
683 | public function getDiv( $value ) { |
684 | [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value ); |
685 | $inputHtml = $this->getInputHTML( $value ); |
686 | $fieldType = $this->getClassName(); |
687 | $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() ); |
688 | $cellAttributes = []; |
689 | $label = $this->getLabelHtml( $cellAttributes ); |
690 | |
691 | $outerDivClass = [ |
692 | 'mw-input', |
693 | 'mw-htmlform-nolabel' => ( $label === '' ) |
694 | ]; |
695 | |
696 | $horizontalLabel = $this->mParams['horizontal-label'] ?? false; |
697 | |
698 | if ( $horizontalLabel ) { |
699 | $field = "\u{00A0}" . $inputHtml . "\n$errors"; |
700 | } else { |
701 | $field = Html::rawElement( |
702 | 'div', |
703 | // @phan-suppress-next-line PhanUselessBinaryAddRight |
704 | [ 'class' => $outerDivClass ] + $cellAttributes, |
705 | $inputHtml . "\n$errors" |
706 | ); |
707 | } |
708 | |
709 | $wrapperAttributes = [ 'class' => [ |
710 | "mw-htmlform-field-$fieldType", |
711 | $this->mClass, |
712 | $this->mVFormClass, |
713 | $errorClass, |
714 | ] ]; |
715 | if ( $this->mCondState ) { |
716 | $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() ); |
717 | $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass ); |
718 | } |
719 | return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) . |
720 | $helptext; |
721 | } |
722 | |
723 | /** |
724 | * Get the OOUI version of the div. Falls back to getDiv by default. |
725 | * @stable to override |
726 | * @since 1.26 |
727 | * |
728 | * @param string $value The value to set the input to. |
729 | * |
730 | * @return \OOUI\FieldLayout |
731 | */ |
732 | public function getOOUI( $value ) { |
733 | $inputField = $this->getInputOOUI( $value ); |
734 | |
735 | if ( !$inputField ) { |
736 | // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to |
737 | // generate the whole field, label and errors and all, then wrap it in a Widget. |
738 | // It might look weird, but it'll work OK. |
739 | return $this->getFieldLayoutOOUI( |
740 | new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ), |
741 | [ 'align' => 'top' ] |
742 | ); |
743 | } |
744 | |
745 | $infusable = true; |
746 | if ( is_string( $inputField ) ) { |
747 | // We have an OOUI implementation, but it's not proper, and we got a load of HTML. |
748 | // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side |
749 | // JavaScript doesn't know how to rebuilt the contents. |
750 | $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] ); |
751 | $infusable = false; |
752 | } |
753 | |
754 | $fieldType = $this->getClassName(); |
755 | $help = $this->getHelpText(); |
756 | $errors = $this->getErrorsRaw( $value ); |
757 | foreach ( $errors as &$error ) { |
758 | $error = new \OOUI\HtmlSnippet( $error ); |
759 | } |
760 | |
761 | $config = [ |
762 | 'classes' => [ "mw-htmlform-field-$fieldType" ], |
763 | 'align' => $this->getLabelAlignOOUI(), |
764 | 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null, |
765 | 'errors' => $errors, |
766 | 'infusable' => $infusable, |
767 | 'helpInline' => $this->isHelpInline(), |
768 | 'notices' => $this->mNotices ?: [], |
769 | ]; |
770 | if ( $this->mClass !== '' ) { |
771 | $config['classes'][] = $this->mClass; |
772 | } |
773 | |
774 | $preloadModules = false; |
775 | |
776 | if ( $infusable && $this->shouldInfuseOOUI() ) { |
777 | $preloadModules = true; |
778 | $config['classes'][] = 'mw-htmlform-autoinfuse'; |
779 | } |
780 | if ( $this->mCondState ) { |
781 | $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass ); |
782 | } |
783 | |
784 | // the element could specify, that the label doesn't need to be added |
785 | $label = $this->getLabel(); |
786 | if ( $label && $label !== "\u{00A0}" && $label !== ' ' ) { |
787 | $config['label'] = new \OOUI\HtmlSnippet( $label ); |
788 | } |
789 | |
790 | if ( $this->mCondState ) { |
791 | $preloadModules = true; |
792 | $config['condState'] = $this->parseCondStateForClient(); |
793 | } |
794 | |
795 | $config['modules'] = $this->getOOUIModules(); |
796 | |
797 | if ( $preloadModules ) { |
798 | $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' ); |
799 | $this->mParent->getOutput()->addModules( $this->getOOUIModules() ); |
800 | } |
801 | |
802 | return $this->getFieldLayoutOOUI( $inputField, $config ); |
803 | } |
804 | |
805 | /** |
806 | * Get the Codex version of the div. |
807 | * @since 1.42 |
808 | * |
809 | * @param string $value The value to set the input to. |
810 | * @return string HTML |
811 | */ |
812 | public function getCodex( $value ) { |
813 | $isDisabled = ( $this->mParams['disabled'] ?? false ); |
814 | |
815 | // Label |
816 | $labelDiv = ''; |
817 | $labelValue = trim( $this->getLabel() ); |
818 | // For weird historical reasons, a non-breaking space is treated as an empty label |
819 | // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version |
820 | if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== ' ' ) { |
821 | $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : []; |
822 | $labelClasses = [ 'cdx-label' ]; |
823 | if ( $isDisabled ) { |
824 | $labelClasses[] = 'cdx-label--disabled'; |
825 | } |
826 | // <div class="cdx-label"> |
827 | $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ], |
828 | // <label class="cdx-label__label" for="ID"> |
829 | Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor, |
830 | // <span class="cdx-label__label__text"> |
831 | Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ], |
832 | $labelValue |
833 | ) |
834 | ) |
835 | ); |
836 | } |
837 | |
838 | // Help text |
839 | // <div class="cdx-field__help-text"> |
840 | $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] ); |
841 | |
842 | // Validation message |
843 | // <div class="cdx-field__validation-message"> |
844 | // $errors is a <div class="cdx-message"> |
845 | // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead |
846 | $validationMessage = ''; |
847 | [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value ); |
848 | if ( $errors !== '' ) { |
849 | $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ], |
850 | $errors |
851 | ); |
852 | } |
853 | |
854 | // Control |
855 | $inputHtml = $this->getInputCodex( $value, $errors !== '' ); |
856 | // <div class="cdx-field__control cdx-field__control--has-help-text"> |
857 | $controlClasses = [ 'cdx-field__control' ]; |
858 | if ( $helptext ) { |
859 | $controlClasses[] = 'cdx-field__control--has-help-text'; |
860 | } |
861 | $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml ); |
862 | |
863 | // <div class="cdx-field"> |
864 | $fieldClasses = [ |
865 | "mw-htmlform-field-{$this->getClassName()}", |
866 | $this->mClass, |
867 | $errorClass, |
868 | 'cdx-field' |
869 | ]; |
870 | if ( $isDisabled ) { |
871 | $fieldClasses[] = 'cdx-field--disabled'; |
872 | } |
873 | $fieldAttributes = []; |
874 | // Set data attribute and CSS class for client side handling of hide-if / disable-if |
875 | if ( $this->mCondState ) { |
876 | $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() ); |
877 | $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass ); |
878 | } |
879 | |
880 | return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes, |
881 | $labelDiv . $control . $helptext . $validationMessage |
882 | ); |
883 | } |
884 | |
885 | /** |
886 | * Gets the non namespaced class name |
887 | * |
888 | * @since 1.36 |
889 | * |
890 | * @return string |
891 | */ |
892 | protected function getClassName() { |
893 | $name = explode( '\\', static::class ); |
894 | return end( $name ); |
895 | } |
896 | |
897 | /** |
898 | * Get label alignment when generating field for OOUI. |
899 | * @stable to override |
900 | * @return string 'left', 'right', 'top' or 'inline' |
901 | */ |
902 | protected function getLabelAlignOOUI() { |
903 | return 'top'; |
904 | } |
905 | |
906 | /** |
907 | * Get a FieldLayout (or subclass thereof) to wrap this field in when using OOUI output. |
908 | * @param \OOUI\Widget $inputField |
909 | * @param array $config |
910 | * @return \OOUI\FieldLayout |
911 | */ |
912 | protected function getFieldLayoutOOUI( $inputField, $config ) { |
913 | return new HTMLFormFieldLayout( $inputField, $config ); |
914 | } |
915 | |
916 | /** |
917 | * Whether the field should be automatically infused. Note that all OOUI HTMLForm fields are |
918 | * infusable (you can call OO.ui.infuse() on them), but not all are infused by default, since |
919 | * there is no benefit in doing it e.g. for buttons and it's a small performance hit on page load. |
920 | * @stable to override |
921 | * |
922 | * @return bool |
923 | */ |
924 | protected function shouldInfuseOOUI() { |
925 | // Always infuse fields with popup help text, since the interface for it is nicer with JS |
926 | return !$this->isHelpInline() && $this->getHelpMessages(); |
927 | } |
928 | |
929 | /** |
930 | * Get the list of extra ResourceLoader modules which must be loaded client-side before it's |
931 | * possible to infuse this field's OOUI widget. |
932 | * @stable to override |
933 | * |
934 | * @return string[] |
935 | */ |
936 | protected function getOOUIModules() { |
937 | return []; |
938 | } |
939 | |
940 | /** |
941 | * Get the complete raw fields for the input, including help text, |
942 | * labels, and whatever. |
943 | * @stable to override |
944 | * @since 1.20 |
945 | * |
946 | * @param string $value The value to set the input to. |
947 | * |
948 | * @return string Complete HTML table row. |
949 | */ |
950 | public function getRaw( $value ) { |
951 | [ $errors, ] = $this->getErrorsAndErrorClass( $value ); |
952 | return "\n" . $errors . |
953 | $this->getLabelHtml() . |
954 | $this->getInputHTML( $value ) . |
955 | $this->getHelpTextHtmlRaw( $this->getHelpText() ); |
956 | } |
957 | |
958 | /** |
959 | * Get the complete field for the input, including help text, |
960 | * labels, and whatever. Fall back from 'vform' to 'div' when not overridden. |
961 | * |
962 | * @stable to override |
963 | * @since 1.25 |
964 | * @param string $value The value to set the input to. |
965 | * @return string Complete HTML field. |
966 | */ |
967 | public function getVForm( $value ) { |
968 | // Ewwww |
969 | $this->mVFormClass = ' mw-ui-vform-field'; |
970 | return $this->getDiv( $value ); |
971 | } |
972 | |
973 | /** |
974 | * Get the complete field as an inline element. |
975 | * @stable to override |
976 | * @since 1.25 |
977 | * @param string $value The value to set the input to. |
978 | * @return string Complete HTML inline element |
979 | */ |
980 | public function getInline( $value ) { |
981 | [ $errors, ] = $this->getErrorsAndErrorClass( $value ); |
982 | return "\n" . $errors . |
983 | $this->getLabelHtml() . |
984 | "\u{00A0}" . |
985 | $this->getInputHTML( $value ) . |
986 | $this->getHelpTextHtmlDiv( $this->getHelpText() ); |
987 | } |
988 | |
989 | /** |
990 | * Generate help text HTML in table format |
991 | * @since 1.20 |
992 | * |
993 | * @param string|null $helptext |
994 | * @return string |
995 | */ |
996 | public function getHelpTextHtmlTable( $helptext ) { |
997 | if ( $helptext === null ) { |
998 | return ''; |
999 | } |
1000 | |
1001 | $rowAttributes = []; |
1002 | if ( $this->mCondState ) { |
1003 | $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() ); |
1004 | $rowAttributes['class'] = $this->mCondStateClass; |
1005 | } |
1006 | |
1007 | $tdClasses = [ 'htmlform-tip' ]; |
1008 | if ( $this->mHelpClass !== false ) { |
1009 | $tdClasses[] = $this->mHelpClass; |
1010 | } |
1011 | return Html::rawElement( 'tr', $rowAttributes, |
1012 | Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext ) |
1013 | ); |
1014 | } |
1015 | |
1016 | /** |
1017 | * Generate help text HTML in div format |
1018 | * @since 1.20 |
1019 | * |
1020 | * @param string|null $helptext |
1021 | * @param string[] $cssClasses |
1022 | * |
1023 | * @return string |
1024 | */ |
1025 | public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) { |
1026 | if ( $helptext === null ) { |
1027 | return ''; |
1028 | } |
1029 | |
1030 | $wrapperAttributes = [ |
1031 | 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ), |
1032 | ]; |
1033 | if ( $this->mHelpClass !== false ) { |
1034 | $wrapperAttributes['class'][] = $this->mHelpClass; |
1035 | } |
1036 | if ( $this->mCondState ) { |
1037 | $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() ); |
1038 | $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass ); |
1039 | } |
1040 | return Html::rawElement( 'div', $wrapperAttributes, $helptext ); |
1041 | } |
1042 | |
1043 | /** |
1044 | * Generate help text HTML formatted for raw output |
1045 | * @since 1.20 |
1046 | * |
1047 | * @param string|null $helptext |
1048 | * @return string |
1049 | */ |
1050 | public function getHelpTextHtmlRaw( $helptext ) { |
1051 | return $this->getHelpTextHtmlDiv( $helptext ); |
1052 | } |
1053 | |
1054 | private function getHelpMessages(): array { |
1055 | if ( isset( $this->mParams['help-message'] ) ) { |
1056 | return [ $this->mParams['help-message'] ]; |
1057 | } elseif ( isset( $this->mParams['help-messages'] ) ) { |
1058 | return $this->mParams['help-messages']; |
1059 | } elseif ( isset( $this->mParams['help'] ) ) { |
1060 | return [ new HtmlArmor( $this->mParams['help'] ) ]; |
1061 | } |
1062 | |
1063 | return []; |
1064 | } |
1065 | |
1066 | /** |
1067 | * Determine the help text to display |
1068 | * @stable to override |
1069 | * @since 1.20 |
1070 | * @return string|null HTML |
1071 | */ |
1072 | public function getHelpText() { |
1073 | $html = []; |
1074 | |
1075 | foreach ( $this->getHelpMessages() as $msg ) { |
1076 | if ( $msg instanceof HtmlArmor ) { |
1077 | $html[] = HtmlArmor::getHtml( $msg ); |
1078 | } else { |
1079 | $msg = $this->getMessage( $msg ); |
1080 | if ( $msg->exists() ) { |
1081 | $html[] = $msg->parse(); |
1082 | } |
1083 | } |
1084 | } |
1085 | |
1086 | return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null; |
1087 | } |
1088 | |
1089 | /** |
1090 | * Determine if the help text should be displayed inline. |
1091 | * |
1092 | * Only applies to OOUI forms. |
1093 | * |
1094 | * @since 1.31 |
1095 | * @return bool |
1096 | */ |
1097 | public function isHelpInline() { |
1098 | return $this->mParams['help-inline'] ?? true; |
1099 | } |
1100 | |
1101 | /** |
1102 | * Determine form errors to display and their classes |
1103 | * @since 1.20 |
1104 | * |
1105 | * phan-taint-check gets confused with returning both classes |
1106 | * and errors and thinks double escaping is happening, so specify |
1107 | * that return value has no taint. |
1108 | * |
1109 | * @param string $value The value of the input |
1110 | * @return array [ $errors, $errorClass ] |
1111 | * @return-taint none |
1112 | */ |
1113 | public function getErrorsAndErrorClass( $value ) { |
1114 | $errors = $this->validate( $value, $this->mParent->mFieldData ); |
1115 | |
1116 | if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) { |
1117 | return [ '', '' ]; |
1118 | } |
1119 | |
1120 | return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ]; |
1121 | } |
1122 | |
1123 | /** |
1124 | * Determine form errors to display, returning them in an array. |
1125 | * |
1126 | * @since 1.26 |
1127 | * @param string $value The value of the input |
1128 | * @return string[] Array of error HTML strings |
1129 | */ |
1130 | public function getErrorsRaw( $value ) { |
1131 | $errors = $this->validate( $value, $this->mParent->mFieldData ); |
1132 | |
1133 | if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) { |
1134 | return []; |
1135 | } |
1136 | |
1137 | if ( !is_array( $errors ) ) { |
1138 | $errors = [ $errors ]; |
1139 | } |
1140 | foreach ( $errors as &$error ) { |
1141 | if ( $error instanceof Message ) { |
1142 | $error = $error->parse(); |
1143 | } |
1144 | } |
1145 | |
1146 | return $errors; |
1147 | } |
1148 | |
1149 | /** |
1150 | * @stable to override |
1151 | * @return string HTML |
1152 | */ |
1153 | public function getLabel() { |
1154 | return $this->mLabel ?? ''; |
1155 | } |
1156 | |
1157 | /** |
1158 | * @stable to override |
1159 | * @param array $cellAttributes |
1160 | * |
1161 | * @return string |
1162 | */ |
1163 | public function getLabelHtml( $cellAttributes = [] ) { |
1164 | # Don't output a for= attribute for labels with no associated input. |
1165 | # Kind of hacky here, possibly we don't want these to be <label>s at all. |
1166 | $for = $this->needsLabel() ? [ 'for' => $this->mID ] : []; |
1167 | |
1168 | $labelValue = trim( $this->getLabel() ); |
1169 | $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== ' '; |
1170 | |
1171 | $displayFormat = $this->mParent->getDisplayFormat(); |
1172 | $horizontalLabel = $this->mParams['horizontal-label'] ?? false; |
1173 | |
1174 | if ( $displayFormat === 'table' ) { |
1175 | return Html::rawElement( 'td', |
1176 | [ 'class' => 'mw-label' ] + $cellAttributes, |
1177 | Html::rawElement( 'label', $for, $labelValue ) ); |
1178 | } elseif ( $hasLabel || $this->mShowEmptyLabels ) { |
1179 | if ( $displayFormat === 'div' && !$horizontalLabel ) { |
1180 | return Html::rawElement( 'div', |
1181 | [ 'class' => 'mw-label' ] + $cellAttributes, |
1182 | Html::rawElement( 'label', $for, $labelValue ) ); |
1183 | } else { |
1184 | return Html::rawElement( 'label', $for, $labelValue ); |
1185 | } |
1186 | } |
1187 | |
1188 | return ''; |
1189 | } |
1190 | |
1191 | /** |
1192 | * @stable to override |
1193 | * @return mixed |
1194 | */ |
1195 | public function getDefault() { |
1196 | return $this->mDefault ?? null; |
1197 | } |
1198 | |
1199 | /** |
1200 | * Returns the attributes required for the tooltip and accesskey, for Html::element() etc. |
1201 | * |
1202 | * @return array Attributes |
1203 | */ |
1204 | public function getTooltipAndAccessKey() { |
1205 | if ( empty( $this->mParams['tooltip'] ) ) { |
1206 | return []; |
1207 | } |
1208 | |
1209 | return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] ); |
1210 | } |
1211 | |
1212 | /** |
1213 | * Returns the attributes required for the tooltip and accesskey, for OOUI widgets' config. |
1214 | * |
1215 | * @return array Attributes |
1216 | */ |
1217 | public function getTooltipAndAccessKeyOOUI() { |
1218 | if ( empty( $this->mParams['tooltip'] ) ) { |
1219 | return []; |
1220 | } |
1221 | |
1222 | return [ |
1223 | 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ), |
1224 | 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ), |
1225 | ]; |
1226 | } |
1227 | |
1228 | /** |
1229 | * Returns the given attributes from the parameters |
1230 | * @stable to override |
1231 | * |
1232 | * @param array $list List of attributes to get |
1233 | * @return array Attributes |
1234 | */ |
1235 | public function getAttributes( array $list ) { |
1236 | static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ]; |
1237 | |
1238 | $ret = []; |
1239 | foreach ( $list as $key ) { |
1240 | if ( in_array( $key, $boolAttribs ) ) { |
1241 | if ( !empty( $this->mParams[$key] ) ) { |
1242 | $ret[$key] = ''; |
1243 | } |
1244 | } elseif ( isset( $this->mParams[$key] ) ) { |
1245 | $ret[$key] = $this->mParams[$key]; |
1246 | } |
1247 | } |
1248 | |
1249 | return $ret; |
1250 | } |
1251 | |
1252 | /** |
1253 | * Given an array of msg-key => value mappings, returns an array with keys |
1254 | * being the message texts. It also forces values to strings. |
1255 | * |
1256 | * @param array $options |
1257 | * @param bool $needsParse |
1258 | * @return array |
1259 | * @return-taint tainted |
1260 | */ |
1261 | private function lookupOptionsKeys( $options, $needsParse ) { |
1262 | $ret = []; |
1263 | foreach ( $options as $key => $value ) { |
1264 | $msg = $this->msg( $key ); |
1265 | $msgAsText = $needsParse ? $msg->parse() : $msg->plain(); |
1266 | if ( array_key_exists( $msgAsText, $ret ) ) { |
1267 | LoggerFactory::getInstance( 'error' )->error( |
1268 | 'The option that uses the message key {msg_key_one} has the same translation as ' . |
1269 | 'another option in {lang}. This means that {msg_key_one} will not be used as an option.', |
1270 | [ |
1271 | 'msg_key_one' => $key, |
1272 | 'lang' => $this->mParent ? |
1273 | $this->mParent->getLanguageCode()->toBcp47Code() : |
1274 | RequestContext::getMain()->getLanguageCode()->toBcp47Code(), |
1275 | ] |
1276 | ); |
1277 | continue; |
1278 | } |
1279 | $ret[$msgAsText] = is_array( $value ) |
1280 | ? $this->lookupOptionsKeys( $value, $needsParse ) |
1281 | : strval( $value ); |
1282 | } |
1283 | return $ret; |
1284 | } |
1285 | |
1286 | /** |
1287 | * Recursively forces values in an array to strings, because issues arise |
1288 | * with integer 0 as a value. |
1289 | * |
1290 | * @param array|string $array |
1291 | * @return array|string |
1292 | */ |
1293 | public static function forceToStringRecursive( $array ) { |
1294 | if ( is_array( $array ) ) { |
1295 | return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array ); |
1296 | } else { |
1297 | return strval( $array ); |
1298 | } |
1299 | } |
1300 | |
1301 | /** |
1302 | * Fetch the array of options from the field's parameters. In order, this |
1303 | * checks 'options-messages', 'options', then 'options-message'. |
1304 | * |
1305 | * @return array|null |
1306 | */ |
1307 | public function getOptions() { |
1308 | if ( $this->mOptions === false ) { |
1309 | if ( array_key_exists( 'options-messages', $this->mParams ) ) { |
1310 | $needsParse = $this->mParams['options-messages-parse'] ?? false; |
1311 | if ( $needsParse ) { |
1312 | $this->mOptionsLabelsNotFromMessage = true; |
1313 | } |
1314 | $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse ); |
1315 | } elseif ( array_key_exists( 'options', $this->mParams ) ) { |
1316 | $this->mOptionsLabelsNotFromMessage = true; |
1317 | $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] ); |
1318 | } elseif ( array_key_exists( 'options-message', $this->mParams ) ) { |
1319 | $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain(); |
1320 | $this->mOptions = Html::listDropdownOptions( $message ); |
1321 | } else { |
1322 | $this->mOptions = null; |
1323 | } |
1324 | } |
1325 | |
1326 | return $this->mOptions; |
1327 | } |
1328 | |
1329 | /** |
1330 | * Get options and make them into arrays suitable for OOUI. |
1331 | * @stable to override |
1332 | * @return array|null Options for inclusion in a select or whatever. |
1333 | */ |
1334 | public function getOptionsOOUI() { |
1335 | $oldoptions = $this->getOptions(); |
1336 | |
1337 | if ( $oldoptions === null ) { |
1338 | return null; |
1339 | } |
1340 | |
1341 | return Html::listDropdownOptionsOoui( $oldoptions ); |
1342 | } |
1343 | |
1344 | /** |
1345 | * flatten an array of options to a single array, for instance, |
1346 | * a set of "<options>" inside "<optgroups>". |
1347 | * |
1348 | * @param array $options Associative Array with values either Strings or Arrays |
1349 | * @return array Flattened input |
1350 | */ |
1351 | public static function flattenOptions( $options ) { |
1352 | $flatOpts = []; |
1353 | |
1354 | foreach ( $options as $value ) { |
1355 | if ( is_array( $value ) ) { |
1356 | $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) ); |
1357 | } else { |
1358 | $flatOpts[] = $value; |
1359 | } |
1360 | } |
1361 | |
1362 | return $flatOpts; |
1363 | } |
1364 | |
1365 | /** |
1366 | * Formats one or more errors as accepted by field validation-callback. |
1367 | * |
1368 | * @param string|Message|array $errors Array of strings or Message instances |
1369 | * To work around limitations in phan-taint-check the calling |
1370 | * class has taintedness disabled. So instead we pretend that |
1371 | * this method outputs html, since the result is eventually |
1372 | * outputted anyways without escaping and this allows us to verify |
1373 | * stuff is safe even though the caller has taintedness cleared. |
1374 | * @param-taint $errors exec_html |
1375 | * @return string HTML |
1376 | * @since 1.18 |
1377 | */ |
1378 | protected static function formatErrors( $errors ) { |
1379 | if ( is_array( $errors ) && count( $errors ) === 1 ) { |
1380 | $errors = array_shift( $errors ); |
1381 | } |
1382 | |
1383 | if ( is_array( $errors ) ) { |
1384 | foreach ( $errors as &$error ) { |
1385 | $error = Html::rawElement( 'li', [], |
1386 | $error instanceof Message ? $error->parse() : $error |
1387 | ); |
1388 | } |
1389 | $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) ); |
1390 | } elseif ( $errors instanceof Message ) { |
1391 | $errors = $errors->parse(); |
1392 | } |
1393 | |
1394 | return Html::errorBox( $errors ); |
1395 | } |
1396 | |
1397 | /** |
1398 | * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a |
1399 | * name + parameters array) into a Message. |
1400 | * @param mixed $value |
1401 | * @return Message |
1402 | */ |
1403 | protected function getMessage( $value ) { |
1404 | $message = Message::newFromSpecifier( $value ); |
1405 | |
1406 | if ( $this->mParent ) { |
1407 | $message->setContext( $this->mParent ); |
1408 | } |
1409 | |
1410 | return $message; |
1411 | } |
1412 | |
1413 | /** |
1414 | * Skip this field when collecting data. |
1415 | * @stable to override |
1416 | * @param WebRequest $request |
1417 | * @return bool |
1418 | * @since 1.27 |
1419 | */ |
1420 | public function skipLoadData( $request ) { |
1421 | return !empty( $this->mParams['nodata'] ); |
1422 | } |
1423 | |
1424 | /** |
1425 | * Whether this field requires the user agent to have JavaScript enabled for the client-side HTML5 |
1426 | * form validation to work correctly. |
1427 | * |
1428 | * @return bool |
1429 | * @since 1.29 |
1430 | */ |
1431 | public function needsJSForHtml5FormValidation() { |
1432 | // This is probably more restrictive than it needs to be, but better safe than sorry |
1433 | return (bool)$this->mCondState; |
1434 | } |
1435 | } |
1436 | |
1437 | /** @deprecated class alias since 1.42 */ |
1438 | class_alias( HTMLFormField::class, 'HTMLFormField' ); |