Completed
Branch master (5cbada)
by
unknown
28:59
created

HTMLForm::getClassFromDescriptor()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 6
nop 2
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * HTML form generation and submission handling.
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along
17
 * with this program; if not, write to the Free Software Foundation, Inc.,
18
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 * http://www.gnu.org/copyleft/gpl.html
20
 *
21
 * @file
22
 */
23
24
/**
25
 * Object handling generic submission, CSRF protection, layout and
26
 * other logic for UI forms. in a reusable manner.
27
 *
28
 * In order to generate the form, the HTMLForm object takes an array
29
 * structure detailing the form fields available. Each element of the
30
 * array is a basic property-list, including the type of field, the
31
 * label it is to be given in the form, callbacks for validation and
32
 * 'filtering', and other pertinent information.
33
 *
34
 * Field types are implemented as subclasses of the generic HTMLFormField
35
 * object, and typically implement at least getInputHTML, which generates
36
 * the HTML for the input field to be placed in the table.
37
 *
38
 * You can find extensive documentation on the www.mediawiki.org wiki:
39
 *  - https://www.mediawiki.org/wiki/HTMLForm
40
 *  - https://www.mediawiki.org/wiki/HTMLForm/tutorial
41
 *
42
 * The constructor input is an associative array of $fieldname => $info,
43
 * where $info is an Associative Array with any of the following:
44
 *
45
 *    'class'               -- the subclass of HTMLFormField that will be used
46
 *                             to create the object.  *NOT* the CSS class!
47
 *    'type'                -- roughly translates into the <select> type attribute.
48
 *                             if 'class' is not specified, this is used as a map
49
 *                             through HTMLForm::$typeMappings to get the class name.
50
 *    'default'             -- default value when the form is displayed
51
 *    'id'                  -- HTML id attribute
52
 *    'cssclass'            -- CSS class
53
 *    'csshelpclass'        -- CSS class used to style help text
54
 *    'dir'                 -- Direction of the element.
55
 *    'options'             -- associative array mapping labels to values.
56
 *                             Some field types support multi-level arrays.
57
 *    'options-messages'    -- associative array mapping message keys to values.
58
 *                             Some field types support multi-level arrays.
59
 *    'options-message'     -- message key or object to be parsed to extract the list of
60
 *                             options (like 'ipbreason-dropdown').
61
 *    'label-message'       -- message key or object for a message to use as the label.
62
 *                             can be an array of msg key and then parameters to
63
 *                             the message.
64
 *    'label'               -- alternatively, a raw text message. Overridden by
65
 *                             label-message
66
 *    'help'                -- message text for a message to use as a help text.
67
 *    'help-message'        -- message key or object for a message to use as a help text.
68
 *                             can be an array of msg key and then parameters to
69
 *                             the message.
70
 *                             Overwrites 'help-messages' and 'help'.
71
 *    'help-messages'       -- array of message keys/objects. As above, each item can
72
 *                             be an array of msg key and then parameters.
73
 *                             Overwrites 'help'.
74
 *    'required'            -- passed through to the object, indicating that it
75
 *                             is a required field.
76
 *    'size'                -- the length of text fields
77
 *    'filter-callback'     -- a function name to give you the chance to
78
 *                             massage the inputted value before it's processed.
79
 *                             @see HTMLFormField::filter()
80
 *    'validation-callback' -- a function name to give you the chance
81
 *                             to impose extra validation on the field input.
82
 *                             @see HTMLFormField::validate()
83
 *    'name'                -- By default, the 'name' attribute of the input field
84
 *                             is "wp{$fieldname}".  If you want a different name
85
 *                             (eg one without the "wp" prefix), specify it here and
86
 *                             it will be used without modification.
87
 *    'hide-if'             -- expression given as an array stating when the field
88
 *                             should be hidden. The first array value has to be the
89
 *                             expression's logic operator. Supported expressions:
90
 *                               'NOT'
91
 *                                 [ 'NOT', array $expression ]
92
 *                                 To hide a field if a given expression is not true.
93
 *                               '==='
94
 *                                 [ '===', string $fieldName, string $value ]
95
 *                                 To hide a field if another field identified by
96
 *                                 $field has the value $value.
97
 *                               '!=='
98
 *                                 [ '!==', string $fieldName, string $value ]
99
 *                                 Same as [ 'NOT', [ '===', $fieldName, $value ]
100
 *                               'OR', 'AND', 'NOR', 'NAND'
101
 *                                 [ 'XXX', array $expression1, ..., array $expressionN ]
102
 *                                 To hide a field if one or more (OR), all (AND),
103
 *                                 neither (NOR) or not all (NAND) given expressions
104
 *                                 are evaluated as true.
105
 *                             The expressions will be given to a JavaScript frontend
106
 *                             module which will continually update the field's
107
 *                             visibility.
108
 *
109
 * Since 1.20, you can chain mutators to ease the form generation:
110
 * @par Example:
111
 * @code
112
 * $form = new HTMLForm( $someFields );
113
 * $form->setMethod( 'get' )
114
 *      ->setWrapperLegendMsg( 'message-key' )
115
 *      ->prepareForm()
116
 *      ->displayForm( '' );
117
 * @endcode
118
 * Note that you will have prepareForm and displayForm at the end. Other
119
 * methods call done after that would simply not be part of the form :(
120
 *
121
 * @todo Document 'section' / 'subsection' stuff
122
 */
123
class HTMLForm extends ContextSource {
124
	// A mapping of 'type' inputs onto standard HTMLFormField subclasses
125
	public static $typeMappings = [
126
		'api' => 'HTMLApiField',
127
		'text' => 'HTMLTextField',
128
		'textwithbutton' => 'HTMLTextFieldWithButton',
129
		'textarea' => 'HTMLTextAreaField',
130
		'select' => 'HTMLSelectField',
131
		'combobox' => 'HTMLComboboxField',
132
		'radio' => 'HTMLRadioField',
133
		'multiselect' => 'HTMLMultiSelectField',
134
		'limitselect' => 'HTMLSelectLimitField',
135
		'check' => 'HTMLCheckField',
136
		'toggle' => 'HTMLCheckField',
137
		'int' => 'HTMLIntField',
138
		'float' => 'HTMLFloatField',
139
		'info' => 'HTMLInfoField',
140
		'selectorother' => 'HTMLSelectOrOtherField',
141
		'selectandother' => 'HTMLSelectAndOtherField',
142
		'namespaceselect' => 'HTMLSelectNamespace',
143
		'namespaceselectwithbutton' => 'HTMLSelectNamespaceWithButton',
144
		'tagfilter' => 'HTMLTagFilter',
145
		'submit' => 'HTMLSubmitField',
146
		'hidden' => 'HTMLHiddenField',
147
		'edittools' => 'HTMLEditTools',
148
		'checkmatrix' => 'HTMLCheckMatrix',
149
		'cloner' => 'HTMLFormFieldCloner',
150
		'autocompleteselect' => 'HTMLAutoCompleteSelectField',
151
		// HTMLTextField will output the correct type="" attribute automagically.
152
		// There are about four zillion other HTML5 input types, like range, but
153
		// we don't use those at the moment, so no point in adding all of them.
154
		'email' => 'HTMLTextField',
155
		'password' => 'HTMLTextField',
156
		'url' => 'HTMLTextField',
157
		'title' => 'HTMLTitleTextField',
158
		'user' => 'HTMLUserTextField',
159
	];
160
161
	public $mFieldData;
162
163
	protected $mMessagePrefix;
164
165
	/** @var HTMLFormField[] */
166
	protected $mFlatFields;
167
168
	protected $mFieldTree;
169
	protected $mShowReset = false;
170
	protected $mShowSubmit = true;
171
	protected $mSubmitFlags = [ 'constructive', 'primary' ];
172
173
	protected $mSubmitCallback;
174
	protected $mValidationErrorMessage;
175
176
	protected $mPre = '';
177
	protected $mHeader = '';
178
	protected $mFooter = '';
179
	protected $mSectionHeaders = [];
180
	protected $mSectionFooters = [];
181
	protected $mPost = '';
182
	protected $mId;
183
	protected $mName;
184
	protected $mTableId = '';
185
186
	protected $mSubmitID;
187
	protected $mSubmitName;
188
	protected $mSubmitText;
189
	protected $mSubmitTooltip;
190
191
	protected $mTitle;
192
	protected $mMethod = 'post';
193
	protected $mWasSubmitted = false;
194
195
	/**
196
	 * Form action URL. false means we will use the URL to set Title
197
	 * @since 1.19
198
	 * @var bool|string
199
	 */
200
	protected $mAction = false;
201
202
	/**
203
	 * Form attribute autocomplete. false does not set the attribute
204
	 * @since 1.27
205
	 * @var bool|string
206
	 */
207
	protected $mAutocomplete = false;
208
209
	protected $mUseMultipart = false;
210
	protected $mHiddenFields = [];
211
	protected $mButtons = [];
212
213
	protected $mWrapperLegend = false;
214
215
	/**
216
	 * Salt for the edit token.
217
	 * @var string|array
218
	 */
219
	protected $mTokenSalt = '';
220
221
	/**
222
	 * If true, sections that contain both fields and subsections will
223
	 * render their subsections before their fields.
224
	 *
225
	 * Subclasses may set this to false to render subsections after fields
226
	 * instead.
227
	 */
228
	protected $mSubSectionBeforeFields = true;
229
230
	/**
231
	 * Format in which to display form. For viable options,
232
	 * @see $availableDisplayFormats
233
	 * @var string
234
	 */
235
	protected $displayFormat = 'table';
236
237
	/**
238
	 * Available formats in which to display the form
239
	 * @var array
240
	 */
241
	protected $availableDisplayFormats = [
242
		'table',
243
		'div',
244
		'raw',
245
		'inline',
246
	];
247
248
	/**
249
	 * Available formats in which to display the form
250
	 * @var array
251
	 */
252
	protected $availableSubclassDisplayFormats = [
253
		'vform',
254
		'ooui',
255
	];
256
257
	/**
258
	 * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
259
	 *
260
	 * @param string $displayFormat
261
	 * @param mixed $arguments... Additional arguments to pass to the constructor.
0 ignored issues
show
Bug introduced by
There is no parameter named $arguments.... Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
262
	 * @return HTMLForm
263
	 */
264
	public static function factory( $displayFormat/*, $arguments...*/ ) {
265
		$arguments = func_get_args();
266
		array_shift( $arguments );
267
268
		switch ( $displayFormat ) {
269
			case 'vform':
270
				$reflector = new ReflectionClass( 'VFormHTMLForm' );
271
				return $reflector->newInstanceArgs( $arguments );
272
			case 'ooui':
273
				$reflector = new ReflectionClass( 'OOUIHTMLForm' );
274
				return $reflector->newInstanceArgs( $arguments );
275
			default:
276
				$reflector = new ReflectionClass( 'HTMLForm' );
277
				$form = $reflector->newInstanceArgs( $arguments );
278
				$form->setDisplayFormat( $displayFormat );
279
				return $form;
280
		}
281
	}
282
283
	/**
284
	 * Build a new HTMLForm from an array of field attributes
285
	 *
286
	 * @param array $descriptor Array of Field constructs, as described above
287
	 * @param IContextSource $context Available since 1.18, will become compulsory in 1.18.
288
	 *     Obviates the need to call $form->setTitle()
289
	 * @param string $messagePrefix A prefix to go in front of default messages
290
	 */
291
	public function __construct( $descriptor, /*IContextSource*/ $context = null,
292
		$messagePrefix = ''
293
	) {
294
		if ( $context instanceof IContextSource ) {
295
			$this->setContext( $context );
296
			$this->mTitle = false; // We don't need them to set a title
297
			$this->mMessagePrefix = $messagePrefix;
298
		} elseif ( $context === null && $messagePrefix !== '' ) {
299
			$this->mMessagePrefix = $messagePrefix;
300
		} elseif ( is_string( $context ) && $messagePrefix === '' ) {
301
			// B/C since 1.18
302
			// it's actually $messagePrefix
303
			$this->mMessagePrefix = $context;
304
		}
305
306
		// Evil hack for mobile :(
307
		if (
308
			!$this->getConfig()->get( 'HTMLFormAllowTableFormat' )
309
			&& $this->displayFormat === 'table'
310
		) {
311
			$this->displayFormat = 'div';
312
		}
313
314
		// Expand out into a tree.
315
		$loadedDescriptor = [];
316
		$this->mFlatFields = [];
317
318
		foreach ( $descriptor as $fieldname => $info ) {
319
			$section = isset( $info['section'] )
320
				? $info['section']
321
				: '';
322
323
			if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
324
				$this->mUseMultipart = true;
325
			}
326
327
			$field = static::loadInputFromParameters( $fieldname, $info, $this );
328
329
			$setSection =& $loadedDescriptor;
330
			if ( $section ) {
331
				$sectionParts = explode( '/', $section );
332
333
				while ( count( $sectionParts ) ) {
334
					$newName = array_shift( $sectionParts );
335
336
					if ( !isset( $setSection[$newName] ) ) {
337
						$setSection[$newName] = [];
338
					}
339
340
					$setSection =& $setSection[$newName];
341
				}
342
			}
343
344
			$setSection[$fieldname] = $field;
345
			$this->mFlatFields[$fieldname] = $field;
346
		}
347
348
		$this->mFieldTree = $loadedDescriptor;
349
	}
350
351
	/**
352
	 * Set format in which to display the form
353
	 *
354
	 * @param string $format The name of the format to use, must be one of
355
	 *   $this->availableDisplayFormats
356
	 *
357
	 * @throws MWException
358
	 * @since 1.20
359
	 * @return HTMLForm $this for chaining calls (since 1.20)
360
	 */
361
	public function setDisplayFormat( $format ) {
362
		if (
363
			in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
364
			in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
365
		) {
366
			throw new MWException( 'Cannot change display format after creation, ' .
367
				'use HTMLForm::factory() instead' );
368
		}
369
370
		if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
371
			throw new MWException( 'Display format must be one of ' .
372
				print_r( $this->availableDisplayFormats, true ) );
373
		}
374
375
		// Evil hack for mobile :(
376
		if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
377
			$format = 'div';
378
		}
379
380
		$this->displayFormat = $format;
381
382
		return $this;
383
	}
384
385
	/**
386
	 * Getter for displayFormat
387
	 * @since 1.20
388
	 * @return string
389
	 */
390
	public function getDisplayFormat() {
391
		return $this->displayFormat;
392
	}
393
394
	/**
395
	 * Test if displayFormat is 'vform'
396
	 * @since 1.22
397
	 * @deprecated since 1.25
398
	 * @return bool
399
	 */
400
	public function isVForm() {
401
		wfDeprecated( __METHOD__, '1.25' );
402
		return false;
403
	}
404
405
	/**
406
	 * Get the HTMLFormField subclass for this descriptor.
407
	 *
408
	 * The descriptor can be passed either 'class' which is the name of
409
	 * a HTMLFormField subclass, or a shorter 'type' which is an alias.
410
	 * This makes sure the 'class' is always set, and also is returned by
411
	 * this function for ease.
412
	 *
413
	 * @since 1.23
414
	 *
415
	 * @param string $fieldname Name of the field
416
	 * @param array $descriptor Input Descriptor, as described above
417
	 *
418
	 * @throws MWException
419
	 * @return string Name of a HTMLFormField subclass
420
	 */
421
	public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
422
		if ( isset( $descriptor['class'] ) ) {
423
			$class = $descriptor['class'];
424
		} elseif ( isset( $descriptor['type'] ) ) {
425
			$class = static::$typeMappings[$descriptor['type']];
426
			$descriptor['class'] = $class;
427
		} else {
428
			$class = null;
429
		}
430
431
		if ( !$class ) {
432
			throw new MWException( "Descriptor with no class for $fieldname: "
433
				. print_r( $descriptor, true ) );
434
		}
435
436
		return $class;
437
	}
438
439
	/**
440
	 * Initialise a new Object for the field
441
	 *
442
	 * @param string $fieldname Name of the field
443
	 * @param array $descriptor Input Descriptor, as described above
444
	 * @param HTMLForm|null $parent Parent instance of HTMLForm
445
	 *
446
	 * @throws MWException
447
	 * @return HTMLFormField Instance of a subclass of HTMLFormField
448
	 */
449
	public static function loadInputFromParameters( $fieldname, $descriptor,
450
		HTMLForm $parent = null
451
	) {
452
		$class = static::getClassFromDescriptor( $fieldname, $descriptor );
453
454
		$descriptor['fieldname'] = $fieldname;
455
		if ( $parent ) {
456
			$descriptor['parent'] = $parent;
457
		}
458
459
		# @todo This will throw a fatal error whenever someone try to use
460
		# 'class' to feed a CSS class instead of 'cssclass'. Would be
461
		# great to avoid the fatal error and show a nice error.
462
		return new $class( $descriptor );
463
	}
464
465
	/**
466
	 * Prepare form for submission.
467
	 *
468
	 * @attention When doing method chaining, that should be the very last
469
	 * method call before displayForm().
470
	 *
471
	 * @throws MWException
472
	 * @return HTMLForm $this for chaining calls (since 1.20)
473
	 */
474
	public function prepareForm() {
475
		# Check if we have the info we need
476
		if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
477
			throw new MWException( 'You must call setTitle() on an HTMLForm' );
478
		}
479
480
		# Load data from the request.
481
		$this->loadData();
482
483
		return $this;
484
	}
485
486
	/**
487
	 * Try submitting, with edit token check first
488
	 * @return Status|bool
489
	 */
490
	public function tryAuthorizedSubmit() {
491
		$result = false;
492
493
		$submit = false;
494
		if ( $this->getMethod() !== 'post' ) {
495
			$submit = true; // no session check needed
496
		} elseif ( $this->getRequest()->wasPosted() ) {
497
			$editToken = $this->getRequest()->getVal( 'wpEditToken' );
498
			if ( $this->getUser()->isLoggedIn() || $editToken !== null ) {
499
				// Session tokens for logged-out users have no security value.
500
				// However, if the user gave one, check it in order to give a nice
501
				// "session expired" error instead of "permission denied" or such.
502
				$submit = $this->getUser()->matchEditToken( $editToken, $this->mTokenSalt );
0 ignored issues
show
Bug introduced by
It seems like $this->mTokenSalt can also be of type array; however, User::matchEditToken() 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...
503
			} else {
504
				$submit = true;
505
			}
506
		}
507
508
		if ( $submit ) {
509
			$this->mWasSubmitted = true;
510
			$result = $this->trySubmit();
511
		}
512
513
		return $result;
514
	}
515
516
	/**
517
	 * The here's-one-I-made-earlier option: do the submission if
518
	 * posted, or display the form with or without funky validation
519
	 * errors
520
	 * @return bool|Status Whether submission was successful.
521
	 */
522
	public function show() {
523
		$this->prepareForm();
524
525
		$result = $this->tryAuthorizedSubmit();
526
		if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
527
			return $result;
528
		}
529
530
		$this->displayForm( $result );
531
532
		return false;
533
	}
534
535
	/**
536
	 * Same as self::show with the difference, that the form will be
537
	 * added to the output, no matter, if the validation was good or not.
538
	 * @return bool|Status Whether submission was successful.
539
	 */
540
	public function showAlways() {
541
		$this->prepareForm();
542
543
		$result = $this->tryAuthorizedSubmit();
544
545
		$this->displayForm( $result );
546
547
		return $result;
548
	}
549
550
	/**
551
	 * Validate all the fields, and call the submission callback
552
	 * function if everything is kosher.
553
	 * @throws MWException
554
	 * @return bool|string|array|Status
555
	 *     - Bool true or a good Status object indicates success,
556
	 *     - Bool false indicates no submission was attempted,
557
	 *     - Anything else indicates failure. The value may be a fatal Status
558
	 *       object, an HTML string, or an array of arrays (message keys and
559
	 *       params) or strings (message keys)
560
	 */
561
	public function trySubmit() {
562
		$valid = true;
563
		$hoistedErrors = [];
564
		$hoistedErrors[] = isset( $this->mValidationErrorMessage )
565
			? $this->mValidationErrorMessage
566
			: [ 'htmlform-invalid-input' ];
567
568
		$this->mWasSubmitted = true;
569
570
		# Check for cancelled submission
571
		foreach ( $this->mFlatFields as $fieldname => $field ) {
572
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
573
				continue;
574
			}
575
			if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
576
				$this->mWasSubmitted = false;
577
				return false;
578
			}
579
		}
580
581
		# Check for validation
582
		foreach ( $this->mFlatFields as $fieldname => $field ) {
583
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
584
				continue;
585
			}
586
			if ( $field->isHidden( $this->mFieldData ) ) {
587
				continue;
588
			}
589
			$res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
590
			if ( $res !== true ) {
591
				$valid = false;
592
				if ( $res !== false && !$field->canDisplayErrors() ) {
593
					$hoistedErrors[] = [ 'rawmessage', $res ];
594
				}
595
			}
596
		}
597
598
		if ( !$valid ) {
599
			if ( count( $hoistedErrors ) === 1 ) {
600
				$hoistedErrors = $hoistedErrors[0];
601
			}
602
			return $hoistedErrors;
603
		}
604
605
		$callback = $this->mSubmitCallback;
606
		if ( !is_callable( $callback ) ) {
607
			throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
608
				'setSubmitCallback() to set one.' );
609
		}
610
611
		$data = $this->filterDataForSubmit( $this->mFieldData );
612
613
		$res = call_user_func( $callback, $data, $this );
614
		if ( $res === false ) {
615
			$this->mWasSubmitted = false;
616
		}
617
618
		return $res;
619
	}
620
621
	/**
622
	 * Test whether the form was considered to have been submitted or not, i.e.
623
	 * whether the last call to tryAuthorizedSubmit or trySubmit returned
624
	 * non-false.
625
	 *
626
	 * This will return false until HTMLForm::tryAuthorizedSubmit or
627
	 * HTMLForm::trySubmit is called.
628
	 *
629
	 * @since 1.23
630
	 * @return bool
631
	 */
632
	public function wasSubmitted() {
633
		return $this->mWasSubmitted;
634
	}
635
636
	/**
637
	 * Set a callback to a function to do something with the form
638
	 * once it's been successfully validated.
639
	 *
640
	 * @param callable $cb The function will be passed the output from
641
	 *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
642
	 *   return as documented for HTMLForm::trySubmit
643
	 *
644
	 * @return HTMLForm $this for chaining calls (since 1.20)
645
	 */
646
	public function setSubmitCallback( $cb ) {
647
		$this->mSubmitCallback = $cb;
648
649
		return $this;
650
	}
651
652
	/**
653
	 * Set a message to display on a validation error.
654
	 *
655
	 * @param string|array $msg String or Array of valid inputs to wfMessage()
656
	 *     (so each entry can be either a String or Array)
657
	 *
658
	 * @return HTMLForm $this for chaining calls (since 1.20)
659
	 */
660
	public function setValidationErrorMessage( $msg ) {
661
		$this->mValidationErrorMessage = $msg;
662
663
		return $this;
664
	}
665
666
	/**
667
	 * Set the introductory message, overwriting any existing message.
668
	 *
669
	 * @param string $msg Complete text of message to display
670
	 *
671
	 * @return HTMLForm $this for chaining calls (since 1.20)
672
	 */
673
	public function setIntro( $msg ) {
674
		$this->setPreText( $msg );
675
676
		return $this;
677
	}
678
679
	/**
680
	 * Set the introductory message HTML, overwriting any existing message.
681
	 * @since 1.19
682
	 *
683
	 * @param string $msg Complete HTML of message to display
684
	 *
685
	 * @return HTMLForm $this for chaining calls (since 1.20)
686
	 */
687
	public function setPreText( $msg ) {
688
		$this->mPre = $msg;
689
690
		return $this;
691
	}
692
693
	/**
694
	 * Add HTML to introductory message.
695
	 *
696
	 * @param string $msg Complete HTML of message to display
697
	 *
698
	 * @return HTMLForm $this for chaining calls (since 1.20)
699
	 */
700
	public function addPreText( $msg ) {
701
		$this->mPre .= $msg;
702
703
		return $this;
704
	}
705
706
	/**
707
	 * Add HTML to the header, inside the form.
708
	 *
709
	 * @param string $msg Additional HTML to display in header
710
	 * @param string|null $section The section to add the header to
711
	 *
712
	 * @return HTMLForm $this for chaining calls (since 1.20)
713
	 */
714 View Code Duplication
	public function addHeaderText( $msg, $section = null ) {
715
		if ( $section === null ) {
716
			$this->mHeader .= $msg;
717
		} else {
718
			if ( !isset( $this->mSectionHeaders[$section] ) ) {
719
				$this->mSectionHeaders[$section] = '';
720
			}
721
			$this->mSectionHeaders[$section] .= $msg;
722
		}
723
724
		return $this;
725
	}
726
727
	/**
728
	 * Set header text, inside the form.
729
	 * @since 1.19
730
	 *
731
	 * @param string $msg Complete HTML of header to display
732
	 * @param string|null $section The section to add the header to
733
	 *
734
	 * @return HTMLForm $this for chaining calls (since 1.20)
735
	 */
736
	public function setHeaderText( $msg, $section = null ) {
737
		if ( $section === null ) {
738
			$this->mHeader = $msg;
739
		} else {
740
			$this->mSectionHeaders[$section] = $msg;
741
		}
742
743
		return $this;
744
	}
745
746
	/**
747
	 * Get header text.
748
	 *
749
	 * @param string|null $section The section to get the header text for
750
	 * @since 1.26
751
	 * @return string HTML
752
	 */
753
	public function getHeaderText( $section = null ) {
754
		if ( $section === null ) {
755
			return $this->mHeader;
756
		} else {
757
			return isset( $this->mSectionHeaders[$section] ) ? $this->mSectionHeaders[$section] : '';
758
		}
759
	}
760
761
	/**
762
	 * Add footer text, inside the form.
763
	 *
764
	 * @param string $msg Complete text of message to display
765
	 * @param string|null $section The section to add the footer text to
766
	 *
767
	 * @return HTMLForm $this for chaining calls (since 1.20)
768
	 */
769 View Code Duplication
	public function addFooterText( $msg, $section = null ) {
770
		if ( $section === null ) {
771
			$this->mFooter .= $msg;
772
		} else {
773
			if ( !isset( $this->mSectionFooters[$section] ) ) {
774
				$this->mSectionFooters[$section] = '';
775
			}
776
			$this->mSectionFooters[$section] .= $msg;
777
		}
778
779
		return $this;
780
	}
781
782
	/**
783
	 * Set footer text, inside the form.
784
	 * @since 1.19
785
	 *
786
	 * @param string $msg Complete text of message to display
787
	 * @param string|null $section The section to add the footer text to
788
	 *
789
	 * @return HTMLForm $this for chaining calls (since 1.20)
790
	 */
791
	public function setFooterText( $msg, $section = null ) {
792
		if ( $section === null ) {
793
			$this->mFooter = $msg;
794
		} else {
795
			$this->mSectionFooters[$section] = $msg;
796
		}
797
798
		return $this;
799
	}
800
801
	/**
802
	 * Get footer text.
803
	 *
804
	 * @param string|null $section The section to get the footer text for
805
	 * @since 1.26
806
	 * @return string
807
	 */
808
	public function getFooterText( $section = null ) {
809
		if ( $section === null ) {
810
			return $this->mFooter;
811
		} else {
812
			return isset( $this->mSectionFooters[$section] ) ? $this->mSectionFooters[$section] : '';
813
		}
814
	}
815
816
	/**
817
	 * Add text to the end of the display.
818
	 *
819
	 * @param string $msg Complete text of message to display
820
	 *
821
	 * @return HTMLForm $this for chaining calls (since 1.20)
822
	 */
823
	public function addPostText( $msg ) {
824
		$this->mPost .= $msg;
825
826
		return $this;
827
	}
828
829
	/**
830
	 * Set text at the end of the display.
831
	 *
832
	 * @param string $msg Complete text of message to display
833
	 *
834
	 * @return HTMLForm $this for chaining calls (since 1.20)
835
	 */
836
	public function setPostText( $msg ) {
837
		$this->mPost = $msg;
838
839
		return $this;
840
	}
841
842
	/**
843
	 * Add a hidden field to the output
844
	 *
845
	 * @param string $name Field name.  This will be used exactly as entered
846
	 * @param string $value Field value
847
	 * @param array $attribs
848
	 *
849
	 * @return HTMLForm $this for chaining calls (since 1.20)
850
	 */
851
	public function addHiddenField( $name, $value, array $attribs = [] ) {
852
		$attribs += [ 'name' => $name ];
853
		$this->mHiddenFields[] = [ $value, $attribs ];
854
855
		return $this;
856
	}
857
858
	/**
859
	 * Add an array of hidden fields to the output
860
	 *
861
	 * @since 1.22
862
	 *
863
	 * @param array $fields Associative array of fields to add;
864
	 *        mapping names to their values
865
	 *
866
	 * @return HTMLForm $this for chaining calls
867
	 */
868
	public function addHiddenFields( array $fields ) {
869
		foreach ( $fields as $name => $value ) {
870
			$this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
871
		}
872
873
		return $this;
874
	}
875
876
	/**
877
	 * Add a button to the form
878
	 *
879
	 * @since 1.27 takes an array as shown. Earlier versions accepted
880
	 *  'name', 'value', 'id', and 'attribs' as separate parameters in that
881
	 *  order.
882
	 * @note Custom labels ('label', 'label-message', 'label-raw') are not
883
	 *  supported for IE6 and IE7 due to bugs in those browsers. If detected,
884
	 *  they will be served buttons using 'value' as the button label.
885
	 * @param array $data Data to define the button:
886
	 *  - name: (string) Button name.
887
	 *  - value: (string) Button value.
888
	 *  - label-message: (string, optional) Button label message key to use
889
	 *    instead of 'value'. Overrides 'label' and 'label-raw'.
890
	 *  - label: (string, optional) Button label text to use instead of
891
	 *    'value'. Overrides 'label-raw'.
892
	 *  - label-raw: (string, optional) Button label HTML to use instead of
893
	 *    'value'.
894
	 *  - id: (string, optional) DOM id for the button.
895
	 *  - attribs: (array, optional) Additional HTML attributes.
896
	 *  - flags: (string|string[], optional) OOUI flags.
897
	 * @return HTMLForm $this for chaining calls (since 1.20)
898
	 */
899
	public function addButton( $data ) {
900
		if ( !is_array( $data ) ) {
901
			$args = func_get_args();
902
			if ( count( $args ) < 2 || count( $args ) > 4 ) {
903
				throw new InvalidArgumentException(
904
					'Incorrect number of arguments for deprecated calling style'
905
				);
906
			}
907
			$data = [
908
				'name' => $args[0],
909
				'value' => $args[1],
910
				'id' => isset( $args[2] ) ? $args[2] : null,
911
				'attribs' => isset( $args[3] ) ? $args[3] : null,
912
			];
913
		} else {
914
			if ( !isset( $data['name'] ) ) {
915
				throw new InvalidArgumentException( 'A name is required' );
916
			}
917
			if ( !isset( $data['value'] ) ) {
918
				throw new InvalidArgumentException( 'A value is required' );
919
			}
920
		}
921
		$this->mButtons[] = $data + [
922
			'id' => null,
923
			'attribs' => null,
924
			'flags' => null,
925
		];
926
927
		return $this;
928
	}
929
930
	/**
931
	 * Set the salt for the edit token.
932
	 *
933
	 * Only useful when the method is "post".
934
	 *
935
	 * @since 1.24
936
	 * @param string|array $salt Salt to use
937
	 * @return HTMLForm $this For chaining calls
938
	 */
939
	public function setTokenSalt( $salt ) {
940
		$this->mTokenSalt = $salt;
941
942
		return $this;
943
	}
944
945
	/**
946
	 * Display the form (sending to the context's OutputPage object), with an
947
	 * appropriate error message or stack of messages, and any validation errors, etc.
948
	 *
949
	 * @attention You should call prepareForm() before calling this function.
950
	 * Moreover, when doing method chaining this should be the very last method
951
	 * call just after prepareForm().
952
	 *
953
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
954
	 *
955
	 * @return void Nothing, should be last call
956
	 */
957
	public function displayForm( $submitResult ) {
958
		$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
959
	}
960
961
	/**
962
	 * Returns the raw HTML generated by the form
963
	 *
964
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
965
	 *
966
	 * @return string HTML
967
	 */
968
	public function getHTML( $submitResult ) {
969
		# For good measure (it is the default)
970
		$this->getOutput()->preventClickjacking();
971
		$this->getOutput()->addModules( 'mediawiki.htmlform' );
972
		$this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
973
974
		$html = ''
975
			. $this->getErrors( $submitResult )
976
			. $this->getHeaderText()
977
			. $this->getBody()
978
			. $this->getHiddenFields()
979
			. $this->getButtons()
980
			. $this->getFooterText();
981
982
		$html = $this->wrapForm( $html );
983
984
		return '' . $this->mPre . $html . $this->mPost;
985
	}
986
987
	/**
988
	 * Get HTML attributes for the `<form>` tag.
989
	 * @return array
990
	 */
991
	protected function getFormAttributes() {
992
		# Use multipart/form-data
993
		$encType = $this->mUseMultipart
994
			? 'multipart/form-data'
995
			: 'application/x-www-form-urlencoded';
996
		# Attributes
997
		$attribs = [
998
			'action' => $this->getAction(),
999
			'method' => $this->getMethod(),
1000
			'enctype' => $encType,
1001
		];
1002
		if ( $this->mId ) {
1003
			$attribs['id'] = $this->mId;
1004
		}
1005
		if ( $this->mAutocomplete ) {
1006
			$attribs['autocomplete'] = $this->mAutocomplete;
1007
		}
1008
		if ( $this->mName ) {
1009
			$attribs['name'] = $this->mName;
1010
		}
1011
		return $attribs;
1012
	}
1013
1014
	/**
1015
	 * Wrap the form innards in an actual "<form>" element
1016
	 *
1017
	 * @param string $html HTML contents to wrap.
1018
	 *
1019
	 * @return string Wrapped HTML.
1020
	 */
1021
	public function wrapForm( $html ) {
1022
		# Include a <fieldset> wrapper for style, if requested.
1023
		if ( $this->mWrapperLegend !== false ) {
1024
			$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1025
			$html = Xml::fieldset( $legend, $html );
1026
		}
1027
1028
		return Html::rawElement(
1029
			'form',
1030
			$this->getFormAttributes() + [ 'class' => 'visualClear' ],
1031
			$html
1032
		);
1033
	}
1034
1035
	/**
1036
	 * Get the hidden fields that should go inside the form.
1037
	 * @return string HTML.
1038
	 */
1039
	public function getHiddenFields() {
1040
		$html = '';
1041
		if ( $this->getMethod() === 'post' ) {
1042
			$html .= Html::hidden(
1043
				'wpEditToken',
1044
				$this->getUser()->getEditToken( $this->mTokenSalt ),
1045
				[ 'id' => 'wpEditToken' ]
1046
			) . "\n";
1047
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1048
		}
1049
1050
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1051
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1052
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1053
		}
1054
1055
		foreach ( $this->mHiddenFields as $data ) {
1056
			list( $value, $attribs ) = $data;
1057
			$html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1058
		}
1059
1060
		return $html;
1061
	}
1062
1063
	/**
1064
	 * Get the submit and (potentially) reset buttons.
1065
	 * @return string HTML.
1066
	 */
1067
	public function getButtons() {
1068
		$buttons = '';
1069
		$useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
1070
1071
		if ( $this->mShowSubmit ) {
1072
			$attribs = [];
1073
1074
			if ( isset( $this->mSubmitID ) ) {
1075
				$attribs['id'] = $this->mSubmitID;
1076
			}
1077
1078
			if ( isset( $this->mSubmitName ) ) {
1079
				$attribs['name'] = $this->mSubmitName;
1080
			}
1081
1082
			if ( isset( $this->mSubmitTooltip ) ) {
1083
				$attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1084
			}
1085
1086
			$attribs['class'] = [ 'mw-htmlform-submit' ];
1087
1088
			if ( $useMediaWikiUIEverywhere ) {
1089
				foreach ( $this->mSubmitFlags as $flag ) {
1090
					$attribs['class'][] = 'mw-ui-' . $flag;
1091
				}
1092
				$attribs['class'][] = 'mw-ui-button';
1093
			}
1094
1095
			$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1096
		}
1097
1098 View Code Duplication
		if ( $this->mShowReset ) {
1099
			$buttons .= Html::element(
1100
				'input',
1101
				[
1102
					'type' => 'reset',
1103
					'value' => $this->msg( 'htmlform-reset' )->text(),
1104
					'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1105
				]
1106
			) . "\n";
1107
		}
1108
1109
		// IE<8 has bugs with <button>, so we'll need to avoid them.
1110
		$isBadIE = preg_match( '/MSIE [1-7]\./i', $this->getRequest()->getHeader( 'User-Agent' ) );
1111
1112
		foreach ( $this->mButtons as $button ) {
1113
			$attrs = [
1114
				'type' => 'submit',
1115
				'name' => $button['name'],
1116
				'value' => $button['value']
1117
			];
1118
1119
			if ( isset( $button['label-message'] ) ) {
1120
				$label = $this->getMessage( $button['label-message'] )->parse();
1121
			} elseif ( isset( $button['label'] ) ) {
1122
				$label = htmlspecialchars( $button['label'] );
1123
			} elseif ( isset( $button['label-raw'] ) ) {
1124
				$label = $button['label-raw'];
1125
			} else {
1126
				$label = htmlspecialchars( $button['value'] );
1127
			}
1128
1129
			if ( $button['attribs'] ) {
1130
				$attrs += $button['attribs'];
1131
			}
1132
1133
			if ( isset( $button['id'] ) ) {
1134
				$attrs['id'] = $button['id'];
1135
			}
1136
1137
			if ( $useMediaWikiUIEverywhere ) {
1138
				$attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : [];
1139
				$attrs['class'][] = 'mw-ui-button';
1140
			}
1141
1142
			if ( $isBadIE ) {
1143
				$buttons .= Html::element( 'input', $attrs ) . "\n";
1144
			} else {
1145
				$buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1146
			}
1147
		}
1148
1149
		if ( !$buttons ) {
1150
			return '';
1151
		}
1152
1153
		return Html::rawElement( 'span',
1154
			[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1155
	}
1156
1157
	/**
1158
	 * Get the whole body of the form.
1159
	 * @return string
1160
	 */
1161
	public function getBody() {
1162
		return $this->displaySection( $this->mFieldTree, $this->mTableId );
1163
	}
1164
1165
	/**
1166
	 * Format and display an error message stack.
1167
	 *
1168
	 * @param string|array|Status $errors
1169
	 *
1170
	 * @return string
1171
	 */
1172
	public function getErrors( $errors ) {
1173
		if ( $errors instanceof Status ) {
1174
			if ( $errors->isOK() ) {
1175
				$errorstr = '';
1176
			} else {
1177
				$errorstr = $this->getOutput()->parse( $errors->getWikiText() );
1178
			}
1179
		} elseif ( is_array( $errors ) ) {
1180
			$errorstr = $this->formatErrors( $errors );
1181
		} else {
1182
			$errorstr = $errors;
1183
		}
1184
1185
		return $errorstr
1186
			? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr )
1187
			: '';
1188
	}
1189
1190
	/**
1191
	 * Format a stack of error messages into a single HTML string
1192
	 *
1193
	 * @param array $errors Array of message keys/values
1194
	 *
1195
	 * @return string HTML, a "<ul>" list of errors
1196
	 */
1197
	public function formatErrors( $errors ) {
1198
		$errorstr = '';
1199
1200
		foreach ( $errors as $error ) {
1201
			$errorstr .= Html::rawElement(
1202
				'li',
1203
				[],
1204
				$this->getMessage( $error )->parse()
1205
			);
1206
		}
1207
1208
		$errorstr = Html::rawElement( 'ul', [], $errorstr );
1209
1210
		return $errorstr;
1211
	}
1212
1213
	/**
1214
	 * Set the text for the submit button
1215
	 *
1216
	 * @param string $t Plaintext
1217
	 *
1218
	 * @return HTMLForm $this for chaining calls (since 1.20)
1219
	 */
1220
	public function setSubmitText( $t ) {
1221
		$this->mSubmitText = $t;
1222
1223
		return $this;
1224
	}
1225
1226
	/**
1227
	 * Identify that the submit button in the form has a destructive action
1228
	 * @since 1.24
1229
	 *
1230
	 * @return HTMLForm $this for chaining calls (since 1.28)
1231
	 */
1232
	public function setSubmitDestructive() {
1233
		$this->mSubmitFlags = [ 'destructive', 'primary' ];
1234
1235
		return $this;
1236
	}
1237
1238
	/**
1239
	 * Identify that the submit button in the form has a progressive action
1240
	 * @since 1.25
1241
	 *
1242
	 * @return HTMLForm $this for chaining calls (since 1.28)
1243
	 */
1244
	public function setSubmitProgressive() {
1245
		$this->mSubmitFlags = [ 'progressive', 'primary' ];
1246
1247
		return $this;
1248
	}
1249
1250
	/**
1251
	 * Set the text for the submit button to a message
1252
	 * @since 1.19
1253
	 *
1254
	 * @param string|Message $msg Message key or Message object
1255
	 *
1256
	 * @return HTMLForm $this for chaining calls (since 1.20)
1257
	 */
1258
	public function setSubmitTextMsg( $msg ) {
1259
		if ( !$msg instanceof Message ) {
1260
			$msg = $this->msg( $msg );
1261
		}
1262
		$this->setSubmitText( $msg->text() );
1263
1264
		return $this;
1265
	}
1266
1267
	/**
1268
	 * Get the text for the submit button, either customised or a default.
1269
	 * @return string
1270
	 */
1271
	public function getSubmitText() {
1272
		return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1273
	}
1274
1275
	/**
1276
	 * @param string $name Submit button name
1277
	 *
1278
	 * @return HTMLForm $this for chaining calls (since 1.20)
1279
	 */
1280
	public function setSubmitName( $name ) {
1281
		$this->mSubmitName = $name;
1282
1283
		return $this;
1284
	}
1285
1286
	/**
1287
	 * @param string $name Tooltip for the submit button
1288
	 *
1289
	 * @return HTMLForm $this for chaining calls (since 1.20)
1290
	 */
1291
	public function setSubmitTooltip( $name ) {
1292
		$this->mSubmitTooltip = $name;
1293
1294
		return $this;
1295
	}
1296
1297
	/**
1298
	 * Set the id for the submit button.
1299
	 *
1300
	 * @param string $t
1301
	 *
1302
	 * @todo FIXME: Integrity of $t is *not* validated
1303
	 * @return HTMLForm $this for chaining calls (since 1.20)
1304
	 */
1305
	public function setSubmitID( $t ) {
1306
		$this->mSubmitID = $t;
1307
1308
		return $this;
1309
	}
1310
1311
	/**
1312
	 * Stop a default submit button being shown for this form. This implies that an
1313
	 * alternate submit method must be provided manually.
1314
	 *
1315
	 * @since 1.22
1316
	 *
1317
	 * @param bool $suppressSubmit Set to false to re-enable the button again
1318
	 *
1319
	 * @return HTMLForm $this for chaining calls
1320
	 */
1321
	public function suppressDefaultSubmit( $suppressSubmit = true ) {
1322
		$this->mShowSubmit = !$suppressSubmit;
1323
1324
		return $this;
1325
	}
1326
1327
	/**
1328
	 * Set the id of the \<table\> or outermost \<div\> element.
1329
	 *
1330
	 * @since 1.22
1331
	 *
1332
	 * @param string $id New value of the id attribute, or "" to remove
1333
	 *
1334
	 * @return HTMLForm $this for chaining calls
1335
	 */
1336
	public function setTableId( $id ) {
1337
		$this->mTableId = $id;
1338
1339
		return $this;
1340
	}
1341
1342
	/**
1343
	 * @param string $id DOM id for the form
1344
	 *
1345
	 * @return HTMLForm $this for chaining calls (since 1.20)
1346
	 */
1347
	public function setId( $id ) {
1348
		$this->mId = $id;
1349
1350
		return $this;
1351
	}
1352
1353
	/**
1354
	 * @param string $name 'name' attribute for the form
1355
	 * @return HTMLForm $this for chaining calls
1356
	 */
1357
	public function setName( $name ) {
1358
		$this->mName = $name;
1359
1360
		return $this;
1361
	}
1362
1363
	/**
1364
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1365
	 * this text as its "<legend>" element.
1366
	 *
1367
	 * @param string|bool $legend If false, no wrapper or legend will be displayed.
1368
	 *     If true, a wrapper will be displayed, but no legend.
1369
	 *     If a string, a wrapper will be displayed with that string as a legend.
1370
	 *     The string will be escaped before being output (this doesn't support HTML).
1371
	 *
1372
	 * @return HTMLForm $this for chaining calls (since 1.20)
1373
	 */
1374
	public function setWrapperLegend( $legend ) {
1375
		$this->mWrapperLegend = $legend;
0 ignored issues
show
Documentation Bug introduced by
It seems like $legend can also be of type string. However, the property $mWrapperLegend is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1376
1377
		return $this;
1378
	}
1379
1380
	/**
1381
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1382
	 * this message as its "<legend>" element.
1383
	 * @since 1.19
1384
	 *
1385
	 * @param string|Message $msg Message key or Message object
1386
	 *
1387
	 * @return HTMLForm $this for chaining calls (since 1.20)
1388
	 */
1389
	public function setWrapperLegendMsg( $msg ) {
1390
		if ( !$msg instanceof Message ) {
1391
			$msg = $this->msg( $msg );
1392
		}
1393
		$this->setWrapperLegend( $msg->text() );
1394
1395
		return $this;
1396
	}
1397
1398
	/**
1399
	 * Set the prefix for various default messages
1400
	 * @todo Currently only used for the "<fieldset>" legend on forms
1401
	 * with multiple sections; should be used elsewhere?
1402
	 *
1403
	 * @param string $p
1404
	 *
1405
	 * @return HTMLForm $this for chaining calls (since 1.20)
1406
	 */
1407
	public function setMessagePrefix( $p ) {
1408
		$this->mMessagePrefix = $p;
1409
1410
		return $this;
1411
	}
1412
1413
	/**
1414
	 * Set the title for form submission
1415
	 *
1416
	 * @param Title $t Title of page the form is on/should be posted to
1417
	 *
1418
	 * @return HTMLForm $this for chaining calls (since 1.20)
1419
	 */
1420
	public function setTitle( $t ) {
1421
		$this->mTitle = $t;
1422
1423
		return $this;
1424
	}
1425
1426
	/**
1427
	 * Get the title
1428
	 * @return Title
1429
	 */
1430
	public function getTitle() {
1431
		return $this->mTitle === false
1432
			? $this->getContext()->getTitle()
1433
			: $this->mTitle;
1434
	}
1435
1436
	/**
1437
	 * Set the method used to submit the form
1438
	 *
1439
	 * @param string $method
1440
	 *
1441
	 * @return HTMLForm $this for chaining calls (since 1.20)
1442
	 */
1443
	public function setMethod( $method = 'post' ) {
1444
		$this->mMethod = strtolower( $method );
1445
1446
		return $this;
1447
	}
1448
1449
	/**
1450
	 * @return string Always lowercase
1451
	 */
1452
	public function getMethod() {
1453
		return $this->mMethod;
1454
	}
1455
1456
	/**
1457
	 * Wraps the given $section into an user-visible fieldset.
1458
	 *
1459
	 * @param string $legend Legend text for the fieldset
1460
	 * @param string $section The section content in plain Html
1461
	 * @param array $attributes Additional attributes for the fieldset
1462
	 * @return string The fieldset's Html
1463
	 */
1464
	protected function wrapFieldSetSection( $legend, $section, $attributes ) {
1465
		return Xml::fieldset( $legend, $section, $attributes ) . "\n";
1466
	}
1467
1468
	/**
1469
	 * @todo Document
1470
	 *
1471
	 * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1472
	 *   objects).
1473
	 * @param string $sectionName ID attribute of the "<table>" tag for this
1474
	 *   section, ignored if empty.
1475
	 * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1476
	 *   each subsection, ignored if empty.
1477
	 * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1478
	 * @throws LogicException When called on uninitialized field data, e.g. When
1479
	 *  HTMLForm::displayForm was called without calling HTMLForm::prepareForm
1480
	 *  first.
1481
	 *
1482
	 * @return string
1483
	 */
1484
	public function displaySection( $fields,
1485
		$sectionName = '',
1486
		$fieldsetIDPrefix = '',
1487
		&$hasUserVisibleFields = false
1488
	) {
1489
		if ( $this->mFieldData === null ) {
1490
			throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1491
				. 'You probably called displayForm() without calling prepareForm() first.' );
1492
		}
1493
1494
		$displayFormat = $this->getDisplayFormat();
1495
1496
		$html = [];
1497
		$subsectionHtml = '';
1498
		$hasLabel = false;
1499
1500
		// Conveniently, PHP method names are case-insensitive.
1501
		// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
1502
		$getFieldHtmlMethod = $displayFormat === 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
1503
1504
		foreach ( $fields as $key => $value ) {
1505
			if ( $value instanceof HTMLFormField ) {
1506
				$v = array_key_exists( $key, $this->mFieldData )
1507
					? $this->mFieldData[$key]
1508
					: $value->getDefault();
1509
1510
				$retval = $value->$getFieldHtmlMethod( $v );
1511
1512
				// check, if the form field should be added to
1513
				// the output.
1514
				if ( $value->hasVisibleOutput() ) {
1515
					$html[] = $retval;
1516
1517
					$labelValue = trim( $value->getLabel() );
1518
					if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
1519
						$hasLabel = true;
1520
					}
1521
1522
					$hasUserVisibleFields = true;
1523
				}
1524
			} elseif ( is_array( $value ) ) {
1525
				$subsectionHasVisibleFields = false;
1526
				$section =
1527
					$this->displaySection( $value,
1528
						"mw-htmlform-$key",
1529
						"$fieldsetIDPrefix$key-",
1530
						$subsectionHasVisibleFields );
1531
				$legend = null;
0 ignored issues
show
Unused Code introduced by
$legend is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1532
1533
				if ( $subsectionHasVisibleFields === true ) {
1534
					// Display the section with various niceties.
1535
					$hasUserVisibleFields = true;
1536
1537
					$legend = $this->getLegend( $key );
1538
1539
					$section = $this->getHeaderText( $key ) .
1540
						$section .
1541
						$this->getFooterText( $key );
1542
1543
					$attributes = [];
1544
					if ( $fieldsetIDPrefix ) {
1545
						$attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1546
					}
1547
					$subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
1548
				} else {
1549
					// Just return the inputs, nothing fancy.
1550
					$subsectionHtml .= $section;
1551
				}
1552
			}
1553
		}
1554
1555
		$html = $this->formatSection( $html, $sectionName, $hasLabel );
1556
1557
		if ( $subsectionHtml ) {
1558
			if ( $this->mSubSectionBeforeFields ) {
1559
				return $subsectionHtml . "\n" . $html;
1560
			} else {
1561
				return $html . "\n" . $subsectionHtml;
1562
			}
1563
		} else {
1564
			return $html;
1565
		}
1566
	}
1567
1568
	/**
1569
	 * Put a form section together from the individual fields' HTML, merging it and wrapping.
1570
	 * @param array $fieldsHtml
1571
	 * @param string $sectionName
1572
	 * @param bool $anyFieldHasLabel
1573
	 * @return string HTML
1574
	 */
1575
	protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1576
		$displayFormat = $this->getDisplayFormat();
1577
		$html = implode( '', $fieldsHtml );
1578
1579
		if ( $displayFormat === 'raw' ) {
1580
			return $html;
1581
		}
1582
1583
		$classes = [];
1584
1585
		if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
1586
			$classes[] = 'mw-htmlform-nolabel';
1587
		}
1588
1589
		$attribs = [
1590
			'class' => implode( ' ', $classes ),
1591
		];
1592
1593
		if ( $sectionName ) {
1594
			$attribs['id'] = Sanitizer::escapeId( $sectionName );
1595
		}
1596
1597
		if ( $displayFormat === 'table' ) {
1598
			return Html::rawElement( 'table',
1599
					$attribs,
1600
					Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1601
		} elseif ( $displayFormat === 'inline' ) {
1602
			return Html::rawElement( 'span', $attribs, "\n$html\n" );
1603
		} else {
1604
			return Html::rawElement( 'div', $attribs, "\n$html\n" );
1605
		}
1606
	}
1607
1608
	/**
1609
	 * Construct the form fields from the Descriptor array
1610
	 */
1611
	public function loadData() {
1612
		$fieldData = [];
1613
1614 View Code Duplication
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1615
			$request = $this->getRequest();
1616
			if ( $field->skipLoadData( $request ) ) {
1617
				continue;
1618
			} elseif ( !empty( $field->mParams['disabled'] ) ) {
1619
				$fieldData[$fieldname] = $field->getDefault();
1620
			} else {
1621
				$fieldData[$fieldname] = $field->loadDataFromRequest( $request );
1622
			}
1623
		}
1624
1625
		# Filter data.
1626
		foreach ( $fieldData as $name => &$value ) {
1627
			$field = $this->mFlatFields[$name];
1628
			$value = $field->filter( $value, $this->mFlatFields );
1629
		}
1630
1631
		$this->mFieldData = $fieldData;
1632
	}
1633
1634
	/**
1635
	 * Stop a reset button being shown for this form
1636
	 *
1637
	 * @param bool $suppressReset Set to false to re-enable the button again
1638
	 *
1639
	 * @return HTMLForm $this for chaining calls (since 1.20)
1640
	 */
1641
	public function suppressReset( $suppressReset = true ) {
1642
		$this->mShowReset = !$suppressReset;
1643
1644
		return $this;
1645
	}
1646
1647
	/**
1648
	 * Overload this if you want to apply special filtration routines
1649
	 * to the form as a whole, after it's submitted but before it's
1650
	 * processed.
1651
	 *
1652
	 * @param array $data
1653
	 *
1654
	 * @return array
1655
	 */
1656
	public function filterDataForSubmit( $data ) {
1657
		return $data;
1658
	}
1659
1660
	/**
1661
	 * Get a string to go in the "<legend>" of a section fieldset.
1662
	 * Override this if you want something more complicated.
1663
	 *
1664
	 * @param string $key
1665
	 *
1666
	 * @return string
1667
	 */
1668
	public function getLegend( $key ) {
1669
		return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1670
	}
1671
1672
	/**
1673
	 * Set the value for the action attribute of the form.
1674
	 * When set to false (which is the default state), the set title is used.
1675
	 *
1676
	 * @since 1.19
1677
	 *
1678
	 * @param string|bool $action
1679
	 *
1680
	 * @return HTMLForm $this for chaining calls (since 1.20)
1681
	 */
1682
	public function setAction( $action ) {
1683
		$this->mAction = $action;
1684
1685
		return $this;
1686
	}
1687
1688
	/**
1689
	 * Get the value for the action attribute of the form.
1690
	 *
1691
	 * @since 1.22
1692
	 *
1693
	 * @return string
1694
	 */
1695
	public function getAction() {
1696
		// If an action is alredy provided, return it
1697
		if ( $this->mAction !== false ) {
1698
			return $this->mAction;
1699
		}
1700
1701
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1702
		// Check whether we are in GET mode and the ArticlePath contains a "?"
1703
		// meaning that getLocalURL() would return something like "index.php?title=...".
1704
		// As browser remove the query string before submitting GET forms,
1705
		// it means that the title would be lost. In such case use wfScript() instead
1706
		// and put title in an hidden field (see getHiddenFields()).
1707
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1708
			return wfScript();
1709
		}
1710
1711
		return $this->getTitle()->getLocalURL();
1712
	}
1713
1714
	/**
1715
	 * Set the value for the autocomplete attribute of the form.
1716
	 * When set to false (which is the default state), the attribute get not set.
1717
	 *
1718
	 * @since 1.27
1719
	 *
1720
	 * @param string|bool $autocomplete
1721
	 *
1722
	 * @return HTMLForm $this for chaining calls
1723
	 */
1724
	public function setAutocomplete( $autocomplete ) {
1725
		$this->mAutocomplete = $autocomplete;
1726
1727
		return $this;
1728
	}
1729
1730
	/**
1731
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1732
	 * name + parameters array) into a Message.
1733
	 * @param mixed $value
1734
	 * @return Message
1735
	 */
1736
	protected function getMessage( $value ) {
1737
		return Message::newFromSpecifier( $value )->setContext( $this );
1738
	}
1739
}
1740