Completed
Branch master (54277f)
by
unknown
24:54
created

HTMLFormField::getMessage()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 12
rs 9.2
cc 4
eloc 10
nc 4
nop 1
1
<?php
2
3
/**
4
 * The parent class to generate form fields.  Any field type should
5
 * be a subclass of this.
6
 */
7
abstract class HTMLFormField {
8
	public $mParams;
9
10
	protected $mValidationCallback;
11
	protected $mFilterCallback;
12
	protected $mName;
13
	protected $mDir;
14
	protected $mLabel; # String label, as HTML. Set on construction.
15
	protected $mID;
16
	protected $mClass = '';
17
	protected $mVFormClass = '';
18
	protected $mHelpClass = false;
19
	protected $mDefault;
20
	protected $mOptions = false;
21
	protected $mOptionsLabelsNotFromMessage = false;
22
	protected $mHideIf = null;
23
24
	/**
25
	 * @var bool If true will generate an empty div element with no label
26
	 * @since 1.22
27
	 */
28
	protected $mShowEmptyLabels = true;
29
30
	/**
31
	 * @var HTMLForm
32
	 */
33
	public $mParent;
34
35
	/**
36
	 * This function must be implemented to return the HTML to generate
37
	 * the input object itself.  It should not implement the surrounding
38
	 * table cells/rows, or labels/help messages.
39
	 *
40
	 * @param string $value The value to set the input to; eg a default
41
	 *     text for a text input.
42
	 *
43
	 * @return string Valid HTML.
44
	 */
45
	abstract function getInputHTML( $value );
46
47
	/**
48
	 * Same as getInputHTML, but returns an OOUI object.
49
	 * Defaults to false, which getOOUI will interpret as "use the HTML version"
50
	 *
51
	 * @param string $value
52
	 * @return OOUI\Widget|false
53
	 */
54
	function getInputOOUI( $value ) {
55
		return false;
56
	}
57
58
	/**
59
	 * True if this field type is able to display errors; false if validation errors need to be
60
	 * displayed in the main HTMLForm error area.
61
	 * @return bool
62
	 */
63
	public function canDisplayErrors() {
64
		return true;
65
	}
66
67
	/**
68
	 * Get a translated interface message
69
	 *
70
	 * This is a wrapper around $this->mParent->msg() if $this->mParent is set
71
	 * and wfMessage() otherwise.
72
	 *
73
	 * Parameters are the same as wfMessage().
74
	 *
75
	 * @return Message
76
	 */
77
	function msg() {
78
		$args = func_get_args();
79
80
		if ( $this->mParent ) {
81
			$callback = [ $this->mParent, 'msg' ];
82
		} else {
83
			$callback = 'wfMessage';
84
		}
85
86
		return call_user_func_array( $callback, $args );
87
	}
88
89
	/**
90
	 * If this field has a user-visible output or not. If not,
91
	 * it will not be rendered
92
	 *
93
	 * @return bool
94
	 */
95
	public function hasVisibleOutput() {
96
		return true;
97
	}
98
99
	/**
100
	 * Fetch a field value from $alldata for the closest field matching a given
101
	 * name.
102
	 *
103
	 * This is complex because it needs to handle array fields like the user
104
	 * would expect. The general algorithm is to look for $name as a sibling
105
	 * of $this, then a sibling of $this's parent, and so on. Keeping in mind
106
	 * that $name itself might be referencing an array.
107
	 *
108
	 * @param array $alldata
109
	 * @param string $name
110
	 * @return string
111
	 */
112
	protected function getNearestFieldByName( $alldata, $name ) {
113
		$tmp = $this->mName;
114
		$thisKeys = [];
115
		while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) {
116
			array_unshift( $thisKeys, $m[2] );
117
			$tmp = $m[1];
118
		}
119
		if ( substr( $tmp, 0, 2 ) == 'wp' &&
120
			!isset( $alldata[$tmp] ) &&
121
			isset( $alldata[substr( $tmp, 2 )] )
122
		) {
123
			// Adjust for name mangling.
124
			$tmp = substr( $tmp, 2 );
125
		}
126
		array_unshift( $thisKeys, $tmp );
127
128
		$tmp = $name;
129
		$nameKeys = [];
130
		while ( preg_match( '/^(.+)\[([^\]]+)\]$/', $tmp, $m ) ) {
131
			array_unshift( $nameKeys, $m[2] );
132
			$tmp = $m[1];
133
		}
134
		array_unshift( $nameKeys, $tmp );
135
136
		$testValue = '';
137
		for ( $i = count( $thisKeys ) - 1; $i >= 0; $i-- ) {
138
			$keys = array_merge( array_slice( $thisKeys, 0, $i ), $nameKeys );
139
			$data = $alldata;
140
			while ( $keys ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
141
				$key = array_shift( $keys );
142
				if ( !is_array( $data ) || !isset( $data[$key] ) ) {
143
					continue 2;
144
				}
145
				$data = $data[$key];
146
			}
147
			$testValue = (string)$data;
148
			break;
149
		}
150
151
		return $testValue;
152
	}
153
154
	/**
155
	 * Helper function for isHidden to handle recursive data structures.
156
	 *
157
	 * @param array $alldata
158
	 * @param array $params
159
	 * @return bool
160
	 * @throws MWException
161
	 */
162
	protected function isHiddenRecurse( array $alldata, array $params ) {
163
		$origParams = $params;
164
		$op = array_shift( $params );
165
166
		try {
167
			switch ( $op ) {
168 View Code Duplication
				case 'AND':
169
					foreach ( $params as $i => $p ) {
170
						if ( !is_array( $p ) ) {
171
							throw new MWException(
172
								"Expected array, found " . gettype( $p ) . " at index $i"
173
							);
174
						}
175
						if ( !$this->isHiddenRecurse( $alldata, $p ) ) {
176
							return false;
177
						}
178
					}
179
					return true;
180
181
				case 'OR':
182
					foreach ( $params as $i => $p ) {
183
						if ( !is_array( $p ) ) {
184
							throw new MWException(
185
								"Expected array, found " . gettype( $p ) . " at index $i"
186
							);
187
						}
188
						if ( $this->isHiddenRecurse( $alldata, $p ) ) {
189
							return true;
190
						}
191
					}
192
					return false;
193
194 View Code Duplication
				case 'NAND':
195
					foreach ( $params as $i => $p ) {
196
						if ( !is_array( $p ) ) {
197
							throw new MWException(
198
								"Expected array, found " . gettype( $p ) . " at index $i"
199
							);
200
						}
201
						if ( !$this->isHiddenRecurse( $alldata, $p ) ) {
202
							return true;
203
						}
204
					}
205
					return false;
206
207
				case 'NOR':
208
					foreach ( $params as $i => $p ) {
209
						if ( !is_array( $p ) ) {
210
							throw new MWException(
211
								"Expected array, found " . gettype( $p ) . " at index $i"
212
							);
213
						}
214
						if ( $this->isHiddenRecurse( $alldata, $p ) ) {
215
							return false;
216
						}
217
					}
218
					return true;
219
220
				case 'NOT':
221
					if ( count( $params ) !== 1 ) {
222
						throw new MWException( "NOT takes exactly one parameter" );
223
					}
224
					$p = $params[0];
225
					if ( !is_array( $p ) ) {
226
						throw new MWException(
227
							"Expected array, found " . gettype( $p ) . " at index 0"
228
						);
229
					}
230
					return !$this->isHiddenRecurse( $alldata, $p );
231
232
				case '===':
233
				case '!==':
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
234
					if ( count( $params ) !== 2 ) {
235
						throw new MWException( "$op takes exactly two parameters" );
236
					}
237
					list( $field, $value ) = $params;
238
					if ( !is_string( $field ) || !is_string( $value ) ) {
239
						throw new MWException( "Parameters for $op must be strings" );
240
					}
241
					$testValue = $this->getNearestFieldByName( $alldata, $field );
242
					switch ( $op ) {
243
						case '===':
244
							return ( $value === $testValue );
245
						case '!==':
246
							return ( $value !== $testValue );
247
					}
248
249
				default:
250
					throw new MWException( "Unknown operation" );
251
			}
252
		} catch ( Exception $ex ) {
253
			throw new MWException(
254
				"Invalid hide-if specification for $this->mName: " .
255
				$ex->getMessage() . " in " . var_export( $origParams, true ),
256
				0, $ex
257
			);
258
		}
259
	}
260
261
	/**
262
	 * Test whether this field is supposed to be hidden, based on the values of
263
	 * the other form fields.
264
	 *
265
	 * @since 1.23
266
	 * @param array $alldata The data collected from the form
267
	 * @return bool
268
	 */
269
	function isHidden( $alldata ) {
270
		if ( !$this->mHideIf ) {
271
			return false;
272
		}
273
274
		return $this->isHiddenRecurse( $alldata, $this->mHideIf );
275
	}
276
277
	/**
278
	 * Override this function if the control can somehow trigger a form
279
	 * submission that shouldn't actually submit the HTMLForm.
280
	 *
281
	 * @since 1.23
282
	 * @param string|array $value The value the field was submitted with
283
	 * @param array $alldata The data collected from the form
284
	 *
285
	 * @return bool True to cancel the submission
286
	 */
287
	function cancelSubmit( $value, $alldata ) {
288
		return false;
289
	}
290
291
	/**
292
	 * Override this function to add specific validation checks on the
293
	 * field input.  Don't forget to call parent::validate() to ensure
294
	 * that the user-defined callback mValidationCallback is still run
295
	 *
296
	 * @param string|array $value The value the field was submitted with
297
	 * @param array $alldata The data collected from the form
298
	 *
299
	 * @return bool|string True on success, or String error to display, or
300
	 *   false to fail validation without displaying an error.
301
	 */
302
	function validate( $value, $alldata ) {
303
		if ( $this->isHidden( $alldata ) ) {
304
			return true;
305
		}
306
307 View Code Duplication
		if ( isset( $this->mParams['required'] )
308
			&& $this->mParams['required'] !== false
309
			&& $value === ''
310
		) {
311
			return $this->msg( 'htmlform-required' )->parse();
312
		}
313
314
		if ( isset( $this->mValidationCallback ) ) {
315
			return call_user_func( $this->mValidationCallback, $value, $alldata, $this->mParent );
316
		}
317
318
		return true;
319
	}
320
321
	function filter( $value, $alldata ) {
322
		if ( isset( $this->mFilterCallback ) ) {
323
			$value = call_user_func( $this->mFilterCallback, $value, $alldata, $this->mParent );
324
		}
325
326
		return $value;
327
	}
328
329
	/**
330
	 * Should this field have a label, or is there no input element with the
331
	 * appropriate id for the label to point to?
332
	 *
333
	 * @return bool True to output a label, false to suppress
334
	 */
335
	protected function needsLabel() {
336
		return true;
337
	}
338
339
	/**
340
	 * Tell the field whether to generate a separate label element if its label
341
	 * is blank.
342
	 *
343
	 * @since 1.22
344
	 *
345
	 * @param bool $show Set to false to not generate a label.
346
	 * @return void
347
	 */
348
	public function setShowEmptyLabel( $show ) {
349
		$this->mShowEmptyLabels = $show;
350
	}
351
352
	/**
353
	 * Get the value that this input has been set to from a posted form,
354
	 * or the input's default value if it has not been set.
355
	 *
356
	 * @param WebRequest $request
357
	 * @return string The value
358
	 */
359
	function loadDataFromRequest( $request ) {
360
		if ( $request->getCheck( $this->mName ) ) {
361
			return $request->getText( $this->mName );
362
		} else {
363
			return $this->getDefault();
364
		}
365
	}
366
367
	/**
368
	 * Initialise the object
369
	 *
370
	 * @param array $params Associative Array. See HTMLForm doc for syntax.
371
	 *
372
	 * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead
373
	 * @throws MWException
374
	 */
375
	function __construct( $params ) {
376
		$this->mParams = $params;
377
378
		if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
379
			$this->mParent = $params['parent'];
380
		}
381
382
		# Generate the label from a message, if possible
383
		if ( isset( $params['label-message'] ) ) {
384
			$this->mLabel = $this->getMessage( $params['label-message'] )->parse();
385
		} elseif ( isset( $params['label'] ) ) {
386
			if ( $params['label'] === '&#160;' ) {
387
				// Apparently some things set &nbsp directly and in an odd format
388
				$this->mLabel = '&#160;';
389
			} else {
390
				$this->mLabel = htmlspecialchars( $params['label'] );
391
			}
392
		} elseif ( isset( $params['label-raw'] ) ) {
393
			$this->mLabel = $params['label-raw'];
394
		}
395
396
		$this->mName = "wp{$params['fieldname']}";
397
		if ( isset( $params['name'] ) ) {
398
			$this->mName = $params['name'];
399
		}
400
401
		if ( isset( $params['dir'] ) ) {
402
			$this->mDir = $params['dir'];
403
		}
404
405
		$validName = Sanitizer::escapeId( $this->mName );
406
		$validName = str_replace( [ '.5B', '.5D' ], [ '[', ']' ], $validName );
407
		if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
408
			throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
409
		}
410
411
		$this->mID = "mw-input-{$this->mName}";
412
413
		if ( isset( $params['default'] ) ) {
414
			$this->mDefault = $params['default'];
415
		}
416
417
		if ( isset( $params['id'] ) ) {
418
			$id = $params['id'];
419
			$validId = Sanitizer::escapeId( $id );
420
421
			if ( $id != $validId ) {
422
				throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
423
			}
424
425
			$this->mID = $id;
426
		}
427
428
		if ( isset( $params['cssclass'] ) ) {
429
			$this->mClass = $params['cssclass'];
430
		}
431
432
		if ( isset( $params['csshelpclass'] ) ) {
433
			$this->mHelpClass = $params['csshelpclass'];
434
		}
435
436
		if ( isset( $params['validation-callback'] ) ) {
437
			$this->mValidationCallback = $params['validation-callback'];
438
		}
439
440
		if ( isset( $params['filter-callback'] ) ) {
441
			$this->mFilterCallback = $params['filter-callback'];
442
		}
443
444
		if ( isset( $params['flatlist'] ) ) {
445
			$this->mClass .= ' mw-htmlform-flatlist';
446
		}
447
448
		if ( isset( $params['hidelabel'] ) ) {
449
			$this->mShowEmptyLabels = false;
450
		}
451
452
		if ( isset( $params['hide-if'] ) ) {
453
			$this->mHideIf = $params['hide-if'];
454
		}
455
	}
456
457
	/**
458
	 * Get the complete table row for the input, including help text,
459
	 * labels, and whatever.
460
	 *
461
	 * @param string $value The value to set the input to.
462
	 *
463
	 * @return string Complete HTML table row.
464
	 */
465
	function getTableRow( $value ) {
466
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
467
		$inputHtml = $this->getInputHTML( $value );
468
		$fieldType = get_class( $this );
469
		$helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
0 ignored issues
show
Bug introduced by
It seems like $this->getHelpText() targeting HTMLFormField::getHelpText() can also be of type array<integer,?,{"0":"?"}>; however, HTMLFormField::getHelpTextHtmlTable() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
470
		$cellAttributes = [];
471
		$rowAttributes = [];
472
		$rowClasses = '';
473
474
		if ( !empty( $this->mParams['vertical-label'] ) ) {
475
			$cellAttributes['colspan'] = 2;
476
			$verticalLabel = true;
477
		} else {
478
			$verticalLabel = false;
479
		}
480
481
		$label = $this->getLabelHtml( $cellAttributes );
482
483
		$field = Html::rawElement(
484
			'td',
485
			[ 'class' => 'mw-input' ] + $cellAttributes,
486
			$inputHtml . "\n$errors"
487
		);
488
489
		if ( $this->mHideIf ) {
490
			$rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
491
			$rowClasses .= ' mw-htmlform-hide-if';
492
		}
493
494
		if ( $verticalLabel ) {
495
			$html = Html::rawElement( 'tr',
496
				$rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
497
			$html .= Html::rawElement( 'tr',
498
				$rowAttributes + [
499
					'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
500
				],
501
				$field );
502
		} else {
503
			$html =
504
				Html::rawElement( 'tr',
505
					$rowAttributes + [
506
						'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
507
					],
508
					$label . $field );
509
		}
510
511
		return $html . $helptext;
512
	}
513
514
	/**
515
	 * Get the complete div for the input, including help text,
516
	 * labels, and whatever.
517
	 * @since 1.20
518
	 *
519
	 * @param string $value The value to set the input to.
520
	 *
521
	 * @return string Complete HTML table row.
522
	 */
523
	public function getDiv( $value ) {
524
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
525
		$inputHtml = $this->getInputHTML( $value );
526
		$fieldType = get_class( $this );
527
		$helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
0 ignored issues
show
Bug introduced by
It seems like $this->getHelpText() targeting HTMLFormField::getHelpText() can also be of type array<integer,?,{"0":"?"}>; however, HTMLFormField::getHelpTextHtmlDiv() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
528
		$cellAttributes = [];
529
		$label = $this->getLabelHtml( $cellAttributes );
530
531
		$outerDivClass = [
532
			'mw-input',
533
			'mw-htmlform-nolabel' => ( $label === '' )
534
		];
535
536
		$horizontalLabel = isset( $this->mParams['horizontal-label'] )
537
			? $this->mParams['horizontal-label'] : false;
538
539
		if ( $horizontalLabel ) {
540
			$field = '&#160;' . $inputHtml . "\n$errors";
541
		} else {
542
			$field = Html::rawElement(
543
				'div',
544
				[ 'class' => $outerDivClass ] + $cellAttributes,
545
				$inputHtml . "\n$errors"
546
			);
547
		}
548
		$divCssClasses = [ "mw-htmlform-field-$fieldType",
549
			$this->mClass, $this->mVFormClass, $errorClass ];
550
551
		$wrapperAttributes = [
552
			'class' => $divCssClasses,
553
		];
554 View Code Duplication
		if ( $this->mHideIf ) {
555
			$wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
556
			$wrapperAttributes['class'][] = ' mw-htmlform-hide-if';
557
		}
558
		$html = Html::rawElement( 'div', $wrapperAttributes, $label . $field );
559
		$html .= $helptext;
560
561
		return $html;
562
	}
563
564
	/**
565
	 * Get the OOUI version of the div. Falls back to getDiv by default.
566
	 * @since 1.26
567
	 *
568
	 * @param string $value The value to set the input to.
569
	 *
570
	 * @return OOUI\FieldLayout|OOUI\ActionFieldLayout
571
	 */
572
	public function getOOUI( $value ) {
573
		$inputField = $this->getInputOOUI( $value );
574
575
		if ( !$inputField ) {
576
			// This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
577
			// generate the whole field, label and errors and all, then wrap it in a Widget.
578
			// It might look weird, but it'll work OK.
579
			return $this->getFieldLayoutOOUI(
580
				new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
581
				[ 'infusable' => false, 'align' => 'top' ]
582
			);
583
		}
584
585
		$infusable = true;
586
		if ( is_string( $inputField ) ) {
587
			// We have an OOUI implementation, but it's not proper, and we got a load of HTML.
588
			// Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
589
			// JavaScript doesn't know how to rebuilt the contents.
590
			$inputField = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $inputField ) ] );
591
			$infusable = false;
592
		}
593
594
		$fieldType = get_class( $this );
595
		$helpText = $this->getHelpText();
596
		$errors = $this->getErrorsRaw( $value );
597
		foreach ( $errors as &$error ) {
598
			$error = new OOUI\HtmlSnippet( $error );
599
		}
600
601
		$config = [
602
			'classes' => [ "mw-htmlform-field-$fieldType", $this->mClass ],
603
			'align' => $this->getLabelAlignOOUI(),
604
			'help' => $helpText !== null ? new OOUI\HtmlSnippet( $helpText ) : null,
0 ignored issues
show
Bug introduced by
It seems like $helpText defined by $this->getHelpText() on line 595 can also be of type array<integer,?,{"0":"?"}>; however, OOUI\HtmlSnippet::__construct() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
605
			'errors' => $errors,
606
			'infusable' => $infusable,
607
		];
608
609
		// the element could specify, that the label doesn't need to be added
610
		$label = $this->getLabel();
611
		if ( $label ) {
612
			$config['label'] = new OOUI\HtmlSnippet( $label );
613
		}
614
615
		return $this->getFieldLayoutOOUI( $inputField, $config );
616
	}
617
618
	/**
619
	 * Get label alignment when generating field for OOUI.
620
	 * @return string 'left', 'right', 'top' or 'inline'
621
	 */
622
	protected function getLabelAlignOOUI() {
623
		return 'top';
624
	}
625
626
	/**
627
	 * Get a FieldLayout (or subclass thereof) to wrap this field in when using OOUI output.
628
	 * @return OOUI\FieldLayout|OOUI\ActionFieldLayout
629
	 */
630
	protected function getFieldLayoutOOUI( $inputField, $config ) {
631
		if ( isset( $this->mClassWithButton ) ) {
632
			$buttonWidget = $this->mClassWithButton->getInputOOUI( '' );
0 ignored issues
show
Bug introduced by
The property mClassWithButton does not seem to exist. Did you mean mClass?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
633
			return new OOUI\ActionFieldLayout( $inputField, $buttonWidget, $config );
634
		}
635
		return new OOUI\FieldLayout( $inputField, $config );
636
	}
637
638
	/**
639
	 * Get the complete raw fields for the input, including help text,
640
	 * labels, and whatever.
641
	 * @since 1.20
642
	 *
643
	 * @param string $value The value to set the input to.
644
	 *
645
	 * @return string Complete HTML table row.
646
	 */
647
	public function getRaw( $value ) {
648
		list( $errors, ) = $this->getErrorsAndErrorClass( $value );
649
		$inputHtml = $this->getInputHTML( $value );
650
		$helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
0 ignored issues
show
Bug introduced by
It seems like $this->getHelpText() targeting HTMLFormField::getHelpText() can also be of type array<integer,?,{"0":"?"}>; however, HTMLFormField::getHelpTextHtmlRaw() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
651
		$cellAttributes = [];
652
		$label = $this->getLabelHtml( $cellAttributes );
653
654
		$html = "\n$errors";
655
		$html .= $label;
656
		$html .= $inputHtml;
657
		$html .= $helptext;
658
659
		return $html;
660
	}
661
662
	/**
663
	 * Get the complete field for the input, including help text,
664
	 * labels, and whatever. Fall back from 'vform' to 'div' when not overridden.
665
	 *
666
	 * @since 1.25
667
	 * @param string $value The value to set the input to.
668
	 * @return string Complete HTML field.
669
	 */
670
	public function getVForm( $value ) {
671
		// Ewwww
672
		$this->mVFormClass = ' mw-ui-vform-field';
673
		return $this->getDiv( $value );
674
	}
675
676
	/**
677
	 * Get the complete field as an inline element.
678
	 * @since 1.25
679
	 * @param string $value The value to set the input to.
680
	 * @return string Complete HTML inline element
681
	 */
682
	public function getInline( $value ) {
683
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
0 ignored issues
show
Unused Code introduced by
The assignment to $errorClass is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
684
		$inputHtml = $this->getInputHTML( $value );
685
		$helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
0 ignored issues
show
Bug introduced by
It seems like $this->getHelpText() targeting HTMLFormField::getHelpText() can also be of type array<integer,?,{"0":"?"}>; however, HTMLFormField::getHelpTextHtmlDiv() does only seem to accept string|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
686
		$cellAttributes = [];
687
		$label = $this->getLabelHtml( $cellAttributes );
688
689
		$html = "\n" . $errors .
690
			$label . '&#160;' .
691
			$inputHtml .
692
			$helptext;
693
694
		return $html;
695
	}
696
697
	/**
698
	 * Generate help text HTML in table format
699
	 * @since 1.20
700
	 *
701
	 * @param string|null $helptext
702
	 * @return string
703
	 */
704
	public function getHelpTextHtmlTable( $helptext ) {
705
		if ( is_null( $helptext ) ) {
706
			return '';
707
		}
708
709
		$rowAttributes = [];
710 View Code Duplication
		if ( $this->mHideIf ) {
711
			$rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
712
			$rowAttributes['class'] = 'mw-htmlform-hide-if';
713
		}
714
715
		$tdClasses = [ 'htmlform-tip' ];
716
		if ( $this->mHelpClass !== false ) {
717
			$tdClasses[] = $this->mHelpClass;
718
		}
719
		$row = Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext );
720
		$row = Html::rawElement( 'tr', $rowAttributes, $row );
721
722
		return $row;
723
	}
724
725
	/**
726
	 * Generate help text HTML in div format
727
	 * @since 1.20
728
	 *
729
	 * @param string|null $helptext
730
	 *
731
	 * @return string
732
	 */
733
	public function getHelpTextHtmlDiv( $helptext ) {
734
		if ( is_null( $helptext ) ) {
735
			return '';
736
		}
737
738
		$wrapperAttributes = [
739
			'class' => 'htmlform-tip',
740
		];
741
		if ( $this->mHelpClass !== false ) {
742
			$wrapperAttributes['class'] .= " {$this->mHelpClass}";
743
		}
744 View Code Duplication
		if ( $this->mHideIf ) {
745
			$wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
746
			$wrapperAttributes['class'] .= ' mw-htmlform-hide-if';
747
		}
748
		$div = Html::rawElement( 'div', $wrapperAttributes, $helptext );
749
750
		return $div;
751
	}
752
753
	/**
754
	 * Generate help text HTML formatted for raw output
755
	 * @since 1.20
756
	 *
757
	 * @param string|null $helptext
758
	 * @return string
759
	 */
760
	public function getHelpTextHtmlRaw( $helptext ) {
761
		return $this->getHelpTextHtmlDiv( $helptext );
762
	}
763
764
	/**
765
	 * Determine the help text to display
766
	 * @since 1.20
767
	 * @return string HTML
768
	 */
769
	public function getHelpText() {
770
		$helptext = null;
771
772
		if ( isset( $this->mParams['help-message'] ) ) {
773
			$this->mParams['help-messages'] = [ $this->mParams['help-message'] ];
774
		}
775
776
		if ( isset( $this->mParams['help-messages'] ) ) {
777
			foreach ( $this->mParams['help-messages'] as $msg ) {
778
				$msg = $this->getMessage( $msg );
779
780
				if ( $msg->exists() ) {
781
					if ( is_null( $helptext ) ) {
782
						$helptext = '';
783
					} else {
784
						$helptext .= $this->msg( 'word-separator' )->escaped(); // some space
785
					}
786
					$helptext .= $msg->parse(); // Append message
787
				}
788
			}
789
		} elseif ( isset( $this->mParams['help'] ) ) {
790
			$helptext = $this->mParams['help'];
791
		}
792
793
		return $helptext;
794
	}
795
796
	/**
797
	 * Determine form errors to display and their classes
798
	 * @since 1.20
799
	 *
800
	 * @param string $value The value of the input
801
	 * @return array array( $errors, $errorClass )
802
	 */
803
	public function getErrorsAndErrorClass( $value ) {
804
		$errors = $this->validate( $value, $this->mParent->mFieldData );
805
806
		if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
807
			$errors = '';
808
			$errorClass = '';
809
		} else {
810
			$errors = self::formatErrors( $errors );
811
			$errorClass = 'mw-htmlform-invalid-input';
812
		}
813
814
		return [ $errors, $errorClass ];
815
	}
816
817
	/**
818
	 * Determine form errors to display, returning them in an array.
819
	 *
820
	 * @since 1.26
821
	 * @param string $value The value of the input
822
	 * @return string[] Array of error HTML strings
823
	 */
824
	public function getErrorsRaw( $value ) {
825
		$errors = $this->validate( $value, $this->mParent->mFieldData );
826
827
		if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
828
			$errors = [];
829
		}
830
831
		if ( !is_array( $errors ) ) {
832
			$errors = [ $errors ];
833
		}
834
		foreach ( $errors as &$error ) {
835
			if ( $error instanceof Message ) {
836
				$error = $error->parse();
837
			}
838
		}
839
840
		return $errors;
841
	}
842
843
	/**
844
	 * @return string HTML
845
	 */
846
	function getLabel() {
847
		return is_null( $this->mLabel ) ? '' : $this->mLabel;
848
	}
849
850
	function getLabelHtml( $cellAttributes = [] ) {
851
		# Don't output a for= attribute for labels with no associated input.
852
		# Kind of hacky here, possibly we don't want these to be <label>s at all.
853
		$for = [];
854
855
		if ( $this->needsLabel() ) {
856
			$for['for'] = $this->mID;
857
		}
858
859
		$labelValue = trim( $this->getLabel() );
860
		$hasLabel = false;
861
		if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
862
			$hasLabel = true;
863
		}
864
865
		$displayFormat = $this->mParent->getDisplayFormat();
866
		$html = '';
867
		$horizontalLabel = isset( $this->mParams['horizontal-label'] )
868
			? $this->mParams['horizontal-label'] : false;
869
870
		if ( $displayFormat === 'table' ) {
871
			$html =
872
				Html::rawElement( 'td',
873
					[ 'class' => 'mw-label' ] + $cellAttributes,
874
					Html::rawElement( 'label', $for, $labelValue ) );
875
		} elseif ( $hasLabel || $this->mShowEmptyLabels ) {
876
			if ( $displayFormat === 'div' && !$horizontalLabel ) {
877
				$html =
878
					Html::rawElement( 'div',
879
						[ 'class' => 'mw-label' ] + $cellAttributes,
880
						Html::rawElement( 'label', $for, $labelValue ) );
881
			} else {
882
				$html = Html::rawElement( 'label', $for, $labelValue );
883
			}
884
		}
885
886
		return $html;
887
	}
888
889
	function getDefault() {
890
		if ( isset( $this->mDefault ) ) {
891
			return $this->mDefault;
892
		} else {
893
			return null;
894
		}
895
	}
896
897
	/**
898
	 * Returns the attributes required for the tooltip and accesskey.
899
	 *
900
	 * @return array Attributes
901
	 */
902
	public function getTooltipAndAccessKey() {
903
		if ( empty( $this->mParams['tooltip'] ) ) {
904
			return [];
905
		}
906
907
		return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
908
	}
909
910
	/**
911
	 * Returns the given attributes from the parameters
912
	 *
913
	 * @param array $list List of attributes to get
914
	 * @return array Attributes
915
	 */
916
	public function getAttributes( array $list ) {
917
		static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
918
919
		$ret = [];
920
		foreach ( $list as $key ) {
921
			if ( in_array( $key, $boolAttribs ) ) {
922
				if ( !empty( $this->mParams[$key] ) ) {
923
					$ret[$key] = '';
924
				}
925
			} elseif ( isset( $this->mParams[$key] ) ) {
926
				$ret[$key] = $this->mParams[$key];
927
			}
928
		}
929
930
		return $ret;
931
	}
932
933
	/**
934
	 * Given an array of msg-key => value mappings, returns an array with keys
935
	 * being the message texts. It also forces values to strings.
936
	 *
937
	 * @param array $options
938
	 * @return array
939
	 */
940
	private function lookupOptionsKeys( $options ) {
941
		$ret = [];
942
		foreach ( $options as $key => $value ) {
943
			$key = $this->msg( $key )->plain();
944
			$ret[$key] = is_array( $value )
945
				? $this->lookupOptionsKeys( $value )
946
				: strval( $value );
947
		}
948
		return $ret;
949
	}
950
951
	/**
952
	 * Recursively forces values in an array to strings, because issues arise
953
	 * with integer 0 as a value.
954
	 *
955
	 * @param array $array
956
	 * @return array
957
	 */
958
	static function forceToStringRecursive( $array ) {
959
		if ( is_array( $array ) ) {
960
			return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
961
		} else {
962
			return strval( $array );
963
		}
964
	}
965
966
	/**
967
	 * Fetch the array of options from the field's parameters. In order, this
968
	 * checks 'options-messages', 'options', then 'options-message'.
969
	 *
970
	 * @return array|null Options array
971
	 */
972
	public function getOptions() {
973
		if ( $this->mOptions === false ) {
974
			if ( array_key_exists( 'options-messages', $this->mParams ) ) {
975
				$this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->lookupOptionsKeys...ms['options-messages']) of type array is incompatible with the declared type boolean of property $mOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
976
			} elseif ( array_key_exists( 'options', $this->mParams ) ) {
977
				$this->mOptionsLabelsNotFromMessage = true;
978
				$this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
0 ignored issues
show
Documentation Bug introduced by
It seems like self::forceToStringRecur...is->mParams['options']) of type array or string is incompatible with the declared type boolean of property $mOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
979
			} elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
980
				/** @todo This is copied from Xml::listDropDown(), deprecate/avoid duplication? */
981
				$message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
982
983
				$optgroup = false;
984
				$this->mOptions = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $mOptions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
985
				foreach ( explode( "\n", $message ) as $option ) {
986
					$value = trim( $option );
987
					if ( $value == '' ) {
988
						continue;
989
					} elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
990
						# A new group is starting...
991
						$value = trim( substr( $value, 1 ) );
992
						$optgroup = $value;
993
					} elseif ( substr( $value, 0, 2 ) == '**' ) {
994
						# groupmember
995
						$opt = trim( substr( $value, 2 ) );
996
						if ( $optgroup === false ) {
997
							$this->mOptions[$opt] = $opt;
998
						} else {
999
							$this->mOptions[$optgroup][$opt] = $opt;
1000
						}
1001
					} else {
1002
						# groupless reason list
1003
						$optgroup = false;
1004
						$this->mOptions[$option] = $option;
1005
					}
1006
				}
1007
			} else {
1008
				$this->mOptions = null;
1009
			}
1010
		}
1011
1012
		return $this->mOptions;
1013
	}
1014
1015
	/**
1016
	 * Get options and make them into arrays suitable for OOUI.
1017
	 * @return array Options for inclusion in a select or whatever.
1018
	 */
1019
	public function getOptionsOOUI() {
1020
		$oldoptions = $this->getOptions();
1021
1022
		if ( $oldoptions === null ) {
1023
			return null;
1024
		}
1025
1026
		$options = [];
1027
1028
		foreach ( $oldoptions as $text => $data ) {
0 ignored issues
show
Bug introduced by
The expression $oldoptions of type array|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1029
			$options[] = [
1030
				'data' => (string)$data,
1031
				'label' => (string)$text,
1032
			];
1033
		}
1034
1035
		return $options;
1036
	}
1037
1038
	/**
1039
	 * flatten an array of options to a single array, for instance,
1040
	 * a set of "<options>" inside "<optgroups>".
1041
	 *
1042
	 * @param array $options Associative Array with values either Strings or Arrays
1043
	 * @return array Flattened input
1044
	 */
1045
	public static function flattenOptions( $options ) {
1046
		$flatOpts = [];
1047
1048
		foreach ( $options as $value ) {
1049
			if ( is_array( $value ) ) {
1050
				$flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1051
			} else {
1052
				$flatOpts[] = $value;
1053
			}
1054
		}
1055
1056
		return $flatOpts;
1057
	}
1058
1059
	/**
1060
	 * Formats one or more errors as accepted by field validation-callback.
1061
	 *
1062
	 * @param string|Message|array $errors Array of strings or Message instances
1063
	 * @return string HTML
1064
	 * @since 1.18
1065
	 */
1066
	protected static function formatErrors( $errors ) {
1067
		if ( is_array( $errors ) && count( $errors ) === 1 ) {
1068
			$errors = array_shift( $errors );
1069
		}
1070
1071
		if ( is_array( $errors ) ) {
1072
			$lines = [];
1073
			foreach ( $errors as $error ) {
1074
				if ( $error instanceof Message ) {
1075
					$lines[] = Html::rawElement( 'li', [], $error->parse() );
1076
				} else {
1077
					$lines[] = Html::rawElement( 'li', [], $error );
1078
				}
1079
			}
1080
1081
			return Html::rawElement( 'ul', [ 'class' => 'error' ], implode( "\n", $lines ) );
1082
		} else {
1083
			if ( $errors instanceof Message ) {
1084
				$errors = $errors->parse();
1085
			}
1086
1087
			return Html::rawElement( 'span', [ 'class' => 'error' ], $errors );
1088
		}
1089
	}
1090
1091
	/**
1092
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1093
	 * name + parameters array) into a Message.
1094
	 * @param mixed $value
1095
	 * @return Message
1096
	 */
1097
	protected function getMessage( $value ) {
1098
		if ( $value instanceof Message ) {
1099
			return $value;
1100
		} elseif ( $value instanceof MessageSpecifier ) {
1101
			return Message::newFromKey( $value );
1102
		} elseif ( is_array( $value ) ) {
1103
			$msg = array_shift( $value );
1104
			return $this->msg( $msg, $value );
1105
		} else {
1106
			return $this->msg( $value, [] );
1107
		}
1108
	}
1109
}
1110