Completed
Branch master (62f6c6)
by
unknown
21:31
created

HTMLForm::getButtons()   F

Complexity

Conditions 19
Paths 6596

Size

Total Lines 89
Code Lines 53

Duplication

Lines 10
Ratio 11.24 %

Importance

Changes 0
Metric Value
cc 19
eloc 53
nc 6596
nop 0
dl 10
loc 89
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
	 *  - framed: (boolean=true, optional) OOUI framed attribute.
898
	 * @return HTMLForm $this for chaining calls (since 1.20)
899
	 */
900
	public function addButton( $data ) {
901
		if ( !is_array( $data ) ) {
902
			$args = func_get_args();
903
			if ( count( $args ) < 2 || count( $args ) > 4 ) {
904
				throw new InvalidArgumentException(
905
					'Incorrect number of arguments for deprecated calling style'
906
				);
907
			}
908
			$data = [
909
				'name' => $args[0],
910
				'value' => $args[1],
911
				'id' => isset( $args[2] ) ? $args[2] : null,
912
				'attribs' => isset( $args[3] ) ? $args[3] : null,
913
			];
914
		} else {
915
			if ( !isset( $data['name'] ) ) {
916
				throw new InvalidArgumentException( 'A name is required' );
917
			}
918
			if ( !isset( $data['value'] ) ) {
919
				throw new InvalidArgumentException( 'A value is required' );
920
			}
921
		}
922
		$this->mButtons[] = $data + [
923
			'id' => null,
924
			'attribs' => null,
925
			'flags' => null,
926
			'framed' => true,
927
		];
928
929
		return $this;
930
	}
931
932
	/**
933
	 * Set the salt for the edit token.
934
	 *
935
	 * Only useful when the method is "post".
936
	 *
937
	 * @since 1.24
938
	 * @param string|array $salt Salt to use
939
	 * @return HTMLForm $this For chaining calls
940
	 */
941
	public function setTokenSalt( $salt ) {
942
		$this->mTokenSalt = $salt;
943
944
		return $this;
945
	}
946
947
	/**
948
	 * Display the form (sending to the context's OutputPage object), with an
949
	 * appropriate error message or stack of messages, and any validation errors, etc.
950
	 *
951
	 * @attention You should call prepareForm() before calling this function.
952
	 * Moreover, when doing method chaining this should be the very last method
953
	 * call just after prepareForm().
954
	 *
955
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
956
	 *
957
	 * @return void Nothing, should be last call
958
	 */
959
	public function displayForm( $submitResult ) {
960
		$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
961
	}
962
963
	/**
964
	 * Returns the raw HTML generated by the form
965
	 *
966
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
967
	 *
968
	 * @return string HTML
969
	 */
970
	public function getHTML( $submitResult ) {
971
		# For good measure (it is the default)
972
		$this->getOutput()->preventClickjacking();
973
		$this->getOutput()->addModules( 'mediawiki.htmlform' );
974
		$this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
975
976
		$html = ''
977
			. $this->getErrors( $submitResult )
978
			. $this->getHeaderText()
979
			. $this->getBody()
980
			. $this->getHiddenFields()
981
			. $this->getButtons()
982
			. $this->getFooterText();
983
984
		$html = $this->wrapForm( $html );
985
986
		return '' . $this->mPre . $html . $this->mPost;
987
	}
988
989
	/**
990
	 * Get HTML attributes for the `<form>` tag.
991
	 * @return array
992
	 */
993
	protected function getFormAttributes() {
994
		# Use multipart/form-data
995
		$encType = $this->mUseMultipart
996
			? 'multipart/form-data'
997
			: 'application/x-www-form-urlencoded';
998
		# Attributes
999
		$attribs = [
1000
			'action' => $this->getAction(),
1001
			'method' => $this->getMethod(),
1002
			'enctype' => $encType,
1003
		];
1004
		if ( $this->mId ) {
1005
			$attribs['id'] = $this->mId;
1006
		}
1007
		if ( $this->mAutocomplete ) {
1008
			$attribs['autocomplete'] = $this->mAutocomplete;
1009
		}
1010
		if ( $this->mName ) {
1011
			$attribs['name'] = $this->mName;
1012
		}
1013
		return $attribs;
1014
	}
1015
1016
	/**
1017
	 * Wrap the form innards in an actual "<form>" element
1018
	 *
1019
	 * @param string $html HTML contents to wrap.
1020
	 *
1021
	 * @return string Wrapped HTML.
1022
	 */
1023
	public function wrapForm( $html ) {
1024
		# Include a <fieldset> wrapper for style, if requested.
1025
		if ( $this->mWrapperLegend !== false ) {
1026
			$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1027
			$html = Xml::fieldset( $legend, $html );
1028
		}
1029
1030
		return Html::rawElement(
1031
			'form',
1032
			$this->getFormAttributes() + [ 'class' => 'visualClear' ],
1033
			$html
1034
		);
1035
	}
1036
1037
	/**
1038
	 * Get the hidden fields that should go inside the form.
1039
	 * @return string HTML.
1040
	 */
1041
	public function getHiddenFields() {
1042
		$html = '';
1043
		if ( $this->getMethod() === 'post' ) {
1044
			$html .= Html::hidden(
1045
				'wpEditToken',
1046
				$this->getUser()->getEditToken( $this->mTokenSalt ),
1047
				[ 'id' => 'wpEditToken' ]
1048
			) . "\n";
1049
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1050
		}
1051
1052
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1053
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1054
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1055
		}
1056
1057
		foreach ( $this->mHiddenFields as $data ) {
1058
			list( $value, $attribs ) = $data;
1059
			$html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1060
		}
1061
1062
		return $html;
1063
	}
1064
1065
	/**
1066
	 * Get the submit and (potentially) reset buttons.
1067
	 * @return string HTML.
1068
	 */
1069
	public function getButtons() {
1070
		$buttons = '';
1071
		$useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
1072
1073
		if ( $this->mShowSubmit ) {
1074
			$attribs = [];
1075
1076
			if ( isset( $this->mSubmitID ) ) {
1077
				$attribs['id'] = $this->mSubmitID;
1078
			}
1079
1080
			if ( isset( $this->mSubmitName ) ) {
1081
				$attribs['name'] = $this->mSubmitName;
1082
			}
1083
1084
			if ( isset( $this->mSubmitTooltip ) ) {
1085
				$attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1086
			}
1087
1088
			$attribs['class'] = [ 'mw-htmlform-submit' ];
1089
1090
			if ( $useMediaWikiUIEverywhere ) {
1091
				foreach ( $this->mSubmitFlags as $flag ) {
1092
					$attribs['class'][] = 'mw-ui-' . $flag;
1093
				}
1094
				$attribs['class'][] = 'mw-ui-button';
1095
			}
1096
1097
			$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1098
		}
1099
1100 View Code Duplication
		if ( $this->mShowReset ) {
1101
			$buttons .= Html::element(
1102
				'input',
1103
				[
1104
					'type' => 'reset',
1105
					'value' => $this->msg( 'htmlform-reset' )->text(),
1106
					'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1107
				]
1108
			) . "\n";
1109
		}
1110
1111
		// IE<8 has bugs with <button>, so we'll need to avoid them.
1112
		$isBadIE = preg_match( '/MSIE [1-7]\./i', $this->getRequest()->getHeader( 'User-Agent' ) );
1113
1114
		foreach ( $this->mButtons as $button ) {
1115
			$attrs = [
1116
				'type' => 'submit',
1117
				'name' => $button['name'],
1118
				'value' => $button['value']
1119
			];
1120
1121
			if ( isset( $button['label-message'] ) ) {
1122
				$label = $this->getMessage( $button['label-message'] )->parse();
1123
			} elseif ( isset( $button['label'] ) ) {
1124
				$label = htmlspecialchars( $button['label'] );
1125
			} elseif ( isset( $button['label-raw'] ) ) {
1126
				$label = $button['label-raw'];
1127
			} else {
1128
				$label = htmlspecialchars( $button['value'] );
1129
			}
1130
1131
			if ( $button['attribs'] ) {
1132
				$attrs += $button['attribs'];
1133
			}
1134
1135
			if ( isset( $button['id'] ) ) {
1136
				$attrs['id'] = $button['id'];
1137
			}
1138
1139
			if ( $useMediaWikiUIEverywhere ) {
1140
				$attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : [];
1141
				$attrs['class'][] = 'mw-ui-button';
1142
			}
1143
1144
			if ( $isBadIE ) {
1145
				$buttons .= Html::element( 'input', $attrs ) . "\n";
1146
			} else {
1147
				$buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1148
			}
1149
		}
1150
1151
		if ( !$buttons ) {
1152
			return '';
1153
		}
1154
1155
		return Html::rawElement( 'span',
1156
			[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1157
	}
1158
1159
	/**
1160
	 * Get the whole body of the form.
1161
	 * @return string
1162
	 */
1163
	public function getBody() {
1164
		return $this->displaySection( $this->mFieldTree, $this->mTableId );
1165
	}
1166
1167
	/**
1168
	 * Format and display an error message stack.
1169
	 *
1170
	 * @param string|array|Status $errors
1171
	 *
1172
	 * @return string
1173
	 */
1174
	public function getErrors( $errors ) {
1175
		if ( $errors instanceof Status ) {
1176
			if ( $errors->isOK() ) {
1177
				$errorstr = '';
1178
			} else {
1179
				$errorstr = $this->getOutput()->parse( $errors->getWikiText() );
1180
			}
1181
		} elseif ( is_array( $errors ) ) {
1182
			$errorstr = $this->formatErrors( $errors );
1183
		} else {
1184
			$errorstr = $errors;
1185
		}
1186
1187
		return $errorstr
1188
			? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr )
1189
			: '';
1190
	}
1191
1192
	/**
1193
	 * Format a stack of error messages into a single HTML string
1194
	 *
1195
	 * @param array $errors Array of message keys/values
1196
	 *
1197
	 * @return string HTML, a "<ul>" list of errors
1198
	 */
1199
	public function formatErrors( $errors ) {
1200
		$errorstr = '';
1201
1202
		foreach ( $errors as $error ) {
1203
			$errorstr .= Html::rawElement(
1204
				'li',
1205
				[],
1206
				$this->getMessage( $error )->parse()
1207
			);
1208
		}
1209
1210
		$errorstr = Html::rawElement( 'ul', [], $errorstr );
1211
1212
		return $errorstr;
1213
	}
1214
1215
	/**
1216
	 * Set the text for the submit button
1217
	 *
1218
	 * @param string $t Plaintext
1219
	 *
1220
	 * @return HTMLForm $this for chaining calls (since 1.20)
1221
	 */
1222
	public function setSubmitText( $t ) {
1223
		$this->mSubmitText = $t;
1224
1225
		return $this;
1226
	}
1227
1228
	/**
1229
	 * Identify that the submit button in the form has a destructive action
1230
	 * @since 1.24
1231
	 *
1232
	 * @return HTMLForm $this for chaining calls (since 1.28)
1233
	 */
1234
	public function setSubmitDestructive() {
1235
		$this->mSubmitFlags = [ 'destructive', 'primary' ];
1236
1237
		return $this;
1238
	}
1239
1240
	/**
1241
	 * Identify that the submit button in the form has a progressive action
1242
	 * @since 1.25
1243
	 *
1244
	 * @return HTMLForm $this for chaining calls (since 1.28)
1245
	 */
1246
	public function setSubmitProgressive() {
1247
		$this->mSubmitFlags = [ 'progressive', 'primary' ];
1248
1249
		return $this;
1250
	}
1251
1252
	/**
1253
	 * Set the text for the submit button to a message
1254
	 * @since 1.19
1255
	 *
1256
	 * @param string|Message $msg Message key or Message object
1257
	 *
1258
	 * @return HTMLForm $this for chaining calls (since 1.20)
1259
	 */
1260
	public function setSubmitTextMsg( $msg ) {
1261
		if ( !$msg instanceof Message ) {
1262
			$msg = $this->msg( $msg );
1263
		}
1264
		$this->setSubmitText( $msg->text() );
1265
1266
		return $this;
1267
	}
1268
1269
	/**
1270
	 * Get the text for the submit button, either customised or a default.
1271
	 * @return string
1272
	 */
1273
	public function getSubmitText() {
1274
		return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1275
	}
1276
1277
	/**
1278
	 * @param string $name Submit button name
1279
	 *
1280
	 * @return HTMLForm $this for chaining calls (since 1.20)
1281
	 */
1282
	public function setSubmitName( $name ) {
1283
		$this->mSubmitName = $name;
1284
1285
		return $this;
1286
	}
1287
1288
	/**
1289
	 * @param string $name Tooltip for the submit button
1290
	 *
1291
	 * @return HTMLForm $this for chaining calls (since 1.20)
1292
	 */
1293
	public function setSubmitTooltip( $name ) {
1294
		$this->mSubmitTooltip = $name;
1295
1296
		return $this;
1297
	}
1298
1299
	/**
1300
	 * Set the id for the submit button.
1301
	 *
1302
	 * @param string $t
1303
	 *
1304
	 * @todo FIXME: Integrity of $t is *not* validated
1305
	 * @return HTMLForm $this for chaining calls (since 1.20)
1306
	 */
1307
	public function setSubmitID( $t ) {
1308
		$this->mSubmitID = $t;
1309
1310
		return $this;
1311
	}
1312
1313
	/**
1314
	 * Stop a default submit button being shown for this form. This implies that an
1315
	 * alternate submit method must be provided manually.
1316
	 *
1317
	 * @since 1.22
1318
	 *
1319
	 * @param bool $suppressSubmit Set to false to re-enable the button again
1320
	 *
1321
	 * @return HTMLForm $this for chaining calls
1322
	 */
1323
	public function suppressDefaultSubmit( $suppressSubmit = true ) {
1324
		$this->mShowSubmit = !$suppressSubmit;
1325
1326
		return $this;
1327
	}
1328
1329
	/**
1330
	 * Set the id of the \<table\> or outermost \<div\> element.
1331
	 *
1332
	 * @since 1.22
1333
	 *
1334
	 * @param string $id New value of the id attribute, or "" to remove
1335
	 *
1336
	 * @return HTMLForm $this for chaining calls
1337
	 */
1338
	public function setTableId( $id ) {
1339
		$this->mTableId = $id;
1340
1341
		return $this;
1342
	}
1343
1344
	/**
1345
	 * @param string $id DOM id for the form
1346
	 *
1347
	 * @return HTMLForm $this for chaining calls (since 1.20)
1348
	 */
1349
	public function setId( $id ) {
1350
		$this->mId = $id;
1351
1352
		return $this;
1353
	}
1354
1355
	/**
1356
	 * @param string $name 'name' attribute for the form
1357
	 * @return HTMLForm $this for chaining calls
1358
	 */
1359
	public function setName( $name ) {
1360
		$this->mName = $name;
1361
1362
		return $this;
1363
	}
1364
1365
	/**
1366
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1367
	 * this text as its "<legend>" element.
1368
	 *
1369
	 * @param string|bool $legend If false, no wrapper or legend will be displayed.
1370
	 *     If true, a wrapper will be displayed, but no legend.
1371
	 *     If a string, a wrapper will be displayed with that string as a legend.
1372
	 *     The string will be escaped before being output (this doesn't support HTML).
1373
	 *
1374
	 * @return HTMLForm $this for chaining calls (since 1.20)
1375
	 */
1376
	public function setWrapperLegend( $legend ) {
1377
		$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...
1378
1379
		return $this;
1380
	}
1381
1382
	/**
1383
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1384
	 * this message as its "<legend>" element.
1385
	 * @since 1.19
1386
	 *
1387
	 * @param string|Message $msg Message key or Message object
1388
	 *
1389
	 * @return HTMLForm $this for chaining calls (since 1.20)
1390
	 */
1391
	public function setWrapperLegendMsg( $msg ) {
1392
		if ( !$msg instanceof Message ) {
1393
			$msg = $this->msg( $msg );
1394
		}
1395
		$this->setWrapperLegend( $msg->text() );
1396
1397
		return $this;
1398
	}
1399
1400
	/**
1401
	 * Set the prefix for various default messages
1402
	 * @todo Currently only used for the "<fieldset>" legend on forms
1403
	 * with multiple sections; should be used elsewhere?
1404
	 *
1405
	 * @param string $p
1406
	 *
1407
	 * @return HTMLForm $this for chaining calls (since 1.20)
1408
	 */
1409
	public function setMessagePrefix( $p ) {
1410
		$this->mMessagePrefix = $p;
1411
1412
		return $this;
1413
	}
1414
1415
	/**
1416
	 * Set the title for form submission
1417
	 *
1418
	 * @param Title $t Title of page the form is on/should be posted to
1419
	 *
1420
	 * @return HTMLForm $this for chaining calls (since 1.20)
1421
	 */
1422
	public function setTitle( $t ) {
1423
		$this->mTitle = $t;
1424
1425
		return $this;
1426
	}
1427
1428
	/**
1429
	 * Get the title
1430
	 * @return Title
1431
	 */
1432
	public function getTitle() {
1433
		return $this->mTitle === false
1434
			? $this->getContext()->getTitle()
1435
			: $this->mTitle;
1436
	}
1437
1438
	/**
1439
	 * Set the method used to submit the form
1440
	 *
1441
	 * @param string $method
1442
	 *
1443
	 * @return HTMLForm $this for chaining calls (since 1.20)
1444
	 */
1445
	public function setMethod( $method = 'post' ) {
1446
		$this->mMethod = strtolower( $method );
1447
1448
		return $this;
1449
	}
1450
1451
	/**
1452
	 * @return string Always lowercase
1453
	 */
1454
	public function getMethod() {
1455
		return $this->mMethod;
1456
	}
1457
1458
	/**
1459
	 * Wraps the given $section into an user-visible fieldset.
1460
	 *
1461
	 * @param string $legend Legend text for the fieldset
1462
	 * @param string $section The section content in plain Html
1463
	 * @param array $attributes Additional attributes for the fieldset
1464
	 * @return string The fieldset's Html
1465
	 */
1466
	protected function wrapFieldSetSection( $legend, $section, $attributes ) {
1467
		return Xml::fieldset( $legend, $section, $attributes ) . "\n";
1468
	}
1469
1470
	/**
1471
	 * @todo Document
1472
	 *
1473
	 * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1474
	 *   objects).
1475
	 * @param string $sectionName ID attribute of the "<table>" tag for this
1476
	 *   section, ignored if empty.
1477
	 * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1478
	 *   each subsection, ignored if empty.
1479
	 * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1480
	 * @throws LogicException When called on uninitialized field data, e.g. When
1481
	 *  HTMLForm::displayForm was called without calling HTMLForm::prepareForm
1482
	 *  first.
1483
	 *
1484
	 * @return string
1485
	 */
1486
	public function displaySection( $fields,
1487
		$sectionName = '',
1488
		$fieldsetIDPrefix = '',
1489
		&$hasUserVisibleFields = false
1490
	) {
1491
		if ( $this->mFieldData === null ) {
1492
			throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1493
				. 'You probably called displayForm() without calling prepareForm() first.' );
1494
		}
1495
1496
		$displayFormat = $this->getDisplayFormat();
1497
1498
		$html = [];
1499
		$subsectionHtml = '';
1500
		$hasLabel = false;
1501
1502
		// Conveniently, PHP method names are case-insensitive.
1503
		// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
1504
		$getFieldHtmlMethod = $displayFormat === 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
1505
1506
		foreach ( $fields as $key => $value ) {
1507
			if ( $value instanceof HTMLFormField ) {
1508
				$v = array_key_exists( $key, $this->mFieldData )
1509
					? $this->mFieldData[$key]
1510
					: $value->getDefault();
1511
1512
				$retval = $value->$getFieldHtmlMethod( $v );
1513
1514
				// check, if the form field should be added to
1515
				// the output.
1516
				if ( $value->hasVisibleOutput() ) {
1517
					$html[] = $retval;
1518
1519
					$labelValue = trim( $value->getLabel() );
1520
					if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
1521
						$hasLabel = true;
1522
					}
1523
1524
					$hasUserVisibleFields = true;
1525
				}
1526
			} elseif ( is_array( $value ) ) {
1527
				$subsectionHasVisibleFields = false;
1528
				$section =
1529
					$this->displaySection( $value,
1530
						"mw-htmlform-$key",
1531
						"$fieldsetIDPrefix$key-",
1532
						$subsectionHasVisibleFields );
1533
				$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...
1534
1535
				if ( $subsectionHasVisibleFields === true ) {
1536
					// Display the section with various niceties.
1537
					$hasUserVisibleFields = true;
1538
1539
					$legend = $this->getLegend( $key );
1540
1541
					$section = $this->getHeaderText( $key ) .
1542
						$section .
1543
						$this->getFooterText( $key );
1544
1545
					$attributes = [];
1546
					if ( $fieldsetIDPrefix ) {
1547
						$attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1548
					}
1549
					$subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
1550
				} else {
1551
					// Just return the inputs, nothing fancy.
1552
					$subsectionHtml .= $section;
1553
				}
1554
			}
1555
		}
1556
1557
		$html = $this->formatSection( $html, $sectionName, $hasLabel );
1558
1559
		if ( $subsectionHtml ) {
1560
			if ( $this->mSubSectionBeforeFields ) {
1561
				return $subsectionHtml . "\n" . $html;
1562
			} else {
1563
				return $html . "\n" . $subsectionHtml;
1564
			}
1565
		} else {
1566
			return $html;
1567
		}
1568
	}
1569
1570
	/**
1571
	 * Put a form section together from the individual fields' HTML, merging it and wrapping.
1572
	 * @param array $fieldsHtml
1573
	 * @param string $sectionName
1574
	 * @param bool $anyFieldHasLabel
1575
	 * @return string HTML
1576
	 */
1577
	protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1578
		$displayFormat = $this->getDisplayFormat();
1579
		$html = implode( '', $fieldsHtml );
1580
1581
		if ( $displayFormat === 'raw' ) {
1582
			return $html;
1583
		}
1584
1585
		$classes = [];
1586
1587
		if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
1588
			$classes[] = 'mw-htmlform-nolabel';
1589
		}
1590
1591
		$attribs = [
1592
			'class' => implode( ' ', $classes ),
1593
		];
1594
1595
		if ( $sectionName ) {
1596
			$attribs['id'] = Sanitizer::escapeId( $sectionName );
1597
		}
1598
1599
		if ( $displayFormat === 'table' ) {
1600
			return Html::rawElement( 'table',
1601
					$attribs,
1602
					Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1603
		} elseif ( $displayFormat === 'inline' ) {
1604
			return Html::rawElement( 'span', $attribs, "\n$html\n" );
1605
		} else {
1606
			return Html::rawElement( 'div', $attribs, "\n$html\n" );
1607
		}
1608
	}
1609
1610
	/**
1611
	 * Construct the form fields from the Descriptor array
1612
	 */
1613
	public function loadData() {
1614
		$fieldData = [];
1615
1616 View Code Duplication
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1617
			$request = $this->getRequest();
1618
			if ( $field->skipLoadData( $request ) ) {
1619
				continue;
1620
			} elseif ( !empty( $field->mParams['disabled'] ) ) {
1621
				$fieldData[$fieldname] = $field->getDefault();
1622
			} else {
1623
				$fieldData[$fieldname] = $field->loadDataFromRequest( $request );
1624
			}
1625
		}
1626
1627
		# Filter data.
1628
		foreach ( $fieldData as $name => &$value ) {
1629
			$field = $this->mFlatFields[$name];
1630
			$value = $field->filter( $value, $this->mFlatFields );
1631
		}
1632
1633
		$this->mFieldData = $fieldData;
1634
	}
1635
1636
	/**
1637
	 * Stop a reset button being shown for this form
1638
	 *
1639
	 * @param bool $suppressReset Set to false to re-enable the button again
1640
	 *
1641
	 * @return HTMLForm $this for chaining calls (since 1.20)
1642
	 */
1643
	public function suppressReset( $suppressReset = true ) {
1644
		$this->mShowReset = !$suppressReset;
1645
1646
		return $this;
1647
	}
1648
1649
	/**
1650
	 * Overload this if you want to apply special filtration routines
1651
	 * to the form as a whole, after it's submitted but before it's
1652
	 * processed.
1653
	 *
1654
	 * @param array $data
1655
	 *
1656
	 * @return array
1657
	 */
1658
	public function filterDataForSubmit( $data ) {
1659
		return $data;
1660
	}
1661
1662
	/**
1663
	 * Get a string to go in the "<legend>" of a section fieldset.
1664
	 * Override this if you want something more complicated.
1665
	 *
1666
	 * @param string $key
1667
	 *
1668
	 * @return string
1669
	 */
1670
	public function getLegend( $key ) {
1671
		return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1672
	}
1673
1674
	/**
1675
	 * Set the value for the action attribute of the form.
1676
	 * When set to false (which is the default state), the set title is used.
1677
	 *
1678
	 * @since 1.19
1679
	 *
1680
	 * @param string|bool $action
1681
	 *
1682
	 * @return HTMLForm $this for chaining calls (since 1.20)
1683
	 */
1684
	public function setAction( $action ) {
1685
		$this->mAction = $action;
1686
1687
		return $this;
1688
	}
1689
1690
	/**
1691
	 * Get the value for the action attribute of the form.
1692
	 *
1693
	 * @since 1.22
1694
	 *
1695
	 * @return string
1696
	 */
1697
	public function getAction() {
1698
		// If an action is alredy provided, return it
1699
		if ( $this->mAction !== false ) {
1700
			return $this->mAction;
1701
		}
1702
1703
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1704
		// Check whether we are in GET mode and the ArticlePath contains a "?"
1705
		// meaning that getLocalURL() would return something like "index.php?title=...".
1706
		// As browser remove the query string before submitting GET forms,
1707
		// it means that the title would be lost. In such case use wfScript() instead
1708
		// and put title in an hidden field (see getHiddenFields()).
1709
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1710
			return wfScript();
1711
		}
1712
1713
		return $this->getTitle()->getLocalURL();
1714
	}
1715
1716
	/**
1717
	 * Set the value for the autocomplete attribute of the form.
1718
	 * When set to false (which is the default state), the attribute get not set.
1719
	 *
1720
	 * @since 1.27
1721
	 *
1722
	 * @param string|bool $autocomplete
1723
	 *
1724
	 * @return HTMLForm $this for chaining calls
1725
	 */
1726
	public function setAutocomplete( $autocomplete ) {
1727
		$this->mAutocomplete = $autocomplete;
1728
1729
		return $this;
1730
	}
1731
1732
	/**
1733
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1734
	 * name + parameters array) into a Message.
1735
	 * @param mixed $value
1736
	 * @return Message
1737
	 */
1738
	protected function getMessage( $value ) {
1739
		return Message::newFromSpecifier( $value )->setContext( $this );
1740
	}
1741
}
1742