Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
42.22% |
209 / 495 |
|
32.76% |
19 / 58 |
CRAP | |
0.00% |
0 / 1 |
HTMLFormField | |
42.31% |
209 / 494 |
|
32.76% |
19 / 58 |
10210.13 | |
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 | |
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\MessageSpecifier; |
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):(StatusValue|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 = get_debug_type( $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 |