HTMLForm::trySubmit()   C
last analyzed

Complexity

Conditions 15
Paths 182

Size

Total Lines 59
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 36
nc 182
nop 0
dl 0
loc 59
rs 5.7872
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * HTML form generation and submission handling.
5
 *
6
 * This program is free software; you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation; either version 2 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License along
17
 * with this program; if not, write to the Free Software Foundation, Inc.,
18
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
 * http://www.gnu.org/copyleft/gpl.html
20
 *
21
 * @file
22
 */
23
24
/**
25
 * Object handling generic submission, CSRF protection, layout and
26
 * other logic for UI forms. in a reusable manner.
27
 *
28
 * In order to generate the form, the HTMLForm object takes an array
29
 * structure detailing the form fields available. Each element of the
30
 * array is a basic property-list, including the type of field, the
31
 * label it is to be given in the form, callbacks for validation and
32
 * 'filtering', and other pertinent information.
33
 *
34
 * Field types are implemented as subclasses of the generic HTMLFormField
35
 * object, and typically implement at least getInputHTML, which generates
36
 * the HTML for the input field to be placed in the table.
37
 *
38
 * You can find extensive documentation on the www.mediawiki.org wiki:
39
 *  - https://www.mediawiki.org/wiki/HTMLForm
40
 *  - https://www.mediawiki.org/wiki/HTMLForm/tutorial
41
 *
42
 * The constructor input is an associative array of $fieldname => $info,
43
 * where $info is an Associative Array with any of the following:
44
 *
45
 *    'class'               -- the subclass of HTMLFormField that will be used
46
 *                             to create the object.  *NOT* the CSS class!
47
 *    'type'                -- roughly translates into the <select> type attribute.
48
 *                             if 'class' is not specified, this is used as a map
49
 *                             through HTMLForm::$typeMappings to get the class name.
50
 *    'default'             -- default value when the form is displayed
51
 *    'id'                  -- HTML id attribute
52
 *    'cssclass'            -- CSS class
53
 *    'csshelpclass'        -- CSS class used to style help text
54
 *    'dir'                 -- Direction of the element.
55
 *    'options'             -- associative array mapping labels to values.
56
 *                             Some field types support multi-level arrays.
57
 *    'options-messages'    -- associative array mapping message keys to values.
58
 *                             Some field types support multi-level arrays.
59
 *    'options-message'     -- message key or object to be parsed to extract the list of
60
 *                             options (like 'ipbreason-dropdown').
61
 *    'label-message'       -- message key or object for a message to use as the label.
62
 *                             can be an array of msg key and then parameters to
63
 *                             the message.
64
 *    'label'               -- alternatively, a raw text message. Overridden by
65
 *                             label-message
66
 *    'help'                -- message text for a message to use as a help text.
67
 *    'help-message'        -- message key or object for a message to use as a help text.
68
 *                             can be an array of msg key and then parameters to
69
 *                             the message.
70
 *                             Overwrites 'help-messages' and 'help'.
71
 *    'help-messages'       -- array of message keys/objects. As above, each item can
72
 *                             be an array of msg key and then parameters.
73
 *                             Overwrites 'help'.
74
 *    '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
		'sizefilter' => 'HTMLSizeFilterField',
151
		'submit' => 'HTMLSubmitField',
152
		'hidden' => 'HTMLHiddenField',
153
		'edittools' => 'HTMLEditTools',
154
		'checkmatrix' => 'HTMLCheckMatrix',
155
		'cloner' => 'HTMLFormFieldCloner',
156
		'autocompleteselect' => 'HTMLAutoCompleteSelectField',
157
		'date' => 'HTMLDateTimeField',
158
		'time' => 'HTMLDateTimeField',
159
		'datetime' => 'HTMLDateTimeField',
160
		// HTMLTextField will output the correct type="" attribute automagically.
161
		// There are about four zillion other HTML5 input types, like range, but
162
		// we don't use those at the moment, so no point in adding all of them.
163
		'email' => 'HTMLTextField',
164
		'password' => 'HTMLTextField',
165
		'url' => 'HTMLTextField',
166
		'title' => 'HTMLTitleTextField',
167
		'user' => 'HTMLUserTextField',
168
	];
169
170
	public $mFieldData;
171
172
	protected $mMessagePrefix;
173
174
	/** @var HTMLFormField[] */
175
	protected $mFlatFields;
176
177
	protected $mFieldTree;
178
	protected $mShowReset = false;
179
	protected $mShowSubmit = true;
180
	protected $mSubmitFlags = [ 'primary', 'progressive' ];
181
	protected $mShowCancel = false;
182
	protected $mCancelTarget;
183
184
	protected $mSubmitCallback;
185
	protected $mValidationErrorMessage;
186
187
	protected $mPre = '';
188
	protected $mHeader = '';
189
	protected $mFooter = '';
190
	protected $mSectionHeaders = [];
191
	protected $mSectionFooters = [];
192
	protected $mPost = '';
193
	protected $mId;
194
	protected $mName;
195
	protected $mTableId = '';
196
197
	protected $mSubmitID;
198
	protected $mSubmitName;
199
	protected $mSubmitText;
200
	protected $mSubmitTooltip;
201
202
	protected $mFormIdentifier;
203
	protected $mTitle;
204
	protected $mMethod = 'post';
205
	protected $mWasSubmitted = false;
206
207
	/**
208
	 * Form action URL. false means we will use the URL to set Title
209
	 * @since 1.19
210
	 * @var bool|string
211
	 */
212
	protected $mAction = false;
213
214
	/**
215
	 * Form attribute autocomplete. false does not set the attribute
216
	 * @since 1.27
217
	 * @var bool|string
218
	 */
219
	protected $mAutocomplete = false;
220
221
	protected $mUseMultipart = false;
222
	protected $mHiddenFields = [];
223
	protected $mButtons = [];
224
225
	protected $mWrapperLegend = false;
226
227
	/**
228
	 * Salt for the edit token.
229
	 * @var string|array
230
	 */
231
	protected $mTokenSalt = '';
232
233
	/**
234
	 * If true, sections that contain both fields and subsections will
235
	 * render their subsections before their fields.
236
	 *
237
	 * Subclasses may set this to false to render subsections after fields
238
	 * instead.
239
	 */
240
	protected $mSubSectionBeforeFields = true;
241
242
	/**
243
	 * Format in which to display form. For viable options,
244
	 * @see $availableDisplayFormats
245
	 * @var string
246
	 */
247
	protected $displayFormat = 'table';
248
249
	/**
250
	 * Available formats in which to display the form
251
	 * @var array
252
	 */
253
	protected $availableDisplayFormats = [
254
		'table',
255
		'div',
256
		'raw',
257
		'inline',
258
	];
259
260
	/**
261
	 * Available formats in which to display the form
262
	 * @var array
263
	 */
264
	protected $availableSubclassDisplayFormats = [
265
		'vform',
266
		'ooui',
267
	];
268
269
	/**
270
	 * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
271
	 *
272
	 * @param string $displayFormat
273
	 * @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...
274
	 * @return HTMLForm
275
	 */
276
	public static function factory( $displayFormat/*, $arguments...*/ ) {
277
		$arguments = func_get_args();
278
		array_shift( $arguments );
279
280
		switch ( $displayFormat ) {
281
			case 'vform':
282
				return ObjectFactory::constructClassInstance( VFormHTMLForm::class, $arguments );
283
			case 'ooui':
284
				return ObjectFactory::constructClassInstance( OOUIHTMLForm::class, $arguments );
285
			default:
286
				/** @var HTMLForm $form */
287
				$form = ObjectFactory::constructClassInstance( HTMLForm::class, $arguments );
288
				$form->setDisplayFormat( $displayFormat );
289
				return $form;
290
		}
291
	}
292
293
	/**
294
	 * Build a new HTMLForm from an array of field attributes
295
	 *
296
	 * @param array $descriptor Array of Field constructs, as described above
297
	 * @param IContextSource $context Available since 1.18, will become compulsory in 1.18.
298
	 *     Obviates the need to call $form->setTitle()
299
	 * @param string $messagePrefix A prefix to go in front of default messages
300
	 */
301
	public function __construct( $descriptor, /*IContextSource*/ $context = null,
302
		$messagePrefix = ''
303
	) {
304
		if ( $context instanceof IContextSource ) {
305
			$this->setContext( $context );
306
			$this->mTitle = false; // We don't need them to set a title
307
			$this->mMessagePrefix = $messagePrefix;
308
		} elseif ( $context === null && $messagePrefix !== '' ) {
309
			$this->mMessagePrefix = $messagePrefix;
310
		} elseif ( is_string( $context ) && $messagePrefix === '' ) {
311
			// B/C since 1.18
312
			// it's actually $messagePrefix
313
			$this->mMessagePrefix = $context;
314
		}
315
316
		// Evil hack for mobile :(
317
		if (
318
			!$this->getConfig()->get( 'HTMLFormAllowTableFormat' )
319
			&& $this->displayFormat === 'table'
320
		) {
321
			$this->displayFormat = 'div';
322
		}
323
324
		// Expand out into a tree.
325
		$loadedDescriptor = [];
326
		$this->mFlatFields = [];
327
328
		foreach ( $descriptor as $fieldname => $info ) {
329
			$section = isset( $info['section'] )
330
				? $info['section']
331
				: '';
332
333
			if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
334
				$this->mUseMultipart = true;
335
			}
336
337
			$field = static::loadInputFromParameters( $fieldname, $info, $this );
338
339
			$setSection =& $loadedDescriptor;
340
			if ( $section ) {
341
				$sectionParts = explode( '/', $section );
342
343
				while ( count( $sectionParts ) ) {
344
					$newName = array_shift( $sectionParts );
345
346
					if ( !isset( $setSection[$newName] ) ) {
347
						$setSection[$newName] = [];
348
					}
349
350
					$setSection =& $setSection[$newName];
351
				}
352
			}
353
354
			$setSection[$fieldname] = $field;
355
			$this->mFlatFields[$fieldname] = $field;
356
		}
357
358
		$this->mFieldTree = $loadedDescriptor;
359
	}
360
361
	/**
362
	 * @param string $fieldname
363
	 * @return bool
364
	 */
365
	public function hasField( $fieldname ) {
366
		return isset( $this->mFlatFields[$fieldname] );
367
	}
368
369
	/**
370
	 * @param string $fieldname
371
	 * @return HTMLFormField
372
	 * @throws DomainException on invalid field name
373
	 */
374
	public function getField( $fieldname ) {
375
		if ( !$this->hasField( $fieldname ) ) {
376
			throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
377
		}
378
		return $this->mFlatFields[$fieldname];
379
	}
380
381
	/**
382
	 * Set format in which to display the form
383
	 *
384
	 * @param string $format The name of the format to use, must be one of
385
	 *   $this->availableDisplayFormats
386
	 *
387
	 * @throws MWException
388
	 * @since 1.20
389
	 * @return HTMLForm $this for chaining calls (since 1.20)
390
	 */
391
	public function setDisplayFormat( $format ) {
392
		if (
393
			in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
394
			in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
395
		) {
396
			throw new MWException( 'Cannot change display format after creation, ' .
397
				'use HTMLForm::factory() instead' );
398
		}
399
400
		if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
401
			throw new MWException( 'Display format must be one of ' .
402
				print_r( $this->availableDisplayFormats, true ) );
403
		}
404
405
		// Evil hack for mobile :(
406
		if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
407
			$format = 'div';
408
		}
409
410
		$this->displayFormat = $format;
411
412
		return $this;
413
	}
414
415
	/**
416
	 * Getter for displayFormat
417
	 * @since 1.20
418
	 * @return string
419
	 */
420
	public function getDisplayFormat() {
421
		return $this->displayFormat;
422
	}
423
424
	/**
425
	 * Test if displayFormat is 'vform'
426
	 * @since 1.22
427
	 * @deprecated since 1.25
428
	 * @return bool
429
	 */
430
	public function isVForm() {
431
		wfDeprecated( __METHOD__, '1.25' );
432
		return false;
433
	}
434
435
	/**
436
	 * Get the HTMLFormField subclass for this descriptor.
437
	 *
438
	 * The descriptor can be passed either 'class' which is the name of
439
	 * a HTMLFormField subclass, or a shorter 'type' which is an alias.
440
	 * This makes sure the 'class' is always set, and also is returned by
441
	 * this function for ease.
442
	 *
443
	 * @since 1.23
444
	 *
445
	 * @param string $fieldname Name of the field
446
	 * @param array $descriptor Input Descriptor, as described above
447
	 *
448
	 * @throws MWException
449
	 * @return string Name of a HTMLFormField subclass
450
	 */
451
	public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
452
		if ( isset( $descriptor['class'] ) ) {
453
			$class = $descriptor['class'];
454
		} elseif ( isset( $descriptor['type'] ) ) {
455
			$class = static::$typeMappings[$descriptor['type']];
456
			$descriptor['class'] = $class;
457
		} else {
458
			$class = null;
459
		}
460
461
		if ( !$class ) {
462
			throw new MWException( "Descriptor with no class for $fieldname: "
463
				. print_r( $descriptor, true ) );
464
		}
465
466
		return $class;
467
	}
468
469
	/**
470
	 * Initialise a new Object for the field
471
	 *
472
	 * @param string $fieldname Name of the field
473
	 * @param array $descriptor Input Descriptor, as described above
474
	 * @param HTMLForm|null $parent Parent instance of HTMLForm
475
	 *
476
	 * @throws MWException
477
	 * @return HTMLFormField Instance of a subclass of HTMLFormField
478
	 */
479
	public static function loadInputFromParameters( $fieldname, $descriptor,
480
		HTMLForm $parent = null
481
	) {
482
		$class = static::getClassFromDescriptor( $fieldname, $descriptor );
483
484
		$descriptor['fieldname'] = $fieldname;
485
		if ( $parent ) {
486
			$descriptor['parent'] = $parent;
487
		}
488
489
		# @todo This will throw a fatal error whenever someone try to use
490
		# 'class' to feed a CSS class instead of 'cssclass'. Would be
491
		# great to avoid the fatal error and show a nice error.
492
		return new $class( $descriptor );
493
	}
494
495
	/**
496
	 * Prepare form for submission.
497
	 *
498
	 * @attention When doing method chaining, that should be the very last
499
	 * method call before displayForm().
500
	 *
501
	 * @throws MWException
502
	 * @return HTMLForm $this for chaining calls (since 1.20)
503
	 */
504
	public function prepareForm() {
505
		# Check if we have the info we need
506
		if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
507
			throw new MWException( 'You must call setTitle() on an HTMLForm' );
508
		}
509
510
		# Load data from the request.
511
		if (
512
			$this->mFormIdentifier === null ||
513
			$this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier
514
		) {
515
			$this->loadData();
516
		} else {
517
			$this->mFieldData = [];
518
		}
519
520
		return $this;
521
	}
522
523
	/**
524
	 * Try submitting, with edit token check first
525
	 * @return Status|bool
526
	 */
527
	public function tryAuthorizedSubmit() {
528
		$result = false;
529
530
		$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...
531
		if ( $this->mFormIdentifier === null ) {
532
			$identOkay = true;
533
		} else {
534
			$identOkay = $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier;
535
		}
536
537
		$tokenOkay = false;
538
		if ( $this->getMethod() !== 'post' ) {
539
			$tokenOkay = true; // no session check needed
540
		} elseif ( $this->getRequest()->wasPosted() ) {
541
			$editToken = $this->getRequest()->getVal( 'wpEditToken' );
542
			if ( $this->getUser()->isLoggedIn() || $editToken !== null ) {
543
				// Session tokens for logged-out users have no security value.
544
				// However, if the user gave one, check it in order to give a nice
545
				// "session expired" error instead of "permission denied" or such.
546
				$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...
547
			} else {
548
				$tokenOkay = true;
549
			}
550
		}
551
552
		if ( $tokenOkay && $identOkay ) {
553
			$this->mWasSubmitted = true;
554
			$result = $this->trySubmit();
555
		}
556
557
		return $result;
558
	}
559
560
	/**
561
	 * The here's-one-I-made-earlier option: do the submission if
562
	 * posted, or display the form with or without funky validation
563
	 * errors
564
	 * @return bool|Status Whether submission was successful.
565
	 */
566
	public function show() {
567
		$this->prepareForm();
568
569
		$result = $this->tryAuthorizedSubmit();
570
		if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
571
			return $result;
572
		}
573
574
		$this->displayForm( $result );
575
576
		return false;
577
	}
578
579
	/**
580
	 * Same as self::show with the difference, that the form will be
581
	 * added to the output, no matter, if the validation was good or not.
582
	 * @return bool|Status Whether submission was successful.
583
	 */
584
	public function showAlways() {
585
		$this->prepareForm();
586
587
		$result = $this->tryAuthorizedSubmit();
588
589
		$this->displayForm( $result );
590
591
		return $result;
592
	}
593
594
	/**
595
	 * Validate all the fields, and call the submission callback
596
	 * function if everything is kosher.
597
	 * @throws MWException
598
	 * @return bool|string|array|Status
599
	 *     - Bool true or a good Status object indicates success,
600
	 *     - Bool false indicates no submission was attempted,
601
	 *     - Anything else indicates failure. The value may be a fatal Status
602
	 *       object, an HTML string, or an array of arrays (message keys and
603
	 *       params) or strings (message keys)
604
	 */
605
	public function trySubmit() {
606
		$valid = true;
607
		$hoistedErrors = [];
608
		$hoistedErrors[] = isset( $this->mValidationErrorMessage )
609
			? $this->mValidationErrorMessage
610
			: [ 'htmlform-invalid-input' ];
611
612
		$this->mWasSubmitted = true;
613
614
		# Check for cancelled submission
615
		foreach ( $this->mFlatFields as $fieldname => $field ) {
616
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
617
				continue;
618
			}
619
			if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
620
				$this->mWasSubmitted = false;
621
				return false;
622
			}
623
		}
624
625
		# Check for validation
626
		foreach ( $this->mFlatFields as $fieldname => $field ) {
627
			if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
628
				continue;
629
			}
630
			if ( $field->isHidden( $this->mFieldData ) ) {
631
				continue;
632
			}
633
			$res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
634
			if ( $res !== true ) {
635
				$valid = false;
636
				if ( $res !== false && !$field->canDisplayErrors() ) {
637
					$hoistedErrors[] = [ 'rawmessage', $res ];
638
				}
639
			}
640
		}
641
642
		if ( !$valid ) {
643
			if ( count( $hoistedErrors ) === 1 ) {
644
				$hoistedErrors = $hoistedErrors[0];
645
			}
646
			return $hoistedErrors;
647
		}
648
649
		$callback = $this->mSubmitCallback;
650
		if ( !is_callable( $callback ) ) {
651
			throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
652
				'setSubmitCallback() to set one.' );
653
		}
654
655
		$data = $this->filterDataForSubmit( $this->mFieldData );
656
657
		$res = call_user_func( $callback, $data, $this );
658
		if ( $res === false ) {
659
			$this->mWasSubmitted = false;
660
		}
661
662
		return $res;
663
	}
664
665
	/**
666
	 * Test whether the form was considered to have been submitted or not, i.e.
667
	 * whether the last call to tryAuthorizedSubmit or trySubmit returned
668
	 * non-false.
669
	 *
670
	 * This will return false until HTMLForm::tryAuthorizedSubmit or
671
	 * HTMLForm::trySubmit is called.
672
	 *
673
	 * @since 1.23
674
	 * @return bool
675
	 */
676
	public function wasSubmitted() {
677
		return $this->mWasSubmitted;
678
	}
679
680
	/**
681
	 * Set a callback to a function to do something with the form
682
	 * once it's been successfully validated.
683
	 *
684
	 * @param callable $cb The function will be passed the output from
685
	 *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
686
	 *   return as documented for HTMLForm::trySubmit
687
	 *
688
	 * @return HTMLForm $this for chaining calls (since 1.20)
689
	 */
690
	public function setSubmitCallback( $cb ) {
691
		$this->mSubmitCallback = $cb;
692
693
		return $this;
694
	}
695
696
	/**
697
	 * Set a message to display on a validation error.
698
	 *
699
	 * @param string|array $msg String or Array of valid inputs to wfMessage()
700
	 *     (so each entry can be either a String or Array)
701
	 *
702
	 * @return HTMLForm $this for chaining calls (since 1.20)
703
	 */
704
	public function setValidationErrorMessage( $msg ) {
705
		$this->mValidationErrorMessage = $msg;
706
707
		return $this;
708
	}
709
710
	/**
711
	 * Set the introductory message, overwriting any existing message.
712
	 *
713
	 * @param string $msg Complete text of message to display
714
	 *
715
	 * @return HTMLForm $this for chaining calls (since 1.20)
716
	 */
717
	public function setIntro( $msg ) {
718
		$this->setPreText( $msg );
719
720
		return $this;
721
	}
722
723
	/**
724
	 * Set the introductory message HTML, overwriting any existing message.
725
	 * @since 1.19
726
	 *
727
	 * @param string $msg Complete HTML of message to display
728
	 *
729
	 * @return HTMLForm $this for chaining calls (since 1.20)
730
	 */
731
	public function setPreText( $msg ) {
732
		$this->mPre = $msg;
733
734
		return $this;
735
	}
736
737
	/**
738
	 * Add HTML to introductory message.
739
	 *
740
	 * @param string $msg Complete HTML of message to display
741
	 *
742
	 * @return HTMLForm $this for chaining calls (since 1.20)
743
	 */
744
	public function addPreText( $msg ) {
745
		$this->mPre .= $msg;
746
747
		return $this;
748
	}
749
750
	/**
751
	 * Add HTML to the header, inside the form.
752
	 *
753
	 * @param string $msg Additional HTML to display in header
754
	 * @param string|null $section The section to add the header to
755
	 *
756
	 * @return HTMLForm $this for chaining calls (since 1.20)
757
	 */
758 View Code Duplication
	public function addHeaderText( $msg, $section = null ) {
759
		if ( $section === null ) {
760
			$this->mHeader .= $msg;
761
		} else {
762
			if ( !isset( $this->mSectionHeaders[$section] ) ) {
763
				$this->mSectionHeaders[$section] = '';
764
			}
765
			$this->mSectionHeaders[$section] .= $msg;
766
		}
767
768
		return $this;
769
	}
770
771
	/**
772
	 * Set header text, inside the form.
773
	 * @since 1.19
774
	 *
775
	 * @param string $msg Complete HTML of header to display
776
	 * @param string|null $section The section to add the header to
777
	 *
778
	 * @return HTMLForm $this for chaining calls (since 1.20)
779
	 */
780
	public function setHeaderText( $msg, $section = null ) {
781
		if ( $section === null ) {
782
			$this->mHeader = $msg;
783
		} else {
784
			$this->mSectionHeaders[$section] = $msg;
785
		}
786
787
		return $this;
788
	}
789
790
	/**
791
	 * Get header text.
792
	 *
793
	 * @param string|null $section The section to get the header text for
794
	 * @since 1.26
795
	 * @return string HTML
796
	 */
797
	public function getHeaderText( $section = null ) {
798
		if ( $section === null ) {
799
			return $this->mHeader;
800
		} else {
801
			return isset( $this->mSectionHeaders[$section] ) ? $this->mSectionHeaders[$section] : '';
802
		}
803
	}
804
805
	/**
806
	 * Add footer text, inside the form.
807
	 *
808
	 * @param string $msg Complete text of message to display
809
	 * @param string|null $section The section to add the footer text to
810
	 *
811
	 * @return HTMLForm $this for chaining calls (since 1.20)
812
	 */
813 View Code Duplication
	public function addFooterText( $msg, $section = null ) {
814
		if ( $section === null ) {
815
			$this->mFooter .= $msg;
816
		} else {
817
			if ( !isset( $this->mSectionFooters[$section] ) ) {
818
				$this->mSectionFooters[$section] = '';
819
			}
820
			$this->mSectionFooters[$section] .= $msg;
821
		}
822
823
		return $this;
824
	}
825
826
	/**
827
	 * Set footer text, inside the form.
828
	 * @since 1.19
829
	 *
830
	 * @param string $msg Complete text of message to display
831
	 * @param string|null $section The section to add the footer text to
832
	 *
833
	 * @return HTMLForm $this for chaining calls (since 1.20)
834
	 */
835
	public function setFooterText( $msg, $section = null ) {
836
		if ( $section === null ) {
837
			$this->mFooter = $msg;
838
		} else {
839
			$this->mSectionFooters[$section] = $msg;
840
		}
841
842
		return $this;
843
	}
844
845
	/**
846
	 * Get footer text.
847
	 *
848
	 * @param string|null $section The section to get the footer text for
849
	 * @since 1.26
850
	 * @return string
851
	 */
852
	public function getFooterText( $section = null ) {
853
		if ( $section === null ) {
854
			return $this->mFooter;
855
		} else {
856
			return isset( $this->mSectionFooters[$section] ) ? $this->mSectionFooters[$section] : '';
857
		}
858
	}
859
860
	/**
861
	 * Add text to the end of the display.
862
	 *
863
	 * @param string $msg Complete text of message to display
864
	 *
865
	 * @return HTMLForm $this for chaining calls (since 1.20)
866
	 */
867
	public function addPostText( $msg ) {
868
		$this->mPost .= $msg;
869
870
		return $this;
871
	}
872
873
	/**
874
	 * Set text at the end of the display.
875
	 *
876
	 * @param string $msg Complete text of message to display
877
	 *
878
	 * @return HTMLForm $this for chaining calls (since 1.20)
879
	 */
880
	public function setPostText( $msg ) {
881
		$this->mPost = $msg;
882
883
		return $this;
884
	}
885
886
	/**
887
	 * Add a hidden field to the output
888
	 *
889
	 * @param string $name Field name.  This will be used exactly as entered
890
	 * @param string $value Field value
891
	 * @param array $attribs
892
	 *
893
	 * @return HTMLForm $this for chaining calls (since 1.20)
894
	 */
895
	public function addHiddenField( $name, $value, array $attribs = [] ) {
896
		$attribs += [ 'name' => $name ];
897
		$this->mHiddenFields[] = [ $value, $attribs ];
898
899
		return $this;
900
	}
901
902
	/**
903
	 * Add an array of hidden fields to the output
904
	 *
905
	 * @since 1.22
906
	 *
907
	 * @param array $fields Associative array of fields to add;
908
	 *        mapping names to their values
909
	 *
910
	 * @return HTMLForm $this for chaining calls
911
	 */
912
	public function addHiddenFields( array $fields ) {
913
		foreach ( $fields as $name => $value ) {
914
			$this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
915
		}
916
917
		return $this;
918
	}
919
920
	/**
921
	 * Add a button to the form
922
	 *
923
	 * @since 1.27 takes an array as shown. Earlier versions accepted
924
	 *  'name', 'value', 'id', and 'attribs' as separate parameters in that
925
	 *  order.
926
	 * @note Custom labels ('label', 'label-message', 'label-raw') are not
927
	 *  supported for IE6 and IE7 due to bugs in those browsers. If detected,
928
	 *  they will be served buttons using 'value' as the button label.
929
	 * @param array $data Data to define the button:
930
	 *  - name: (string) Button name.
931
	 *  - value: (string) Button value.
932
	 *  - label-message: (string, optional) Button label message key to use
933
	 *    instead of 'value'. Overrides 'label' and 'label-raw'.
934
	 *  - label: (string, optional) Button label text to use instead of
935
	 *    'value'. Overrides 'label-raw'.
936
	 *  - label-raw: (string, optional) Button label HTML to use instead of
937
	 *    'value'.
938
	 *  - id: (string, optional) DOM id for the button.
939
	 *  - attribs: (array, optional) Additional HTML attributes.
940
	 *  - flags: (string|string[], optional) OOUI flags.
941
	 *  - framed: (boolean=true, optional) OOUI framed attribute.
942
	 * @return HTMLForm $this for chaining calls (since 1.20)
943
	 */
944
	public function addButton( $data ) {
945
		if ( !is_array( $data ) ) {
946
			$args = func_get_args();
947
			if ( count( $args ) < 2 || count( $args ) > 4 ) {
948
				throw new InvalidArgumentException(
949
					'Incorrect number of arguments for deprecated calling style'
950
				);
951
			}
952
			$data = [
953
				'name' => $args[0],
954
				'value' => $args[1],
955
				'id' => isset( $args[2] ) ? $args[2] : null,
956
				'attribs' => isset( $args[3] ) ? $args[3] : null,
957
			];
958
		} else {
959
			if ( !isset( $data['name'] ) ) {
960
				throw new InvalidArgumentException( 'A name is required' );
961
			}
962
			if ( !isset( $data['value'] ) ) {
963
				throw new InvalidArgumentException( 'A value is required' );
964
			}
965
		}
966
		$this->mButtons[] = $data + [
967
			'id' => null,
968
			'attribs' => null,
969
			'flags' => null,
970
			'framed' => true,
971
		];
972
973
		return $this;
974
	}
975
976
	/**
977
	 * Set the salt for the edit token.
978
	 *
979
	 * Only useful when the method is "post".
980
	 *
981
	 * @since 1.24
982
	 * @param string|array $salt Salt to use
983
	 * @return HTMLForm $this For chaining calls
984
	 */
985
	public function setTokenSalt( $salt ) {
986
		$this->mTokenSalt = $salt;
987
988
		return $this;
989
	}
990
991
	/**
992
	 * Display the form (sending to the context's OutputPage object), with an
993
	 * appropriate error message or stack of messages, and any validation errors, etc.
994
	 *
995
	 * @attention You should call prepareForm() before calling this function.
996
	 * Moreover, when doing method chaining this should be the very last method
997
	 * call just after prepareForm().
998
	 *
999
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
1000
	 *
1001
	 * @return void Nothing, should be last call
1002
	 */
1003
	public function displayForm( $submitResult ) {
1004
		$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
1005
	}
1006
1007
	/**
1008
	 * Returns the raw HTML generated by the form
1009
	 *
1010
	 * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
1011
	 *
1012
	 * @return string HTML
1013
	 */
1014
	public function getHTML( $submitResult ) {
1015
		# For good measure (it is the default)
1016
		$this->getOutput()->preventClickjacking();
1017
		$this->getOutput()->addModules( 'mediawiki.htmlform' );
1018
		$this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
1019
1020
		$html = ''
1021
			. $this->getErrorsOrWarnings( $submitResult, 'error' )
1022
			. $this->getErrorsOrWarnings( $submitResult, 'warning' )
1023
			. $this->getHeaderText()
1024
			. $this->getBody()
1025
			. $this->getHiddenFields()
1026
			. $this->getButtons()
1027
			. $this->getFooterText();
1028
1029
		$html = $this->wrapForm( $html );
1030
1031
		return '' . $this->mPre . $html . $this->mPost;
1032
	}
1033
1034
	/**
1035
	 * Get HTML attributes for the `<form>` tag.
1036
	 * @return array
1037
	 */
1038
	protected function getFormAttributes() {
1039
		# Use multipart/form-data
1040
		$encType = $this->mUseMultipart
1041
			? 'multipart/form-data'
1042
			: 'application/x-www-form-urlencoded';
1043
		# Attributes
1044
		$attribs = [
1045
			'action' => $this->getAction(),
1046
			'method' => $this->getMethod(),
1047
			'enctype' => $encType,
1048
		];
1049
		if ( $this->mId ) {
1050
			$attribs['id'] = $this->mId;
1051
		}
1052
		if ( $this->mAutocomplete ) {
1053
			$attribs['autocomplete'] = $this->mAutocomplete;
1054
		}
1055
		if ( $this->mName ) {
1056
			$attribs['name'] = $this->mName;
1057
		}
1058
		return $attribs;
1059
	}
1060
1061
	/**
1062
	 * Wrap the form innards in an actual "<form>" element
1063
	 *
1064
	 * @param string $html HTML contents to wrap.
1065
	 *
1066
	 * @return string Wrapped HTML.
1067
	 */
1068
	public function wrapForm( $html ) {
1069
		# Include a <fieldset> wrapper for style, if requested.
1070
		if ( $this->mWrapperLegend !== false ) {
1071
			$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1072
			$html = Xml::fieldset( $legend, $html );
1073
		}
1074
1075
		return Html::rawElement(
1076
			'form',
1077
			$this->getFormAttributes() + [ 'class' => 'visualClear' ],
1078
			$html
1079
		);
1080
	}
1081
1082
	/**
1083
	 * Get the hidden fields that should go inside the form.
1084
	 * @return string HTML.
1085
	 */
1086
	public function getHiddenFields() {
1087
		$html = '';
1088
		if ( $this->mFormIdentifier !== null ) {
1089
			$html .= Html::hidden(
1090
				'wpFormIdentifier',
1091
				$this->mFormIdentifier
1092
			) . "\n";
1093
		}
1094
		if ( $this->getMethod() === 'post' ) {
1095
			$html .= Html::hidden(
1096
				'wpEditToken',
1097
				$this->getUser()->getEditToken( $this->mTokenSalt ),
1098
				[ 'id' => 'wpEditToken' ]
1099
			) . "\n";
1100
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1101
		}
1102
1103
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1104
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1105
			$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1106
		}
1107
1108
		foreach ( $this->mHiddenFields as $data ) {
1109
			list( $value, $attribs ) = $data;
1110
			$html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1111
		}
1112
1113
		return $html;
1114
	}
1115
1116
	/**
1117
	 * Get the submit and (potentially) reset buttons.
1118
	 * @return string HTML.
1119
	 */
1120
	public function getButtons() {
1121
		$buttons = '';
1122
		$useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
1123
1124
		if ( $this->mShowSubmit ) {
1125
			$attribs = [];
1126
1127
			if ( isset( $this->mSubmitID ) ) {
1128
				$attribs['id'] = $this->mSubmitID;
1129
			}
1130
1131
			if ( isset( $this->mSubmitName ) ) {
1132
				$attribs['name'] = $this->mSubmitName;
1133
			}
1134
1135
			if ( isset( $this->mSubmitTooltip ) ) {
1136
				$attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1137
			}
1138
1139
			$attribs['class'] = [ 'mw-htmlform-submit' ];
1140
1141
			if ( $useMediaWikiUIEverywhere ) {
1142
				foreach ( $this->mSubmitFlags as $flag ) {
1143
					$attribs['class'][] = 'mw-ui-' . $flag;
1144
				}
1145
				$attribs['class'][] = 'mw-ui-button';
1146
			}
1147
1148
			$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1149
		}
1150
1151 View Code Duplication
		if ( $this->mShowReset ) {
1152
			$buttons .= Html::element(
1153
				'input',
1154
				[
1155
					'type' => 'reset',
1156
					'value' => $this->msg( 'htmlform-reset' )->text(),
1157
					'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1158
				]
1159
			) . "\n";
1160
		}
1161
1162 View Code Duplication
		if ( $this->mShowCancel ) {
1163
			$target = $this->mCancelTarget ?: Title::newMainPage();
1164
			if ( $target instanceof Title ) {
1165
				$target = $target->getLocalURL();
1166
			}
1167
			$buttons .= Html::element(
1168
					'a',
1169
					[
1170
						'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
1171
						'href' => $target,
1172
					],
1173
					$this->msg( 'cancel' )->text()
1174
				) . "\n";
1175
		}
1176
1177
		// IE<8 has bugs with <button>, so we'll need to avoid them.
1178
		$isBadIE = preg_match( '/MSIE [1-7]\./i', $this->getRequest()->getHeader( 'User-Agent' ) );
1179
1180
		foreach ( $this->mButtons as $button ) {
1181
			$attrs = [
1182
				'type' => 'submit',
1183
				'name' => $button['name'],
1184
				'value' => $button['value']
1185
			];
1186
1187
			if ( isset( $button['label-message'] ) ) {
1188
				$label = $this->getMessage( $button['label-message'] )->parse();
1189
			} elseif ( isset( $button['label'] ) ) {
1190
				$label = htmlspecialchars( $button['label'] );
1191
			} elseif ( isset( $button['label-raw'] ) ) {
1192
				$label = $button['label-raw'];
1193
			} else {
1194
				$label = htmlspecialchars( $button['value'] );
1195
			}
1196
1197
			if ( $button['attribs'] ) {
1198
				$attrs += $button['attribs'];
1199
			}
1200
1201
			if ( isset( $button['id'] ) ) {
1202
				$attrs['id'] = $button['id'];
1203
			}
1204
1205
			if ( $useMediaWikiUIEverywhere ) {
1206
				$attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : [];
1207
				$attrs['class'][] = 'mw-ui-button';
1208
			}
1209
1210
			if ( $isBadIE ) {
1211
				$buttons .= Html::element( 'input', $attrs ) . "\n";
1212
			} else {
1213
				$buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1214
			}
1215
		}
1216
1217
		if ( !$buttons ) {
1218
			return '';
1219
		}
1220
1221
		return Html::rawElement( 'span',
1222
			[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1223
	}
1224
1225
	/**
1226
	 * Get the whole body of the form.
1227
	 * @return string
1228
	 */
1229
	public function getBody() {
1230
		return $this->displaySection( $this->mFieldTree, $this->mTableId );
1231
	}
1232
1233
	/**
1234
	 * Format and display an error message stack.
1235
	 *
1236
	 * @param string|array|Status $errors
1237
	 *
1238
	 * @deprecated since 1.28, use getErrorsOrWarnings() instead
1239
	 *
1240
	 * @return string
1241
	 */
1242
	public function getErrors( $errors ) {
1243
		wfDeprecated( __METHOD__ );
1244
		return $this->getErrorsOrWarnings( $errors, 'error' );
1245
	}
1246
1247
	/**
1248
	 * Returns a formatted list of errors or warnings from the given elements.
1249
	 *
1250
	 * @param string|array|Status $elements The set of errors/warnings to process.
1251
	 * @param string $elementsType Should warnings or errors be returned.  This is meant
1252
	 * 	for Status objects, all other valid types are always considered as errors.
1253
	 * @return string
1254
	 */
1255
	public function getErrorsOrWarnings( $elements, $elementsType ) {
1256 View Code Duplication
		if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) {
1257
			throw new DomainException( $elementsType . ' is not a valid type.' );
1258
		}
1259
		$elementstr = false;
1260
		if ( $elements instanceof Status ) {
1261
			list( $errorStatus, $warningStatus ) = $elements->splitByErrorType();
1262
			$status = $elementsType === 'error' ? $errorStatus : $warningStatus;
1263
			if ( $status->isGood() ) {
1264
				$elementstr = '';
1265
			} else {
1266
				$elementstr = $this->getOutput()->parse(
1267
					$status->getWikiText()
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class StatusValue as the method getWikiText() does only exist in the following sub-classes of StatusValue: FileRepoStatus, Status. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1268
				);
1269
			}
1270
		} elseif ( is_array( $elements ) && $elementsType === 'error' ) {
1271
			$elementstr = $this->formatErrors( $elements );
1272
		} elseif ( $elementsType === 'error' ) {
1273
			$elementstr = $elements;
1274
		}
1275
1276
		return $elementstr
1277
			? Html::rawElement( 'div', [ 'class' => $elementsType ], $elementstr )
0 ignored issues
show
Bug introduced by
It seems like $elementstr can also be of type array; however, Html::rawElement() 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...
1278
			: '';
1279
	}
1280
1281
	/**
1282
	 * Format a stack of error messages into a single HTML string
1283
	 *
1284
	 * @param array $errors Array of message keys/values
1285
	 *
1286
	 * @return string HTML, a "<ul>" list of errors
1287
	 */
1288
	public function formatErrors( $errors ) {
1289
		$errorstr = '';
1290
1291
		foreach ( $errors as $error ) {
1292
			$errorstr .= Html::rawElement(
1293
				'li',
1294
				[],
1295
				$this->getMessage( $error )->parse()
1296
			);
1297
		}
1298
1299
		$errorstr = Html::rawElement( 'ul', [], $errorstr );
1300
1301
		return $errorstr;
1302
	}
1303
1304
	/**
1305
	 * Set the text for the submit button
1306
	 *
1307
	 * @param string $t Plaintext
1308
	 *
1309
	 * @return HTMLForm $this for chaining calls (since 1.20)
1310
	 */
1311
	public function setSubmitText( $t ) {
1312
		$this->mSubmitText = $t;
1313
1314
		return $this;
1315
	}
1316
1317
	/**
1318
	 * Identify that the submit button in the form has a destructive action
1319
	 * @since 1.24
1320
	 *
1321
	 * @return HTMLForm $this for chaining calls (since 1.28)
1322
	 */
1323
	public function setSubmitDestructive() {
1324
		$this->mSubmitFlags = [ 'destructive', 'primary' ];
1325
1326
		return $this;
1327
	}
1328
1329
	/**
1330
	 * Identify that the submit button in the form has a progressive action
1331
	 * @since 1.25
1332
	 *
1333
	 * @return HTMLForm $this for chaining calls (since 1.28)
1334
	 */
1335
	public function setSubmitProgressive() {
1336
		$this->mSubmitFlags = [ 'progressive', 'primary' ];
1337
1338
		return $this;
1339
	}
1340
1341
	/**
1342
	 * Set the text for the submit button to a message
1343
	 * @since 1.19
1344
	 *
1345
	 * @param string|Message $msg Message key or Message object
1346
	 *
1347
	 * @return HTMLForm $this for chaining calls (since 1.20)
1348
	 */
1349
	public function setSubmitTextMsg( $msg ) {
1350
		if ( !$msg instanceof Message ) {
1351
			$msg = $this->msg( $msg );
1352
		}
1353
		$this->setSubmitText( $msg->text() );
1354
1355
		return $this;
1356
	}
1357
1358
	/**
1359
	 * Get the text for the submit button, either customised or a default.
1360
	 * @return string
1361
	 */
1362
	public function getSubmitText() {
1363
		return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1364
	}
1365
1366
	/**
1367
	 * @param string $name Submit button name
1368
	 *
1369
	 * @return HTMLForm $this for chaining calls (since 1.20)
1370
	 */
1371
	public function setSubmitName( $name ) {
1372
		$this->mSubmitName = $name;
1373
1374
		return $this;
1375
	}
1376
1377
	/**
1378
	 * @param string $name Tooltip for the submit button
1379
	 *
1380
	 * @return HTMLForm $this for chaining calls (since 1.20)
1381
	 */
1382
	public function setSubmitTooltip( $name ) {
1383
		$this->mSubmitTooltip = $name;
1384
1385
		return $this;
1386
	}
1387
1388
	/**
1389
	 * Set the id for the submit button.
1390
	 *
1391
	 * @param string $t
1392
	 *
1393
	 * @todo FIXME: Integrity of $t is *not* validated
1394
	 * @return HTMLForm $this for chaining calls (since 1.20)
1395
	 */
1396
	public function setSubmitID( $t ) {
1397
		$this->mSubmitID = $t;
1398
1399
		return $this;
1400
	}
1401
1402
	/**
1403
	 * Set an internal identifier for this form. It will be submitted as a hidden form field, allowing
1404
	 * HTMLForm to determine whether the form was submitted (or merely viewed). Setting this serves
1405
	 * two purposes:
1406
	 *
1407
	 * - If you use two or more forms on one page, it allows HTMLForm to identify which of the forms
1408
	 *   was submitted, and not attempt to validate the other ones.
1409
	 * - If you use checkbox or multiselect fields inside a form using the GET method, it allows
1410
	 *   HTMLForm to distinguish between the initial page view and a form submission with all
1411
	 *   checkboxes or select options unchecked.
1412
	 *
1413
	 * @since 1.28
1414
	 * @param string $ident
1415
	 * @return $this
1416
	 */
1417
	public function setFormIdentifier( $ident ) {
1418
		$this->mFormIdentifier = $ident;
1419
1420
		return $this;
1421
	}
1422
1423
	/**
1424
	 * Stop a default submit button being shown for this form. This implies that an
1425
	 * alternate submit method must be provided manually.
1426
	 *
1427
	 * @since 1.22
1428
	 *
1429
	 * @param bool $suppressSubmit Set to false to re-enable the button again
1430
	 *
1431
	 * @return HTMLForm $this for chaining calls
1432
	 */
1433
	public function suppressDefaultSubmit( $suppressSubmit = true ) {
1434
		$this->mShowSubmit = !$suppressSubmit;
1435
1436
		return $this;
1437
	}
1438
1439
	/**
1440
	 * Show a cancel button (or prevent it). The button is not shown by default.
1441
	 * @param bool $show
1442
	 * @return HTMLForm $this for chaining calls
1443
	 * @since 1.27
1444
	 */
1445
	public function showCancel( $show = true ) {
1446
		$this->mShowCancel = $show;
1447
		return $this;
1448
	}
1449
1450
	/**
1451
	 * Sets the target where the user is redirected to after clicking cancel.
1452
	 * @param Title|string $target Target as a Title object or an URL
1453
	 * @return HTMLForm $this for chaining calls
1454
	 * @since 1.27
1455
	 */
1456
	public function setCancelTarget( $target ) {
1457
		$this->mCancelTarget = $target;
1458
		return $this;
1459
	}
1460
1461
	/**
1462
	 * Set the id of the \<table\> or outermost \<div\> element.
1463
	 *
1464
	 * @since 1.22
1465
	 *
1466
	 * @param string $id New value of the id attribute, or "" to remove
1467
	 *
1468
	 * @return HTMLForm $this for chaining calls
1469
	 */
1470
	public function setTableId( $id ) {
1471
		$this->mTableId = $id;
1472
1473
		return $this;
1474
	}
1475
1476
	/**
1477
	 * @param string $id DOM id for the form
1478
	 *
1479
	 * @return HTMLForm $this for chaining calls (since 1.20)
1480
	 */
1481
	public function setId( $id ) {
1482
		$this->mId = $id;
1483
1484
		return $this;
1485
	}
1486
1487
	/**
1488
	 * @param string $name 'name' attribute for the form
1489
	 * @return HTMLForm $this for chaining calls
1490
	 */
1491
	public function setName( $name ) {
1492
		$this->mName = $name;
1493
1494
		return $this;
1495
	}
1496
1497
	/**
1498
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1499
	 * this text as its "<legend>" element.
1500
	 *
1501
	 * @param string|bool $legend If false, no wrapper or legend will be displayed.
1502
	 *     If true, a wrapper will be displayed, but no legend.
1503
	 *     If a string, a wrapper will be displayed with that string as a legend.
1504
	 *     The string will be escaped before being output (this doesn't support HTML).
1505
	 *
1506
	 * @return HTMLForm $this for chaining calls (since 1.20)
1507
	 */
1508
	public function setWrapperLegend( $legend ) {
1509
		$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...
1510
1511
		return $this;
1512
	}
1513
1514
	/**
1515
	 * Prompt the whole form to be wrapped in a "<fieldset>", with
1516
	 * this message as its "<legend>" element.
1517
	 * @since 1.19
1518
	 *
1519
	 * @param string|Message $msg Message key or Message object
1520
	 *
1521
	 * @return HTMLForm $this for chaining calls (since 1.20)
1522
	 */
1523
	public function setWrapperLegendMsg( $msg ) {
1524
		if ( !$msg instanceof Message ) {
1525
			$msg = $this->msg( $msg );
1526
		}
1527
		$this->setWrapperLegend( $msg->text() );
1528
1529
		return $this;
1530
	}
1531
1532
	/**
1533
	 * Set the prefix for various default messages
1534
	 * @todo Currently only used for the "<fieldset>" legend on forms
1535
	 * with multiple sections; should be used elsewhere?
1536
	 *
1537
	 * @param string $p
1538
	 *
1539
	 * @return HTMLForm $this for chaining calls (since 1.20)
1540
	 */
1541
	public function setMessagePrefix( $p ) {
1542
		$this->mMessagePrefix = $p;
1543
1544
		return $this;
1545
	}
1546
1547
	/**
1548
	 * Set the title for form submission
1549
	 *
1550
	 * @param Title $t Title of page the form is on/should be posted to
1551
	 *
1552
	 * @return HTMLForm $this for chaining calls (since 1.20)
1553
	 */
1554
	public function setTitle( $t ) {
1555
		$this->mTitle = $t;
1556
1557
		return $this;
1558
	}
1559
1560
	/**
1561
	 * Get the title
1562
	 * @return Title
1563
	 */
1564
	public function getTitle() {
1565
		return $this->mTitle === false
1566
			? $this->getContext()->getTitle()
1567
			: $this->mTitle;
1568
	}
1569
1570
	/**
1571
	 * Set the method used to submit the form
1572
	 *
1573
	 * @param string $method
1574
	 *
1575
	 * @return HTMLForm $this for chaining calls (since 1.20)
1576
	 */
1577
	public function setMethod( $method = 'post' ) {
1578
		$this->mMethod = strtolower( $method );
1579
1580
		return $this;
1581
	}
1582
1583
	/**
1584
	 * @return string Always lowercase
1585
	 */
1586
	public function getMethod() {
1587
		return $this->mMethod;
1588
	}
1589
1590
	/**
1591
	 * Wraps the given $section into an user-visible fieldset.
1592
	 *
1593
	 * @param string $legend Legend text for the fieldset
1594
	 * @param string $section The section content in plain Html
1595
	 * @param array $attributes Additional attributes for the fieldset
1596
	 * @return string The fieldset's Html
1597
	 */
1598
	protected function wrapFieldSetSection( $legend, $section, $attributes ) {
1599
		return Xml::fieldset( $legend, $section, $attributes ) . "\n";
1600
	}
1601
1602
	/**
1603
	 * @todo Document
1604
	 *
1605
	 * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1606
	 *   objects).
1607
	 * @param string $sectionName ID attribute of the "<table>" tag for this
1608
	 *   section, ignored if empty.
1609
	 * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1610
	 *   each subsection, ignored if empty.
1611
	 * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1612
	 * @throws LogicException When called on uninitialized field data, e.g. When
1613
	 *  HTMLForm::displayForm was called without calling HTMLForm::prepareForm
1614
	 *  first.
1615
	 *
1616
	 * @return string
1617
	 */
1618
	public function displaySection( $fields,
1619
		$sectionName = '',
1620
		$fieldsetIDPrefix = '',
1621
		&$hasUserVisibleFields = false
1622
	) {
1623
		if ( $this->mFieldData === null ) {
1624
			throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1625
				. 'You probably called displayForm() without calling prepareForm() first.' );
1626
		}
1627
1628
		$displayFormat = $this->getDisplayFormat();
1629
1630
		$html = [];
1631
		$subsectionHtml = '';
1632
		$hasLabel = false;
1633
1634
		// Conveniently, PHP method names are case-insensitive.
1635
		// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
1636
		$getFieldHtmlMethod = $displayFormat === 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
1637
1638
		foreach ( $fields as $key => $value ) {
1639
			if ( $value instanceof HTMLFormField ) {
1640
				$v = array_key_exists( $key, $this->mFieldData )
1641
					? $this->mFieldData[$key]
1642
					: $value->getDefault();
1643
1644
				$retval = $value->$getFieldHtmlMethod( $v );
1645
1646
				// check, if the form field should be added to
1647
				// the output.
1648
				if ( $value->hasVisibleOutput() ) {
1649
					$html[] = $retval;
1650
1651
					$labelValue = trim( $value->getLabel() );
1652
					if ( $labelValue !== '&#160;' && $labelValue !== '' ) {
1653
						$hasLabel = true;
1654
					}
1655
1656
					$hasUserVisibleFields = true;
1657
				}
1658
			} elseif ( is_array( $value ) ) {
1659
				$subsectionHasVisibleFields = false;
1660
				$section =
1661
					$this->displaySection( $value,
1662
						"mw-htmlform-$key",
1663
						"$fieldsetIDPrefix$key-",
1664
						$subsectionHasVisibleFields );
1665
				$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...
1666
1667
				if ( $subsectionHasVisibleFields === true ) {
1668
					// Display the section with various niceties.
1669
					$hasUserVisibleFields = true;
1670
1671
					$legend = $this->getLegend( $key );
1672
1673
					$section = $this->getHeaderText( $key ) .
1674
						$section .
1675
						$this->getFooterText( $key );
1676
1677
					$attributes = [];
1678
					if ( $fieldsetIDPrefix ) {
1679
						$attributes['id'] = Sanitizer::escapeId( "$fieldsetIDPrefix$key" );
1680
					}
1681
					$subsectionHtml .= $this->wrapFieldSetSection( $legend, $section, $attributes );
1682
				} else {
1683
					// Just return the inputs, nothing fancy.
1684
					$subsectionHtml .= $section;
1685
				}
1686
			}
1687
		}
1688
1689
		$html = $this->formatSection( $html, $sectionName, $hasLabel );
1690
1691
		if ( $subsectionHtml ) {
1692
			if ( $this->mSubSectionBeforeFields ) {
1693
				return $subsectionHtml . "\n" . $html;
1694
			} else {
1695
				return $html . "\n" . $subsectionHtml;
1696
			}
1697
		} else {
1698
			return $html;
1699
		}
1700
	}
1701
1702
	/**
1703
	 * Put a form section together from the individual fields' HTML, merging it and wrapping.
1704
	 * @param array $fieldsHtml
1705
	 * @param string $sectionName
1706
	 * @param bool $anyFieldHasLabel
1707
	 * @return string HTML
1708
	 */
1709
	protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1710
		$displayFormat = $this->getDisplayFormat();
1711
		$html = implode( '', $fieldsHtml );
1712
1713
		if ( $displayFormat === 'raw' ) {
1714
			return $html;
1715
		}
1716
1717
		$classes = [];
1718
1719
		if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
1720
			$classes[] = 'mw-htmlform-nolabel';
1721
		}
1722
1723
		$attribs = [
1724
			'class' => implode( ' ', $classes ),
1725
		];
1726
1727
		if ( $sectionName ) {
1728
			$attribs['id'] = Sanitizer::escapeId( $sectionName );
1729
		}
1730
1731
		if ( $displayFormat === 'table' ) {
1732
			return Html::rawElement( 'table',
1733
					$attribs,
1734
					Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1735
		} elseif ( $displayFormat === 'inline' ) {
1736
			return Html::rawElement( 'span', $attribs, "\n$html\n" );
1737
		} else {
1738
			return Html::rawElement( 'div', $attribs, "\n$html\n" );
1739
		}
1740
	}
1741
1742
	/**
1743
	 * Construct the form fields from the Descriptor array
1744
	 */
1745
	public function loadData() {
1746
		$fieldData = [];
1747
1748 View Code Duplication
		foreach ( $this->mFlatFields as $fieldname => $field ) {
1749
			$request = $this->getRequest();
1750
			if ( $field->skipLoadData( $request ) ) {
1751
				continue;
1752
			} elseif ( !empty( $field->mParams['disabled'] ) ) {
1753
				$fieldData[$fieldname] = $field->getDefault();
1754
			} else {
1755
				$fieldData[$fieldname] = $field->loadDataFromRequest( $request );
1756
			}
1757
		}
1758
1759
		# Filter data.
1760
		foreach ( $fieldData as $name => &$value ) {
1761
			$field = $this->mFlatFields[$name];
1762
			$value = $field->filter( $value, $this->mFlatFields );
1763
		}
1764
1765
		$this->mFieldData = $fieldData;
1766
	}
1767
1768
	/**
1769
	 * Stop a reset button being shown for this form
1770
	 *
1771
	 * @param bool $suppressReset Set to false to re-enable the button again
1772
	 *
1773
	 * @return HTMLForm $this for chaining calls (since 1.20)
1774
	 */
1775
	public function suppressReset( $suppressReset = true ) {
1776
		$this->mShowReset = !$suppressReset;
1777
1778
		return $this;
1779
	}
1780
1781
	/**
1782
	 * Overload this if you want to apply special filtration routines
1783
	 * to the form as a whole, after it's submitted but before it's
1784
	 * processed.
1785
	 *
1786
	 * @param array $data
1787
	 *
1788
	 * @return array
1789
	 */
1790
	public function filterDataForSubmit( $data ) {
1791
		return $data;
1792
	}
1793
1794
	/**
1795
	 * Get a string to go in the "<legend>" of a section fieldset.
1796
	 * Override this if you want something more complicated.
1797
	 *
1798
	 * @param string $key
1799
	 *
1800
	 * @return string
1801
	 */
1802
	public function getLegend( $key ) {
1803
		return $this->msg( "{$this->mMessagePrefix}-$key" )->text();
1804
	}
1805
1806
	/**
1807
	 * Set the value for the action attribute of the form.
1808
	 * When set to false (which is the default state), the set title is used.
1809
	 *
1810
	 * @since 1.19
1811
	 *
1812
	 * @param string|bool $action
1813
	 *
1814
	 * @return HTMLForm $this for chaining calls (since 1.20)
1815
	 */
1816
	public function setAction( $action ) {
1817
		$this->mAction = $action;
1818
1819
		return $this;
1820
	}
1821
1822
	/**
1823
	 * Get the value for the action attribute of the form.
1824
	 *
1825
	 * @since 1.22
1826
	 *
1827
	 * @return string
1828
	 */
1829
	public function getAction() {
1830
		// If an action is alredy provided, return it
1831
		if ( $this->mAction !== false ) {
1832
			return $this->mAction;
1833
		}
1834
1835
		$articlePath = $this->getConfig()->get( 'ArticlePath' );
1836
		// Check whether we are in GET mode and the ArticlePath contains a "?"
1837
		// meaning that getLocalURL() would return something like "index.php?title=...".
1838
		// As browser remove the query string before submitting GET forms,
1839
		// it means that the title would be lost. In such case use wfScript() instead
1840
		// and put title in an hidden field (see getHiddenFields()).
1841
		if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
1842
			return wfScript();
1843
		}
1844
1845
		return $this->getTitle()->getLocalURL();
1846
	}
1847
1848
	/**
1849
	 * Set the value for the autocomplete attribute of the form.
1850
	 * When set to false (which is the default state), the attribute get not set.
1851
	 *
1852
	 * @since 1.27
1853
	 *
1854
	 * @param string|bool $autocomplete
1855
	 *
1856
	 * @return HTMLForm $this for chaining calls
1857
	 */
1858
	public function setAutocomplete( $autocomplete ) {
1859
		$this->mAutocomplete = $autocomplete;
1860
1861
		return $this;
1862
	}
1863
1864
	/**
1865
	 * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
1866
	 * name + parameters array) into a Message.
1867
	 * @param mixed $value
1868
	 * @return Message
1869
	 */
1870
	protected function getMessage( $value ) {
1871
		return Message::newFromSpecifier( $value )->setContext( $this );
1872
	}
1873
}
1874