Completed
Branch master (d7c4e6)
by
unknown
29:20
created

HTMLForm::getField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 6
rs 9.4285
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
 *    'notice'              -- message text for a message to use as a notice in the field.
75
 *                             Currently used by OOUI form fields only.
76
 *    'notice-messages'     -- array of message keys/objects to use for notice.
77
 *                             Overrides 'notice'.
78
 *    'notice-message'      -- message key or object to use as a notice.
79
 *    'required'            -- passed through to the object, indicating that it
80
 *                             is a required field.
81
 *    'size'                -- the length of text fields
82
 *    'filter-callback'     -- a function name to give you the chance to
83
 *                             massage the inputted value before it's processed.
84
 *                             @see HTMLFormField::filter()
85
 *    'validation-callback' -- a function name to give you the chance
86
 *                             to impose extra validation on the field input.
87
 *                             @see HTMLFormField::validate()
88
 *    'name'                -- By default, the 'name' attribute of the input field
89
 *                             is "wp{$fieldname}".  If you want a different name
90
 *                             (eg one without the "wp" prefix), specify it here and
91
 *                             it will be used without modification.
92
 *    'hide-if'             -- expression given as an array stating when the field
93
 *                             should be hidden. The first array value has to be the
94
 *                             expression's logic operator. Supported expressions:
95
 *                               'NOT'
96
 *                                 [ 'NOT', array $expression ]
97
 *                                 To hide a field if a given expression is not true.
98
 *                               '==='
99
 *                                 [ '===', string $fieldName, string $value ]
100
 *                                 To hide a field if another field identified by
101
 *                                 $field has the value $value.
102
 *                               '!=='
103
 *                                 [ '!==', string $fieldName, string $value ]
104
 *                                 Same as [ 'NOT', [ '===', $fieldName, $value ]
105
 *                               'OR', 'AND', 'NOR', 'NAND'
106
 *                                 [ 'XXX', array $expression1, ..., array $expressionN ]
107
 *                                 To hide a field if one or more (OR), all (AND),
108
 *                                 neither (NOR) or not all (NAND) given expressions
109
 *                                 are evaluated as true.
110
 *                             The expressions will be given to a JavaScript frontend
111
 *                             module which will continually update the field's
112
 *                             visibility.
113
 *
114
 * Since 1.20, you can chain mutators to ease the form generation:
115
 * @par Example:
116
 * @code
117
 * $form = new HTMLForm( $someFields );
118
 * $form->setMethod( 'get' )
119
 *      ->setWrapperLegendMsg( 'message-key' )
120
 *      ->prepareForm()
121
 *      ->displayForm( '' );
122
 * @endcode
123
 * Note that you will have prepareForm and displayForm at the end. Other
124
 * methods call done after that would simply not be part of the form :(
125
 *
126
 * @todo Document 'section' / 'subsection' stuff
127
 */
128
class HTMLForm extends ContextSource {
129
	// A mapping of 'type' inputs onto standard HTMLFormField subclasses
130
	public static $typeMappings = [
131
		'api' => 'HTMLApiField',
132
		'text' => 'HTMLTextField',
133
		'textwithbutton' => 'HTMLTextFieldWithButton',
134
		'textarea' => 'HTMLTextAreaField',
135
		'select' => 'HTMLSelectField',
136
		'combobox' => 'HTMLComboboxField',
137
		'radio' => 'HTMLRadioField',
138
		'multiselect' => 'HTMLMultiSelectField',
139
		'limitselect' => 'HTMLSelectLimitField',
140
		'check' => 'HTMLCheckField',
141
		'toggle' => 'HTMLCheckField',
142
		'int' => 'HTMLIntField',
143
		'float' => 'HTMLFloatField',
144
		'info' => 'HTMLInfoField',
145
		'selectorother' => 'HTMLSelectOrOtherField',
146
		'selectandother' => 'HTMLSelectAndOtherField',
147
		'namespaceselect' => 'HTMLSelectNamespace',
148
		'namespaceselectwithbutton' => 'HTMLSelectNamespaceWithButton',
149
		'tagfilter' => 'HTMLTagFilter',
150
		'submit' => 'HTMLSubmitField',
151
		'hidden' => 'HTMLHiddenField',
152
		'edittools' => 'HTMLEditTools',
153
		'checkmatrix' => 'HTMLCheckMatrix',
154
		'cloner' => 'HTMLFormFieldCloner',
155
		'autocompleteselect' => 'HTMLAutoCompleteSelectField',
156
		// HTMLTextField will output the correct type="" attribute automagically.
157
		// There are about four zillion other HTML5 input types, like range, but
158
		// we don't use those at the moment, so no point in adding all of them.
159
		'email' => 'HTMLTextField',
160
		'password' => 'HTMLTextField',
161
		'url' => 'HTMLTextField',
162
		'title' => 'HTMLTitleTextField',
163
		'user' => 'HTMLUserTextField',
164
	];
165
166
	public $mFieldData;
167
168
	protected $mMessagePrefix;
169
170
	/** @var HTMLFormField[] */
171
	protected $mFlatFields;
172
173
	protected $mFieldTree;
174
	protected $mShowReset = false;
175
	protected $mShowSubmit = true;
176
	protected $mSubmitFlags = [ 'constructive', 'primary' ];
177
	protected $mShowCancel = false;
178
	protected $mCancelTarget;
179
180
	protected $mSubmitCallback;
181
	protected $mValidationErrorMessage;
182
183
	protected $mPre = '';
184
	protected $mHeader = '';
185
	protected $mFooter = '';
186
	protected $mSectionHeaders = [];
187
	protected $mSectionFooters = [];
188
	protected $mPost = '';
189
	protected $mId;
190
	protected $mName;
191
	protected $mTableId = '';
192
193
	protected $mSubmitID;
194
	protected $mSubmitName;
195
	protected $mSubmitText;
196
	protected $mSubmitTooltip;
197
198
	protected $mFormIdentifier;
199
	protected $mTitle;
200
	protected $mMethod = 'post';
201
	protected $mWasSubmitted = false;
202
203
	/**
204
	 * Form action URL. false means we will use the URL to set Title
205
	 * @since 1.19
206
	 * @var bool|string
207
	 */
208
	protected $mAction = false;
209
210
	/**
211
	 * Form attribute autocomplete. false does not set the attribute
212
	 * @since 1.27
213
	 * @var bool|string
214
	 */
215
	protected $mAutocomplete = false;
216
217
	protected $mUseMultipart = false;
218
	protected $mHiddenFields = [];
219
	protected $mButtons = [];
220
221
	protected $mWrapperLegend = false;
222
223
	/**
224
	 * Salt for the edit token.
225
	 * @var string|array
226
	 */
227
	protected $mTokenSalt = '';
228
229
	/**
230
	 * If true, sections that contain both fields and subsections will
231
	 * render their subsections before their fields.
232
	 *
233
	 * Subclasses may set this to false to render subsections after fields
234
	 * instead.
235
	 */
236
	protected $mSubSectionBeforeFields = true;
237
238
	/**
239
	 * Format in which to display form. For viable options,
240
	 * @see $availableDisplayFormats
241
	 * @var string
242
	 */
243
	protected $displayFormat = 'table';
244
245
	/**
246
	 * Available formats in which to display the form
247
	 * @var array
248
	 */
249
	protected $availableDisplayFormats = [
250
		'table',
251
		'div',
252
		'raw',
253
		'inline',
254
	];
255
256
	/**
257
	 * Available formats in which to display the form
258
	 * @var array
259
	 */
260
	protected $availableSubclassDisplayFormats = [
261
		'vform',
262
		'ooui',
263
	];
264
265
	/**
266
	 * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
267
	 *
268
	 * @param string $displayFormat
269
	 * @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...
270
	 * @return HTMLForm
271
	 */
272
	public static function factory( $displayFormat/*, $arguments...*/ ) {
273
		$arguments = func_get_args();
274
		array_shift( $arguments );
275
276
		switch ( $displayFormat ) {
277
			case 'vform':
278
				return ObjectFactory::constructClassInstance( VFormHTMLForm::class, $arguments );
279
			case 'ooui':
280
				return ObjectFactory::constructClassInstance( OOUIHTMLForm::class, $arguments );
281
			default:
282
				/** @var HTMLForm $form */
283
				$form = ObjectFactory::constructClassInstance( HTMLForm::class, $arguments );
284
				$form->setDisplayFormat( $displayFormat );
285
				return $form;
286
		}
287
	}
288
289
	/**
290
	 * Build a new HTMLForm from an array of field attributes
291
	 *
292
	 * @param array $descriptor Array of Field constructs, as described above
293
	 * @param IContextSource $context Available since 1.18, will become compulsory in 1.18.
294
	 *     Obviates the need to call $form->setTitle()
295
	 * @param string $messagePrefix A prefix to go in front of default messages
296
	 */
297
	public function __construct( $descriptor, /*IContextSource*/ $context = null,
298
		$messagePrefix = ''
299
	) {
300
		if ( $context instanceof IContextSource ) {
301
			$this->setContext( $context );
302
			$this->mTitle = false; // We don't need them to set a title
303
			$this->mMessagePrefix = $messagePrefix;
304
		} elseif ( $context === null && $messagePrefix !== '' ) {
305
			$this->mMessagePrefix = $messagePrefix;
306
		} elseif ( is_string( $context ) && $messagePrefix === '' ) {
307
			// B/C since 1.18
308
			// it's actually $messagePrefix
309
			$this->mMessagePrefix = $context;
310
		}
311
312
		// Evil hack for mobile :(
313
		if (
314
			!$this->getConfig()->get( 'HTMLFormAllowTableFormat' )
315
			&& $this->displayFormat === 'table'
316
		) {
317
			$this->displayFormat = 'div';
318
		}
319
320
		// Expand out into a tree.
321
		$loadedDescriptor = [];
322
		$this->mFlatFields = [];
323
324
		foreach ( $descriptor as $fieldname => $info ) {
325
			$section = isset( $info['section'] )
326
				? $info['section']
327
				: '';
328
329
			if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
330
				$this->mUseMultipart = true;
331
			}
332
333
			$field = static::loadInputFromParameters( $fieldname, $info, $this );
334
335
			$setSection =& $loadedDescriptor;
336
			if ( $section ) {
337
				$sectionParts = explode( '/', $section );
338
339
				while ( count( $sectionParts ) ) {
340
					$newName = array_shift( $sectionParts );
341
342
					if ( !isset( $setSection[$newName] ) ) {
343
						$setSection[$newName] = [];
344
					}
345
346
					$setSection =& $setSection[$newName];
347
				}
348
			}
349
350
			$setSection[$fieldname] = $field;
351
			$this->mFlatFields[$fieldname] = $field;
352
		}
353
354
		$this->mFieldTree = $loadedDescriptor;
355
	}
356
357
	/**
358
	 * @param string $fieldname
359
	 * @return bool
360
	 */
361
	public function hasField( $fieldname ) {
362
		return isset( $this->mFlatFields[$fieldname] );
363
	}
364
365
	/**
366
	 * @param string $fieldname
367
	 * @return HTMLFormField
368
	 * @throws DomainException on invalid field name
369
	 */
370
	public function getField( $fieldname ) {
371
		if ( !$this->hasField( $fieldname ) ) {
372
			throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
373
		}
374
		return $this->mFlatFields[$fieldname];
375
	}
376
377
	/**
378
	 * Set format in which to display the form
379
	 *
380
	 * @param string $format The name of the format to use, must be one of
381
	 *   $this->availableDisplayFormats
382
	 *
383
	 * @throws MWException
384
	 * @since 1.20
385
	 * @return HTMLForm $this for chaining calls (since 1.20)
386
	 */
387
	public function setDisplayFormat( $format ) {
388
		if (
389
			in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
390
			in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
391
		) {
392
			throw new MWException( 'Cannot change display format after creation, ' .
393
				'use HTMLForm::factory() instead' );
394
		}
395
396
		if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
397
			throw new MWException( 'Display format must be one of ' .
398
				print_r( $this->availableDisplayFormats, true ) );
399
		}
400
401
		// Evil hack for mobile :(
402
		if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
403
			$format = 'div';
404
		}
405
406
		$this->displayFormat = $format;
407
408
		return $this;
409
	}
410
411
	/**
412
	 * Getter for displayFormat
413
	 * @since 1.20
414
	 * @return string
415
	 */
416
	public function getDisplayFormat() {
417
		return $this->displayFormat;
418
	}
419
420
	/**
421
	 * Test if displayFormat is 'vform'
422
	 * @since 1.22
423
	 * @deprecated since 1.25
424
	 * @return bool
425
	 */
426
	public function isVForm() {
427
		wfDeprecated( __METHOD__, '1.25' );
428
		return false;
429
	}
430
431
	/**
432
	 * Get the HTMLFormField subclass for this descriptor.
433
	 *
434
	 * The descriptor can be passed either 'class' which is the name of
435
	 * a HTMLFormField subclass, or a shorter 'type' which is an alias.
436
	 * This makes sure the 'class' is always set, and also is returned by
437
	 * this function for ease.
438
	 *
439
	 * @since 1.23
440
	 *
441
	 * @param string $fieldname Name of the field
442
	 * @param array $descriptor Input Descriptor, as described above
443
	 *
444
	 * @throws MWException
445
	 * @return string Name of a HTMLFormField subclass
446
	 */
447
	public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
448
		if ( isset( $descriptor['class'] ) ) {
449
			$class = $descriptor['class'];
450
		} elseif ( isset( $descriptor['type'] ) ) {
451
			$class = static::$typeMappings[$descriptor['type']];
452
			$descriptor['class'] = $class;
453
		} else {
454
			$class = null;
455
		}
456
457
		if ( !$class ) {
458
			throw new MWException( "Descriptor with no class for $fieldname: "
459
				. print_r( $descriptor, true ) );
460
		}
461
462
		return $class;
463
	}
464
465
	/**
466
	 * Initialise a new Object for the field
467
	 *
468
	 * @param string $fieldname Name of the field
469
	 * @param array $descriptor Input Descriptor, as described above
470
	 * @param HTMLForm|null $parent Parent instance of HTMLForm
471
	 *
472
	 * @throws MWException
473
	 * @return HTMLFormField Instance of a subclass of HTMLFormField
474
	 */
475
	public static function loadInputFromParameters( $fieldname, $descriptor,
476
		HTMLForm $parent = null
477
	) {
478
		$class = static::getClassFromDescriptor( $fieldname, $descriptor );
479
480
		$descriptor['fieldname'] = $fieldname;
481
		if ( $parent ) {
482
			$descriptor['parent'] = $parent;
483
		}
484
485
		# @todo This will throw a fatal error whenever someone try to use
486
		# 'class' to feed a CSS class instead of 'cssclass'. Would be
487
		# great to avoid the fatal error and show a nice error.
488
		return new $class( $descriptor );
489
	}
490
491
	/**
492
	 * Prepare form for submission.
493
	 *
494
	 * @attention When doing method chaining, that should be the very last
495
	 * method call before displayForm().
496
	 *
497
	 * @throws MWException
498
	 * @return HTMLForm $this for chaining calls (since 1.20)
499
	 */
500
	public function prepareForm() {
501
		# Check if we have the info we need
502
		if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
503
			throw new MWException( 'You must call setTitle() on an HTMLForm' );
504
		}
505
506
		# Load data from the request.
507
		if (
508
			$this->mFormIdentifier === null ||
509
			$this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier
510
		) {
511
			$this->loadData();
512
		} else {
513
			$this->mFieldData = [];
514
		}
515
516
		return $this;
517
	}
518
519
	/**
520
	 * Try submitting, with edit token check first
521
	 * @return Status|bool
522
	 */
523
	public function tryAuthorizedSubmit() {
524
		$result = false;
525
526
		$identOkay = false;
0 ignored issues
show
Unused Code introduced by
$identOkay 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...
527
		if ( $this->mFormIdentifier === null ) {
528
			$identOkay = true;
529
		} else {
530
			$identOkay = $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier;
531
		}
532
533
		$tokenOkay = false;
534
		if ( $this->getMethod() !== 'post' ) {
535
			$tokenOkay = true; // no session check needed
536
		} elseif ( $this->getRequest()->wasPosted() ) {
537
			$editToken = $this->getRequest()->getVal( 'wpEditToken' );
538
			if ( $this->getUser()->isLoggedIn() || $editToken !== null ) {
539
				// Session tokens for logged-out users have no security value.
540
				// However, if the user gave one, check it in order to give a nice
541
				// "session expired" error instead of "permission denied" or such.
542
				$tokenOkay = $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...
543
			} else {
544
				$tokenOkay = true;
545
			}
546
		}
547
548
		if ( $tokenOkay && $identOkay ) {
549
			$this->mWasSubmitted = true;
550
			$result = $this->trySubmit();
551
		}
552
553
		return $result;
554
	}
555
556
	/**
557
	 * The here's-one-I-made-earlier option: do the submission if
558
	 * posted, or display the form with or without funky validation
559
	 * errors
560
	 * @return bool|Status Whether submission was successful.
561
	 */
562
	public function show() {
563
		$this->prepareForm();
564
565
		$result = $this->tryAuthorizedSubmit();
566
		if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
567
			return $result;
568
		}
569
570
		$this->displayForm( $result );
571
572
		return false;
573
	}
574
575
	/**
576
	 * Same as self::show with the difference, that the form will be
577
	 * added to the output, no matter, if the validation was good or not.
578
	 * @return bool|Status Whether submission was successful.
579
	 */
580
	public function showAlways() {
581
		$this->prepareForm();
582
583
		$result = $this->tryAuthorizedSubmit();
584
585
		$this->displayForm( $result );
586
587
		return $result;
588
	}
589
590
	/**
591
	 * Validate all the fields, and call the submission callback
592
	 * function if everything is kosher.
593
	 * @throws MWException
594
	 * @return bool|string|array|Status
595
	 *     - Bool true or a good Status object indicates success,
596
	 *     - Bool false indicates no submission was attempted,
597
	 *     - Anything else indicates failure. The value may be a fatal Status
598
	 *       object, an HTML string, or an array of arrays (message keys and
599
	 *       params) or strings (message keys)
600
	 */
601
	public function trySubmit() {
602
		$valid = true;
603
		$hoistedErrors = [];
604
		$hoistedErrors[] = isset( $this->mValidationErrorMessage )
605
			? $this->mValidationErrorMessage
606
			: [ 'htmlform-invalid-input' ];
607
608
		$this->mWasSubmitted = true;
609
610
		# Check for cancelled submission
611
		foreach ( $this->mFlatFields as $fieldname => $field ) {
612
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
613
				continue;
614
			}
615
			if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
616
				$this->mWasSubmitted = false;
617
				return false;
618
			}
619
		}
620
621
		# Check for validation
622
		foreach ( $this->mFlatFields as $fieldname => $field ) {
623
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
624
				continue;
625
			}
626
			if ( $field->isHidden( $this->mFieldData ) ) {
627
				continue;
628
			}
629
			$res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
630
			if ( $res !== true ) {
631
				$valid = false;
632
				if ( $res !== false && !$field->canDisplayErrors() ) {
633
					$hoistedErrors[] = [ 'rawmessage', $res ];
634
				}
635
			}
636
		}
637
638
		if ( !$valid ) {
639
			if ( count( $hoistedErrors ) === 1 ) {
640
				$hoistedErrors = $hoistedErrors[0];
641
			}
642
			return $hoistedErrors;
643
		}
644
645
		$callback = $this->mSubmitCallback;
646
		if ( !is_callable( $callback ) ) {
647
			throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
648
				'setSubmitCallback() to set one.' );
649
		}
650
651
		$data = $this->filterDataForSubmit( $this->mFieldData );
652
653
		$res = call_user_func( $callback, $data, $this );
654
		if ( $res === false ) {
655
			$this->mWasSubmitted = false;
656
		}
657
658
		return $res;
659
	}
660
661
	/**
662
	 * Test whether the form was considered to have been submitted or not, i.e.
663
	 * whether the last call to tryAuthorizedSubmit or trySubmit returned
664
	 * non-false.
665
	 *
666
	 * This will return false until HTMLForm::tryAuthorizedSubmit or
667
	 * HTMLForm::trySubmit is called.
668
	 *
669
	 * @since 1.23
670
	 * @return bool
671
	 */
672
	public function wasSubmitted() {
673
		return $this->mWasSubmitted;
674
	}
675
676
	/**
677
	 * Set a callback to a function to do something with the form
678
	 * once it's been successfully validated.
679
	 *
680
	 * @param callable $cb The function will be passed the output from
681
	 *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
682
	 *   return as documented for HTMLForm::trySubmit
683
	 *
684
	 * @return HTMLForm $this for chaining calls (since 1.20)
685
	 */
686
	public function setSubmitCallback( $cb ) {
687
		$this->mSubmitCallback = $cb;
688
689
		return $this;
690
	}
691
692
	/**
693
	 * Set a message to display on a validation error.
694
	 *
695
	 * @param string|array $msg String or Array of valid inputs to wfMessage()
696
	 *     (so each entry can be either a String or Array)
697
	 *
698
	 * @return HTMLForm $this for chaining calls (since 1.20)
699
	 */
700
	public function setValidationErrorMessage( $msg ) {
701
		$this->mValidationErrorMessage = $msg;
702
703
		return $this;
704
	}
705
706
	/**
707
	 * Set the introductory message, overwriting any existing message.
708
	 *
709
	 * @param string $msg Complete text of message to display
710
	 *
711
	 * @return HTMLForm $this for chaining calls (since 1.20)
712
	 */
713
	public function setIntro( $msg ) {
714
		$this->setPreText( $msg );
715
716
		return $this;
717
	}
718
719
	/**
720
	 * Set the introductory message HTML, overwriting any existing message.
721
	 * @since 1.19
722
	 *
723
	 * @param string $msg Complete HTML of message to display
724
	 *
725
	 * @return HTMLForm $this for chaining calls (since 1.20)
726
	 */
727
	public function setPreText( $msg ) {
728
		$this->mPre = $msg;
729
730
		return $this;
731
	}
732
733
	/**
734
	 * Add HTML to introductory message.
735
	 *
736
	 * @param string $msg Complete HTML of message to display
737
	 *
738
	 * @return HTMLForm $this for chaining calls (since 1.20)
739
	 */
740
	public function addPreText( $msg ) {
741
		$this->mPre .= $msg;
742
743
		return $this;
744
	}
745
746
	/**
747
	 * Add HTML to the header, inside the form.
748
	 *
749
	 * @param string $msg Additional HTML to display in header
750
	 * @param string|null $section The section to add the header to
751
	 *
752
	 * @return HTMLForm $this for chaining calls (since 1.20)
753
	 */
754 View Code Duplication
	public function addHeaderText( $msg, $section = null ) {
755
		if ( $section === null ) {
756
			$this->mHeader .= $msg;
757
		} else {
758
			if ( !isset( $this->mSectionHeaders[$section] ) ) {
759
				$this->mSectionHeaders[$section] = '';
760
			}
761
			$this->mSectionHeaders[$section] .= $msg;
762
		}
763
764
		return $this;
765
	}
766
767
	/**
768
	 * Set header text, inside the form.
769
	 * @since 1.19
770
	 *
771
	 * @param string $msg Complete HTML of header to display
772
	 * @param string|null $section The section to add the header to
773
	 *
774
	 * @return HTMLForm $this for chaining calls (since 1.20)
775
	 */
776
	public function setHeaderText( $msg, $section = null ) {
777
		if ( $section === null ) {
778
			$this->mHeader = $msg;
779
		} else {
780
			$this->mSectionHeaders[$section] = $msg;
781
		}
782
783
		return $this;
784
	}
785
786
	/**
787
	 * Get header text.
788
	 *
789
	 * @param string|null $section The section to get the header text for
790
	 * @since 1.26
791
	 * @return string HTML
792
	 */
793
	public function getHeaderText( $section = null ) {
794
		if ( $section === null ) {
795
			return $this->mHeader;
796
		} else {
797
			return isset( $this->mSectionHeaders[$section] ) ? $this->mSectionHeaders[$section] : '';
798
		}
799
	}
800
801
	/**
802
	 * Add footer text, inside the form.
803
	 *
804
	 * @param string $msg Complete text of message to display
805
	 * @param string|null $section The section to add the footer text to
806
	 *
807
	 * @return HTMLForm $this for chaining calls (since 1.20)
808
	 */
809 View Code Duplication
	public function addFooterText( $msg, $section = null ) {
810
		if ( $section === null ) {
811
			$this->mFooter .= $msg;
812
		} else {
813
			if ( !isset( $this->mSectionFooters[$section] ) ) {
814
				$this->mSectionFooters[$section] = '';
815
			}
816
			$this->mSectionFooters[$section] .= $msg;
817
		}
818
819
		return $this;
820
	}
821
822
	/**
823
	 * Set footer text, inside the form.
824
	 * @since 1.19
825
	 *
826
	 * @param string $msg Complete text of message to display
827
	 * @param string|null $section The section to add the footer text to
828
	 *
829
	 * @return HTMLForm $this for chaining calls (since 1.20)
830
	 */
831
	public function setFooterText( $msg, $section = null ) {
832
		if ( $section === null ) {
833
			$this->mFooter = $msg;
834
		} else {
835
			$this->mSectionFooters[$section] = $msg;
836
		}
837
838
		return $this;
839
	}
840
841
	/**
842
	 * Get footer text.
843
	 *
844
	 * @param string|null $section The section to get the footer text for
845
	 * @since 1.26
846
	 * @return string
847
	 */
848
	public function getFooterText( $section = null ) {
849
		if ( $section === null ) {
850
			return $this->mFooter;
851
		} else {
852
			return isset( $this->mSectionFooters[$section] ) ? $this->mSectionFooters[$section] : '';
853
		}
854
	}
855
856
	/**
857
	 * Add text to the end of the display.
858
	 *
859
	 * @param string $msg Complete text of message to display
860
	 *
861
	 * @return HTMLForm $this for chaining calls (since 1.20)
862
	 */
863
	public function addPostText( $msg ) {
864
		$this->mPost .= $msg;
865
866
		return $this;
867
	}
868
869
	/**
870
	 * Set text at the end of the display.
871
	 *
872
	 * @param string $msg Complete text of message to display
873
	 *
874
	 * @return HTMLForm $this for chaining calls (since 1.20)
875
	 */
876
	public function setPostText( $msg ) {
877
		$this->mPost = $msg;
878
879
		return $this;
880
	}
881
882
	/**
883
	 * Add a hidden field to the output
884
	 *
885
	 * @param string $name Field name.  This will be used exactly as entered
886
	 * @param string $value Field value
887
	 * @param array $attribs
888
	 *
889
	 * @return HTMLForm $this for chaining calls (since 1.20)
890
	 */
891
	public function addHiddenField( $name, $value, array $attribs = [] ) {
892
		$attribs += [ 'name' => $name ];
893
		$this->mHiddenFields[] = [ $value, $attribs ];
894
895
		return $this;
896
	}
897
898
	/**
899
	 * Add an array of hidden fields to the output
900
	 *
901
	 * @since 1.22
902
	 *
903
	 * @param array $fields Associative array of fields to add;
904
	 *        mapping names to their values
905
	 *
906
	 * @return HTMLForm $this for chaining calls
907
	 */
908
	public function addHiddenFields( array $fields ) {
909
		foreach ( $fields as $name => $value ) {
910
			$this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
911
		}
912
913
		return $this;
914
	}
915
916
	/**
917
	 * Add a button to the form
918
	 *
919
	 * @since 1.27 takes an array as shown. Earlier versions accepted
920
	 *  'name', 'value', 'id', and 'attribs' as separate parameters in that
921
	 *  order.
922
	 * @note Custom labels ('label', 'label-message', 'label-raw') are not
923
	 *  supported for IE6 and IE7 due to bugs in those browsers. If detected,
924
	 *  they will be served buttons using 'value' as the button label.
925
	 * @param array $data Data to define the button:
926
	 *  - name: (string) Button name.
927
	 *  - value: (string) Button value.
928
	 *  - label-message: (string, optional) Button label message key to use
929
	 *    instead of 'value'. Overrides 'label' and 'label-raw'.
930
	 *  - label: (string, optional) Button label text to use instead of
931
	 *    'value'. Overrides 'label-raw'.
932
	 *  - label-raw: (string, optional) Button label HTML to use instead of
933
	 *    'value'.
934
	 *  - id: (string, optional) DOM id for the button.
935
	 *  - attribs: (array, optional) Additional HTML attributes.
936
	 *  - flags: (string|string[], optional) OOUI flags.
937
	 *  - framed: (boolean=true, optional) OOUI framed attribute.
938
	 * @return HTMLForm $this for chaining calls (since 1.20)
939
	 */
940
	public function addButton( $data ) {
941
		if ( !is_array( $data ) ) {
942
			$args = func_get_args();
943
			if ( count( $args ) < 2 || count( $args ) > 4 ) {
944
				throw new InvalidArgumentException(
945
					'Incorrect number of arguments for deprecated calling style'
946
				);
947
			}
948
			$data = [
949
				'name' => $args[0],
950
				'value' => $args[1],
951
				'id' => isset( $args[2] ) ? $args[2] : null,
952
				'attribs' => isset( $args[3] ) ? $args[3] : null,
953
			];
954
		} else {
955
			if ( !isset( $data['name'] ) ) {
956
				throw new InvalidArgumentException( 'A name is required' );
957
			}
958
			if ( !isset( $data['value'] ) ) {
959
				throw new InvalidArgumentException( 'A value is required' );
960
			}
961
		}
962
		$this->mButtons[] = $data + [
963
			'id' => null,
964
			'attribs' => null,
965
			'flags' => null,
966
			'framed' => true,
967
		];
968
969
		return $this;
970
	}
971
972
	/**
973
	 * Set the salt for the edit token.
974
	 *
975
	 * Only useful when the method is "post".
976
	 *
977
	 * @since 1.24
978
	 * @param string|array $salt Salt to use
979
	 * @return HTMLForm $this For chaining calls
980
	 */
981
	public function setTokenSalt( $salt ) {
982
		$this->mTokenSalt = $salt;
983
984
		return $this;
985
	}
986
987
	/**
988
	 * Display the form (sending to the context's OutputPage object), with an
989
	 * appropriate error message or stack of messages, and any validation errors, etc.
990
	 *
991
	 * @attention You should call prepareForm() before calling this function.
992
	 * Moreover, when doing method chaining this should be the very last method
993
	 * call just after prepareForm().
994
	 *
995
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
996
	 *
997
	 * @return void Nothing, should be last call
998
	 */
999
	public function displayForm( $submitResult ) {
1000
		$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
1001
	}
1002
1003
	/**
1004
	 * Returns the raw HTML generated by the form
1005
	 *
1006
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
1007
	 *
1008
	 * @return string HTML
1009
	 */
1010
	public function getHTML( $submitResult ) {
1011
		# For good measure (it is the default)
1012
		$this->getOutput()->preventClickjacking();
1013
		$this->getOutput()->addModules( 'mediawiki.htmlform' );
1014
		$this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
1015
1016
		$html = ''
1017
			. $this->getErrors( $submitResult )
1018
			. $this->getHeaderText()
1019
			. $this->getBody()
1020
			. $this->getHiddenFields()
1021
			. $this->getButtons()
1022
			. $this->getFooterText();
1023
1024
		$html = $this->wrapForm( $html );
1025
1026
		return '' . $this->mPre . $html . $this->mPost;
1027
	}
1028
1029
	/**
1030
	 * Get HTML attributes for the `<form>` tag.
1031
	 * @return array
1032
	 */
1033
	protected function getFormAttributes() {
1034
		# Use multipart/form-data
1035
		$encType = $this->mUseMultipart
1036
			? 'multipart/form-data'
1037
			: 'application/x-www-form-urlencoded';
1038
		# Attributes
1039
		$attribs = [
1040
			'action' => $this->getAction(),
1041
			'method' => $this->getMethod(),
1042
			'enctype' => $encType,
1043
		];
1044
		if ( $this->mId ) {
1045
			$attribs['id'] = $this->mId;
1046
		}
1047
		if ( $this->mAutocomplete ) {
1048
			$attribs['autocomplete'] = $this->mAutocomplete;
1049
		}
1050
		if ( $this->mName ) {
1051
			$attribs['name'] = $this->mName;
1052
		}
1053
		return $attribs;
1054
	}
1055
1056
	/**
1057
	 * Wrap the form innards in an actual "<form>" element
1058
	 *
1059
	 * @param string $html HTML contents to wrap.
1060
	 *
1061
	 * @return string Wrapped HTML.
1062
	 */
1063
	public function wrapForm( $html ) {
1064
		# Include a <fieldset> wrapper for style, if requested.
1065
		if ( $this->mWrapperLegend !== false ) {
1066
			$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1067
			$html = Xml::fieldset( $legend, $html );
1068
		}
1069
1070
		return Html::rawElement(
1071
			'form',
1072
			$this->getFormAttributes() + [ 'class' => 'visualClear' ],
1073
			$html
1074
		);
1075
	}
1076
1077
	/**
1078
	 * Get the hidden fields that should go inside the form.
1079
	 * @return string HTML.
1080
	 */
1081
	public function getHiddenFields() {
1082
		$html = '';
1083
		if ( $this->mFormIdentifier !== null ) {
1084
			$html .= Html::hidden(
1085
				'wpFormIdentifier',
1086
				$this->mFormIdentifier
1087
			) . "\n";
1088
		}
1089
		if ( $this->getMethod() === 'post' ) {
1090
			$html .= Html::hidden(
1091
				'wpEditToken',
1092
				$this->getUser()->getEditToken( $this->mTokenSalt ),
1093
				[ 'id' => 'wpEditToken' ]
1094
			) . "\n";
1095
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1096
		}
1097
1098
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1099
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1100
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1101
		}
1102
1103
		foreach ( $this->mHiddenFields as $data ) {
1104
			list( $value, $attribs ) = $data;
1105
			$html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1106
		}
1107
1108
		return $html;
1109
	}
1110
1111
	/**
1112
	 * Get the submit and (potentially) reset buttons.
1113
	 * @return string HTML.
1114
	 */
1115
	public function getButtons() {
1116
		$buttons = '';
1117
		$useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
1118
1119
		if ( $this->mShowSubmit ) {
1120
			$attribs = [];
1121
1122
			if ( isset( $this->mSubmitID ) ) {
1123
				$attribs['id'] = $this->mSubmitID;
1124
			}
1125
1126
			if ( isset( $this->mSubmitName ) ) {
1127
				$attribs['name'] = $this->mSubmitName;
1128
			}
1129
1130
			if ( isset( $this->mSubmitTooltip ) ) {
1131
				$attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1132
			}
1133
1134
			$attribs['class'] = [ 'mw-htmlform-submit' ];
1135
1136
			if ( $useMediaWikiUIEverywhere ) {
1137
				foreach ( $this->mSubmitFlags as $flag ) {
1138
					$attribs['class'][] = 'mw-ui-' . $flag;
1139
				}
1140
				$attribs['class'][] = 'mw-ui-button';
1141
			}
1142
1143
			$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1144
		}
1145
1146 View Code Duplication
		if ( $this->mShowReset ) {
1147
			$buttons .= Html::element(
1148
				'input',
1149
				[
1150
					'type' => 'reset',
1151
					'value' => $this->msg( 'htmlform-reset' )->text(),
1152
					'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1153
				]
1154
			) . "\n";
1155
		}
1156
1157 View Code Duplication
		if ( $this->mShowCancel ) {
1158
			$target = $this->mCancelTarget ?: Title::newMainPage();
1159
			if ( $target instanceof Title ) {
1160
				$target = $target->getLocalURL();
1161
			}
1162
			$buttons .= Html::element(
1163
					'a',
1164
					[
1165
						'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1166
						'href' => $target,
1167
					],
1168
					$this->msg( 'cancel' )->text()
1169
				) . "\n";
1170
		}
1171
1172
		// IE<8 has bugs with <button>, so we'll need to avoid them.
1173
		$isBadIE = preg_match( '/MSIE [1-7]\./i', $this->getRequest()->getHeader( 'User-Agent' ) );
1174
1175
		foreach ( $this->mButtons as $button ) {
1176
			$attrs = [
1177
				'type' => 'submit',
1178
				'name' => $button['name'],
1179
				'value' => $button['value']
1180
			];
1181
1182
			if ( isset( $button['label-message'] ) ) {
1183
				$label = $this->getMessage( $button['label-message'] )->parse();
1184
			} elseif ( isset( $button['label'] ) ) {
1185
				$label = htmlspecialchars( $button['label'] );
1186
			} elseif ( isset( $button['label-raw'] ) ) {
1187
				$label = $button['label-raw'];
1188
			} else {
1189
				$label = htmlspecialchars( $button['value'] );
1190
			}
1191
1192
			if ( $button['attribs'] ) {
1193
				$attrs += $button['attribs'];
1194
			}
1195
1196
			if ( isset( $button['id'] ) ) {
1197
				$attrs['id'] = $button['id'];
1198
			}
1199
1200
			if ( $useMediaWikiUIEverywhere ) {
1201
				$attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : [];
1202
				$attrs['class'][] = 'mw-ui-button';
1203
			}
1204
1205
			if ( $isBadIE ) {
1206
				$buttons .= Html::element( 'input', $attrs ) . "\n";
1207
			} else {
1208
				$buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1209
			}
1210
		}
1211
1212
		if ( !$buttons ) {
1213
			return '';
1214
		}
1215
1216
		return Html::rawElement( 'span',
1217
			[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1218
	}
1219
1220
	/**
1221
	 * Get the whole body of the form.
1222
	 * @return string
1223
	 */
1224
	public function getBody() {
1225
		return $this->displaySection( $this->mFieldTree, $this->mTableId );
1226
	}
1227
1228
	/**
1229
	 * Format and display an error message stack.
1230
	 *
1231
	 * @param string|array|Status $errors
1232
	 *
1233
	 * @return string
1234
	 */
1235
	public function getErrors( $errors ) {
1236
		if ( $errors instanceof Status ) {
1237
			if ( $errors->isOK() ) {
1238
				$errorstr = '';
1239
			} else {
1240
				$errorstr = $this->getOutput()->parse( $errors->getWikiText() );
1241
			}
1242
		} elseif ( is_array( $errors ) ) {
1243
			$errorstr = $this->formatErrors( $errors );
1244
		} else {
1245
			$errorstr = $errors;
1246
		}
1247
1248
		return $errorstr
1249
			? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr )
1250
			: '';
1251
	}
1252
1253
	/**
1254
	 * Format a stack of error messages into a single HTML string
1255
	 *
1256
	 * @param array $errors Array of message keys/values
1257
	 *
1258
	 * @return string HTML, a "<ul>" list of errors
1259
	 */
1260
	public function formatErrors( $errors ) {
1261
		$errorstr = '';
1262
1263
		foreach ( $errors as $error ) {
1264
			$errorstr .= Html::rawElement(
1265
				'li',
1266
				[],
1267
				$this->getMessage( $error )->parse()
1268
			);
1269
		}
1270
1271
		$errorstr = Html::rawElement( 'ul', [], $errorstr );
1272
1273
		return $errorstr;
1274
	}
1275
1276
	/**
1277
	 * Set the text for the submit button
1278
	 *
1279
	 * @param string $t Plaintext
1280
	 *
1281
	 * @return HTMLForm $this for chaining calls (since 1.20)
1282
	 */
1283
	public function setSubmitText( $t ) {
1284
		$this->mSubmitText = $t;
1285
1286
		return $this;
1287
	}
1288
1289
	/**
1290
	 * Identify that the submit button in the form has a destructive action
1291
	 * @since 1.24
1292
	 *
1293
	 * @return HTMLForm $this for chaining calls (since 1.28)
1294
	 */
1295
	public function setSubmitDestructive() {
1296
		$this->mSubmitFlags = [ 'destructive', 'primary' ];
1297
1298
		return $this;
1299
	}
1300
1301
	/**
1302
	 * Identify that the submit button in the form has a progressive action
1303
	 * @since 1.25
1304
	 *
1305
	 * @return HTMLForm $this for chaining calls (since 1.28)
1306
	 */
1307
	public function setSubmitProgressive() {
1308
		$this->mSubmitFlags = [ 'progressive', 'primary' ];
1309
1310
		return $this;
1311
	}
1312
1313
	/**
1314
	 * Set the text for the submit button to a message
1315
	 * @since 1.19
1316
	 *
1317
	 * @param string|Message $msg Message key or Message object
1318
	 *
1319
	 * @return HTMLForm $this for chaining calls (since 1.20)
1320
	 */
1321
	public function setSubmitTextMsg( $msg ) {
1322
		if ( !$msg instanceof Message ) {
1323
			$msg = $this->msg( $msg );
1324
		}
1325
		$this->setSubmitText( $msg->text() );
1326
1327
		return $this;
1328
	}
1329
1330
	/**
1331
	 * Get the text for the submit button, either customised or a default.
1332
	 * @return string
1333
	 */
1334
	public function getSubmitText() {
1335
		return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1336
	}
1337
1338
	/**
1339
	 * @param string $name Submit button name
1340
	 *
1341
	 * @return HTMLForm $this for chaining calls (since 1.20)
1342
	 */
1343
	public function setSubmitName( $name ) {
1344
		$this->mSubmitName = $name;
1345
1346
		return $this;
1347
	}
1348
1349
	/**
1350
	 * @param string $name Tooltip for the submit button
1351
	 *
1352
	 * @return HTMLForm $this for chaining calls (since 1.20)
1353
	 */
1354
	public function setSubmitTooltip( $name ) {
1355
		$this->mSubmitTooltip = $name;
1356
1357
		return $this;
1358
	}
1359
1360
	/**
1361
	 * Set the id for the submit button.
1362
	 *
1363
	 * @param string $t
1364
	 *
1365
	 * @todo FIXME: Integrity of $t is *not* validated
1366
	 * @return HTMLForm $this for chaining calls (since 1.20)
1367
	 */
1368
	public function setSubmitID( $t ) {
1369
		$this->mSubmitID = $t;
1370
1371
		return $this;
1372
	}
1373
1374
	/**
1375
	 * Set an internal identifier for this form. It will be submitted as a hidden form field, allowing
1376
	 * HTMLForm to determine whether the form was submitted (or merely viewed). Setting this serves
1377
	 * two purposes:
1378
	 *
1379
	 * - If you use two or more forms on one page, it allows HTMLForm to identify which of the forms
1380
	 *   was submitted, and not attempt to validate the other ones.
1381
	 * - If you use checkbox or multiselect fields inside a form using the GET method, it allows
1382
	 *   HTMLForm to distinguish between the initial page view and a form submission with all
1383
	 *   checkboxes or select options unchecked.
1384
	 *
1385
	 * @since 1.28
1386
	 * @param string $ident
1387
	 * @return $this
1388
	 */
1389
	public function setFormIdentifier( $ident ) {
1390
		$this->mFormIdentifier = $ident;
1391
1392
		return $this;
1393
	}
1394
1395
	/**
1396
	 * Stop a default submit button being shown for this form. This implies that an
1397
	 * alternate submit method must be provided manually.
1398
	 *
1399
	 * @since 1.22
1400
	 *
1401
	 * @param bool $suppressSubmit Set to false to re-enable the button again
1402
	 *
1403
	 * @return HTMLForm $this for chaining calls
1404
	 */
1405
	public function suppressDefaultSubmit( $suppressSubmit = true ) {
1406
		$this->mShowSubmit = !$suppressSubmit;
1407
1408
		return $this;
1409
	}
1410
1411
	/**
1412
	 * Show a cancel button (or prevent it). The button is not shown by default.
1413
	 * @param bool $show
1414
	 * @return HTMLForm $this for chaining calls
1415
	 * @since 1.27
1416
	 */
1417
	public function showCancel( $show = true ) {
1418
		$this->mShowCancel = $show;
1419
		return $this;
1420
	}
1421
1422
	/**
1423
	 * Sets the target where the user is redirected to after clicking cancel.
1424
	 * @param Title|string $target Target as a Title object or an URL
1425
	 * @return HTMLForm $this for chaining calls
1426
	 * @since 1.27
1427
	 */
1428
	public function setCancelTarget( $target ) {
1429
		$this->mCancelTarget = $target;
1430
		return $this;
1431
	}
1432
1433
	/**
1434
	 * Set the id of the \<table\> or outermost \<div\> element.
1435
	 *
1436
	 * @since 1.22
1437
	 *
1438
	 * @param string $id New value of the id attribute, or "" to remove
1439
	 *
1440
	 * @return HTMLForm $this for chaining calls
1441
	 */
1442
	public function setTableId( $id ) {
1443
		$this->mTableId = $id;
1444
1445
		return $this;
1446
	}
1447
1448
	/**
1449
	 * @param string $id DOM id for the form
1450
	 *
1451
	 * @return HTMLForm $this for chaining calls (since 1.20)
1452
	 */
1453
	public function setId( $id ) {
1454
		$this->mId = $id;
1455
1456
		return $this;
1457
	}
1458
1459
	/**
1460
	 * @param string $name 'name' attribute for the form
1461
	 * @return HTMLForm $this for chaining calls
1462
	 */
1463
	public function setName( $name ) {
1464
		$this->mName = $name;
1465
1466
		return $this;
1467
	}
1468
1469
	/**
1470
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1471
	 * this text as its "<legend>" element.
1472
	 *
1473
	 * @param string|bool $legend If false, no wrapper or legend will be displayed.
1474
	 *     If true, a wrapper will be displayed, but no legend.
1475
	 *     If a string, a wrapper will be displayed with that string as a legend.
1476
	 *     The string will be escaped before being output (this doesn't support HTML).
1477
	 *
1478
	 * @return HTMLForm $this for chaining calls (since 1.20)
1479
	 */
1480
	public function setWrapperLegend( $legend ) {
1481
		$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...
1482
1483
		return $this;
1484
	}
1485
1486
	/**
1487
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1488
	 * this message as its "<legend>" element.
1489
	 * @since 1.19
1490
	 *
1491
	 * @param string|Message $msg Message key or Message object
1492
	 *
1493
	 * @return HTMLForm $this for chaining calls (since 1.20)
1494
	 */
1495
	public function setWrapperLegendMsg( $msg ) {
1496
		if ( !$msg instanceof Message ) {
1497
			$msg = $this->msg( $msg );
1498
		}
1499
		$this->setWrapperLegend( $msg->text() );
1500
1501
		return $this;
1502
	}
1503
1504
	/**
1505
	 * Set the prefix for various default messages
1506
	 * @todo Currently only used for the "<fieldset>" legend on forms
1507
	 * with multiple sections; should be used elsewhere?
1508
	 *
1509
	 * @param string $p
1510
	 *
1511
	 * @return HTMLForm $this for chaining calls (since 1.20)
1512
	 */
1513
	public function setMessagePrefix( $p ) {
1514
		$this->mMessagePrefix = $p;
1515
1516
		return $this;
1517
	}
1518
1519
	/**
1520
	 * Set the title for form submission
1521
	 *
1522
	 * @param Title $t Title of page the form is on/should be posted to
1523
	 *
1524
	 * @return HTMLForm $this for chaining calls (since 1.20)
1525
	 */
1526
	public function setTitle( $t ) {
1527
		$this->mTitle = $t;
1528
1529
		return $this;
1530
	}
1531
1532
	/**
1533
	 * Get the title
1534
	 * @return Title
1535
	 */
1536
	public function getTitle() {
1537
		return $this->mTitle === false
1538
			? $this->getContext()->getTitle()
1539
			: $this->mTitle;
1540
	}
1541
1542
	/**
1543
	 * Set the method used to submit the form
1544
	 *
1545
	 * @param string $method
1546
	 *
1547
	 * @return HTMLForm $this for chaining calls (since 1.20)
1548
	 */
1549
	public function setMethod( $method = 'post' ) {
1550
		$this->mMethod = strtolower( $method );
1551
1552
		return $this;
1553
	}
1554
1555
	/**
1556
	 * @return string Always lowercase
1557
	 */
1558
	public function getMethod() {
1559
		return $this->mMethod;
1560
	}
1561
1562
	/**
1563
	 * Wraps the given $section into an user-visible fieldset.
1564
	 *
1565
	 * @param string $legend Legend text for the fieldset
1566
	 * @param string $section The section content in plain Html
1567
	 * @param array $attributes Additional attributes for the fieldset
1568
	 * @return string The fieldset's Html
1569
	 */
1570
	protected function wrapFieldSetSection( $legend, $section, $attributes ) {
1571
		return Xml::fieldset( $legend, $section, $attributes ) . "\n";
1572
	}
1573
1574
	/**
1575
	 * @todo Document
1576
	 *
1577
	 * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1578
	 *   objects).
1579
	 * @param string $sectionName ID attribute of the "<table>" tag for this
1580
	 *   section, ignored if empty.
1581
	 * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1582
	 *   each subsection, ignored if empty.
1583
	 * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1584
	 * @throws LogicException When called on uninitialized field data, e.g. When
1585
	 *  HTMLForm::displayForm was called without calling HTMLForm::prepareForm
1586
	 *  first.
1587
	 *
1588
	 * @return string
1589
	 */
1590
	public function displaySection( $fields,
1591
		$sectionName = '',
1592
		$fieldsetIDPrefix = '',
1593
		&$hasUserVisibleFields = false
1594
	) {
1595
		if ( $this->mFieldData === null ) {
1596
			throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1597
				. 'You probably called displayForm() without calling prepareForm() first.' );
1598
		}
1599
1600
		$displayFormat = $this->getDisplayFormat();
1601
1602
		$html = [];
1603
		$subsectionHtml = '';
1604
		$hasLabel = false;
1605
1606
		// Conveniently, PHP method names are case-insensitive.
1607
		// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
1608
		$getFieldHtmlMethod = $displayFormat === 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
1609
1610
		foreach ( $fields as $key => $value ) {
1611
			if ( $value instanceof HTMLFormField ) {
1612
				$v = array_key_exists( $key, $this->mFieldData )
1613
					? $this->mFieldData[$key]
1614
					: $value->getDefault();
1615
1616
				$retval = $value->$getFieldHtmlMethod( $v );
1617
1618
				// check, if the form field should be added to
1619
				// the output.
1620
				if ( $value->hasVisibleOutput() ) {
1621
					$html[] = $retval;
1622
1623
					$labelValue = trim( $value->getLabel() );
1624
					if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
1625
						$hasLabel = true;
1626
					}
1627
1628
					$hasUserVisibleFields = true;
1629
				}
1630
			} elseif ( is_array( $value ) ) {
1631
				$subsectionHasVisibleFields = false;
1632
				$section =
1633
					$this->displaySection( $value,
1634
						"mw-htmlform-$key",
1635
						"$fieldsetIDPrefix$key-",
1636
						$subsectionHasVisibleFields );
1637
				$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...
1638
1639
				if ( $subsectionHasVisibleFields === true ) {
1640
					// Display the section with various niceties.
1641
					$hasUserVisibleFields = true;
1642
1643
					$legend = $this->getLegend( $key );
1644
1645
					$section = $this->getHeaderText( $key ) .
1646
						$section .
1647
						$this->getFooterText( $key );
1648
1649
					$attributes = [];
1650
					if ( $fieldsetIDPrefix ) {
1651
						$attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1652
					}
1653
					$subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
1654
				} else {
1655
					// Just return the inputs, nothing fancy.
1656
					$subsectionHtml .= $section;
1657
				}
1658
			}
1659
		}
1660
1661
		$html = $this->formatSection( $html, $sectionName, $hasLabel );
1662
1663
		if ( $subsectionHtml ) {
1664
			if ( $this->mSubSectionBeforeFields ) {
1665
				return $subsectionHtml . "\n" . $html;
1666
			} else {
1667
				return $html . "\n" . $subsectionHtml;
1668
			}
1669
		} else {
1670
			return $html;
1671
		}
1672
	}
1673
1674
	/**
1675
	 * Put a form section together from the individual fields' HTML, merging it and wrapping.
1676
	 * @param array $fieldsHtml
1677
	 * @param string $sectionName
1678
	 * @param bool $anyFieldHasLabel
1679
	 * @return string HTML
1680
	 */
1681
	protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1682
		$displayFormat = $this->getDisplayFormat();
1683
		$html = implode( '', $fieldsHtml );
1684
1685
		if ( $displayFormat === 'raw' ) {
1686
			return $html;
1687
		}
1688
1689
		$classes = [];
1690
1691
		if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
1692
			$classes[] = 'mw-htmlform-nolabel';
1693
		}
1694
1695
		$attribs = [
1696
			'class' => implode( ' ', $classes ),
1697
		];
1698
1699
		if ( $sectionName ) {
1700
			$attribs['id'] = Sanitizer::escapeId( $sectionName );
1701
		}
1702
1703
		if ( $displayFormat === 'table' ) {
1704
			return Html::rawElement( 'table',
1705
					$attribs,
1706
					Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1707
		} elseif ( $displayFormat === 'inline' ) {
1708
			return Html::rawElement( 'span', $attribs, "\n$html\n" );
1709
		} else {
1710
			return Html::rawElement( 'div', $attribs, "\n$html\n" );
1711
		}
1712
	}
1713
1714
	/**
1715
	 * Construct the form fields from the Descriptor array
1716
	 */
1717
	public function loadData() {
1718
		$fieldData = [];
1719
1720 View Code Duplication
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1721
			$request = $this->getRequest();
1722
			if ( $field->skipLoadData( $request ) ) {
1723
				continue;
1724
			} elseif ( !empty( $field->mParams['disabled'] ) ) {
1725
				$fieldData[$fieldname] = $field->getDefault();
1726
			} else {
1727
				$fieldData[$fieldname] = $field->loadDataFromRequest( $request );
1728
			}
1729
		}
1730
1731
		# Filter data.
1732
		foreach ( $fieldData as $name => &$value ) {
1733
			$field = $this->mFlatFields[$name];
1734
			$value = $field->filter( $value, $this->mFlatFields );
1735
		}
1736
1737
		$this->mFieldData = $fieldData;
1738
	}
1739
1740
	/**
1741
	 * Stop a reset button being shown for this form
1742
	 *
1743
	 * @param bool $suppressReset Set to false to re-enable the button again
1744
	 *
1745
	 * @return HTMLForm $this for chaining calls (since 1.20)
1746
	 */
1747
	public function suppressReset( $suppressReset = true ) {
1748
		$this->mShowReset = !$suppressReset;
1749
1750
		return $this;
1751
	}
1752
1753
	/**
1754
	 * Overload this if you want to apply special filtration routines
1755
	 * to the form as a whole, after it's submitted but before it's
1756
	 * processed.
1757
	 *
1758
	 * @param array $data
1759
	 *
1760
	 * @return array
1761
	 */
1762
	public function filterDataForSubmit( $data ) {
1763
		return $data;
1764
	}
1765
1766
	/**
1767
	 * Get a string to go in the "<legend>" of a section fieldset.
1768
	 * Override this if you want something more complicated.
1769
	 *
1770
	 * @param string $key
1771
	 *
1772
	 * @return string
1773
	 */
1774
	public function getLegend( $key ) {
1775
		return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1776
	}
1777
1778
	/**
1779
	 * Set the value for the action attribute of the form.
1780
	 * When set to false (which is the default state), the set title is used.
1781
	 *
1782
	 * @since 1.19
1783
	 *
1784
	 * @param string|bool $action
1785
	 *
1786
	 * @return HTMLForm $this for chaining calls (since 1.20)
1787
	 */
1788
	public function setAction( $action ) {
1789
		$this->mAction = $action;
1790
1791
		return $this;
1792
	}
1793
1794
	/**
1795
	 * Get the value for the action attribute of the form.
1796
	 *
1797
	 * @since 1.22
1798
	 *
1799
	 * @return string
1800
	 */
1801
	public function getAction() {
1802
		// If an action is alredy provided, return it
1803
		if ( $this->mAction !== false ) {
1804
			return $this->mAction;
1805
		}
1806
1807
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1808
		// Check whether we are in GET mode and the ArticlePath contains a "?"
1809
		// meaning that getLocalURL() would return something like "index.php?title=...".
1810
		// As browser remove the query string before submitting GET forms,
1811
		// it means that the title would be lost. In such case use wfScript() instead
1812
		// and put title in an hidden field (see getHiddenFields()).
1813
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1814
			return wfScript();
1815
		}
1816
1817
		return $this->getTitle()->getLocalURL();
1818
	}
1819
1820
	/**
1821
	 * Set the value for the autocomplete attribute of the form.
1822
	 * When set to false (which is the default state), the attribute get not set.
1823
	 *
1824
	 * @since 1.27
1825
	 *
1826
	 * @param string|bool $autocomplete
1827
	 *
1828
	 * @return HTMLForm $this for chaining calls
1829
	 */
1830
	public function setAutocomplete( $autocomplete ) {
1831
		$this->mAutocomplete = $autocomplete;
1832
1833
		return $this;
1834
	}
1835
1836
	/**
1837
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1838
	 * name + parameters array) into a Message.
1839
	 * @param mixed $value
1840
	 * @return Message
1841
	 */
1842
	protected function getMessage( $value ) {
1843
		return Message::newFromSpecifier( $value )->setContext( $this );
1844
	}
1845
}
1846