HTMLFormField::getInline()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
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|null
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 public 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
	public 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 $this->hasVisibleOutput();
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
	public 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
			!array_key_exists( $tmp, $alldata ) &&
121
			array_key_exists( substr( $tmp, 2 ), $alldata )
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 ) {
141
				$key = array_shift( $keys );
142
				if ( !is_array( $data ) || !array_key_exists( $key, $data ) ) {
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
	public 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
	public 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
	public 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
	public 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
	 * Can we assume that the request is an attempt to submit a HTMLForm, as opposed to an attempt to
354
	 * just view it? This can't normally be distinguished for e.g. checkboxes.
355
	 *
356
	 * Returns true if the request has a field for a CSRF token (wpEditToken) or a form identifier
357
	 * (wpFormIdentifier).
358
	 *
359
	 * @param WebRequest $request
360
	 * @return boolean
361
	 */
362
	protected function isSubmitAttempt( WebRequest $request ) {
363
		return $request->getCheck( 'wpEditToken' ) || $request->getCheck( 'wpFormIdentifier' );
364
	}
365
366
	/**
367
	 * Get the value that this input has been set to from a posted form,
368
	 * or the input's default value if it has not been set.
369
	 *
370
	 * @param WebRequest $request
371
	 * @return string The value
372
	 */
373
	public function loadDataFromRequest( $request ) {
374
		if ( $request->getCheck( $this->mName ) ) {
375
			return $request->getText( $this->mName );
376
		} else {
377
			return $this->getDefault();
378
		}
379
	}
380
381
	/**
382
	 * Initialise the object
383
	 *
384
	 * @param array $params Associative Array. See HTMLForm doc for syntax.
385
	 *
386
	 * @since 1.22 The 'label' attribute no longer accepts raw HTML, use 'label-raw' instead
387
	 * @throws MWException
388
	 */
389
	public function __construct( $params ) {
390
		$this->mParams = $params;
391
392
		if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
393
			$this->mParent = $params['parent'];
394
		}
395
396
		# Generate the label from a message, if possible
397
		if ( isset( $params['label-message'] ) ) {
398
			$this->mLabel = $this->getMessage( $params['label-message'] )->parse();
399
		} elseif ( isset( $params['label'] ) ) {
400
			if ( $params['label'] === '&#160;' ) {
401
				// Apparently some things set &nbsp directly and in an odd format
402
				$this->mLabel = '&#160;';
403
			} else {
404
				$this->mLabel = htmlspecialchars( $params['label'] );
405
			}
406
		} elseif ( isset( $params['label-raw'] ) ) {
407
			$this->mLabel = $params['label-raw'];
408
		}
409
410
		$this->mName = "wp{$params['fieldname']}";
411
		if ( isset( $params['name'] ) ) {
412
			$this->mName = $params['name'];
413
		}
414
415
		if ( isset( $params['dir'] ) ) {
416
			$this->mDir = $params['dir'];
417
		}
418
419
		$validName = Sanitizer::escapeId( $this->mName );
420
		$validName = str_replace( [ '.5B', '.5D' ], [ '[', ']' ], $validName );
421
		if ( $this->mName != $validName && !isset( $params['nodata'] ) ) {
422
			throw new MWException( "Invalid name '{$this->mName}' passed to " . __METHOD__ );
423
		}
424
425
		$this->mID = "mw-input-{$this->mName}";
426
427
		if ( isset( $params['default'] ) ) {
428
			$this->mDefault = $params['default'];
429
		}
430
431
		if ( isset( $params['id'] ) ) {
432
			$id = $params['id'];
433
			$validId = Sanitizer::escapeId( $id );
434
435
			if ( $id != $validId ) {
436
				throw new MWException( "Invalid id '$id' passed to " . __METHOD__ );
437
			}
438
439
			$this->mID = $id;
440
		}
441
442
		if ( isset( $params['cssclass'] ) ) {
443
			$this->mClass = $params['cssclass'];
444
		}
445
446
		if ( isset( $params['csshelpclass'] ) ) {
447
			$this->mHelpClass = $params['csshelpclass'];
448
		}
449
450
		if ( isset( $params['validation-callback'] ) ) {
451
			$this->mValidationCallback = $params['validation-callback'];
452
		}
453
454
		if ( isset( $params['filter-callback'] ) ) {
455
			$this->mFilterCallback = $params['filter-callback'];
456
		}
457
458
		if ( isset( $params['hidelabel'] ) ) {
459
			$this->mShowEmptyLabels = false;
460
		}
461
462
		if ( isset( $params['hide-if'] ) ) {
463
			$this->mHideIf = $params['hide-if'];
464
		}
465
	}
466
467
	/**
468
	 * Get the complete table row for the input, including help text,
469
	 * labels, and whatever.
470
	 *
471
	 * @param string $value The value to set the input to.
472
	 *
473
	 * @return string Complete HTML table row.
474
	 */
475
	public function getTableRow( $value ) {
476
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
477
		$inputHtml = $this->getInputHTML( $value );
478
		$fieldType = get_class( $this );
479
		$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...
480
		$cellAttributes = [];
481
		$rowAttributes = [];
482
		$rowClasses = '';
483
484
		if ( !empty( $this->mParams['vertical-label'] ) ) {
485
			$cellAttributes['colspan'] = 2;
486
			$verticalLabel = true;
487
		} else {
488
			$verticalLabel = false;
489
		}
490
491
		$label = $this->getLabelHtml( $cellAttributes );
492
493
		$field = Html::rawElement(
494
			'td',
495
			[ 'class' => 'mw-input' ] + $cellAttributes,
496
			$inputHtml . "\n$errors"
497
		);
498
499
		if ( $this->mHideIf ) {
500
			$rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
501
			$rowClasses .= ' mw-htmlform-hide-if';
502
		}
503
504
		if ( $verticalLabel ) {
505
			$html = Html::rawElement( 'tr',
506
				$rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
507
			$html .= Html::rawElement( 'tr',
508
				$rowAttributes + [
509
					'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
510
				],
511
				$field );
512
		} else {
513
			$html =
514
				Html::rawElement( 'tr',
515
					$rowAttributes + [
516
						'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
517
					],
518
					$label . $field );
519
		}
520
521
		return $html . $helptext;
522
	}
523
524
	/**
525
	 * Get the complete div for the input, including help text,
526
	 * labels, and whatever.
527
	 * @since 1.20
528
	 *
529
	 * @param string $value The value to set the input to.
530
	 *
531
	 * @return string Complete HTML table row.
532
	 */
533
	public function getDiv( $value ) {
534
		list( $errors, $errorClass ) = $this->getErrorsAndErrorClass( $value );
535
		$inputHtml = $this->getInputHTML( $value );
536
		$fieldType = get_class( $this );
537
		$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...
538
		$cellAttributes = [];
539
		$label = $this->getLabelHtml( $cellAttributes );
540
541
		$outerDivClass = [
542
			'mw-input',
543
			'mw-htmlform-nolabel' => ( $label === '' )
544
		];
545
546
		$horizontalLabel = isset( $this->mParams['horizontal-label'] )
547
			? $this->mParams['horizontal-label'] : false;
548
549
		if ( $horizontalLabel ) {
550
			$field = '&#160;' . $inputHtml . "\n$errors";
551
		} else {
552
			$field = Html::rawElement(
553
				'div',
554
				[ 'class' => $outerDivClass ] + $cellAttributes,
555
				$inputHtml . "\n$errors"
556
			);
557
		}
558
		$divCssClasses = [ "mw-htmlform-field-$fieldType",
559
			$this->mClass, $this->mVFormClass, $errorClass ];
560
561
		$wrapperAttributes = [
562
			'class' => $divCssClasses,
563
		];
564 View Code Duplication
		if ( $this->mHideIf ) {
565
			$wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
566
			$wrapperAttributes['class'][] = ' mw-htmlform-hide-if';
567
		}
568
		$html = Html::rawElement( 'div', $wrapperAttributes, $label . $field );
569
		$html .= $helptext;
570
571
		return $html;
572
	}
573
574
	/**
575
	 * Get the OOUI version of the div. Falls back to getDiv by default.
576
	 * @since 1.26
577
	 *
578
	 * @param string $value The value to set the input to.
579
	 *
580
	 * @return OOUI\FieldLayout|OOUI\ActionFieldLayout
581
	 */
582
	public function getOOUI( $value ) {
583
		$inputField = $this->getInputOOUI( $value );
584
585
		if ( !$inputField ) {
586
			// This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
587
			// generate the whole field, label and errors and all, then wrap it in a Widget.
588
			// It might look weird, but it'll work OK.
589
			return $this->getFieldLayoutOOUI(
590
				new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
591
				[ 'infusable' => false, 'align' => 'top' ]
592
			);
593
		}
594
595
		$infusable = true;
596
		if ( is_string( $inputField ) ) {
597
			// We have an OOUI implementation, but it's not proper, and we got a load of HTML.
598
			// Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
599
			// JavaScript doesn't know how to rebuilt the contents.
600
			$inputField = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $inputField ) ] );
601
			$infusable = false;
602
		}
603
604
		$fieldType = get_class( $this );
605
		$help = $this->getHelpText();
606
		$errors = $this->getErrorsRaw( $value );
607
		foreach ( $errors as &$error ) {
608
			$error = new OOUI\HtmlSnippet( $error );
609
		}
610
611
		$notices = $this->getNotices();
612
		foreach ( $notices as &$notice ) {
613
			$notice = new OOUI\HtmlSnippet( $notice );
614
		}
615
616
		$config = [
617
			'classes' => [ "mw-htmlform-field-$fieldType", $this->mClass ],
618
			'align' => $this->getLabelAlignOOUI(),
619
			'help' => ( $help !== null && $help !== '' ) ? new OOUI\HtmlSnippet( $help ) : null,
0 ignored issues
show
Bug introduced by
It seems like $help defined by $this->getHelpText() on line 605 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...
620
			'errors' => $errors,
621
			'notices' => $notices,
622
			'infusable' => $infusable,
623
		];
624
625
		$preloadModules = false;
626
627
		if ( $infusable && $this->shouldInfuseOOUI() ) {
628
			$preloadModules = true;
629
			$config['classes'][] = 'mw-htmlform-field-autoinfuse';
630
		}
631
632
		// the element could specify, that the label doesn't need to be added
633
		$label = $this->getLabel();
634
		if ( $label ) {
635
			$config['label'] = new OOUI\HtmlSnippet( $label );
636
		}
637
638
		if ( $this->mHideIf ) {
639
			$preloadModules = true;
640
			$config['hideIf'] = $this->mHideIf;
641
		}
642
643
		$config['modules'] = $this->getOOUIModules();
644
645
		if ( $preloadModules ) {
646
			$this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
647
			$this->mParent->getOutput()->addModules( $this->getOOUIModules() );
648
		}
649
650
		return $this->getFieldLayoutOOUI( $inputField, $config );
651
	}
652
653
	/**
654
	 * Get label alignment when generating field for OOUI.
655
	 * @return string 'left', 'right', 'top' or 'inline'
656
	 */
657
	protected function getLabelAlignOOUI() {
658
		return 'top';
659
	}
660
661
	/**
662
	 * Get a FieldLayout (or subclass thereof) to wrap this field in when using OOUI output.
663
	 * @return OOUI\FieldLayout|OOUI\ActionFieldLayout
664
	 */
665
	protected function getFieldLayoutOOUI( $inputField, $config ) {
666
		if ( isset( $this->mClassWithButton ) ) {
667
			$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...
668
			return new HTMLFormActionFieldLayout( $inputField, $buttonWidget, $config );
669
		}
670
		return new HTMLFormFieldLayout( $inputField, $config );
671
	}
672
673
	/**
674
	 * Whether the field should be automatically infused. Note that all OOjs UI HTMLForm fields are
675
	 * infusable (you can call OO.ui.infuse() on them), but not all are infused by default, since
676
	 * there is no benefit in doing it e.g. for buttons and it's a small performance hit on page load.
677
	 *
678
	 * @return bool
679
	 */
680
	protected function shouldInfuseOOUI() {
681
		// Always infuse fields with help text, since the interface for it is nicer with JS
682
		return $this->getHelpText() !== null;
683
	}
684
685
	/**
686
	 * Get the list of extra ResourceLoader modules which must be loaded client-side before it's
687
	 * possible to infuse this field's OOjs UI widget.
688
	 *
689
	 * @return string[]
690
	 */
691
	protected function getOOUIModules() {
692
		return [];
693
	}
694
695
	/**
696
	 * Get the complete raw fields for the input, including help text,
697
	 * labels, and whatever.
698
	 * @since 1.20
699
	 *
700
	 * @param string $value The value to set the input to.
701
	 *
702
	 * @return string Complete HTML table row.
703
	 */
704
	public function getRaw( $value ) {
705
		list( $errors, ) = $this->getErrorsAndErrorClass( $value );
706
		$inputHtml = $this->getInputHTML( $value );
707
		$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...
708
		$cellAttributes = [];
709
		$label = $this->getLabelHtml( $cellAttributes );
710
711
		$html = "\n$errors";
712
		$html .= $label;
713
		$html .= $inputHtml;
714
		$html .= $helptext;
715
716
		return $html;
717
	}
718
719
	/**
720
	 * Get the complete field for the input, including help text,
721
	 * labels, and whatever. Fall back from 'vform' to 'div' when not overridden.
722
	 *
723
	 * @since 1.25
724
	 * @param string $value The value to set the input to.
725
	 * @return string Complete HTML field.
726
	 */
727
	public function getVForm( $value ) {
728
		// Ewwww
729
		$this->mVFormClass = ' mw-ui-vform-field';
730
		return $this->getDiv( $value );
731
	}
732
733
	/**
734
	 * Get the complete field as an inline element.
735
	 * @since 1.25
736
	 * @param string $value The value to set the input to.
737
	 * @return string Complete HTML inline element
738
	 */
739
	public function getInline( $value ) {
740
		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...
741
		$inputHtml = $this->getInputHTML( $value );
742
		$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...
743
		$cellAttributes = [];
744
		$label = $this->getLabelHtml( $cellAttributes );
745
746
		$html = "\n" . $errors .
747
			$label . '&#160;' .
748
			$inputHtml .
749
			$helptext;
750
751
		return $html;
752
	}
753
754
	/**
755
	 * Generate help text HTML in table format
756
	 * @since 1.20
757
	 *
758
	 * @param string|null $helptext
759
	 * @return string
760
	 */
761
	public function getHelpTextHtmlTable( $helptext ) {
762
		if ( is_null( $helptext ) ) {
763
			return '';
764
		}
765
766
		$rowAttributes = [];
767 View Code Duplication
		if ( $this->mHideIf ) {
768
			$rowAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
769
			$rowAttributes['class'] = 'mw-htmlform-hide-if';
770
		}
771
772
		$tdClasses = [ 'htmlform-tip' ];
773
		if ( $this->mHelpClass !== false ) {
774
			$tdClasses[] = $this->mHelpClass;
775
		}
776
		$row = Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext );
777
		$row = Html::rawElement( 'tr', $rowAttributes, $row );
778
779
		return $row;
780
	}
781
782
	/**
783
	 * Generate help text HTML in div format
784
	 * @since 1.20
785
	 *
786
	 * @param string|null $helptext
787
	 *
788
	 * @return string
789
	 */
790
	public function getHelpTextHtmlDiv( $helptext ) {
791
		if ( is_null( $helptext ) ) {
792
			return '';
793
		}
794
795
		$wrapperAttributes = [
796
			'class' => 'htmlform-tip',
797
		];
798
		if ( $this->mHelpClass !== false ) {
799
			$wrapperAttributes['class'] .= " {$this->mHelpClass}";
800
		}
801 View Code Duplication
		if ( $this->mHideIf ) {
802
			$wrapperAttributes['data-hide-if'] = FormatJson::encode( $this->mHideIf );
803
			$wrapperAttributes['class'] .= ' mw-htmlform-hide-if';
804
		}
805
		$div = Html::rawElement( 'div', $wrapperAttributes, $helptext );
806
807
		return $div;
808
	}
809
810
	/**
811
	 * Generate help text HTML formatted for raw output
812
	 * @since 1.20
813
	 *
814
	 * @param string|null $helptext
815
	 * @return string
816
	 */
817
	public function getHelpTextHtmlRaw( $helptext ) {
818
		return $this->getHelpTextHtmlDiv( $helptext );
819
	}
820
821
	/**
822
	 * Determine the help text to display
823
	 * @since 1.20
824
	 * @return string|null HTML
825
	 */
826
	public function getHelpText() {
827
		$helptext = null;
828
829
		if ( isset( $this->mParams['help-message'] ) ) {
830
			$this->mParams['help-messages'] = [ $this->mParams['help-message'] ];
831
		}
832
833
		if ( isset( $this->mParams['help-messages'] ) ) {
834
			foreach ( $this->mParams['help-messages'] as $msg ) {
835
				$msg = $this->getMessage( $msg );
836
837
				if ( $msg->exists() ) {
838
					if ( is_null( $helptext ) ) {
839
						$helptext = '';
840
					} else {
841
						$helptext .= $this->msg( 'word-separator' )->escaped(); // some space
842
					}
843
					$helptext .= $msg->parse(); // Append message
844
				}
845
			}
846
		} elseif ( isset( $this->mParams['help'] ) ) {
847
			$helptext = $this->mParams['help'];
848
		}
849
850
		return $helptext;
851
	}
852
853
	/**
854
	 * Determine form errors to display and their classes
855
	 * @since 1.20
856
	 *
857
	 * @param string $value The value of the input
858
	 * @return array array( $errors, $errorClass )
859
	 */
860
	public function getErrorsAndErrorClass( $value ) {
861
		$errors = $this->validate( $value, $this->mParent->mFieldData );
862
863
		if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
864
			$errors = '';
865
			$errorClass = '';
866
		} else {
867
			$errors = self::formatErrors( $errors );
868
			$errorClass = 'mw-htmlform-invalid-input';
869
		}
870
871
		return [ $errors, $errorClass ];
872
	}
873
874
	/**
875
	 * Determine form errors to display, returning them in an array.
876
	 *
877
	 * @since 1.26
878
	 * @param string $value The value of the input
879
	 * @return string[] Array of error HTML strings
880
	 */
881
	public function getErrorsRaw( $value ) {
882
		$errors = $this->validate( $value, $this->mParent->mFieldData );
883
884
		if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
885
			$errors = [];
886
		}
887
888
		if ( !is_array( $errors ) ) {
889
			$errors = [ $errors ];
890
		}
891
		foreach ( $errors as &$error ) {
892
			if ( $error instanceof Message ) {
893
				$error = $error->parse();
894
			}
895
		}
896
897
		return $errors;
898
	}
899
900
	/**
901
	 * Determine notices to display for the field.
902
	 *
903
	 * @since 1.28
904
	 * @return string[]
905
	 */
906
	public function getNotices() {
907
		$notices = [];
908
909
		if ( isset( $this->mParams['notice-message'] ) ) {
910
			$notices[] = $this->getMessage( $this->mParams['notice-message'] )->parse();
911
		}
912
913
		if ( isset( $this->mParams['notice-messages'] ) ) {
914
			foreach ( $this->mParams['notice-messages'] as $msg ) {
915
				$notices[] = $this->getMessage( $msg )->parse();
916
			}
917
		} elseif ( isset( $this->mParams['notice'] ) ) {
918
			$notices[] = $this->mParams['notice'];
919
		}
920
921
		return $notices;
922
	}
923
924
	/**
925
	 * @return string HTML
926
	 */
927
	public function getLabel() {
928
		return is_null( $this->mLabel ) ? '' : $this->mLabel;
929
	}
930
931
	public function getLabelHtml( $cellAttributes = [] ) {
932
		# Don't output a for= attribute for labels with no associated input.
933
		# Kind of hacky here, possibly we don't want these to be <label>s at all.
934
		$for = [];
935
936
		if ( $this->needsLabel() ) {
937
			$for['for'] = $this->mID;
938
		}
939
940
		$labelValue = trim( $this->getLabel() );
941
		$hasLabel = false;
942
		if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
943
			$hasLabel = true;
944
		}
945
946
		$displayFormat = $this->mParent->getDisplayFormat();
947
		$html = '';
948
		$horizontalLabel = isset( $this->mParams['horizontal-label'] )
949
			? $this->mParams['horizontal-label'] : false;
950
951
		if ( $displayFormat === 'table' ) {
952
			$html =
953
				Html::rawElement( 'td',
954
					[ 'class' => 'mw-label' ] + $cellAttributes,
955
					Html::rawElement( 'label', $for, $labelValue ) );
956
		} elseif ( $hasLabel || $this->mShowEmptyLabels ) {
957
			if ( $displayFormat === 'div' && !$horizontalLabel ) {
958
				$html =
959
					Html::rawElement( 'div',
960
						[ 'class' => 'mw-label' ] + $cellAttributes,
961
						Html::rawElement( 'label', $for, $labelValue ) );
962
			} else {
963
				$html = Html::rawElement( 'label', $for, $labelValue );
964
			}
965
		}
966
967
		return $html;
968
	}
969
970
	public function getDefault() {
971
		if ( isset( $this->mDefault ) ) {
972
			return $this->mDefault;
973
		} else {
974
			return null;
975
		}
976
	}
977
978
	/**
979
	 * Returns the attributes required for the tooltip and accesskey.
980
	 *
981
	 * @return array Attributes
982
	 */
983
	public function getTooltipAndAccessKey() {
984
		if ( empty( $this->mParams['tooltip'] ) ) {
985
			return [];
986
		}
987
988
		return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
989
	}
990
991
	/**
992
	 * Returns the given attributes from the parameters
993
	 *
994
	 * @param array $list List of attributes to get
995
	 * @return array Attributes
996
	 */
997
	public function getAttributes( array $list ) {
998
		static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
999
1000
		$ret = [];
1001
		foreach ( $list as $key ) {
1002
			if ( in_array( $key, $boolAttribs ) ) {
1003
				if ( !empty( $this->mParams[$key] ) ) {
1004
					$ret[$key] = '';
1005
				}
1006
			} elseif ( isset( $this->mParams[$key] ) ) {
1007
				$ret[$key] = $this->mParams[$key];
1008
			}
1009
		}
1010
1011
		return $ret;
1012
	}
1013
1014
	/**
1015
	 * Given an array of msg-key => value mappings, returns an array with keys
1016
	 * being the message texts. It also forces values to strings.
1017
	 *
1018
	 * @param array $options
1019
	 * @return array
1020
	 */
1021
	private function lookupOptionsKeys( $options ) {
1022
		$ret = [];
1023
		foreach ( $options as $key => $value ) {
1024
			$key = $this->msg( $key )->plain();
1025
			$ret[$key] = is_array( $value )
1026
				? $this->lookupOptionsKeys( $value )
1027
				: strval( $value );
1028
		}
1029
		return $ret;
1030
	}
1031
1032
	/**
1033
	 * Recursively forces values in an array to strings, because issues arise
1034
	 * with integer 0 as a value.
1035
	 *
1036
	 * @param array $array
1037
	 * @return array
1038
	 */
1039
	public static function forceToStringRecursive( $array ) {
1040
		if ( is_array( $array ) ) {
1041
			return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1042
		} else {
1043
			return strval( $array );
1044
		}
1045
	}
1046
1047
	/**
1048
	 * Fetch the array of options from the field's parameters. In order, this
1049
	 * checks 'options-messages', 'options', then 'options-message'.
1050
	 *
1051
	 * @return array|null Options array
1052
	 */
1053
	public function getOptions() {
1054
		if ( $this->mOptions === false ) {
1055
			if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1056
				$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...
1057
			} elseif ( array_key_exists( 'options', $this->mParams ) ) {
1058
				$this->mOptionsLabelsNotFromMessage = true;
1059
				$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...
1060
			} elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1061
				/** @todo This is copied from Xml::listDropDown(), deprecate/avoid duplication? */
1062
				$message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1063
1064
				$optgroup = false;
1065
				$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...
1066
				foreach ( explode( "\n", $message ) as $option ) {
1067
					$value = trim( $option );
1068
					if ( $value == '' ) {
1069
						continue;
1070
					} elseif ( substr( $value, 0, 1 ) == '*' && substr( $value, 1, 1 ) != '*' ) {
1071
						# A new group is starting...
1072
						$value = trim( substr( $value, 1 ) );
1073
						$optgroup = $value;
1074
					} elseif ( substr( $value, 0, 2 ) == '**' ) {
1075
						# groupmember
1076
						$opt = trim( substr( $value, 2 ) );
1077
						if ( $optgroup === false ) {
1078
							$this->mOptions[$opt] = $opt;
1079
						} else {
1080
							$this->mOptions[$optgroup][$opt] = $opt;
1081
						}
1082
					} else {
1083
						# groupless reason list
1084
						$optgroup = false;
1085
						$this->mOptions[$option] = $option;
1086
					}
1087
				}
1088
			} else {
1089
				$this->mOptions = null;
1090
			}
1091
		}
1092
1093
		return $this->mOptions;
1094
	}
1095
1096
	/**
1097
	 * Get options and make them into arrays suitable for OOUI.
1098
	 * @return array Options for inclusion in a select or whatever.
1099
	 */
1100
	public function getOptionsOOUI() {
1101
		$oldoptions = $this->getOptions();
1102
1103
		if ( $oldoptions === null ) {
1104
			return null;
1105
		}
1106
1107
		$options = [];
1108
1109
		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...
1110
			$options[] = [
1111
				'data' => (string)$data,
1112
				'label' => (string)$text,
1113
			];
1114
		}
1115
1116
		return $options;
1117
	}
1118
1119
	/**
1120
	 * flatten an array of options to a single array, for instance,
1121
	 * a set of "<options>" inside "<optgroups>".
1122
	 *
1123
	 * @param array $options Associative Array with values either Strings or Arrays
1124
	 * @return array Flattened input
1125
	 */
1126
	public static function flattenOptions( $options ) {
1127
		$flatOpts = [];
1128
1129
		foreach ( $options as $value ) {
1130
			if ( is_array( $value ) ) {
1131
				$flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1132
			} else {
1133
				$flatOpts[] = $value;
1134
			}
1135
		}
1136
1137
		return $flatOpts;
1138
	}
1139
1140
	/**
1141
	 * Formats one or more errors as accepted by field validation-callback.
1142
	 *
1143
	 * @param string|Message|array $errors Array of strings or Message instances
1144
	 * @return string HTML
1145
	 * @since 1.18
1146
	 */
1147
	protected static function formatErrors( $errors ) {
1148
		if ( is_array( $errors ) && count( $errors ) === 1 ) {
1149
			$errors = array_shift( $errors );
1150
		}
1151
1152
		if ( is_array( $errors ) ) {
1153
			$lines = [];
1154
			foreach ( $errors as $error ) {
1155
				if ( $error instanceof Message ) {
1156
					$lines[] = Html::rawElement( 'li', [], $error->parse() );
1157
				} else {
1158
					$lines[] = Html::rawElement( 'li', [], $error );
1159
				}
1160
			}
1161
1162
			return Html::rawElement( 'ul', [ 'class' => 'error' ], implode( "\n", $lines ) );
1163
		} else {
1164
			if ( $errors instanceof Message ) {
1165
				$errors = $errors->parse();
1166
			}
1167
1168
			return Html::rawElement( 'span', [ 'class' => 'error' ], $errors );
1169
		}
1170
	}
1171
1172
	/**
1173
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1174
	 * name + parameters array) into a Message.
1175
	 * @param mixed $value
1176
	 * @return Message
1177
	 */
1178
	protected function getMessage( $value ) {
1179
		$message = Message::newFromSpecifier( $value );
1180
1181
		if ( $this->mParent ) {
1182
			$message->setContext( $this->mParent );
1183
		}
1184
1185
		return $message;
1186
	}
1187
1188
	/**
1189
	 * Skip this field when collecting data.
1190
	 * @param WebRequest $request
1191
	 * @return bool
1192
	 * @since 1.27
1193
	 */
1194
	public function skipLoadData( $request ) {
1195
		return !empty( $this->mParams['nodata'] );
1196
	}
1197
}
1198