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

HTMLForm::setName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
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 ( is_null( $context ) && $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 ) ||
364
			in_array( $this->displayFormat, $this->availableSubclassDisplayFormats )
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 ) ) {
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
		$obj = new $class( $descriptor );
463
464
		return $obj;
465
	}
466
467
	/**
468
	 * Prepare form for submission.
469
	 *
470
	 * @attention When doing method chaining, that should be the very last
471
	 * method call before displayForm().
472
	 *
473
	 * @throws MWException
474
	 * @return HTMLForm $this for chaining calls (since 1.20)
475
	 */
476
	function prepareForm() {
477
		# Check if we have the info we need
478
		if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
479
			throw new MWException( "You must call setTitle() on an HTMLForm" );
480
		}
481
482
		# Load data from the request.
483
		$this->loadData();
484
485
		return $this;
486
	}
487
488
	/**
489
	 * Try submitting, with edit token check first
490
	 * @return Status|bool
491
	 */
492
	function tryAuthorizedSubmit() {
493
		$result = false;
494
495
		$submit = false;
496
		if ( $this->getMethod() != 'post' ) {
497
			$submit = true; // no session check needed
498
		} elseif ( $this->getRequest()->wasPosted() ) {
499
			$editToken = $this->getRequest()->getVal( 'wpEditToken' );
500
			if ( $this->getUser()->isLoggedIn() || $editToken != null ) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $editToken of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
501
				// Session tokens for logged-out users have no security value.
502
				// However, if the user gave one, check it in order to give a nice
503
				// "session expired" error instead of "permission denied" or such.
504
				$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...
505
			} else {
506
				$submit = true;
507
			}
508
		}
509
510
		if ( $submit ) {
511
			$this->mWasSubmitted = true;
512
			$result = $this->trySubmit();
513
		}
514
515
		return $result;
516
	}
517
518
	/**
519
	 * The here's-one-I-made-earlier option: do the submission if
520
	 * posted, or display the form with or without funky validation
521
	 * errors
522
	 * @return bool|Status Whether submission was successful.
523
	 */
524
	function show() {
525
		$this->prepareForm();
526
527
		$result = $this->tryAuthorizedSubmit();
528
		if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
529
			return $result;
530
		}
531
532
		$this->displayForm( $result );
533
534
		return false;
535
	}
536
537
	/**
538
	 * Same as self::show with the difference, that the form will be
539
	 * added to the output, no matter, if the validation was good or not.
540
	 * @return bool|Status Whether submission was successful.
541
	 */
542
	function showAlways() {
543
		$this->prepareForm();
544
545
		$result = $this->tryAuthorizedSubmit();
546
547
		$this->displayForm( $result );
548
549
		return $result;
550
	}
551
552
	/**
553
	 * Validate all the fields, and call the submission callback
554
	 * function if everything is kosher.
555
	 * @throws MWException
556
	 * @return bool|string|array|Status
557
	 *     - Bool true or a good Status object indicates success,
558
	 *     - Bool false indicates no submission was attempted,
559
	 *     - Anything else indicates failure. The value may be a fatal Status
560
	 *       object, an HTML string, or an array of arrays (message keys and
561
	 *       params) or strings (message keys)
562
	 */
563
	function trySubmit() {
564
		$valid = true;
565
		$hoistedErrors = [];
566
		$hoistedErrors[] = isset( $this->mValidationErrorMessage )
567
			? $this->mValidationErrorMessage
568
			: [ 'htmlform-invalid-input' ];
569
570
		$this->mWasSubmitted = true;
571
572
		# Check for cancelled submission
573
		foreach ( $this->mFlatFields as $fieldname => $field ) {
574
			if ( !empty( $field->mParams['nodata'] ) ) {
575
				continue;
576
			}
577
			if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
578
				$this->mWasSubmitted = false;
579
				return false;
580
			}
581
		}
582
583
		# Check for validation
584
		foreach ( $this->mFlatFields as $fieldname => $field ) {
585
			if ( !empty( $field->mParams['nodata'] ) ) {
586
				continue;
587
			}
588
			if ( $field->isHidden( $this->mFieldData ) ) {
589
				continue;
590
			}
591
			$res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
592
			if ( $res !== true ) {
593
				$valid = false;
594
				if ( $res !== false && !$field->canDisplayErrors() ) {
595
					$hoistedErrors[] = [ 'rawmessage', $res ];
596
				}
597
			}
598
		}
599
600
		if ( !$valid ) {
601
			if ( count( $hoistedErrors ) === 1 ) {
602
				$hoistedErrors = $hoistedErrors[0];
603
			}
604
			return $hoistedErrors;
605
		}
606
607
		$callback = $this->mSubmitCallback;
608
		if ( !is_callable( $callback ) ) {
609
			throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
610
				'setSubmitCallback() to set one.' );
611
		}
612
613
		$data = $this->filterDataForSubmit( $this->mFieldData );
614
615
		$res = call_user_func( $callback, $data, $this );
616
		if ( $res === false ) {
617
			$this->mWasSubmitted = false;
618
		}
619
620
		return $res;
621
	}
622
623
	/**
624
	 * Test whether the form was considered to have been submitted or not, i.e.
625
	 * whether the last call to tryAuthorizedSubmit or trySubmit returned
626
	 * non-false.
627
	 *
628
	 * This will return false until HTMLForm::tryAuthorizedSubmit or
629
	 * HTMLForm::trySubmit is called.
630
	 *
631
	 * @since 1.23
632
	 * @return bool
633
	 */
634
	function wasSubmitted() {
635
		return $this->mWasSubmitted;
636
	}
637
638
	/**
639
	 * Set a callback to a function to do something with the form
640
	 * once it's been successfully validated.
641
	 *
642
	 * @param callable $cb The function will be passed the output from
643
	 *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
644
	 *   return as documented for HTMLForm::trySubmit
645
	 *
646
	 * @return HTMLForm $this for chaining calls (since 1.20)
647
	 */
648
	function setSubmitCallback( $cb ) {
649
		$this->mSubmitCallback = $cb;
650
651
		return $this;
652
	}
653
654
	/**
655
	 * Set a message to display on a validation error.
656
	 *
657
	 * @param string|array $msg String or Array of valid inputs to wfMessage()
658
	 *     (so each entry can be either a String or Array)
659
	 *
660
	 * @return HTMLForm $this for chaining calls (since 1.20)
661
	 */
662
	function setValidationErrorMessage( $msg ) {
663
		$this->mValidationErrorMessage = $msg;
664
665
		return $this;
666
	}
667
668
	/**
669
	 * Set the introductory message, overwriting any existing message.
670
	 *
671
	 * @param string $msg Complete text of message to display
672
	 *
673
	 * @return HTMLForm $this for chaining calls (since 1.20)
674
	 */
675
	function setIntro( $msg ) {
676
		$this->setPreText( $msg );
677
678
		return $this;
679
	}
680
681
	/**
682
	 * Set the introductory message HTML, overwriting any existing message.
683
	 * @since 1.19
684
	 *
685
	 * @param string $msg Complete HTML of message to display
686
	 *
687
	 * @return HTMLForm $this for chaining calls (since 1.20)
688
	 */
689
	function setPreText( $msg ) {
690
		$this->mPre = $msg;
691
692
		return $this;
693
	}
694
695
	/**
696
	 * Add HTML to introductory message.
697
	 *
698
	 * @param string $msg Complete HTML of message to display
699
	 *
700
	 * @return HTMLForm $this for chaining calls (since 1.20)
701
	 */
702
	function addPreText( $msg ) {
703
		$this->mPre .= $msg;
704
705
		return $this;
706
	}
707
708
	/**
709
	 * Add HTML to the header, inside the form.
710
	 *
711
	 * @param string $msg Additional HTML to display in header
712
	 * @param string|null $section The section to add the header to
713
	 *
714
	 * @return HTMLForm $this for chaining calls (since 1.20)
715
	 */
716 View Code Duplication
	function addHeaderText( $msg, $section = null ) {
717
		if ( is_null( $section ) ) {
718
			$this->mHeader .= $msg;
719
		} else {
720
			if ( !isset( $this->mSectionHeaders[$section] ) ) {
721
				$this->mSectionHeaders[$section] = '';
722
			}
723
			$this->mSectionHeaders[$section] .= $msg;
724
		}
725
726
		return $this;
727
	}
728
729
	/**
730
	 * Set header text, inside the form.
731
	 * @since 1.19
732
	 *
733
	 * @param string $msg Complete HTML of header to display
734
	 * @param string|null $section The section to add the header to
735
	 *
736
	 * @return HTMLForm $this for chaining calls (since 1.20)
737
	 */
738
	function setHeaderText( $msg, $section = null ) {
739
		if ( is_null( $section ) ) {
740
			$this->mHeader = $msg;
741
		} else {
742
			$this->mSectionHeaders[$section] = $msg;
743
		}
744
745
		return $this;
746
	}
747
748
	/**
749
	 * Get header text.
750
	 *
751
	 * @param string|null $section The section to get the header text for
752
	 * @since 1.26
753
	 * @return string HTML
754
	 */
755 View Code Duplication
	function getHeaderText( $section = null ) {
756
		if ( is_null( $section ) ) {
757
			return $this->mHeader;
758
		} else {
759
			return isset( $this->mSectionHeaders[$section] ) ? $this->mSectionHeaders[$section] : '';
760
		}
761
	}
762
763
	/**
764
	 * Add footer text, inside the form.
765
	 *
766
	 * @param string $msg Complete text of message to display
767
	 * @param string|null $section The section to add the footer text to
768
	 *
769
	 * @return HTMLForm $this for chaining calls (since 1.20)
770
	 */
771 View Code Duplication
	function addFooterText( $msg, $section = null ) {
772
		if ( is_null( $section ) ) {
773
			$this->mFooter .= $msg;
774
		} else {
775
			if ( !isset( $this->mSectionFooters[$section] ) ) {
776
				$this->mSectionFooters[$section] = '';
777
			}
778
			$this->mSectionFooters[$section] .= $msg;
779
		}
780
781
		return $this;
782
	}
783
784
	/**
785
	 * Set footer text, inside the form.
786
	 * @since 1.19
787
	 *
788
	 * @param string $msg Complete text of message to display
789
	 * @param string|null $section The section to add the footer text to
790
	 *
791
	 * @return HTMLForm $this for chaining calls (since 1.20)
792
	 */
793
	function setFooterText( $msg, $section = null ) {
794
		if ( is_null( $section ) ) {
795
			$this->mFooter = $msg;
796
		} else {
797
			$this->mSectionFooters[$section] = $msg;
798
		}
799
800
		return $this;
801
	}
802
803
	/**
804
	 * Get footer text.
805
	 *
806
	 * @param string|null $section The section to get the footer text for
807
	 * @since 1.26
808
	 * @return string
809
	 */
810 View Code Duplication
	function getFooterText( $section = null ) {
811
		if ( is_null( $section ) ) {
812
			return $this->mFooter;
813
		} else {
814
			return isset( $this->mSectionFooters[$section] ) ? $this->mSectionFooters[$section] : '';
815
		}
816
	}
817
818
	/**
819
	 * Add text to the end of the display.
820
	 *
821
	 * @param string $msg Complete text of message to display
822
	 *
823
	 * @return HTMLForm $this for chaining calls (since 1.20)
824
	 */
825
	function addPostText( $msg ) {
826
		$this->mPost .= $msg;
827
828
		return $this;
829
	}
830
831
	/**
832
	 * Set text at the end of the display.
833
	 *
834
	 * @param string $msg Complete text of message to display
835
	 *
836
	 * @return HTMLForm $this for chaining calls (since 1.20)
837
	 */
838
	function setPostText( $msg ) {
839
		$this->mPost = $msg;
840
841
		return $this;
842
	}
843
844
	/**
845
	 * Add a hidden field to the output
846
	 *
847
	 * @param string $name Field name.  This will be used exactly as entered
848
	 * @param string $value Field value
849
	 * @param array $attribs
850
	 *
851
	 * @return HTMLForm $this for chaining calls (since 1.20)
852
	 */
853
	public function addHiddenField( $name, $value, $attribs = [] ) {
854
		$attribs += [ 'name' => $name ];
855
		$this->mHiddenFields[] = [ $value, $attribs ];
856
857
		return $this;
858
	}
859
860
	/**
861
	 * Add an array of hidden fields to the output
862
	 *
863
	 * @since 1.22
864
	 *
865
	 * @param array $fields Associative array of fields to add;
866
	 *        mapping names to their values
867
	 *
868
	 * @return HTMLForm $this for chaining calls
869
	 */
870
	public function addHiddenFields( array $fields ) {
871
		foreach ( $fields as $name => $value ) {
872
			$this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
873
		}
874
875
		return $this;
876
	}
877
878
	/**
879
	 * Add a button to the form
880
	 *
881
	 * @since 1.27 takes an array as shown. Earlier versions accepted
882
	 *  'name', 'value', 'id', and 'attribs' as separate parameters in that
883
	 *  order.
884
	 * @note Custom labels ('label', 'label-message', 'label-raw') are not
885
	 *  supported for IE6 and IE7 due to bugs in those browsers. If detected,
886
	 *  they will be served buttons using 'value' as the button label.
887
	 * @param array $data Data to define the button:
888
	 *  - name: (string) Button name.
889
	 *  - value: (string) Button value.
890
	 *  - label-message: (string, optional) Button label message key to use
891
	 *    instead of 'value'. Overrides 'label' and 'label-raw'.
892
	 *  - label: (string, optional) Button label text to use instead of
893
	 *    'value'. Overrides 'label-raw'.
894
	 *  - label-raw: (string, optional) Button label HTML to use instead of
895
	 *    'value'.
896
	 *  - id: (string, optional) DOM id for the button.
897
	 *  - attribs: (array, optional) Additional HTML attributes.
898
	 *  - flags: (string|string[], optional) OOUI flags.
899
	 * @return HTMLForm $this for chaining calls (since 1.20)
900
	 */
901
	public function addButton( $data ) {
902
		if ( !is_array( $data ) ) {
903
			$args = func_get_args();
904
			if ( count( $args ) < 2 || count( $args ) > 4 ) {
905
				throw new InvalidArgumentException(
906
					'Incorrect number of arguments for deprecated calling style'
907
				);
908
			}
909
			$data = [
910
				'name' => $args[0],
911
				'value' => $args[1],
912
				'id' => isset( $args[2] ) ? $args[2] : null,
913
				'attribs' => isset( $args[3] ) ? $args[3] : null,
914
			];
915
		} else {
916
			if ( !isset( $data['name'] ) ) {
917
				throw new InvalidArgumentException( 'A name is required' );
918
			}
919
			if ( !isset( $data['value'] ) ) {
920
				throw new InvalidArgumentException( 'A value is required' );
921
			}
922
		}
923
		$this->mButtons[] = $data + [
924
			'id' => null,
925
			'attribs' => null,
926
			'flags' => null,
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
	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
	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 ( !empty( $this->mId ) ) {
1005
			$attribs['id'] = $this->mId;
1006
		}
1007
		if ( !empty( $this->mAutocomplete ) ) {
1008
			$attribs['autocomplete'] = $this->mAutocomplete;
1009
		}
1010
		if ( !empty ( $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
	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
	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
	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
					array_push( $attribs['class'], 'mw-ui-' . $flag );
1093
				}
1094
				array_push( $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->msg( $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
		$html = Html::rawElement( 'span',
1152
			[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1153
1154
		return $html;
1155
	}
1156
1157
	/**
1158
	 * Get the whole body of the form.
1159
	 * @return string
1160
	 */
1161
	function getBody() {
1162
		return $this->displaySection( $this->mFieldTree, $this->mTableId );
1163
	}
1164
1165
	/**
1166
	 * Format and display an error message stack.
1167
	 *
1168
	 * @param string|array|Status $errors
1169
	 *
1170
	 * @return string
1171
	 */
1172
	function getErrors( $errors ) {
1173
		if ( $errors instanceof Status ) {
1174
			if ( $errors->isOK() ) {
1175
				$errorstr = '';
1176
			} else {
1177
				$errorstr = $this->getOutput()->parse( $errors->getWikiText() );
1178
			}
1179
		} elseif ( is_array( $errors ) ) {
1180
			$errorstr = $this->formatErrors( $errors );
1181
		} else {
1182
			$errorstr = $errors;
1183
		}
1184
1185
		return $errorstr
1186
			? Html::rawElement( 'div', [ 'class' => 'error' ], $errorstr )
1187
			: '';
1188
	}
1189
1190
	/**
1191
	 * Format a stack of error messages into a single HTML string
1192
	 *
1193
	 * @param array $errors Array of message keys/values
1194
	 *
1195
	 * @return string HTML, a "<ul>" list of errors
1196
	 */
1197
	public function formatErrors( $errors ) {
1198
		$errorstr = '';
1199
1200
		foreach ( $errors as $error ) {
1201
			if ( is_array( $error ) ) {
1202
				$msg = array_shift( $error );
1203
			} else {
1204
				$msg = $error;
1205
				$error = [];
1206
			}
1207
1208
			$errorstr .= Html::rawElement(
1209
				'li',
1210
				[],
1211
				$this->msg( $msg, $error )->parse()
1212
			);
1213
		}
1214
1215
		$errorstr = Html::rawElement( 'ul', [], $errorstr );
1216
1217
		return $errorstr;
1218
	}
1219
1220
	/**
1221
	 * Set the text for the submit button
1222
	 *
1223
	 * @param string $t Plaintext
1224
	 *
1225
	 * @return HTMLForm $this for chaining calls (since 1.20)
1226
	 */
1227
	function setSubmitText( $t ) {
1228
		$this->mSubmitText = $t;
1229
1230
		return $this;
1231
	}
1232
1233
	/**
1234
	 * Identify that the submit button in the form has a destructive action
1235
	 * @since 1.24
1236
	 */
1237
	public function setSubmitDestructive() {
1238
		$this->mSubmitFlags = [ 'destructive', 'primary' ];
1239
	}
1240
1241
	/**
1242
	 * Identify that the submit button in the form has a progressive action
1243
	 * @since 1.25
1244
	 */
1245
	public function setSubmitProgressive() {
1246
		$this->mSubmitFlags = [ 'progressive', 'primary' ];
1247
	}
1248
1249
	/**
1250
	 * Set the text for the submit button to a message
1251
	 * @since 1.19
1252
	 *
1253
	 * @param string|Message $msg Message key or Message object
1254
	 *
1255
	 * @return HTMLForm $this for chaining calls (since 1.20)
1256
	 */
1257
	public function setSubmitTextMsg( $msg ) {
1258
		if ( !$msg instanceof Message ) {
1259
			$msg = $this->msg( $msg );
1260
		}
1261
		$this->setSubmitText( $msg->text() );
1262
1263
		return $this;
1264
	}
1265
1266
	/**
1267
	 * Get the text for the submit button, either customised or a default.
1268
	 * @return string
1269
	 */
1270
	function getSubmitText() {
1271
		return $this->mSubmitText
1272
			? $this->mSubmitText
1273
			: $this->msg( 'htmlform-submit' )->text();
1274
	}
1275
1276
	/**
1277
	 * @param string $name Submit button name
1278
	 *
1279
	 * @return HTMLForm $this for chaining calls (since 1.20)
1280
	 */
1281
	public function setSubmitName( $name ) {
1282
		$this->mSubmitName = $name;
1283
1284
		return $this;
1285
	}
1286
1287
	/**
1288
	 * @param string $name Tooltip for the submit button
1289
	 *
1290
	 * @return HTMLForm $this for chaining calls (since 1.20)
1291
	 */
1292
	public function setSubmitTooltip( $name ) {
1293
		$this->mSubmitTooltip = $name;
1294
1295
		return $this;
1296
	}
1297
1298
	/**
1299
	 * Set the id for the submit button.
1300
	 *
1301
	 * @param string $t
1302
	 *
1303
	 * @todo FIXME: Integrity of $t is *not* validated
1304
	 * @return HTMLForm $this for chaining calls (since 1.20)
1305
	 */
1306
	function setSubmitID( $t ) {
1307
		$this->mSubmitID = $t;
1308
1309
		return $this;
1310
	}
1311
1312
	/**
1313
	 * Stop a default submit button being shown for this form. This implies that an
1314
	 * alternate submit method must be provided manually.
1315
	 *
1316
	 * @since 1.22
1317
	 *
1318
	 * @param bool $suppressSubmit Set to false to re-enable the button again
1319
	 *
1320
	 * @return HTMLForm $this for chaining calls
1321
	 */
1322
	function suppressDefaultSubmit( $suppressSubmit = true ) {
1323
		$this->mShowSubmit = !$suppressSubmit;
1324
1325
		return $this;
1326
	}
1327
1328
	/**
1329
	 * Set the id of the \<table\> or outermost \<div\> element.
1330
	 *
1331
	 * @since 1.22
1332
	 *
1333
	 * @param string $id New value of the id attribute, or "" to remove
1334
	 *
1335
	 * @return HTMLForm $this for chaining calls
1336
	 */
1337
	public function setTableId( $id ) {
1338
		$this->mTableId = $id;
1339
1340
		return $this;
1341
	}
1342
1343
	/**
1344
	 * @param string $id DOM id for the form
1345
	 *
1346
	 * @return HTMLForm $this for chaining calls (since 1.20)
1347
	 */
1348
	public function setId( $id ) {
1349
		$this->mId = $id;
1350
1351
		return $this;
1352
	}
1353
1354
	/**
1355
	 * @param string $name 'name' attribute for the form
1356
	 * @return HTMLForm $this for chaining calls
1357
	 */
1358
	public function setName( $name ) {
1359
		$this->mName = $name;
1360
1361
		return $this;
1362
	}
1363
1364
	/**
1365
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1366
	 * this text as its "<legend>" element.
1367
	 *
1368
	 * @param string|bool $legend If false, no wrapper or legend will be displayed.
1369
	 *     If true, a wrapper will be displayed, but no legend.
1370
	 *     If a string, a wrapper will be displayed with that string as a legend.
1371
	 *     The string will be escaped before being output (this doesn't support HTML).
1372
	 *
1373
	 * @return HTMLForm $this for chaining calls (since 1.20)
1374
	 */
1375
	public function setWrapperLegend( $legend ) {
1376
		$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...
1377
1378
		return $this;
1379
	}
1380
1381
	/**
1382
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1383
	 * this message as its "<legend>" element.
1384
	 * @since 1.19
1385
	 *
1386
	 * @param string|Message $msg Message key or Message object
1387
	 *
1388
	 * @return HTMLForm $this for chaining calls (since 1.20)
1389
	 */
1390
	public function setWrapperLegendMsg( $msg ) {
1391
		if ( !$msg instanceof Message ) {
1392
			$msg = $this->msg( $msg );
1393
		}
1394
		$this->setWrapperLegend( $msg->text() );
1395
1396
		return $this;
1397
	}
1398
1399
	/**
1400
	 * Set the prefix for various default messages
1401
	 * @todo Currently only used for the "<fieldset>" legend on forms
1402
	 * with multiple sections; should be used elsewhere?
1403
	 *
1404
	 * @param string $p
1405
	 *
1406
	 * @return HTMLForm $this for chaining calls (since 1.20)
1407
	 */
1408
	function setMessagePrefix( $p ) {
1409
		$this->mMessagePrefix = $p;
1410
1411
		return $this;
1412
	}
1413
1414
	/**
1415
	 * Set the title for form submission
1416
	 *
1417
	 * @param Title $t Title of page the form is on/should be posted to
1418
	 *
1419
	 * @return HTMLForm $this for chaining calls (since 1.20)
1420
	 */
1421
	function setTitle( $t ) {
1422
		$this->mTitle = $t;
1423
1424
		return $this;
1425
	}
1426
1427
	/**
1428
	 * Get the title
1429
	 * @return Title
1430
	 */
1431
	function getTitle() {
1432
		return $this->mTitle === false
1433
			? $this->getContext()->getTitle()
1434
			: $this->mTitle;
1435
	}
1436
1437
	/**
1438
	 * Set the method used to submit the form
1439
	 *
1440
	 * @param string $method
1441
	 *
1442
	 * @return HTMLForm $this for chaining calls (since 1.20)
1443
	 */
1444
	public function setMethod( $method = 'post' ) {
1445
		$this->mMethod = strtolower( $method );
1446
1447
		return $this;
1448
	}
1449
1450
	/**
1451
	 * @return string Always lowercase
1452
	 */
1453
	public function getMethod() {
1454
		return $this->mMethod;
1455
	}
1456
1457
	/**
1458
	 * Wraps the given $section into an user-visible fieldset.
1459
	 *
1460
	 * @param string $legend Legend text for the fieldset
1461
	 * @param string $section The section content in plain Html
1462
	 * @param array $attributes Additional attributes for the fieldset
1463
	 * @return string The fieldset's Html
1464
	 */
1465
	protected function wrapFieldSetSection( $legend, $section, $attributes ) {
1466
		return Xml::fieldset( $legend, $section, $attributes ) . "\n";
1467
	}
1468
1469
	/**
1470
	 * @todo Document
1471
	 *
1472
	 * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1473
	 *   objects).
1474
	 * @param string $sectionName ID attribute of the "<table>" tag for this
1475
	 *   section, ignored if empty.
1476
	 * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1477
	 *   each subsection, ignored if empty.
1478
	 * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1479
	 *
1480
	 * @return string
1481
	 */
1482
	public function displaySection( $fields,
1483
		$sectionName = '',
1484
		$fieldsetIDPrefix = '',
1485
		&$hasUserVisibleFields = false ) {
1486
		$displayFormat = $this->getDisplayFormat();
1487
1488
		$html = [];
1489
		$subsectionHtml = '';
1490
		$hasLabel = false;
1491
1492
		// Conveniently, PHP method names are case-insensitive.
1493
		// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
1494
		$getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
1495
1496
		foreach ( $fields as $key => $value ) {
1497
			if ( $value instanceof HTMLFormField ) {
1498
				$v = empty( $value->mParams['nodata'] )
1499
					? $this->mFieldData[$key]
1500
					: $value->getDefault();
1501
1502
				$retval = $value->$getFieldHtmlMethod( $v );
1503
1504
				// check, if the form field should be added to
1505
				// the output.
1506
				if ( $value->hasVisibleOutput() ) {
1507
					$html[] = $retval;
1508
1509
					$labelValue = trim( $value->getLabel() );
1510
					if ( $labelValue != '&#160;' && $labelValue !== '' ) {
1511
						$hasLabel = true;
1512
					}
1513
1514
					$hasUserVisibleFields = true;
1515
				}
1516
			} elseif ( is_array( $value ) ) {
1517
				$subsectionHasVisibleFields = false;
1518
				$section =
1519
					$this->displaySection( $value,
1520
						"mw-htmlform-$key",
1521
						"$fieldsetIDPrefix$key-",
1522
						$subsectionHasVisibleFields );
1523
				$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...
1524
1525
				if ( $subsectionHasVisibleFields === true ) {
1526
					// Display the section with various niceties.
1527
					$hasUserVisibleFields = true;
1528
1529
					$legend = $this->getLegend( $key );
1530
1531
					$section = $this->getHeaderText( $key ) .
1532
						$section .
1533
						$this->getFooterText( $key );
1534
1535
					$attributes = [];
1536
					if ( $fieldsetIDPrefix ) {
1537
						$attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1538
					}
1539
					$subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
1540
				} else {
1541
					// Just return the inputs, nothing fancy.
1542
					$subsectionHtml .= $section;
1543
				}
1544
			}
1545
		}
1546
1547
		$html = $this->formatSection( $html, $sectionName, $hasLabel );
1548
1549
		if ( $subsectionHtml ) {
1550
			if ( $this->mSubSectionBeforeFields ) {
1551
				return $subsectionHtml . "\n" . $html;
1552
			} else {
1553
				return $html . "\n" . $subsectionHtml;
1554
			}
1555
		} else {
1556
			return $html;
1557
		}
1558
	}
1559
1560
	/**
1561
	 * Put a form section together from the individual fields' HTML, merging it and wrapping.
1562
	 * @param array $fieldsHtml
1563
	 * @param string $sectionName
1564
	 * @param bool $anyFieldHasLabel
1565
	 * @return string HTML
1566
	 */
1567
	protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1568
		$displayFormat = $this->getDisplayFormat();
1569
		$html = implode( '', $fieldsHtml );
1570
1571
		if ( $displayFormat === 'raw' ) {
1572
			return $html;
1573
		}
1574
1575
		$classes = [];
1576
1577
		if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
1578
			$classes[] = 'mw-htmlform-nolabel';
1579
		}
1580
1581
		$attribs = [
1582
			'class' => implode( ' ', $classes ),
1583
		];
1584
1585
		if ( $sectionName ) {
1586
			$attribs['id'] = Sanitizer::escapeId( $sectionName );
1587
		}
1588
1589
		if ( $displayFormat === 'table' ) {
1590
			return Html::rawElement( 'table',
1591
					$attribs,
1592
					Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1593
		} elseif ( $displayFormat === 'inline' ) {
1594
			return Html::rawElement( 'span', $attribs, "\n$html\n" );
1595
		} else {
1596
			return Html::rawElement( 'div', $attribs, "\n$html\n" );
1597
		}
1598
	}
1599
1600
	/**
1601
	 * Construct the form fields from the Descriptor array
1602
	 */
1603
	function loadData() {
1604
		$fieldData = [];
1605
1606
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1607 View Code Duplication
			if ( !empty( $field->mParams['nodata'] ) ) {
1608
				continue;
1609
			} elseif ( !empty( $field->mParams['disabled'] ) ) {
1610
				$fieldData[$fieldname] = $field->getDefault();
1611
			} else {
1612
				$fieldData[$fieldname] = $field->loadDataFromRequest( $this->getRequest() );
1613
			}
1614
		}
1615
1616
		# Filter data.
1617
		foreach ( $fieldData as $name => &$value ) {
1618
			$field = $this->mFlatFields[$name];
1619
			$value = $field->filter( $value, $this->mFlatFields );
1620
		}
1621
1622
		$this->mFieldData = $fieldData;
1623
	}
1624
1625
	/**
1626
	 * Stop a reset button being shown for this form
1627
	 *
1628
	 * @param bool $suppressReset Set to false to re-enable the button again
1629
	 *
1630
	 * @return HTMLForm $this for chaining calls (since 1.20)
1631
	 */
1632
	function suppressReset( $suppressReset = true ) {
1633
		$this->mShowReset = !$suppressReset;
1634
1635
		return $this;
1636
	}
1637
1638
	/**
1639
	 * Overload this if you want to apply special filtration routines
1640
	 * to the form as a whole, after it's submitted but before it's
1641
	 * processed.
1642
	 *
1643
	 * @param array $data
1644
	 *
1645
	 * @return array
1646
	 */
1647
	function filterDataForSubmit( $data ) {
1648
		return $data;
1649
	}
1650
1651
	/**
1652
	 * Get a string to go in the "<legend>" of a section fieldset.
1653
	 * Override this if you want something more complicated.
1654
	 *
1655
	 * @param string $key
1656
	 *
1657
	 * @return string
1658
	 */
1659
	public function getLegend( $key ) {
1660
		return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1661
	}
1662
1663
	/**
1664
	 * Set the value for the action attribute of the form.
1665
	 * When set to false (which is the default state), the set title is used.
1666
	 *
1667
	 * @since 1.19
1668
	 *
1669
	 * @param string|bool $action
1670
	 *
1671
	 * @return HTMLForm $this for chaining calls (since 1.20)
1672
	 */
1673
	public function setAction( $action ) {
1674
		$this->mAction = $action;
1675
1676
		return $this;
1677
	}
1678
1679
	/**
1680
	 * Get the value for the action attribute of the form.
1681
	 *
1682
	 * @since 1.22
1683
	 *
1684
	 * @return string
1685
	 */
1686
	public function getAction() {
1687
		// If an action is alredy provided, return it
1688
		if ( $this->mAction !== false ) {
1689
			return $this->mAction;
1690
		}
1691
1692
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1693
		// Check whether we are in GET mode and the ArticlePath contains a "?"
1694
		// meaning that getLocalURL() would return something like "index.php?title=...".
1695
		// As browser remove the query string before submitting GET forms,
1696
		// it means that the title would be lost. In such case use wfScript() instead
1697
		// and put title in an hidden field (see getHiddenFields()).
1698
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1699
			return wfScript();
1700
		}
1701
1702
		return $this->getTitle()->getLocalURL();
1703
	}
1704
1705
	/**
1706
	 * Set the value for the autocomplete attribute of the form.
1707
	 * When set to false (which is the default state), the attribute get not set.
1708
	 *
1709
	 * @since 1.27
1710
	 *
1711
	 * @param string|bool $autocomplete
1712
	 *
1713
	 * @return HTMLForm $this for chaining calls
1714
	 */
1715
	public function setAutocomplete( $autocomplete ) {
1716
		$this->mAutocomplete = $autocomplete;
1717
1718
		return $this;
1719
	}
1720
}
1721