Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
34.77% covered (danger)
34.77%
185 / 532
25.93% covered (danger)
25.93%
21 / 81
CRAP
0.00% covered (danger)
0.00%
0 / 1
HTMLForm
34.84% covered (danger)
34.84%
185 / 531
25.93% covered (danger)
25.93%
21 / 81
14737.27
0.00% covered (danger)
0.00%
0 / 1
 factory
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addFields
26.67% covered (danger)
26.67%
4 / 15
0.00% covered (danger)
0.00%
0 / 1
20.20
 hasField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getField
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setDisplayFormat
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getClassFromDescriptor
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 loadInputFromParameters
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 prepareForm
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
5.12
 tryAuthorizedSubmit
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
8.09
 show
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 showAlways
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 trySubmit
35.56% covered (danger)
35.56%
16 / 45
0.00% covered (danger)
0.00%
0 / 1
151.54
 wasSubmitted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSubmitCallback
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setValidationErrorMessage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPreHtml
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addPreHtml
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPreHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addHeaderHtml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setHeaderHtml
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getHeaderHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 addFooterHtml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 setFooterHtml
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getFooterHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 addPostHtml
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPostHtml
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPostHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSections
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 addHiddenField
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addHiddenFields
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 addButton
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 setTokenSalt
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 displayForm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHiddenTitle
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getHTML
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
 setCollapsibleOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFormAttributes
77.78% covered (warning)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
6.40
 wrapForm
50.00% covered (danger)
50.00%
6 / 12
0.00% covered (danger)
0.00%
0 / 1
6.00
 getHiddenFields
68.75% covered (warning)
68.75%
11 / 16
0.00% covered (danger)
0.00%
0 / 1
4.49
 getButtons
30.95% covered (danger)
30.95%
13 / 42
0.00% covered (danger)
0.00%
0 / 1
68.63
 getBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrorsOrWarnings
78.26% covered (warning)
78.26%
18 / 23
0.00% covered (danger)
0.00%
0 / 1
12.24
 formatErrors
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 setSubmitText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setSubmitDestructive
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setSubmitTextMsg
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getSubmitText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 setSubmitName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setSubmitTooltip
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setSubmitID
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setFormIdentifier
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 suppressDefaultSubmit
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 showCancel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setCancelTarget
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCancelTargetURL
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 setTableId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setWrapperLegend
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setWrapperAttributes
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setWrapperLegendMsg
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setMessagePrefix
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setTitle
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 setMethod
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wrapFieldSetSection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 displaySection
21.74% covered (danger)
21.74%
10 / 46
0.00% covered (danger)
0.00%
0 / 1
107.95
 formatField
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 formatSection
12.50% covered (danger)
12.50%
2 / 16
0.00% covered (danger)
0.00%
0 / 1
39.83
 loadData
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadFieldData
37.50% covered (danger)
37.50%
6 / 16
0.00% covered (danger)
0.00%
0 / 1
18.96
 filterDataForSubmit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLegend
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 setAction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getAction
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 setAutocomplete
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 needsJSForHtml5FormValidation
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
4.12
1<?php
2
3/**
4 * HTML form generation and submission handling.
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 */
9
10namespace MediaWiki\HTMLForm;
11
12use DomainException;
13use InvalidArgumentException;
14use LogicException;
15use MediaWiki\Context\ContextSource;
16use MediaWiki\Context\IContextSource;
17use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
18use MediaWiki\Html\Html;
19use MediaWiki\HTMLForm\Field\HTMLApiField;
20use MediaWiki\HTMLForm\Field\HTMLAutoCompleteSelectField;
21use MediaWiki\HTMLForm\Field\HTMLCheckField;
22use MediaWiki\HTMLForm\Field\HTMLCheckMatrix;
23use MediaWiki\HTMLForm\Field\HTMLComboboxField;
24use MediaWiki\HTMLForm\Field\HTMLDateTimeField;
25use MediaWiki\HTMLForm\Field\HTMLEditTools;
26use MediaWiki\HTMLForm\Field\HTMLExpiryField;
27use MediaWiki\HTMLForm\Field\HTMLFileField;
28use MediaWiki\HTMLForm\Field\HTMLFloatField;
29use MediaWiki\HTMLForm\Field\HTMLFormFieldCloner;
30use MediaWiki\HTMLForm\Field\HTMLHiddenField;
31use MediaWiki\HTMLForm\Field\HTMLInfoField;
32use MediaWiki\HTMLForm\Field\HTMLIntField;
33use MediaWiki\HTMLForm\Field\HTMLMultiSelectField;
34use MediaWiki\HTMLForm\Field\HTMLNamespacesMultiselectField;
35use MediaWiki\HTMLForm\Field\HTMLOrderedMultiselectField;
36use MediaWiki\HTMLForm\Field\HTMLRadioField;
37use MediaWiki\HTMLForm\Field\HTMLSelectAndOtherField;
38use MediaWiki\HTMLForm\Field\HTMLSelectField;
39use MediaWiki\HTMLForm\Field\HTMLSelectLanguageField;
40use MediaWiki\HTMLForm\Field\HTMLSelectLimitField;
41use MediaWiki\HTMLForm\Field\HTMLSelectNamespace;
42use MediaWiki\HTMLForm\Field\HTMLSelectNamespaceWithButton;
43use MediaWiki\HTMLForm\Field\HTMLSelectOrOtherField;
44use MediaWiki\HTMLForm\Field\HTMLSizeFilterField;
45use MediaWiki\HTMLForm\Field\HTMLSubmitField;
46use MediaWiki\HTMLForm\Field\HTMLTagFilter;
47use MediaWiki\HTMLForm\Field\HTMLTagMultiselectField;
48use MediaWiki\HTMLForm\Field\HTMLTextAreaField;
49use MediaWiki\HTMLForm\Field\HTMLTextField;
50use MediaWiki\HTMLForm\Field\HTMLTextFieldWithButton;
51use MediaWiki\HTMLForm\Field\HTMLTimezoneField;
52use MediaWiki\HTMLForm\Field\HTMLTitlesMultiselectField;
53use MediaWiki\HTMLForm\Field\HTMLTitleTextField;
54use MediaWiki\HTMLForm\Field\HTMLUsersMultiselectField;
55use MediaWiki\HTMLForm\Field\HTMLUserTextField;
56use MediaWiki\Linker\Linker;
57use MediaWiki\Linker\LinkTarget;
58use MediaWiki\MainConfigNames;
59use MediaWiki\Message\Message;
60use MediaWiki\Page\PageReference;
61use MediaWiki\Parser\Sanitizer;
62use MediaWiki\Session\CsrfTokenSet;
63use MediaWiki\Status\Status;
64use MediaWiki\Title\Title;
65use MediaWiki\Title\TitleValue;
66use StatusValue;
67use Stringable;
68use Wikimedia\Message\MessageParam;
69use Wikimedia\Message\MessageSpecifier;
70
71/**
72 * Object handling generic submission, CSRF protection, layout and
73 * other logic for UI forms in a reusable manner.
74 *
75 * In order to generate the form, the HTMLForm object takes an array
76 * structure detailing the form fields available. Each element of the
77 * array is a basic property-list, including the type of field, the
78 * label it is to be given in the form, callbacks for validation and
79 * 'filtering', and other pertinent information.
80 *
81 * Field types are implemented as subclasses of the generic HTMLFormField
82 * object, and typically implement at least getInputHTML, which generates
83 * the HTML for the input field to be placed in the table.
84 *
85 * You can find extensive documentation on the www.mediawiki.org wiki:
86 *  - https://www.mediawiki.org/wiki/HTMLForm
87 *  - https://www.mediawiki.org/wiki/HTMLForm/tutorial
88 *
89 * The constructor input is an associative array of $fieldname => $info,
90 * where $info is an Associative Array with any of the following:
91 *
92 *    'class'               -- the subclass of HTMLFormField that will be used
93 *                             to create the object.  *NOT* the CSS class!
94 *    'type'                -- roughly translates into the <select> type attribute.
95 *                             if 'class' is not specified, this is used as a map
96 *                             through HTMLForm::$typeMappings to get the class name.
97 *    'default'             -- default value when the form is displayed
98 *    'nodata'              -- if set (to any value, which casts to true), the data
99 *                             for this field will not be loaded from the actual request. Instead,
100 *                             always the default data is set as the value of this field.
101 *    'id'                  -- HTML id attribute
102 *    'cssclass'            -- CSS class
103 *    'csshelpclass'        -- CSS class used to style help text
104 *    'dir'                 -- Direction of the element.
105 *    'options'             -- associative array mapping raw HTML labels to values.
106 *                             Some field types support multi-level arrays.
107 *                             Overwrites 'options-message'.
108 *    'options-messages'    -- associative array mapping message keys to values.
109 *                             Some field types support multi-level arrays.
110 *                             Overwrites 'options' and 'options-message'.
111 *    'options-messages-parse' -- Flag to parse the messages in 'options-messages'.
112 *    'options-message'     -- message key or object to be parsed to extract the list of
113 *                             options (like 'ipbreason-dropdown').
114 *    'label-message'       -- message key or object for a message to use as the label.
115 *                             can be an array of msg key and then parameters to
116 *                             the message.
117 *    'label'               -- alternatively, a raw text message. Overridden by
118 *                             label-message
119 *    'help-raw'            -- message text for a message to use as a help text.
120 *    'help-message'        -- message key or object for a message to use as a help text.
121 *                             can be an array of msg key and then parameters to
122 *                             the message.
123 *                             Overwrites 'help-messages' and 'help-raw'.
124 *    'help-messages'       -- array of message keys/objects. As above, each item can
125 *                             be an array of msg key and then parameters.
126 *                             Overwrites 'help-raw'.
127 *    'help-inline'         -- Whether help text (defined using options above) will be shown
128 *                             inline after the input field, rather than in a popup.
129 *                             Defaults to true. Only used by OOUI form fields.
130 *    'notices'             -- Array of plain text notices to display below the input field.
131 *                             Only used by OOUI form fields.
132 *    'required'            -- passed through to the object, indicating that it
133 *                             is a required field.
134 *    'size'                -- the length of text fields
135 *    'filter-callback'     -- a function name to give you the chance to
136 *                             massage the inputted value before it's processed.
137 *                             See {@link HTMLFormField::filter}
138 *    'validation-callback' -- a function name to give you the chance
139 *                             to impose extra validation on the field input. The signature should be
140 *                             as documented in {@link HTMLFormField::$mValidationCallback}.
141 *                             See {@link HTMLFormField::validate}
142 *    'name'                -- By default, the 'name' attribute of the input field
143 *                             is "wp{$fieldname}".  If you want a different name
144 *                             (eg one without the "wp" prefix), specify it here and
145 *                             it will be used without modification.
146 *    'hide-if'             -- expression given as an array stating when the field
147 *                             should be hidden. The first array value has to be the
148 *                             expression's logic operator. Supported expressions:
149 *                               'NOT'
150 *                                 [ 'NOT', array $expression ]
151 *                                 To hide a field if a given expression is not true.
152 *                               '==='
153 *                                 [ '===', string $fieldName, string $value ]
154 *                                 To hide a field if another field identified by
155 *                                 $field has the value $value.
156 *                               '!=='
157 *                                 [ '!==', string $fieldName, string $value ]
158 *                                 Same as [ 'NOT', [ '===', $fieldName, $value ]
159 *                               'OR', 'AND', 'NOR', 'NAND'
160 *                                 [ 'XXX', array $expression1, ..., array $expressionN ]
161 *                                 To hide a field if one or more (OR), all (AND),
162 *                                 neither (NOR) or not all (NAND) given expressions
163 *                                 are evaluated as true.
164 *                             The expressions will be given to a JavaScript frontend
165 *                             module which will continually update the field's
166 *                             visibility.
167 *    'disable-if'          -- expression given as an array stating when the field
168 *                             should be disabled. See 'hide-if' for supported expressions.
169 *                             The 'hide-if' logic would also disable fields, you don't need
170 *                             to set this attribute with the same condition manually.
171 *                             You can pass both 'disabled' and this attribute to omit extra
172 *                             check, but this would function only for not 'disabled' fields.
173 *    'section'             -- A string name for the section of the form to which the field
174 *                             belongs. Subsections may be added using the separator '/', e.g.:
175 *                               'section' => 'section1/subsection1'
176 *                             More levels may be added, e.g.:
177 *                               'section' => 'section1/subsection2/subsubsection1'
178 *                             The message key for a section or subsection header is built from
179 *                             its name and the form's message prefix (if present).
180 *
181 * Since 1.20, you can chain mutators to ease the form generation:
182 * @par Example:
183 * @code
184 * $form = new HTMLForm( $someFields, $this->getContext() );
185 * $form->setMethod( 'get' )
186 *      ->setWrapperLegendMsg( 'message-key' )
187 *      ->prepareForm()
188 *      ->displayForm( '' );
189 * @endcode
190 * Note that you will have prepareForm and displayForm at the end. Other
191 * method calls done after that would simply not be part of the form :(
192 *
193 * @stable to extend
194 */
195class HTMLForm extends ContextSource {
196    use ProtectedHookAccessorTrait;
197
198    /** @var string[] A mapping of 'type' inputs onto standard HTMLFormField subclasses */
199    public static $typeMappings = [
200        'api' => HTMLApiField::class,
201        'text' => HTMLTextField::class,
202        'textwithbutton' => HTMLTextFieldWithButton::class,
203        'textarea' => HTMLTextAreaField::class,
204        'select' => HTMLSelectField::class,
205        'combobox' => HTMLComboboxField::class,
206        'radio' => HTMLRadioField::class,
207        'multiselect' => HTMLMultiSelectField::class,
208        'limitselect' => HTMLSelectLimitField::class,
209        'check' => HTMLCheckField::class,
210        'toggle' => HTMLCheckField::class,
211        'int' => HTMLIntField::class,
212        'file' => HTMLFileField::class,
213        'float' => HTMLFloatField::class,
214        'info' => HTMLInfoField::class,
215        'selectorother' => HTMLSelectOrOtherField::class,
216        'selectandother' => HTMLSelectAndOtherField::class,
217        'namespaceselect' => HTMLSelectNamespace::class,
218        'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
219        'tagfilter' => HTMLTagFilter::class,
220        'sizefilter' => HTMLSizeFilterField::class,
221        'submit' => HTMLSubmitField::class,
222        'hidden' => HTMLHiddenField::class,
223        'edittools' => HTMLEditTools::class,
224        'checkmatrix' => HTMLCheckMatrix::class,
225        'cloner' => HTMLFormFieldCloner::class,
226        'autocompleteselect' => HTMLAutoCompleteSelectField::class,
227        'language' => HTMLSelectLanguageField::class,
228        'date' => HTMLDateTimeField::class,
229        'time' => HTMLDateTimeField::class,
230        'datetime' => HTMLDateTimeField::class,
231        'expiry' => HTMLExpiryField::class,
232        'timezone' => HTMLTimezoneField::class,
233        // HTMLTextField will output the correct type="" attribute automagically.
234        // There are about four zillion other HTML5 input types, like range, but
235        // we don't use those at the moment, so no point in adding all of them.
236        'email' => HTMLTextField::class,
237        'password' => HTMLTextField::class,
238        'url' => HTMLTextField::class,
239        'title' => HTMLTitleTextField::class,
240        'user' => HTMLUserTextField::class,
241        'tagmultiselect' => HTMLTagMultiselectField::class,
242        'orderedmultiselect' => HTMLOrderedMultiselectField::class,
243        'usersmultiselect' => HTMLUsersMultiselectField::class,
244        'titlesmultiselect' => HTMLTitlesMultiselectField::class,
245        'namespacesmultiselect' => HTMLNamespacesMultiselectField::class,
246    ];
247
248    /** @var array */
249    public $mFieldData;
250
251    /** @var string */
252    protected $mMessagePrefix;
253
254    /** @var HTMLFormField[] */
255    protected $mFlatFields = [];
256    /** @var array */
257    protected $mFieldTree = [];
258    /** @var bool */
259    protected $mShowSubmit = true;
260    /** @var string[] */
261    protected $mSubmitFlags = [ 'primary', 'progressive' ];
262    /** @var bool */
263    protected $mShowCancel = false;
264    /** @var LinkTarget|string|null */
265    protected $mCancelTarget;
266
267    /** @var callable|null */
268    protected $mSubmitCallback;
269    /**
270     * @var array[]
271     * @phan-var non-empty-array[]
272     */
273    protected $mValidationErrorMessage;
274
275    /** @var string */
276    protected $mPre = '';
277    /** @var string */
278    protected $mHeader = '';
279    /** @var string */
280    protected $mFooter = '';
281    /** @var string[] */
282    protected $mSectionHeaders = [];
283    /** @var string[] */
284    protected $mSectionFooters = [];
285    /** @var string */
286    protected $mPost = '';
287    /** @var string|null */
288    protected $mId;
289    /** @var string|null */
290    protected $mName;
291    /** @var string */
292    protected $mTableId = '';
293
294    /** @var string|null */
295    protected $mSubmitID;
296    /** @var string|null */
297    protected $mSubmitName;
298    /** @var string|null */
299    protected $mSubmitText;
300    /** @var string|null */
301    protected $mSubmitTooltip;
302
303    /** @var string|null */
304    protected $mFormIdentifier;
305    /** @var bool */
306    protected $mSingleForm = false;
307
308    /** @var Title|null */
309    protected $mTitle;
310    /** @var string */
311    protected $mMethod = 'post';
312    /** @var bool */
313    protected $mWasSubmitted = false;
314
315    /**
316     * Form action URL. false means we will use the URL to set Title
317     * @since 1.19
318     * @var string|false
319     */
320    protected $mAction = false;
321
322    /**
323     * Whether the form can be collapsed
324     * @since 1.34
325     * @var bool
326     */
327    protected $mCollapsible = false;
328
329    /**
330     * Whether the form is collapsed by default
331     * @since 1.34
332     * @var bool
333     */
334    protected $mCollapsed = false;
335
336    /**
337     * Form attribute autocomplete. A typical value is "off". null does not set the attribute
338     * @since 1.27
339     * @var string|null
340     */
341    protected $mAutocomplete = null;
342
343    /** @var bool */
344    protected $mUseMultipart = false;
345    /**
346     * @var array[]
347     * @phan-var array<int,array{0:string,1:array}>
348     */
349    protected $mHiddenFields = [];
350    /**
351     * @var array[]
352     * @phan-var array<array{name:string,value:string,label-message?:string|array<string|MessageParam>|MessageSpecifier,label?:string,label-raw?:string,id?:string,attribs?:array,flags?:string|string[],framed?:bool}>
353     */
354    protected $mButtons = [];
355
356    /** @var string|false */
357    protected $mWrapperLegend = false;
358    /** @var array */
359    protected $mWrapperAttributes = [];
360
361    /**
362     * Salt for the edit token.
363     * @var string|array
364     */
365    protected $mTokenSalt = '';
366
367    /**
368     * Additional information about form sections. Only supported by CodexHTMLForm.
369     *
370     * Array is keyed on section name. Options per section include:
371     * 'description'               -- Description text placed below the section label.
372     * 'description-message'       -- The same, but a message key.
373     * 'description-message-parse' -- Whether to parse the 'description-message'
374     * 'optional'                  -- Whether the section should be marked as optional.
375     *
376     * @since 1.42
377     * @var array[]
378     */
379    protected $mSections = [];
380
381    /**
382     * If true, sections that contain both fields and subsections will
383     * render their subsections before their fields.
384     *
385     * Subclasses may set this to false to render subsections after fields
386     * instead.
387     * @var bool
388     */
389    protected $mSubSectionBeforeFields = true;
390
391    /**
392     * Format in which to display form. For viable options,
393     * @see $availableDisplayFormats
394     * @var string
395     */
396    protected $displayFormat = 'table';
397
398    /**
399     * Available formats in which to display the form
400     * @var array
401     */
402    protected $availableDisplayFormats = [
403        'table',
404        'div',
405        'raw',
406        'inline',
407    ];
408
409    /**
410     * Available formats in which to display the form
411     * @var array
412     */
413    protected $availableSubclassDisplayFormats = [
414        'vform', // deprecated since 1.45
415        'codex',
416        'ooui',
417    ];
418
419    /**
420     * Whether a hidden title field has been added to the form
421     * @var bool
422     */
423    private $hiddenTitleAddedToForm = false;
424
425    /**
426     * Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
427     *
428     * @stable to call
429     *
430     * @param string $displayFormat
431     * @param array $descriptor Array of Field constructs, as described
432     *     in the class documentation
433     * @param IContextSource $context Context used to fetch submitted form fields and
434     *     generate localisation messages
435     * @param string $messagePrefix A prefix to go in front of default messages
436     * @return HTMLForm
437     */
438    public static function factory(
439        $displayFormat, $descriptor, IContextSource $context, $messagePrefix = ''
440    ) {
441        switch ( $displayFormat ) {
442            case 'codex':
443                return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
444            case 'vform':
445                wfDeprecatedMsg( "'vform' HTMLForm display format is deprecated", '1.45' );
446                return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
447            case 'ooui':
448                return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
449            default:
450                $form = new self( $descriptor, $context, $messagePrefix );
451                $form->setDisplayFormat( $displayFormat );
452                return $form;
453        }
454    }
455
456    /**
457     * Build a new HTMLForm from an array of field attributes
458     *
459     * @stable to call
460     *
461     * @param array $descriptor Array of Field constructs, as described
462     *     in the class documentation
463     * @param IContextSource $context Context used to fetch submitted form fields and
464     *     generate localisation messages
465     * @param string $messagePrefix A prefix to go in front of default messages
466     */
467    public function __construct(
468        $descriptor, IContextSource $context, $messagePrefix = ''
469    ) {
470        $this->setContext( $context );
471        $this->mMessagePrefix = $messagePrefix;
472        $this->addFields( $descriptor );
473    }
474
475    /**
476     * Add fields to the form
477     *
478     * @since 1.34
479     *
480     * @param array $descriptor Array of Field constructs, as described
481     *     in the class documentation
482     * @return HTMLForm
483     */
484    public function addFields( $descriptor ) {
485        $loadedDescriptor = [];
486
487        foreach ( $descriptor as $fieldname => $info ) {
488            $section = $info['section'] ?? '';
489
490            if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
491                $this->mUseMultipart = true;
492            }
493
494            $field = static::loadInputFromParameters( $fieldname, $info, $this );
495
496            $setSection =& $loadedDescriptor;
497            if ( $section ) {
498                foreach ( explode( '/', $section ) as $newName ) {
499                    $setSection[$newName] ??= [];
500                    $setSection =& $setSection[$newName];
501                }
502            }
503
504            $setSection[$fieldname] = $field;
505            $this->mFlatFields[$fieldname] = $field;
506        }
507
508        $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
509
510        return $this;
511    }
512
513    /**
514     * @param string $fieldname
515     * @return bool
516     */
517    public function hasField( $fieldname ) {
518        return isset( $this->mFlatFields[$fieldname] );
519    }
520
521    /**
522     * @param string $fieldname
523     * @return HTMLFormField
524     * @throws DomainException on invalid field name
525     */
526    public function getField( $fieldname ) {
527        if ( !$this->hasField( $fieldname ) ) {
528            throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
529        }
530        return $this->mFlatFields[$fieldname];
531    }
532
533    /**
534     * Set format in which to display the form
535     *
536     * @param string $format The name of the format to use, must be one of
537     *   $this->availableDisplayFormats
538     *
539     * @since 1.20
540     * @return HTMLForm $this for chaining calls (since 1.20)
541     */
542    public function setDisplayFormat( $format ) {
543        if (
544            in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
545            in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
546        ) {
547            throw new LogicException( 'Cannot change display format after creation, ' .
548                'use HTMLForm::factory() instead' );
549        }
550
551        if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
552            throw new InvalidArgumentException( 'Display format must be one of ' .
553                print_r(
554                    array_merge(
555                        $this->availableDisplayFormats,
556                        $this->availableSubclassDisplayFormats
557                    ),
558                    true
559                ) );
560        }
561
562        $this->displayFormat = $format;
563
564        return $this;
565    }
566
567    /**
568     * Getter for displayFormat
569     * @since 1.20
570     * @return string
571     */
572    public function getDisplayFormat() {
573        return $this->displayFormat;
574    }
575
576    /**
577     * Get the HTMLFormField subclass for this descriptor.
578     *
579     * The descriptor can be passed either 'class' which is the name of
580     * a HTMLFormField subclass, or a shorter 'type' which is an alias.
581     * This makes sure the 'class' is always set, and also is returned by
582     * this function for ease.
583     *
584     * @since 1.23
585     *
586     * @param string $fieldname Name of the field
587     * @param array &$descriptor Input Descriptor, as described
588     *     in the class documentation
589     *
590     * @return class-string<HTMLFormField> Name of a HTMLFormField subclass
591     */
592    public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
593        if ( isset( $descriptor['class'] ) ) {
594            $class = $descriptor['class'];
595        } elseif ( isset( $descriptor['type'] ) ) {
596            $class = static::$typeMappings[$descriptor['type']];
597            $descriptor['class'] = $class;
598        } else {
599            $class = null;
600        }
601
602        if ( !$class ) {
603            throw new InvalidArgumentException( "Descriptor with no class for $fieldname"
604                . print_r( $descriptor, true ) );
605        }
606
607        return $class;
608    }
609
610    /**
611     * Initialise a new Object for the field
612     * @stable to override
613     *
614     * @param string $fieldname Name of the field
615     * @param array $descriptor Input Descriptor, as described
616     *     in the class documentation
617     * @param HTMLForm|null $parent Parent instance of HTMLForm
618     *
619     * @warning Not passing (or passing null) for $parent is deprecated as of 1.40
620     * @return HTMLFormField Instance of a subclass of HTMLFormField
621     */
622    public static function loadInputFromParameters( $fieldname, $descriptor,
623        ?HTMLForm $parent = null
624    ) {
625        $class = static::getClassFromDescriptor( $fieldname, $descriptor );
626
627        $descriptor['fieldname'] = $fieldname;
628        if ( $parent ) {
629            $descriptor['parent'] = $parent;
630        }
631
632        # @todo This will throw a fatal error whenever someone try to use
633        # 'class' to feed a CSS class instead of 'cssclass'. Would be
634        # great to avoid the fatal error and show a nice error.
635        return new $class( $descriptor );
636    }
637
638    /**
639     * Prepare form for submission.
640     *
641     * @warning When doing method chaining, that should be the very last
642     * method call before displayForm().
643     *
644     * @return HTMLForm $this for chaining calls (since 1.20)
645     */
646    public function prepareForm() {
647        # Load data from the request.
648        if (
649            $this->mFormIdentifier === null ||
650            $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier ||
651            ( $this->mSingleForm && $this->getMethod() === 'get' )
652        ) {
653            $this->loadFieldData();
654        } else {
655            $this->mFieldData = [];
656        }
657
658        return $this;
659    }
660
661    /**
662     * Try submitting, with edit token check first
663     * @return bool|string|array|Status As documented for HTMLForm::trySubmit
664     */
665    public function tryAuthorizedSubmit() {
666        $result = false;
667
668        if ( $this->mFormIdentifier === null ) {
669            $identOkay = true;
670        } else {
671            $identOkay = $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier;
672        }
673
674        $tokenOkay = false;
675        if ( $this->getMethod() !== 'post' ) {
676            $tokenOkay = true; // no session check needed
677        } elseif ( $this->getRequest()->wasPosted() ) {
678            $editToken = $this->getRequest()->getVal( 'wpEditToken' );
679            if ( $this->getUser()->isRegistered() || $editToken !== null ) {
680                // Session tokens for logged-out users have no security value.
681                // However, if the user gave one, check it in order to give a nice
682                // "session expired" error instead of "permission denied" or such.
683                $tokenOkay = $this->getCsrfTokenSet()->matchTokenField(
684                    CsrfTokenSet::DEFAULT_FIELD_NAME, $this->mTokenSalt
685                );
686            } else {
687                $tokenOkay = true;
688            }
689        }
690
691        if ( $tokenOkay && $identOkay ) {
692            $this->mWasSubmitted = true;
693            $result = $this->trySubmit();
694        }
695
696        return $result;
697    }
698
699    /**
700     * The here's-one-I-made-earlier option: do the submission if
701     * posted, or display the form with or without funky validation
702     * errors
703     * @stable to override
704     * @return bool|Status Whether submission was successful.
705     */
706    public function show() {
707        $this->prepareForm();
708
709        $result = $this->tryAuthorizedSubmit();
710        if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
711            return $result;
712        }
713
714        $this->displayForm( $result );
715
716        return false;
717    }
718
719    /**
720     * Same as self::show with the difference, that the form will be
721     * added to the output, no matter, if the validation was good or not.
722     * @return bool|Status Whether submission was successful.
723     */
724    public function showAlways() {
725        $this->prepareForm();
726
727        $result = $this->tryAuthorizedSubmit();
728
729        $this->displayForm( $result );
730
731        return $result;
732    }
733
734    /**
735     * Validate all the fields, and call the submission callback
736     * function if everything is kosher.
737     * @stable to override
738     * @return bool|string|array|Status
739     *     - Bool true or a good Status object indicates success,
740     *     - Bool false indicates no submission was attempted,
741     *     - Anything else indicates failure. The value may be a fatal Status
742     *       object, an HTML string, or an array of arrays (message keys and
743     *       params) or strings (message keys)
744     */
745    public function trySubmit() {
746        $valid = true;
747        $hoistedErrors = Status::newGood();
748        if ( $this->mValidationErrorMessage ) {
749            foreach ( $this->mValidationErrorMessage as $error ) {
750                $hoistedErrors->fatal( ...$error );
751            }
752        } else {
753            $hoistedErrors->fatal( 'htmlform-invalid-input' );
754        }
755
756        $this->mWasSubmitted = true;
757
758        # Check for cancelled submission
759        foreach ( $this->mFlatFields as $fieldname => $field ) {
760            if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
761                continue;
762            }
763            if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
764                $this->mWasSubmitted = false;
765                return false;
766            }
767        }
768
769        # Check for validation
770        $hasNonDefault = false;
771        foreach ( $this->mFlatFields as $fieldname => $field ) {
772            if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
773                continue;
774            }
775            $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
776            if ( $field->isDisabled( $this->mFieldData ) ) {
777                continue;
778            }
779            $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
780            if ( $res !== true ) {
781                $valid = false;
782                if ( $res !== false && !$field->canDisplayErrors() ) {
783                    if ( is_string( $res ) ) {
784                        $hoistedErrors->fatal( 'rawmessage', $res );
785                    } else {
786                        $hoistedErrors->fatal( $res );
787                    }
788                }
789            }
790        }
791
792        if ( !$valid ) {
793            // Treat as not submitted if got nothing from the user on GET forms.
794            if ( !$hasNonDefault && $this->getMethod() === 'get' &&
795                ( $this->mFormIdentifier === null ||
796                    $this->getRequest()->getCheck( 'wpFormIdentifier' ) )
797            ) {
798                $this->mWasSubmitted = false;
799                return false;
800            }
801            return $hoistedErrors;
802        }
803
804        $callback = $this->mSubmitCallback;
805        if ( !is_callable( $callback ) ) {
806            throw new LogicException( 'HTMLForm: no submit callback provided. Use ' .
807                'setSubmitCallback() to set one.' );
808        }
809
810        $data = $this->filterDataForSubmit( $this->mFieldData );
811
812        $res = $callback( $data, $this );
813        if ( $res === false ) {
814            $this->mWasSubmitted = false;
815        } elseif ( $res instanceof StatusValue ) {
816            // DWIM - callbacks are not supposed to return a StatusValue but it's easy to mix up.
817            $res = Status::wrap( $res );
818        }
819
820        return $res;
821    }
822
823    /**
824     * Test whether the form was considered to have been submitted or not, i.e.
825     * whether the last call to tryAuthorizedSubmit or trySubmit returned
826     * non-false.
827     *
828     * This will return false until HTMLForm::tryAuthorizedSubmit or
829     * HTMLForm::trySubmit is called.
830     *
831     * @since 1.23
832     * @return bool
833     */
834    public function wasSubmitted() {
835        return $this->mWasSubmitted;
836    }
837
838    /**
839     * Set a callback to a function to do something with the form
840     * once it's been successfully validated.
841     *
842     * @param callable $cb The function will be passed the output from
843     *   HTMLForm::filterDataForSubmit and this HTMLForm object, and must
844     *   return as documented for HTMLForm::trySubmit
845     *
846     * @return HTMLForm $this for chaining calls (since 1.20)
847     */
848    public function setSubmitCallback( $cb ) {
849        $this->mSubmitCallback = $cb;
850
851        return $this;
852    }
853
854    /**
855     * Set a message to display on a validation error.
856     *
857     * @param array[] $msg Array of valid inputs to wfMessage()
858     *     (so each entry must itself be an array of arguments)
859     * @phan-param non-empty-array[] $msg
860     *
861     * @return HTMLForm $this for chaining calls (since 1.20)
862     */
863    public function setValidationErrorMessage( $msg ) {
864        $this->mValidationErrorMessage = $msg;
865
866        return $this;
867    }
868
869    /**
870     * Set the introductory message HTML, overwriting any existing message.
871     *
872     * @param string $html Complete HTML of message to display
873     *
874     * @since 1.38
875     * @return $this for chaining calls
876     */
877    public function setPreHtml( $html ) {
878        $this->mPre = $html;
879
880        return $this;
881    }
882
883    /**
884     * Add HTML to introductory message.
885     *
886     * @param string $html Complete HTML of message to display
887     *
888     * @since 1.38
889     * @return $this for chaining calls
890     */
891    public function addPreHtml( $html ) {
892        $this->mPre .= $html;
893
894        return $this;
895    }
896
897    /**
898     * Get the introductory message HTML.
899     *
900     * @since 1.38
901     * @return string
902     */
903    public function getPreHtml() {
904        return $this->mPre;
905    }
906
907    /**
908     * Add HTML to the header, inside the form.
909     *
910     * @param string $html Additional HTML to display in header
911     * @param string|null $section The section to add the header to
912     *
913     * @since 1.38
914     * @return $this for chaining calls
915     */
916    public function addHeaderHtml( $html, $section = null ) {
917        if ( $section === null ) {
918            $this->mHeader .= $html;
919        } else {
920            $this->mSectionHeaders[$section] ??= '';
921            $this->mSectionHeaders[$section] .= $html;
922        }
923
924        return $this;
925    }
926
927    /**
928     * Set header HTML, inside the form.
929     *
930     * @param string $html Complete HTML of header to display
931     * @param string|null $section The section to add the header to
932     *
933     * @since 1.38
934     * @return $this for chaining calls
935     */
936    public function setHeaderHtml( $html, $section = null ) {
937        if ( $section === null ) {
938            $this->mHeader = $html;
939        } else {
940            $this->mSectionHeaders[$section] = $html;
941        }
942
943        return $this;
944    }
945
946    /**
947     * Get header HTML.
948     * @stable to override
949     *
950     * @param string|null $section The section to get the header text for
951     * @since 1.38
952     * @return string HTML
953     */
954    public function getHeaderHtml( $section = null ) {
955        return $section ? $this->mSectionHeaders[$section] ?? '' : $this->mHeader;
956    }
957
958    /**
959     * Add footer HTML, inside the form.
960     *
961     * @param string $html Complete text of message to display
962     * @param string|null $section The section to add the footer text to
963     *
964     * @since 1.38
965     * @return $this for chaining calls
966     */
967    public function addFooterHtml( $html, $section = null ) {
968        if ( $section === null ) {
969            $this->mFooter .= $html;
970        } else {
971            $this->mSectionFooters[$section] ??= '';
972            $this->mSectionFooters[$section] .= $html;
973        }
974
975        return $this;
976    }
977
978    /**
979     * Set footer HTML, inside the form.
980     *
981     * @param string $html Complete text of message to display
982     * @param string|null $section The section to add the footer text to
983     *
984     * @since 1.38
985     * @return $this for chaining calls
986     */
987    public function setFooterHtml( $html, $section = null ) {
988        if ( $section === null ) {
989            $this->mFooter = $html;
990        } else {
991            $this->mSectionFooters[$section] = $html;
992        }
993
994        return $this;
995    }
996
997    /**
998     * Get footer HTML.
999     *
1000     * @param string|null $section The section to get the footer text for
1001     * @since 1.38
1002     * @return string
1003     */
1004    public function getFooterHtml( $section = null ) {
1005        return $section ? $this->mSectionFooters[$section] ?? '' : $this->mFooter;
1006    }
1007
1008    /**
1009     * Add HTML to the end of the display.
1010     *
1011     * @param string $html Complete text of message to display
1012     *
1013     * @since 1.38
1014     * @return $this for chaining calls
1015     */
1016    public function addPostHtml( $html ) {
1017        $this->mPost .= $html;
1018
1019        return $this;
1020    }
1021
1022    /**
1023     * Set HTML at the end of the display.
1024     *
1025     * @param string $html Complete text of message to display
1026     *
1027     * @since 1.38
1028     * @return $this for chaining calls
1029     */
1030    public function setPostHtml( $html ) {
1031        $this->mPost = $html;
1032
1033        return $this;
1034    }
1035
1036    /**
1037     * Get HTML at the end of the display.
1038     *
1039     * @since 1.38
1040     * @return string HTML
1041     */
1042    public function getPostHtml() {
1043        return $this->mPost;
1044    }
1045
1046    /**
1047     * Set an array of information about sections.
1048     *
1049     * @since 1.42
1050     *
1051     * @param array[] $sections Array of section information, keyed on section name.
1052     *
1053     * @return HTMLForm $this for chaining calls
1054     */
1055    public function setSections( $sections ) {
1056        if ( $this->getDisplayFormat() !== 'codex' ) {
1057            throw new \InvalidArgumentException(
1058                "Non-Codex HTMLForms do not support additional section information."
1059            );
1060        }
1061
1062        $this->mSections = $sections;
1063
1064        return $this;
1065    }
1066
1067    /**
1068     * Add a hidden field to the output
1069     * Array values are discarded for security reasons (per WebRequest::getVal)
1070     *
1071     * @param string $name Field name.  This will be used exactly as entered
1072     * @param mixed $value Field value
1073     * @param array $attribs
1074     *
1075     * @return HTMLForm $this for chaining calls (since 1.20)
1076     */
1077    public function addHiddenField( $name, $value, array $attribs = [] ) {
1078        if ( !is_array( $value ) ) {
1079            // Per WebRequest::getVal: Array values are discarded for security reasons.
1080            $attribs += [ 'name' => $name ];
1081            $this->mHiddenFields[] = [ $value, $attribs ];
1082        }
1083
1084        return $this;
1085    }
1086
1087    /**
1088     * Add an array of hidden fields to the output
1089     * Array values are discarded for security reasons (per WebRequest::getVal)
1090     *
1091     * @since 1.22
1092     *
1093     * @param array $fields Associative array of fields to add;
1094     *        mapping names to their values
1095     *
1096     * @return HTMLForm $this for chaining calls
1097     */
1098    public function addHiddenFields( array $fields ) {
1099        foreach ( $fields as $name => $value ) {
1100            if ( is_array( $value ) ) {
1101                // Per WebRequest::getVal: Array values are discarded for security reasons.
1102                continue;
1103            }
1104            $this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
1105        }
1106
1107        return $this;
1108    }
1109
1110    /**
1111     * Add a button to the form
1112     *
1113     * @since 1.27 takes an array as shown. Earlier versions accepted
1114     *  'name', 'value', 'id', and 'attribs' as separate parameters in that
1115     *  order.
1116     * @param array $data Data to define the button:
1117     *  - name: (string) Button name.
1118     *  - value: (string) Button value.
1119     *  - label-message: (string|array<string|array>|MessageSpecifier, optional) Button label
1120     *    message key to use instead of 'value'. Overrides 'label' and 'label-raw'.
1121     *  - label: (string, optional) Button label text to use instead of
1122     *    'value'. Overrides 'label-raw'.
1123     *  - label-raw: (string, optional) Button label HTML to use instead of
1124     *    'value'.
1125     *  - id: (string, optional) DOM id for the button.
1126     *  - attribs: (array, optional) Additional HTML attributes.
1127     *  - flags: (string|string[], optional) OOUI flags.
1128     *  - framed: (boolean=true, optional) OOUI framed attribute.
1129     * @phpcs:ignore Generic.Files.LineLength
1130     * @phan-param array{name:string,value:string,label-message?:string|array<string|MessageParam>|MessageSpecifier,label?:string,label-raw?:string,id?:string,attribs?:array,flags?:string|string[],framed?:bool} $data
1131     * @return HTMLForm $this for chaining calls (since 1.20)
1132     */
1133    public function addButton( $data ) {
1134        if ( !is_array( $data ) ) {
1135            $args = func_get_args();
1136            if ( count( $args ) < 2 || count( $args ) > 4 ) {
1137                throw new InvalidArgumentException(
1138                    'Incorrect number of arguments for deprecated calling style'
1139                );
1140            }
1141            $data = [
1142                'name' => $args[0],
1143                'value' => $args[1],
1144                'id' => $args[2] ?? null,
1145                'attribs' => $args[3] ?? null,
1146            ];
1147        } else {
1148            if ( !isset( $data['name'] ) ) {
1149                throw new InvalidArgumentException( 'A name is required' );
1150            }
1151            if ( !isset( $data['value'] ) ) {
1152                throw new InvalidArgumentException( 'A value is required' );
1153            }
1154        }
1155        $this->mButtons[] = $data + [
1156            'id' => null,
1157            'attribs' => null,
1158            'flags' => null,
1159            'framed' => true,
1160        ];
1161
1162        return $this;
1163    }
1164
1165    /**
1166     * Set the salt for the edit token.
1167     *
1168     * Only useful when the method is "post".
1169     *
1170     * @since 1.24
1171     * @param string|array $salt Salt to use
1172     * @return HTMLForm $this For chaining calls
1173     */
1174    public function setTokenSalt( $salt ) {
1175        $this->mTokenSalt = $salt;
1176
1177        return $this;
1178    }
1179
1180    /**
1181     * Display the form (sending to the context's OutputPage object), with an
1182     * appropriate error message or stack of messages, and any validation errors, etc.
1183     *
1184     * @warning You should call prepareForm() before calling this function.
1185     * Moreover, when doing method chaining this should be the very last method
1186     * call just after prepareForm().
1187     *
1188     * @stable to override
1189     *
1190     * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
1191     *
1192     * @return void Nothing, should be last call
1193     */
1194    public function displayForm( $submitResult ) {
1195        $this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
1196    }
1197
1198    /**
1199     * Get a hidden field for the title of the page if necessary (empty string otherwise)
1200     */
1201    private function getHiddenTitle(): string {
1202        if ( $this->hiddenTitleAddedToForm ) {
1203            return '';
1204        }
1205
1206        $html = '';
1207        if ( $this->getMethod() === 'post' ||
1208            $this->getAction() === $this->getConfig()->get( MainConfigNames::Script )
1209        ) {
1210            $html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
1211        }
1212        $this->hiddenTitleAddedToForm = true;
1213        return $html;
1214    }
1215
1216    /**
1217     * Returns the raw HTML generated by the form
1218     *
1219     * @stable to override
1220     *
1221     * @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
1222     *
1223     * @return string HTML
1224     * @return-taint escaped
1225     */
1226    public function getHTML( $submitResult ) {
1227        # For good measure (it is the default)
1228        $this->getOutput()->getMetadata()->setPreventClickjacking( true );
1229        $this->getOutput()->addModules( 'mediawiki.htmlform' );
1230        $this->getOutput()->addModuleStyles( [
1231            'mediawiki.htmlform.styles',
1232            // Html::errorBox and Html::warningBox used by HtmlFormField and HtmlForm::getErrorsOrWarnings
1233            'mediawiki.codex.messagebox.styles'
1234        ] );
1235
1236        if ( $this->mCollapsible ) {
1237            // Preload jquery.makeCollapsible for mediawiki.htmlform
1238            $this->getOutput()->addModules( 'jquery.makeCollapsible' );
1239        }
1240
1241        $headerHtml = $this->getHeaderHtml();
1242        $footerHtml = $this->getFooterHtml();
1243        $html = $this->getErrorsOrWarnings( $submitResult, 'error' )
1244            . $this->getErrorsOrWarnings( $submitResult, 'warning' )
1245            . $headerHtml
1246            . $this->getHiddenTitle()
1247            . $this->getBody()
1248            . $this->getHiddenFields()
1249            . $this->getButtons()
1250            . $footerHtml;
1251
1252        return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1253    }
1254
1255    /**
1256     * Enable collapsible mode, and set whether the form is collapsed by default.
1257     *
1258     * @since 1.34
1259     * @param bool $collapsedByDefault Whether the form is collapsed by default (optional).
1260     * @return HTMLForm $this for chaining calls
1261     */
1262    public function setCollapsibleOptions( $collapsedByDefault = false ) {
1263        $this->mCollapsible = true;
1264        $this->mCollapsed = $collapsedByDefault;
1265        return $this;
1266    }
1267
1268    /**
1269     * Get HTML attributes for the `<form>` tag.
1270     * @stable to override
1271     * @return array
1272     */
1273    protected function getFormAttributes() {
1274        # Use multipart/form-data
1275        $encType = $this->mUseMultipart
1276            ? 'multipart/form-data'
1277            : 'application/x-www-form-urlencoded';
1278        # Attributes
1279        $attribs = [
1280            'class' => 'mw-htmlform',
1281            'action' => $this->getAction(),
1282            'method' => $this->getMethod(),
1283            'enctype' => $encType,
1284        ];
1285        if ( $this->mId ) {
1286            $attribs['id'] = $this->mId;
1287        }
1288        if ( is_string( $this->mAutocomplete ) ) {
1289            $attribs['autocomplete'] = $this->mAutocomplete;
1290        }
1291        if ( $this->mName ) {
1292            $attribs['name'] = $this->mName;
1293        }
1294        if ( $this->needsJSForHtml5FormValidation() ) {
1295            $attribs['novalidate'] = true;
1296        }
1297        return $attribs;
1298    }
1299
1300    /**
1301     * Wrap the form innards in an actual "<form>" element
1302     *
1303     * @stable to override
1304     * @param string $html HTML contents to wrap.
1305     * @return string|\OOUI\Tag Wrapped HTML.
1306     */
1307    public function wrapForm( $html ) {
1308        # Include a <fieldset> wrapper for style, if requested.
1309        if ( $this->mWrapperLegend !== false ) {
1310            $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
1311            $html = Html::rawElement(
1312                'fieldset',
1313                $this->mWrapperAttributes,
1314                ( $legend ? Html::element( 'legend', [], $legend ) : '' ) . $html
1315            );
1316        }
1317
1318        return Html::rawElement(
1319            'form',
1320            $this->getFormAttributes(),
1321            $html
1322        );
1323    }
1324
1325    /**
1326     * Get the hidden fields that should go inside the form.
1327     * @return string HTML.
1328     */
1329    public function getHiddenFields() {
1330        $html = '';
1331
1332        // add the title as a hidden file if it hasn't been added yet and if it is necessary
1333        // added for backward compatibility with the previous version of this public method
1334        $html .= $this->getHiddenTitle();
1335
1336        if ( $this->mFormIdentifier !== null ) {
1337            $html .= Html::hidden(
1338                'wpFormIdentifier',
1339                $this->mFormIdentifier
1340            ) . "\n";
1341        }
1342        if ( $this->getMethod() === 'post' ) {
1343            $html .= Html::hidden(
1344                'wpEditToken',
1345                $this->getUser()->getEditToken( $this->mTokenSalt ),
1346                [ 'id' => 'wpEditToken' ]
1347            ) . "\n";
1348        }
1349
1350        foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1351            $html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
1352        }
1353
1354        return $html;
1355    }
1356
1357    /**
1358     * Get the submit and (potentially) reset buttons.
1359     * @stable to override
1360     * @return string HTML.
1361     */
1362    public function getButtons() {
1363        $buttons = '';
1364
1365        if ( $this->mShowSubmit ) {
1366            $attribs = [];
1367
1368            if ( $this->mSubmitID !== null ) {
1369                $attribs['id'] = $this->mSubmitID;
1370            }
1371
1372            if ( $this->mSubmitName !== null ) {
1373                $attribs['name'] = $this->mSubmitName;
1374            }
1375
1376            if ( $this->mSubmitTooltip !== null ) {
1377                $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1378            }
1379
1380            $attribs['class'] = [ 'mw-htmlform-submit' ];
1381
1382            $buttons .= Html::submitButton( $this->getSubmitText(), $attribs ) . "\n";
1383        }
1384
1385        if ( $this->mShowCancel ) {
1386            $target = $this->getCancelTargetURL();
1387            $buttons .= Html::element(
1388                    'a',
1389                    [
1390                        'href' => $target,
1391                    ],
1392                    $this->msg( 'cancel' )->text()
1393                ) . "\n";
1394        }
1395
1396        foreach ( $this->mButtons as $button ) {
1397            $attrs = [
1398                'type' => 'submit',
1399                'name' => $button['name'],
1400                'value' => $button['value']
1401            ];
1402
1403            if ( isset( $button['label-message'] ) ) {
1404                $label = $this->getMessage( $button['label-message'] )->parse();
1405            } elseif ( isset( $button['label'] ) ) {
1406                $label = htmlspecialchars( $button['label'] );
1407            } elseif ( isset( $button['label-raw'] ) ) {
1408                $label = $button['label-raw'];
1409            } else {
1410                $label = htmlspecialchars( $button['value'] );
1411            }
1412
1413            // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in self::addButton
1414            if ( $button['attribs'] ) {
1415                // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Always set in self::addButton
1416                $attrs += $button['attribs'];
1417            }
1418
1419            if ( isset( $button['id'] ) ) {
1420                $attrs['id'] = $button['id'];
1421            }
1422
1423            $buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
1424        }
1425
1426        if ( !$buttons ) {
1427            return '';
1428        }
1429
1430        return Html::rawElement( 'span',
1431            [ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
1432    }
1433
1434    /**
1435     * Get the whole body of the form.
1436     * @stable to override
1437     * @return string
1438     */
1439    public function getBody() {
1440        return $this->displaySection( $this->mFieldTree, $this->mTableId );
1441    }
1442
1443    /**
1444     * Returns a formatted list of errors or warnings from the given elements.
1445     * @stable to override
1446     *
1447     * @param string|array|Status $elements The set of errors/warnings to process.
1448     * @param string $elementsType Should warnings or errors be returned.  This is meant
1449     *     for Status objects, all other valid types are always considered as errors.
1450     * @return string
1451     */
1452    public function getErrorsOrWarnings( $elements, $elementsType ) {
1453        if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) {
1454            throw new DomainException( $elementsType . ' is not a valid type.' );
1455        }
1456        $elementstr = false;
1457        if ( $elements instanceof Status ) {
1458            [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1459            $status = $elementsType === 'error' ? $errorStatus : $warningStatus;
1460            if ( $status->isGood() ) {
1461                $elementstr = '';
1462            } else {
1463                $elementstr = $status
1464                    ->getMessage()
1465                    ->setContext( $this )
1466                    ->setInterfaceMessageFlag( true )
1467                    ->parse();
1468            }
1469        } elseif ( $elementsType === 'error' ) {
1470            if ( is_array( $elements ) ) {
1471                $elementstr = $this->formatErrors( $elements );
1472            } elseif ( $elements && $elements !== true ) {
1473                $elementstr = (string)$elements;
1474            }
1475        }
1476
1477        if ( !$elementstr ) {
1478            return '';
1479        } elseif ( $elementsType === 'error' ) {
1480            return Html::errorBox( $elementstr );
1481        } else { // $elementsType can only be 'warning'
1482            return Html::warningBox( $elementstr );
1483        }
1484    }
1485
1486    /**
1487     * Format a stack of error messages into a single HTML string
1488     *
1489     * @param array $errors Array of message keys/values
1490     *
1491     * @return string HTML, a "<ul>" list of errors
1492     */
1493    public function formatErrors( $errors ) {
1494        $errorstr = '';
1495
1496        foreach ( $errors as $error ) {
1497            $errorstr .= Html::rawElement(
1498                'li',
1499                [],
1500                $this->getMessage( $error )->parse()
1501            );
1502        }
1503
1504        return Html::rawElement( 'ul', [], $errorstr );
1505    }
1506
1507    /**
1508     * Set the text for the submit button
1509     *
1510     * @param string $t Plaintext
1511     *
1512     * @return HTMLForm $this for chaining calls (since 1.20)
1513     */
1514    public function setSubmitText( $t ) {
1515        $this->mSubmitText = $t;
1516
1517        return $this;
1518    }
1519
1520    /**
1521     * Identify that the submit button in the form has a destructive action
1522     * @since 1.24
1523     *
1524     * @return HTMLForm $this for chaining calls (since 1.28)
1525     */
1526    public function setSubmitDestructive() {
1527        $this->mSubmitFlags = [ 'destructive', 'primary' ];
1528
1529        return $this;
1530    }
1531
1532    /**
1533     * Set the text for the submit button to a message
1534     * @since 1.19
1535     *
1536     * @param string|Message $msg Message key or Message object
1537     *
1538     * @return HTMLForm $this for chaining calls (since 1.20)
1539     */
1540    public function setSubmitTextMsg( $msg ) {
1541        if ( !$msg instanceof Message ) {
1542            $msg = $this->msg( $msg );
1543        }
1544        $this->setSubmitText( $msg->text() );
1545
1546        return $this;
1547    }
1548
1549    /**
1550     * Get the text for the submit button, either customised or a default.
1551     * @return string
1552     */
1553    public function getSubmitText() {
1554        return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
1555    }
1556
1557    /**
1558     * @param string $name Submit button name
1559     *
1560     * @return HTMLForm $this for chaining calls (since 1.20)
1561     */
1562    public function setSubmitName( $name ) {
1563        $this->mSubmitName = $name;
1564
1565        return $this;
1566    }
1567
1568    /**
1569     * @param string $name Tooltip for the submit button
1570     *
1571     * @return HTMLForm $this for chaining calls (since 1.20)
1572     */
1573    public function setSubmitTooltip( $name ) {
1574        $this->mSubmitTooltip = $name;
1575
1576        return $this;
1577    }
1578
1579    /**
1580     * Set the id for the submit button.
1581     *
1582     * @param string $t
1583     *
1584     * @todo FIXME: Integrity of $t is *not* validated
1585     * @return HTMLForm $this for chaining calls (since 1.20)
1586     */
1587    public function setSubmitID( $t ) {
1588        $this->mSubmitID = $t;
1589
1590        return $this;
1591    }
1592
1593    /**
1594     * Set an internal identifier for this form. It will be submitted as a hidden form field, allowing
1595     * HTMLForm to determine whether the form was submitted (or merely viewed). Setting this serves
1596     * two purposes:
1597     *
1598     * - If you use two or more forms on one page with the same submit target, it allows HTMLForm
1599     *   to identify which of the forms was submitted, and not attempt to validate the other ones.
1600     * - If you use checkbox or multiselect fields inside a form using the GET method, it allows
1601     *   HTMLForm to distinguish between the initial page view and a form submission with all
1602     *   checkboxes or select options unchecked. Set the second parameter to true if you are sure
1603     *   this is the only form on the page, which allows form fields to be prefilled with query
1604     *   params.
1605     *
1606     * @since 1.28
1607     * @param string $ident
1608     * @param bool $single Only work with GET form, see above. (since 1.41)
1609     * @return $this
1610     */
1611    public function setFormIdentifier( string $ident, bool $single = false ) {
1612        $this->mFormIdentifier = $ident;
1613        $this->mSingleForm = $single;
1614
1615        return $this;
1616    }
1617
1618    /**
1619     * Stop a default submit button being shown for this form. This implies that an
1620     * alternate submit method must be provided manually.
1621     *
1622     * @since 1.22
1623     *
1624     * @param bool $suppressSubmit Set to false to re-enable the button again
1625     *
1626     * @return HTMLForm $this for chaining calls
1627     */
1628    public function suppressDefaultSubmit( $suppressSubmit = true ) {
1629        $this->mShowSubmit = !$suppressSubmit;
1630
1631        return $this;
1632    }
1633
1634    /**
1635     * Show a cancel button (or prevent it). The button is not shown by default.
1636     * @param bool $show
1637     * @return HTMLForm $this for chaining calls
1638     * @since 1.27
1639     */
1640    public function showCancel( $show = true ) {
1641        $this->mShowCancel = $show;
1642        return $this;
1643    }
1644
1645    /**
1646     * Sets the target where the user is redirected to after clicking cancel.
1647     * @param LinkTarget|PageReference|string $target Target as an object or an URL
1648     * @return HTMLForm $this for chaining calls
1649     * @since 1.27
1650     */
1651    public function setCancelTarget( $target ) {
1652        if ( $target instanceof PageReference ) {
1653            $target = TitleValue::castPageToLinkTarget( $target );
1654        }
1655
1656        $this->mCancelTarget = $target;
1657        return $this;
1658    }
1659
1660    /**
1661     * @since 1.37
1662     * @return string
1663     */
1664    protected function getCancelTargetURL() {
1665        if ( is_string( $this->mCancelTarget ) ) {
1666            return $this->mCancelTarget;
1667        } else {
1668            // TODO: use a service to get the local URL for a LinkTarget, see T282283
1669            $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1670            return $target->getLocalURL();
1671        }
1672    }
1673
1674    /**
1675     * Set the id of the \<table\> or outermost \<div\> element.
1676     *
1677     * @since 1.22
1678     *
1679     * @param string $id New value of the id attribute, or "" to remove
1680     *
1681     * @return HTMLForm $this for chaining calls
1682     */
1683    public function setTableId( $id ) {
1684        $this->mTableId = $id;
1685
1686        return $this;
1687    }
1688
1689    /**
1690     * @param string $id DOM id for the form
1691     *
1692     * @return HTMLForm $this for chaining calls (since 1.20)
1693     */
1694    public function setId( $id ) {
1695        $this->mId = $id;
1696
1697        return $this;
1698    }
1699
1700    /**
1701     * @param string $name 'name' attribute for the form
1702     * @return HTMLForm $this for chaining calls
1703     */
1704    public function setName( $name ) {
1705        $this->mName = $name;
1706
1707        return $this;
1708    }
1709
1710    /**
1711     * Prompt the whole form to be wrapped in a "<fieldset>", with
1712     * this text as its "<legend>" element.
1713     *
1714     * @param string|bool $legend If false, no wrapper or legend will be displayed.
1715     *     If true, a wrapper will be displayed, but no legend.
1716     *     If a string, a wrapper will be displayed with that string as a legend.
1717     *     The string will be escaped before being output (this doesn't support HTML).
1718     *
1719     * @return HTMLForm $this for chaining calls (since 1.20)
1720     */
1721    public function setWrapperLegend( $legend ) {
1722        $this->mWrapperLegend = $legend;
1723
1724        return $this;
1725    }
1726
1727    /**
1728     * For internal use only. Use is discouraged, and should only be used where
1729     * support for gadgets/user scripts is warranted.
1730     * @param array $attributes
1731     * @internal
1732     * @return HTMLForm $this for chaining calls
1733     */
1734    public function setWrapperAttributes( $attributes ) {
1735        $this->mWrapperAttributes = $attributes;
1736
1737        return $this;
1738    }
1739
1740    /**
1741     * Prompt the whole form to be wrapped in a "<fieldset>", with
1742     * this message as its "<legend>" element.
1743     * @since 1.19
1744     *
1745     * @param string|Message $msg Message key or Message object
1746     *
1747     * @return HTMLForm $this for chaining calls (since 1.20)
1748     */
1749    public function setWrapperLegendMsg( $msg ) {
1750        if ( !$msg instanceof Message ) {
1751            $msg = $this->msg( $msg );
1752        }
1753        $this->setWrapperLegend( $msg->text() );
1754
1755        return $this;
1756    }
1757
1758    /**
1759     * Set the prefix for various default messages
1760     * @todo Currently only used for the "<fieldset>" legend on forms
1761     * with multiple sections; should be used elsewhere?
1762     *
1763     * @param string $p
1764     *
1765     * @return HTMLForm $this for chaining calls (since 1.20)
1766     */
1767    public function setMessagePrefix( $p ) {
1768        $this->mMessagePrefix = $p;
1769
1770        return $this;
1771    }
1772
1773    /**
1774     * Set the title for form submission
1775     *
1776     * @param PageReference $t The page the form is on/should be posted to
1777     *
1778     * @return HTMLForm $this for chaining calls (since 1.20)
1779     */
1780    public function setTitle( $t ) {
1781        // TODO: make mTitle a PageReference when we have a better way to get URLs, see T282283.
1782        $this->mTitle = Title::castFromPageReference( $t );
1783
1784        return $this;
1785    }
1786
1787    /**
1788     * @return Title
1789     */
1790    public function getTitle() {
1791        return $this->mTitle ?: $this->getContext()->getTitle();
1792    }
1793
1794    /**
1795     * Set the method used to submit the form
1796     *
1797     * @param string $method
1798     *
1799     * @return HTMLForm $this for chaining calls (since 1.20)
1800     */
1801    public function setMethod( $method = 'post' ) {
1802        $this->mMethod = strtolower( $method );
1803
1804        return $this;
1805    }
1806
1807    /**
1808     * @return string Always lowercase
1809     */
1810    public function getMethod() {
1811        return $this->mMethod;
1812    }
1813
1814    /**
1815     * Wraps the given $section into a user-visible fieldset.
1816     * @stable to override
1817     *
1818     * @param string $legend Legend text for the fieldset
1819     * @param string $section The section content in plain Html
1820     * @param array $attributes Additional attributes for the fieldset
1821     * @param bool $isRoot Section is at the root of the tree
1822     * @return string The fieldset's Html
1823     */
1824    protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
1825        return Html::rawElement(
1826            'fieldset',
1827            $attributes,
1828            Html::element( 'legend', [], $legend ) . $section
1829        ) . "\n";
1830    }
1831
1832    /**
1833     * @todo Document
1834     * @stable to override
1835     *
1836     * Throws an exception when called on uninitialized field data, e.g. when
1837     * HTMLForm::displayForm was called without calling HTMLForm::prepareForm
1838     * first.
1839     *
1840     * @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
1841     *   objects).
1842     * @param string $sectionName ID attribute of the "<table>" tag for this
1843     *   section, ignored if empty.
1844     * @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
1845     *   each subsection, ignored if empty.
1846     * @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
1847     *
1848     * @return string
1849     */
1850    public function displaySection( $fields,
1851        $sectionName = '',
1852        $fieldsetIDPrefix = '',
1853        &$hasUserVisibleFields = false
1854    ) {
1855        if ( $this->mFieldData === null ) {
1856            throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
1857                . 'You probably called displayForm() without calling prepareForm() first.' );
1858        }
1859
1860        $html = [];
1861        $subsectionHtml = '';
1862        $hasLabel = false;
1863
1864        foreach ( $fields as $key => $value ) {
1865            if ( $value instanceof HTMLFormField ) {
1866                $v = array_key_exists( $key, $this->mFieldData )
1867                    ? $this->mFieldData[$key]
1868                    : $value->getDefault();
1869
1870                $retval = $this->formatField( $value, $v ?? '' );
1871
1872                // check, if the form field should be added to
1873                // the output.
1874                if ( $value->hasVisibleOutput() ) {
1875                    $html[] = $retval;
1876
1877                    $labelValue = trim( $value->getLabel() );
1878                    if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
1879                        $hasLabel = true;
1880                    }
1881
1882                    $hasUserVisibleFields = true;
1883                }
1884            } elseif ( is_array( $value ) ) {
1885                $subsectionHasVisibleFields = false;
1886                $section =
1887                    $this->displaySection( $value,
1888                        "mw-htmlform-$key",
1889                        "$fieldsetIDPrefix$key-",
1890                        $subsectionHasVisibleFields );
1891
1892                if ( $subsectionHasVisibleFields === true ) {
1893                    // Display the section with various niceties.
1894                    $hasUserVisibleFields = true;
1895
1896                    $legend = $this->getLegend( $key );
1897
1898                    $headerHtml = $this->getHeaderHtml( $key );
1899                    $footerHtml = $this->getFooterHtml( $key );
1900                    $section = $headerHtml .
1901                        $section .
1902                        $footerHtml;
1903
1904                    $attributes = [];
1905                    if ( $fieldsetIDPrefix ) {
1906                        $attributes['id'] = Sanitizer::escapeIdForAttribute( "$fieldsetIDPrefix$key" );
1907                    }
1908                    $subsectionHtml .= $this->wrapFieldSetSection(
1909                        $legend, $section, $attributes, $fields === $this->mFieldTree
1910                    );
1911                } else {
1912                    // Just return the inputs, nothing fancy.
1913                    $subsectionHtml .= $section;
1914                }
1915            }
1916        }
1917
1918        $html = $this->formatSection( $html, $sectionName, $hasLabel );
1919
1920        if ( $subsectionHtml ) {
1921            if ( $this->mSubSectionBeforeFields ) {
1922                return $subsectionHtml . "\n" . $html;
1923            } else {
1924                return $html . "\n" . $subsectionHtml;
1925            }
1926        } else {
1927            return $html;
1928        }
1929    }
1930
1931    /**
1932     * Generate the HTML for an individual field in the current display format.
1933     * @since 1.41
1934     * @stable to override
1935     * @param HTMLFormField $field
1936     * @param mixed $value
1937     * @return string|Stringable HTML
1938     */
1939    protected function formatField( HTMLFormField $field, $value ) {
1940        $displayFormat = $this->getDisplayFormat();
1941        switch ( $displayFormat ) {
1942            case 'table':
1943                return $field->getTableRow( $value );
1944            case 'div':
1945                return $field->getDiv( $value );
1946            case 'raw':
1947                return $field->getRaw( $value );
1948            case 'inline':
1949                return $field->getInline( $value );
1950            default:
1951                throw new LogicException( 'Not implemented' );
1952        }
1953    }
1954
1955    /**
1956     * Put a form section together from the individual fields' HTML, merging it and wrapping.
1957     * @stable to override
1958     * @param array $fieldsHtml Array of outputs from formatField()
1959     * @param string $sectionName
1960     * @param bool $anyFieldHasLabel
1961     * @return string HTML
1962     */
1963    protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1964        if ( !$fieldsHtml ) {
1965            // Do not generate any wrappers for empty sections. Sections may be empty if they only have
1966            // subsections, but no fields. A legend will still be added in wrapFieldSetSection().
1967            return '';
1968        }
1969
1970        $displayFormat = $this->getDisplayFormat();
1971        $html = implode( '', $fieldsHtml );
1972
1973        if ( $displayFormat === 'raw' ) {
1974            return $html;
1975        }
1976
1977        // Avoid strange spacing when no labels exist
1978        $attribs = $anyFieldHasLabel ? [] : [ 'class' => 'mw-htmlform-nolabel' ];
1979
1980        if ( $sectionName ) {
1981            $attribs['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
1982        }
1983
1984        if ( $displayFormat === 'table' ) {
1985            return Html::rawElement( 'table',
1986                    $attribs,
1987                    Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
1988        } elseif ( $displayFormat === 'inline' ) {
1989            return Html::rawElement( 'span', $attribs, "\n$html\n" );
1990        } else {
1991            return Html::rawElement( 'div', $attribs, "\n$html\n" );
1992        }
1993    }
1994
1995    /**
1996     * @deprecated since 1.39, Use prepareForm() instead.
1997     */
1998    public function loadData() {
1999        $this->prepareForm();
2000    }
2001
2002    /**
2003     * Load data of form fields from the request
2004     */
2005    protected function loadFieldData() {
2006        $fieldData = [];
2007        $request = $this->getRequest();
2008
2009        foreach ( $this->mFlatFields as $fieldname => $field ) {
2010            if ( $field->skipLoadData( $request ) ) {
2011                continue;
2012            }
2013            if ( $field->mParams['disabled'] ?? false ) {
2014                $fieldData[$fieldname] = $field->getDefault();
2015            } else {
2016                $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2017            }
2018        }
2019
2020        // Reset to default for fields that are supposed to be disabled.
2021        // FIXME: Handle dependency chains, fields that a field checks on may need a reset too.
2022        foreach ( $fieldData as $name => &$value ) {
2023            $field = $this->mFlatFields[$name];
2024            if ( $field->isDisabled( $fieldData ) ) {
2025                $value = $field->getDefault();
2026            }
2027        }
2028
2029        # Filter data.
2030        foreach ( $fieldData as $name => &$value ) {
2031            $field = $this->mFlatFields[$name];
2032            $value = $field->filter( $value, $fieldData );
2033        }
2034
2035        $this->mFieldData = $fieldData;
2036    }
2037
2038    /**
2039     * Overload this if you want to apply special filtration routines
2040     * to the form as a whole, after it's submitted but before it's
2041     * processed.
2042     * @stable to override
2043     *
2044     * @param array $data
2045     *
2046     * @return array
2047     */
2048    public function filterDataForSubmit( $data ) {
2049        return $data;
2050    }
2051
2052    /**
2053     * Get a string to go in the "<legend>" of a section fieldset.
2054     * Override this if you want something more complicated.
2055     * @stable to override
2056     *
2057     * @param string $key
2058     *
2059     * @return string Plain text (not HTML-escaped)
2060     */
2061    public function getLegend( $key ) {
2062        return $this->msg( $this->mMessagePrefix ? "{$this->mMessagePrefix}-$key" : $key )->text();
2063    }
2064
2065    /**
2066     * Set the value for the action attribute of the form.
2067     * When set to false (which is the default state), the set title is used.
2068     *
2069     * @since 1.19
2070     *
2071     * @param string|bool $action
2072     *
2073     * @return HTMLForm $this for chaining calls (since 1.20)
2074     */
2075    public function setAction( $action ) {
2076        $this->mAction = $action;
2077
2078        return $this;
2079    }
2080
2081    /**
2082     * Get the value for the action attribute of the form.
2083     *
2084     * @since 1.22
2085     *
2086     * @return string
2087     */
2088    public function getAction() {
2089        // If an action is already provided, return it
2090        if ( $this->mAction !== false ) {
2091            return $this->mAction;
2092        }
2093
2094        $articlePath = $this->getConfig()->get( MainConfigNames::ArticlePath );
2095        // Check whether we are in GET mode and the ArticlePath contains a "?"
2096        // meaning that getLocalURL() would return something like "index.php?title=...".
2097        // As browser remove the query string before submitting GET forms,
2098        // it means that the title would be lost. In such case use script path instead
2099        // and put title in a hidden field (see getHiddenFields()).
2100        if ( str_contains( $articlePath, '?' ) && $this->getMethod() === 'get' ) {
2101            return $this->getConfig()->get( MainConfigNames::Script );
2102        }
2103
2104        return $this->getTitle()->getLocalURL();
2105    }
2106
2107    /**
2108     * Set the value for the autocomplete attribute of the form. A typical value is "off".
2109     * When set to null (which is the default state), the attribute get not set.
2110     *
2111     * @since 1.27
2112     *
2113     * @param string|null $autocomplete
2114     *
2115     * @return HTMLForm $this for chaining calls
2116     */
2117    public function setAutocomplete( $autocomplete ) {
2118        $this->mAutocomplete = $autocomplete;
2119
2120        return $this;
2121    }
2122
2123    /**
2124     * Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
2125     * name + parameters array) into a Message.
2126     * @param mixed $value
2127     * @return Message
2128     */
2129    protected function getMessage( $value ) {
2130        return Message::newFromSpecifier( $value )->setContext( $this );
2131    }
2132
2133    /**
2134     * Whether this form, with its current fields, requires the user agent to have JavaScript enabled
2135     * for the client-side HTML5 form validation to work correctly. If this function returns true, a
2136     * 'novalidate' attribute will be added on the `<form>` element. It will be removed if the user
2137     * agent has JavaScript support, in htmlform.js.
2138     *
2139     * @return bool
2140     * @since 1.29
2141     */
2142    public function needsJSForHtml5FormValidation() {
2143        foreach ( $this->mFlatFields as $field ) {
2144            if ( $field->needsJSForHtml5FormValidation() ) {
2145                return true;
2146            }
2147        }
2148        return false;
2149    }
2150}
2151
2152/** @deprecated class alias since 1.42 */
2153class_alias( HTMLForm::class, 'HTMLForm' );