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