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