wiki.techinc.nl/includes/htmlform/HTMLForm.php
Thiemo Kreuz 1fc8d79ac6 Remove documentation that literally repeats the code
For example, documenting the method getUser() with "get the User
object" does not add any information that's not already there.
But I have to read the text first to understand that it doesn't
document anything that's not already obvious from the code.

Some of this is from a time when we had a PHPCS sniff that was
complaining when a line like `@param User $user` doesn't end
with some descriptive text. Some users started adding text like
`@param User $user The User` back then. Let's please remove
this.

Change-Id: I0ea8d051bc732466c73940de9259f87ffb86ce7a
2020-10-27 19:20:26 +00:00

1978 lines
56 KiB
PHP

<?php
/**
* HTML form generation and submission handling.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
/**
* Object handling generic submission, CSRF protection, layout and
* other logic for UI forms in a reusable manner.
*
* In order to generate the form, the HTMLForm object takes an array
* structure detailing the form fields available. Each element of the
* array is a basic property-list, including the type of field, the
* label it is to be given in the form, callbacks for validation and
* 'filtering', and other pertinent information.
*
* Field types are implemented as subclasses of the generic HTMLFormField
* object, and typically implement at least getInputHTML, which generates
* the HTML for the input field to be placed in the table.
*
* You can find extensive documentation on the www.mediawiki.org wiki:
* - https://www.mediawiki.org/wiki/HTMLForm
* - https://www.mediawiki.org/wiki/HTMLForm/tutorial
*
* The constructor input is an associative array of $fieldname => $info,
* where $info is an Associative Array with any of the following:
*
* 'class' -- the subclass of HTMLFormField that will be used
* to create the object. *NOT* the CSS class!
* 'type' -- roughly translates into the <select> type attribute.
* if 'class' is not specified, this is used as a map
* through HTMLForm::$typeMappings to get the class name.
* 'default' -- default value when the form is displayed
* 'nodata' -- if set (to any value, which casts to true), the data
* for this field will not be loaded from the actual request. Instead,
* always the default data is set as the value of this field.
* 'id' -- HTML id attribute
* 'cssclass' -- CSS class
* 'csshelpclass' -- CSS class used to style help text
* 'dir' -- Direction of the element.
* 'options' -- associative array mapping raw text labels to values.
* Some field types support multi-level arrays.
* Overwrites 'options-message'.
* 'options-messages' -- associative array mapping message keys to values.
* Some field types support multi-level arrays.
* Overwrites 'options' and 'options-message'.
* 'options-message' -- message key or object to be parsed to extract the list of
* options (like 'ipbreason-dropdown').
* 'label-message' -- message key or object for a message to use as the label.
* can be an array of msg key and then parameters to
* the message.
* 'label' -- alternatively, a raw text message. Overridden by
* label-message
* 'help' -- message text for a message to use as a help text.
* 'help-message' -- message key or object for a message to use as a help text.
* can be an array of msg key and then parameters to
* the message.
* Overwrites 'help-messages' and 'help'.
* 'help-messages' -- array of message keys/objects. As above, each item can
* be an array of msg key and then parameters.
* Overwrites 'help'.
* 'help-inline' -- Whether help text (defined using options above) will be shown
* inline after the input field, rather than in a popup.
* Defaults to true. Only used by OOUI form fields.
* 'required' -- passed through to the object, indicating that it
* is a required field.
* 'size' -- the length of text fields
* 'filter-callback' -- a function name to give you the chance to
* massage the inputted value before it's processed.
* @see HTMLFormField::filter()
* 'validation-callback' -- a function name to give you the chance
* to impose extra validation on the field input.
* @see HTMLFormField::validate()
* 'name' -- By default, the 'name' attribute of the input field
* is "wp{$fieldname}". If you want a different name
* (eg one without the "wp" prefix), specify it here and
* it will be used without modification.
* 'hide-if' -- expression given as an array stating when the field
* should be hidden. The first array value has to be the
* expression's logic operator. Supported expressions:
* 'NOT'
* [ 'NOT', array $expression ]
* To hide a field if a given expression is not true.
* '==='
* [ '===', string $fieldName, string $value ]
* To hide a field if another field identified by
* $field has the value $value.
* '!=='
* [ '!==', string $fieldName, string $value ]
* Same as [ 'NOT', [ '===', $fieldName, $value ]
* 'OR', 'AND', 'NOR', 'NAND'
* [ 'XXX', array $expression1, ..., array $expressionN ]
* To hide a field if one or more (OR), all (AND),
* neither (NOR) or not all (NAND) given expressions
* are evaluated as true.
* The expressions will be given to a JavaScript frontend
* module which will continually update the field's
* visibility.
*
* Since 1.20, you can chain mutators to ease the form generation:
* @par Example:
* @code
* $form = new HTMLForm( $someFields );
* $form->setMethod( 'get' )
* ->setWrapperLegendMsg( 'message-key' )
* ->prepareForm()
* ->displayForm( '' );
* @endcode
* Note that you will have prepareForm and displayForm at the end. Other
* method calls done after that would simply not be part of the form :(
*
* @stable to extend
*
* @todo Document 'section' / 'subsection' stuff
*/
class HTMLForm extends ContextSource {
use ProtectedHookAccessorTrait;
// A mapping of 'type' inputs onto standard HTMLFormField subclasses
public static $typeMappings = [
'api' => HTMLApiField::class,
'text' => HTMLTextField::class,
'textwithbutton' => HTMLTextFieldWithButton::class,
'textarea' => HTMLTextAreaField::class,
'select' => HTMLSelectField::class,
'combobox' => HTMLComboboxField::class,
'radio' => HTMLRadioField::class,
'multiselect' => HTMLMultiSelectField::class,
'limitselect' => HTMLSelectLimitField::class,
'check' => HTMLCheckField::class,
'toggle' => HTMLCheckField::class,
'int' => HTMLIntField::class,
'float' => HTMLFloatField::class,
'info' => HTMLInfoField::class,
'selectorother' => HTMLSelectOrOtherField::class,
'selectandother' => HTMLSelectAndOtherField::class,
'namespaceselect' => HTMLSelectNamespace::class,
'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
'tagfilter' => HTMLTagFilter::class,
'sizefilter' => HTMLSizeFilterField::class,
'submit' => HTMLSubmitField::class,
'hidden' => HTMLHiddenField::class,
'edittools' => HTMLEditTools::class,
'checkmatrix' => HTMLCheckMatrix::class,
'cloner' => HTMLFormFieldCloner::class,
'autocompleteselect' => HTMLAutoCompleteSelectField::class,
'language' => HTMLSelectLanguageField::class,
'date' => HTMLDateTimeField::class,
'time' => HTMLDateTimeField::class,
'datetime' => HTMLDateTimeField::class,
'expiry' => HTMLExpiryField::class,
// HTMLTextField will output the correct type="" attribute automagically.
// There are about four zillion other HTML5 input types, like range, but
// we don't use those at the moment, so no point in adding all of them.
'email' => HTMLTextField::class,
'password' => HTMLTextField::class,
'url' => HTMLTextField::class,
'title' => HTMLTitleTextField::class,
'user' => HTMLUserTextField::class,
'usersmultiselect' => HTMLUsersMultiselectField::class,
'titlesmultiselect' => HTMLTitlesMultiselectField::class,
'namespacesmultiselect' => HTMLNamespacesMultiselectField::class,
];
public $mFieldData;
protected $mMessagePrefix;
/** @var HTMLFormField[] */
protected $mFlatFields = [];
protected $mFieldTree = [];
protected $mShowReset = false;
protected $mShowSubmit = true;
/** @var string[] */
protected $mSubmitFlags = [ 'primary', 'progressive' ];
protected $mShowCancel = false;
protected $mCancelTarget;
protected $mSubmitCallback;
protected $mValidationErrorMessage;
protected $mPre = '';
protected $mHeader = '';
protected $mFooter = '';
protected $mSectionHeaders = [];
protected $mSectionFooters = [];
protected $mPost = '';
protected $mId;
protected $mName;
protected $mTableId = '';
protected $mSubmitID;
protected $mSubmitName;
protected $mSubmitText;
protected $mSubmitTooltip;
protected $mFormIdentifier;
protected $mTitle;
protected $mMethod = 'post';
protected $mWasSubmitted = false;
/**
* Form action URL. false means we will use the URL to set Title
* @since 1.19
* @var bool|string
*/
protected $mAction = false;
/**
* Whether the form can be collapsed
* @since 1.34
* @var bool
*/
protected $mCollapsible = false;
/**
* Whether the form is collapsed by default
* @since 1.34
* @var bool
*/
protected $mCollapsed = false;
/**
* Form attribute autocomplete. A typical value is "off". null does not set the attribute
* @since 1.27
* @var string|null
*/
protected $mAutocomplete = null;
protected $mUseMultipart = false;
/**
* @var array[]
* @phan-var array<int,array{0:string,1:array}>
*/
protected $mHiddenFields = [];
/**
* @var array[]
* @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}>
*/
protected $mButtons = [];
protected $mWrapperLegend = false;
protected $mWrapperAttributes = [];
/**
* Salt for the edit token.
* @var string|array
*/
protected $mTokenSalt = '';
/**
* If true, sections that contain both fields and subsections will
* render their subsections before their fields.
*
* Subclasses may set this to false to render subsections after fields
* instead.
*/
protected $mSubSectionBeforeFields = true;
/**
* Format in which to display form. For viable options,
* @see $availableDisplayFormats
* @var string
*/
protected $displayFormat = 'table';
/**
* Available formats in which to display the form
* @var array
*/
protected $availableDisplayFormats = [
'table',
'div',
'raw',
'inline',
];
/**
* Available formats in which to display the form
* @var array
*/
protected $availableSubclassDisplayFormats = [
'vform',
'ooui',
];
/**
* Construct a HTMLForm object for given display type. May return a HTMLForm subclass.
*
* @stable to call
*
* @param string $displayFormat
* @param mixed ...$arguments Additional arguments to pass to the constructor.
* @return HTMLForm
*/
public static function factory( $displayFormat, ...$arguments ) {
switch ( $displayFormat ) {
case 'vform':
return new VFormHTMLForm( ...$arguments );
case 'ooui':
return new OOUIHTMLForm( ...$arguments );
default:
$form = new self( ...$arguments );
$form->setDisplayFormat( $displayFormat );
return $form;
}
}
/**
* Build a new HTMLForm from an array of field attributes
*
* @stable to call
*
* @param array $descriptor Array of Field constructs, as described
* in the class documentation
* @param IContextSource|null $context Available since 1.18, will become compulsory in 1.18.
* Obviates the need to call $form->setTitle()
* @param string $messagePrefix A prefix to go in front of default messages
*/
public function __construct( $descriptor, /*IContextSource*/ $context = null,
$messagePrefix = ''
) {
if ( $context instanceof IContextSource ) {
$this->setContext( $context );
$this->mTitle = false; // We don't need them to set a title
$this->mMessagePrefix = $messagePrefix;
} elseif ( $context === null && $messagePrefix !== '' ) {
$this->mMessagePrefix = $messagePrefix;
} elseif ( is_string( $context ) && $messagePrefix === '' ) {
// B/C since 1.18
// it's actually $messagePrefix
$this->mMessagePrefix = $context;
}
// Evil hack for mobile :(
if (
!$this->getConfig()->get( 'HTMLFormAllowTableFormat' )
&& $this->displayFormat === 'table'
) {
$this->displayFormat = 'div';
}
$this->addFields( $descriptor );
}
/**
* Add fields to the form
*
* @since 1.34
*
* @param array $descriptor Array of Field constructs, as described
* in the class documentation
* @return HTMLForm
*/
public function addFields( $descriptor ) {
$loadedDescriptor = [];
foreach ( $descriptor as $fieldname => $info ) {
$section = $info['section'] ?? '';
if ( isset( $info['type'] ) && $info['type'] === 'file' ) {
$this->mUseMultipart = true;
}
$field = static::loadInputFromParameters( $fieldname, $info, $this );
$setSection =& $loadedDescriptor;
if ( $section ) {
foreach ( explode( '/', $section ) as $newName ) {
if ( !isset( $setSection[$newName] ) ) {
$setSection[$newName] = [];
}
$setSection =& $setSection[$newName];
}
}
$setSection[$fieldname] = $field;
$this->mFlatFields[$fieldname] = $field;
}
$this->mFieldTree = array_merge( $this->mFieldTree, $loadedDescriptor );
return $this;
}
/**
* @param string $fieldname
* @return bool
*/
public function hasField( $fieldname ) {
return isset( $this->mFlatFields[$fieldname] );
}
/**
* @param string $fieldname
* @return HTMLFormField
* @throws DomainException on invalid field name
*/
public function getField( $fieldname ) {
if ( !$this->hasField( $fieldname ) ) {
throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
}
return $this->mFlatFields[$fieldname];
}
/**
* Set format in which to display the form
*
* @param string $format The name of the format to use, must be one of
* $this->availableDisplayFormats
*
* @throws MWException
* @since 1.20
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setDisplayFormat( $format ) {
if (
in_array( $format, $this->availableSubclassDisplayFormats, true ) ||
in_array( $this->displayFormat, $this->availableSubclassDisplayFormats, true )
) {
throw new MWException( 'Cannot change display format after creation, ' .
'use HTMLForm::factory() instead' );
}
if ( !in_array( $format, $this->availableDisplayFormats, true ) ) {
throw new MWException( 'Display format must be one of ' .
print_r(
array_merge(
$this->availableDisplayFormats,
$this->availableSubclassDisplayFormats
),
true
) );
}
// Evil hack for mobile :(
if ( !$this->getConfig()->get( 'HTMLFormAllowTableFormat' ) && $format === 'table' ) {
$format = 'div';
}
$this->displayFormat = $format;
return $this;
}
/**
* Getter for displayFormat
* @since 1.20
* @return string
*/
public function getDisplayFormat() {
return $this->displayFormat;
}
/**
* Get the HTMLFormField subclass for this descriptor.
*
* The descriptor can be passed either 'class' which is the name of
* a HTMLFormField subclass, or a shorter 'type' which is an alias.
* This makes sure the 'class' is always set, and also is returned by
* this function for ease.
*
* @since 1.23
*
* @param string $fieldname Name of the field
* @param array &$descriptor Input Descriptor, as described
* in the class documentation
*
* @throws MWException
* @return string Name of a HTMLFormField subclass
*/
public static function getClassFromDescriptor( $fieldname, &$descriptor ) {
if ( isset( $descriptor['class'] ) ) {
$class = $descriptor['class'];
} elseif ( isset( $descriptor['type'] ) ) {
$class = static::$typeMappings[$descriptor['type']];
$descriptor['class'] = $class;
} else {
$class = null;
}
if ( !$class ) {
throw new MWException( "Descriptor with no class for $fieldname: "
. print_r( $descriptor, true ) );
}
return $class;
}
/**
* Initialise a new Object for the field
* @stable to override
*
* @param string $fieldname Name of the field
* @param array $descriptor Input Descriptor, as described
* in the class documentation
* @param HTMLForm|null $parent Parent instance of HTMLForm
*
* @throws MWException
* @return HTMLFormField Instance of a subclass of HTMLFormField
*/
public static function loadInputFromParameters( $fieldname, $descriptor,
HTMLForm $parent = null
) {
$class = static::getClassFromDescriptor( $fieldname, $descriptor );
$descriptor['fieldname'] = $fieldname;
if ( $parent ) {
$descriptor['parent'] = $parent;
}
# @todo This will throw a fatal error whenever someone try to use
# 'class' to feed a CSS class instead of 'cssclass'. Would be
# great to avoid the fatal error and show a nice error.
return new $class( $descriptor );
}
/**
* Prepare form for submission.
*
* @warning When doing method chaining, that should be the very last
* method call before displayForm().
*
* @throws MWException
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function prepareForm() {
# Check if we have the info we need
if ( !$this->mTitle instanceof Title && $this->mTitle !== false ) {
throw new MWException( 'You must call setTitle() on an HTMLForm' );
}
# Load data from the request.
if (
$this->mFormIdentifier === null ||
$this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier
) {
$this->loadData();
} else {
$this->mFieldData = [];
}
return $this;
}
/**
* Try submitting, with edit token check first
* @return Status|bool
*/
public function tryAuthorizedSubmit() {
$result = false;
if ( $this->mFormIdentifier === null ) {
$identOkay = true;
} else {
$identOkay = $this->getRequest()->getVal( 'wpFormIdentifier' ) === $this->mFormIdentifier;
}
$tokenOkay = false;
if ( $this->getMethod() !== 'post' ) {
$tokenOkay = true; // no session check needed
} elseif ( $this->getRequest()->wasPosted() ) {
$editToken = $this->getRequest()->getVal( 'wpEditToken' );
if ( $this->getUser()->isLoggedIn() || $editToken !== null ) {
// Session tokens for logged-out users have no security value.
// However, if the user gave one, check it in order to give a nice
// "session expired" error instead of "permission denied" or such.
$tokenOkay = $this->getUser()->matchEditToken( $editToken, $this->mTokenSalt );
} else {
$tokenOkay = true;
}
}
if ( $tokenOkay && $identOkay ) {
$this->mWasSubmitted = true;
$result = $this->trySubmit();
}
return $result;
}
/**
* The here's-one-I-made-earlier option: do the submission if
* posted, or display the form with or without funky validation
* errors
* @stable to override
* @return bool|Status Whether submission was successful.
*/
public function show() {
$this->prepareForm();
$result = $this->tryAuthorizedSubmit();
if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) {
return $result;
}
$this->displayForm( $result );
return false;
}
/**
* Same as self::show with the difference, that the form will be
* added to the output, no matter, if the validation was good or not.
* @return bool|Status Whether submission was successful.
*/
public function showAlways() {
$this->prepareForm();
$result = $this->tryAuthorizedSubmit();
$this->displayForm( $result );
return $result;
}
/**
* Validate all the fields, and call the submission callback
* function if everything is kosher.
* @stable to override
* @throws MWException
* @return bool|string|array|Status
* - Bool true or a good Status object indicates success,
* - Bool false indicates no submission was attempted,
* - Anything else indicates failure. The value may be a fatal Status
* object, an HTML string, or an array of arrays (message keys and
* params) or strings (message keys)
*/
public function trySubmit() {
$valid = true;
$hoistedErrors = Status::newGood();
if ( $this->mValidationErrorMessage ) {
foreach ( $this->mValidationErrorMessage as $error ) {
$hoistedErrors->fatal( ...$error );
}
} else {
$hoistedErrors->fatal( 'htmlform-invalid-input' );
}
$this->mWasSubmitted = true;
# Check for cancelled submission
foreach ( $this->mFlatFields as $fieldname => $field ) {
if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
continue;
}
if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
$this->mWasSubmitted = false;
return false;
}
}
# Check for validation
foreach ( $this->mFlatFields as $fieldname => $field ) {
if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
continue;
}
if ( $field->isHidden( $this->mFieldData ) ) {
continue;
}
$res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
if ( $res !== true ) {
$valid = false;
if ( $res !== false && !$field->canDisplayErrors() ) {
if ( is_string( $res ) ) {
$hoistedErrors->fatal( 'rawmessage', $res );
} else {
$hoistedErrors->fatal( $res );
}
}
}
}
if ( !$valid ) {
return $hoistedErrors;
}
$callback = $this->mSubmitCallback;
if ( !is_callable( $callback ) ) {
throw new MWException( 'HTMLForm: no submit callback provided. Use ' .
'setSubmitCallback() to set one.' );
}
$data = $this->filterDataForSubmit( $this->mFieldData );
$res = call_user_func( $callback, $data, $this );
if ( $res === false ) {
$this->mWasSubmitted = false;
}
return $res;
}
/**
* Test whether the form was considered to have been submitted or not, i.e.
* whether the last call to tryAuthorizedSubmit or trySubmit returned
* non-false.
*
* This will return false until HTMLForm::tryAuthorizedSubmit or
* HTMLForm::trySubmit is called.
*
* @since 1.23
* @return bool
*/
public function wasSubmitted() {
return $this->mWasSubmitted;
}
/**
* Set a callback to a function to do something with the form
* once it's been successfully validated.
*
* @param callable $cb The function will be passed the output from
* HTMLForm::filterDataForSubmit and this HTMLForm object, and must
* return as documented for HTMLForm::trySubmit
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitCallback( $cb ) {
$this->mSubmitCallback = $cb;
return $this;
}
/**
* Set a message to display on a validation error.
*
* @param array $msg Array of valid inputs to wfMessage()
* (so each entry must itself be an array of arguments)
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setValidationErrorMessage( $msg ) {
$this->mValidationErrorMessage = $msg;
return $this;
}
/**
* Set the introductory message, overwriting any existing message.
*
* @param string $msg Complete text of message to display
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setIntro( $msg ) {
$this->setPreText( $msg );
return $this;
}
/**
* Set the introductory message HTML, overwriting any existing message.
* @since 1.19
*
* @param string $msg Complete HTML of message to display
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setPreText( $msg ) {
$this->mPre = $msg;
return $this;
}
/**
* Add HTML to introductory message.
*
* @param string $msg Complete HTML of message to display
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addPreText( $msg ) {
$this->mPre .= $msg;
return $this;
}
/**
* Get the introductory message HTML.
*
* @since 1.32
*
* @return string
*/
public function getPreText() {
return $this->mPre;
}
/**
* Add HTML to the header, inside the form.
*
* @param string $msg Additional HTML to display in header
* @param string|null $section The section to add the header to
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addHeaderText( $msg, $section = null ) {
if ( $section === null ) {
$this->mHeader .= $msg;
} else {
if ( !isset( $this->mSectionHeaders[$section] ) ) {
$this->mSectionHeaders[$section] = '';
}
$this->mSectionHeaders[$section] .= $msg;
}
return $this;
}
/**
* Set header text, inside the form.
* @since 1.19
*
* @param string $msg Complete HTML of header to display
* @param string|null $section The section to add the header to
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setHeaderText( $msg, $section = null ) {
if ( $section === null ) {
$this->mHeader = $msg;
} else {
$this->mSectionHeaders[$section] = $msg;
}
return $this;
}
/**
* Get header text.
* @stable to override
*
* @param string|null $section The section to get the header text for
* @since 1.26
* @return string HTML
*/
public function getHeaderText( $section = null ) {
if ( $section === null ) {
return $this->mHeader;
} else {
return $this->mSectionHeaders[$section] ?? '';
}
}
/**
* Add footer text, inside the form.
*
* @param string $msg Complete text of message to display
* @param string|null $section The section to add the footer text to
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addFooterText( $msg, $section = null ) {
if ( $section === null ) {
$this->mFooter .= $msg;
} else {
if ( !isset( $this->mSectionFooters[$section] ) ) {
$this->mSectionFooters[$section] = '';
}
$this->mSectionFooters[$section] .= $msg;
}
return $this;
}
/**
* Set footer text, inside the form.
* @since 1.19
*
* @param string $msg Complete text of message to display
* @param string|null $section The section to add the footer text to
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setFooterText( $msg, $section = null ) {
if ( $section === null ) {
$this->mFooter = $msg;
} else {
$this->mSectionFooters[$section] = $msg;
}
return $this;
}
/**
* Get footer text.
*
* @param string|null $section The section to get the footer text for
* @since 1.26
* @return string
*/
public function getFooterText( $section = null ) {
if ( $section === null ) {
return $this->mFooter;
} else {
return $this->mSectionFooters[$section] ?? '';
}
}
/**
* Add text to the end of the display.
*
* @param string $msg Complete text of message to display
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addPostText( $msg ) {
$this->mPost .= $msg;
return $this;
}
/**
* Set text at the end of the display.
*
* @param string $msg Complete text of message to display
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setPostText( $msg ) {
$this->mPost = $msg;
return $this;
}
/**
* Add a hidden field to the output
*
* @param string $name Field name. This will be used exactly as entered
* @param mixed $value Field value
* @param array $attribs
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addHiddenField( $name, $value, array $attribs = [] ) {
$attribs += [ 'name' => $name ];
$this->mHiddenFields[] = [ $value, $attribs ];
return $this;
}
/**
* Add an array of hidden fields to the output
*
* @since 1.22
*
* @param array $fields Associative array of fields to add;
* mapping names to their values
*
* @return HTMLForm $this for chaining calls
*/
public function addHiddenFields( array $fields ) {
foreach ( $fields as $name => $value ) {
$this->mHiddenFields[] = [ $value, [ 'name' => $name ] ];
}
return $this;
}
/**
* Add a button to the form
*
* @since 1.27 takes an array as shown. Earlier versions accepted
* 'name', 'value', 'id', and 'attribs' as separate parameters in that
* order.
* @param array $data Data to define the button:
* - name: (string) Button name.
* - value: (string) Button value.
* - label-message: (string|string[]|MessageSpecifier, optional) Button label
* message key to use instead of 'value'. Overrides 'label' and 'label-raw'.
* - label: (string, optional) Button label text to use instead of
* 'value'. Overrides 'label-raw'.
* - label-raw: (string, optional) Button label HTML to use instead of
* 'value'.
* - id: (string, optional) DOM id for the button.
* - attribs: (array, optional) Additional HTML attributes.
* - flags: (string|string[], optional) OOUI flags.
* - framed: (boolean=true, optional) OOUI framed attribute.
* @codingStandardsIgnoreStart
* @phan-param array{name:string,value:string,label-message?:string|string[]|MessageSpecifier,label?:string,label-raw?:string,id?:string,attribs?:array,flags?:string|string[],framed?:bool} $data
* @codingStandardsIgnoreEnd
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function addButton( $data ) {
if ( !is_array( $data ) ) {
$args = func_get_args();
if ( count( $args ) < 2 || count( $args ) > 4 ) {
throw new InvalidArgumentException(
'Incorrect number of arguments for deprecated calling style'
);
}
$data = [
'name' => $args[0],
'value' => $args[1],
'id' => $args[2] ?? null,
'attribs' => $args[3] ?? null,
];
} else {
if ( !isset( $data['name'] ) ) {
throw new InvalidArgumentException( 'A name is required' );
}
if ( !isset( $data['value'] ) ) {
throw new InvalidArgumentException( 'A value is required' );
}
}
$this->mButtons[] = $data + [
'id' => null,
'attribs' => null,
'flags' => null,
'framed' => true,
];
return $this;
}
/**
* Set the salt for the edit token.
*
* Only useful when the method is "post".
*
* @since 1.24
* @param string|array $salt Salt to use
* @return HTMLForm $this For chaining calls
*/
public function setTokenSalt( $salt ) {
$this->mTokenSalt = $salt;
return $this;
}
/**
* Display the form (sending to the context's OutputPage object), with an
* appropriate error message or stack of messages, and any validation errors, etc.
*
* @warning You should call prepareForm() before calling this function.
* Moreover, when doing method chaining this should be the very last method
* call just after prepareForm().
*
* @stable to override
*
* @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
*
* @return void Nothing, should be last call
*/
public function displayForm( $submitResult ) {
$this->getOutput()->addHTML( $this->getHTML( $submitResult ) );
}
/**
* Returns the raw HTML generated by the form
*
* @stable to override
*
* @param bool|string|array|Status $submitResult Output from HTMLForm::trySubmit()
*
* @return string HTML
* @return-taint escaped
*/
public function getHTML( $submitResult ) {
# For good measure (it is the default)
$this->getOutput()->preventClickjacking();
$this->getOutput()->addModules( 'mediawiki.htmlform' );
$this->getOutput()->addModuleStyles( 'mediawiki.htmlform.styles' );
if ( $this->mCollapsible ) {
// Preload jquery.makeCollapsible for mediawiki.htmlform
$this->getOutput()->addModules( 'jquery.makeCollapsible' );
}
$html = ''
. $this->getErrorsOrWarnings( $submitResult, 'error' )
. $this->getErrorsOrWarnings( $submitResult, 'warning' )
. $this->getHeaderText()
. $this->getBody()
. $this->getHiddenFields()
. $this->getButtons()
. $this->getFooterText();
$html = $this->wrapForm( $html );
return '' . $this->mPre . $html . $this->mPost;
}
/**
* Enable collapsible mode, and set whether the form is collapsed by default.
*
* @since 1.34
* @param bool $collapsedByDefault Whether the form is collapsed by default (optional).
* @return HTMLForm $this for chaining calls
*/
public function setCollapsibleOptions( $collapsedByDefault = false ) {
$this->mCollapsible = true;
$this->mCollapsed = $collapsedByDefault;
return $this;
}
/**
* Get HTML attributes for the `<form>` tag.
* @stable to override
* @return array
*/
protected function getFormAttributes() {
# Use multipart/form-data
$encType = $this->mUseMultipart
? 'multipart/form-data'
: 'application/x-www-form-urlencoded';
# Attributes
$attribs = [
'class' => 'mw-htmlform',
'action' => $this->getAction(),
'method' => $this->getMethod(),
'enctype' => $encType,
];
if ( $this->mId ) {
$attribs['id'] = $this->mId;
}
if ( is_string( $this->mAutocomplete ) ) {
$attribs['autocomplete'] = $this->mAutocomplete;
}
if ( $this->mName ) {
$attribs['name'] = $this->mName;
}
if ( $this->needsJSForHtml5FormValidation() ) {
$attribs['novalidate'] = true;
}
return $attribs;
}
/**
* Wrap the form innards in an actual "<form>" element
* @stable to override
*
* @param string $html HTML contents to wrap.
*
* @return string Wrapped HTML.
*/
public function wrapForm( $html ) {
# Include a <fieldset> wrapper for style, if requested.
if ( $this->mWrapperLegend !== false ) {
$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
$html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
}
return Html::rawElement(
'form',
$this->getFormAttributes(),
$html
);
}
/**
* Get the hidden fields that should go inside the form.
* @return string HTML.
*/
public function getHiddenFields() {
$html = '';
if ( $this->mFormIdentifier !== null ) {
$html .= Html::hidden(
'wpFormIdentifier',
$this->mFormIdentifier
) . "\n";
}
if ( $this->getMethod() === 'post' ) {
$html .= Html::hidden(
'wpEditToken',
$this->getUser()->getEditToken( $this->mTokenSalt ),
[ 'id' => 'wpEditToken' ]
) . "\n";
$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
}
$articlePath = $this->getConfig()->get( 'ArticlePath' );
if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
$html .= Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . "\n";
}
foreach ( $this->mHiddenFields as $data ) {
list( $value, $attribs ) = $data;
$html .= Html::hidden( $attribs['name'], $value, $attribs ) . "\n";
}
return $html;
}
/**
* Get the submit and (potentially) reset buttons.
* @stable to override
* @return string HTML.
*/
public function getButtons() {
$buttons = '';
$useMediaWikiUIEverywhere = $this->getConfig()->get( 'UseMediaWikiUIEverywhere' );
if ( $this->mShowSubmit ) {
$attribs = [];
if ( isset( $this->mSubmitID ) ) {
$attribs['id'] = $this->mSubmitID;
}
if ( isset( $this->mSubmitName ) ) {
$attribs['name'] = $this->mSubmitName;
}
if ( isset( $this->mSubmitTooltip ) ) {
$attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
}
$attribs['class'] = [ 'mw-htmlform-submit' ];
if ( $useMediaWikiUIEverywhere ) {
foreach ( $this->mSubmitFlags as $flag ) {
$attribs['class'][] = 'mw-ui-' . $flag;
}
$attribs['class'][] = 'mw-ui-button';
}
$buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) . "\n";
}
if ( $this->mShowReset ) {
$buttons .= Html::element(
'input',
[
'type' => 'reset',
'value' => $this->msg( 'htmlform-reset' )->text(),
'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
]
) . "\n";
}
if ( $this->mShowCancel ) {
$target = $this->mCancelTarget ?: Title::newMainPage();
if ( $target instanceof Title ) {
$target = $target->getLocalURL();
}
$buttons .= Html::element(
'a',
[
'class' => $useMediaWikiUIEverywhere ? 'mw-ui-button' : null,
'href' => $target,
],
$this->msg( 'cancel' )->text()
) . "\n";
}
foreach ( $this->mButtons as $button ) {
$attrs = [
'type' => 'submit',
'name' => $button['name'],
'value' => $button['value']
];
if ( isset( $button['label-message'] ) ) {
$label = $this->getMessage( $button['label-message'] )->parse();
} elseif ( isset( $button['label'] ) ) {
$label = htmlspecialchars( $button['label'] );
} elseif ( isset( $button['label-raw'] ) ) {
$label = $button['label-raw'];
} else {
$label = htmlspecialchars( $button['value'] );
}
if ( $button['attribs'] ) {
$attrs += $button['attribs'];
}
if ( isset( $button['id'] ) ) {
$attrs['id'] = $button['id'];
}
if ( $useMediaWikiUIEverywhere ) {
$attrs['class'] = isset( $attrs['class'] ) ? (array)$attrs['class'] : [];
$attrs['class'][] = 'mw-ui-button';
}
$buttons .= Html::rawElement( 'button', $attrs, $label ) . "\n";
}
if ( !$buttons ) {
return '';
}
return Html::rawElement( 'span',
[ 'class' => 'mw-htmlform-submit-buttons' ], "\n$buttons" ) . "\n";
}
/**
* Get the whole body of the form.
* @stable to override
* @return string
*/
public function getBody() {
return $this->displaySection( $this->mFieldTree, $this->mTableId );
}
/**
* Returns a formatted list of errors or warnings from the given elements.
* @stable to override
*
* @param string|array|Status $elements The set of errors/warnings to process.
* @param string $elementsType Should warnings or errors be returned. This is meant
* for Status objects, all other valid types are always considered as errors.
* @return string
*/
public function getErrorsOrWarnings( $elements, $elementsType ) {
if ( !in_array( $elementsType, [ 'error', 'warning' ], true ) ) {
throw new DomainException( $elementsType . ' is not a valid type.' );
}
$elementstr = false;
if ( $elements instanceof Status ) {
list( $errorStatus, $warningStatus ) = $elements->splitByErrorType();
$status = $elementsType === 'error' ? $errorStatus : $warningStatus;
if ( $status->isGood() ) {
$elementstr = '';
} else {
$elementstr = $status
->getMessage()
->setContext( $this )
->setInterfaceMessageFlag( true )
->parse();
}
} elseif ( is_array( $elements ) && $elementsType === 'error' ) {
$elementstr = $this->formatErrors( $elements );
} elseif ( $elementsType === 'error' ) {
$elementstr = $elements;
}
return $elementstr
? Html::rawElement( 'div', [ 'class' => $elementsType . 'box' ], $elementstr )
: '';
}
/**
* Format a stack of error messages into a single HTML string
*
* @param array $errors Array of message keys/values
*
* @return string HTML, a "<ul>" list of errors
*/
public function formatErrors( $errors ) {
$errorstr = '';
foreach ( $errors as $error ) {
$errorstr .= Html::rawElement(
'li',
[],
$this->getMessage( $error )->parse()
);
}
$errorstr = Html::rawElement( 'ul', [], $errorstr );
return $errorstr;
}
/**
* Set the text for the submit button
*
* @param string $t Plaintext
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitText( $t ) {
$this->mSubmitText = $t;
return $this;
}
/**
* Identify that the submit button in the form has a destructive action
* @since 1.24
*
* @return HTMLForm $this for chaining calls (since 1.28)
*/
public function setSubmitDestructive() {
$this->mSubmitFlags = [ 'destructive', 'primary' ];
return $this;
}
/**
* Set the text for the submit button to a message
* @since 1.19
*
* @param string|Message $msg Message key or Message object
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitTextMsg( $msg ) {
if ( !$msg instanceof Message ) {
$msg = $this->msg( $msg );
}
$this->setSubmitText( $msg->text() );
return $this;
}
/**
* Get the text for the submit button, either customised or a default.
* @return string
*/
public function getSubmitText() {
return $this->mSubmitText ?: $this->msg( 'htmlform-submit' )->text();
}
/**
* @param string $name Submit button name
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitName( $name ) {
$this->mSubmitName = $name;
return $this;
}
/**
* @param string $name Tooltip for the submit button
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitTooltip( $name ) {
$this->mSubmitTooltip = $name;
return $this;
}
/**
* Set the id for the submit button.
*
* @param string $t
*
* @todo FIXME: Integrity of $t is *not* validated
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setSubmitID( $t ) {
$this->mSubmitID = $t;
return $this;
}
/**
* Set an internal identifier for this form. It will be submitted as a hidden form field, allowing
* HTMLForm to determine whether the form was submitted (or merely viewed). Setting this serves
* two purposes:
*
* - If you use two or more forms on one page, it allows HTMLForm to identify which of the forms
* was submitted, and not attempt to validate the other ones.
* - If you use checkbox or multiselect fields inside a form using the GET method, it allows
* HTMLForm to distinguish between the initial page view and a form submission with all
* checkboxes or select options unchecked.
*
* @since 1.28
* @param string $ident
* @return $this
*/
public function setFormIdentifier( $ident ) {
$this->mFormIdentifier = $ident;
return $this;
}
/**
* Stop a default submit button being shown for this form. This implies that an
* alternate submit method must be provided manually.
*
* @since 1.22
*
* @param bool $suppressSubmit Set to false to re-enable the button again
*
* @return HTMLForm $this for chaining calls
*/
public function suppressDefaultSubmit( $suppressSubmit = true ) {
$this->mShowSubmit = !$suppressSubmit;
return $this;
}
/**
* Show a cancel button (or prevent it). The button is not shown by default.
* @param bool $show
* @return HTMLForm $this for chaining calls
* @since 1.27
*/
public function showCancel( $show = true ) {
$this->mShowCancel = $show;
return $this;
}
/**
* Sets the target where the user is redirected to after clicking cancel.
* @param Title|string $target Target as a Title object or an URL
* @return HTMLForm $this for chaining calls
* @since 1.27
*/
public function setCancelTarget( $target ) {
$this->mCancelTarget = $target;
return $this;
}
/**
* Set the id of the \<table\> or outermost \<div\> element.
*
* @since 1.22
*
* @param string $id New value of the id attribute, or "" to remove
*
* @return HTMLForm $this for chaining calls
*/
public function setTableId( $id ) {
$this->mTableId = $id;
return $this;
}
/**
* @param string $id DOM id for the form
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setId( $id ) {
$this->mId = $id;
return $this;
}
/**
* @param string $name 'name' attribute for the form
* @return HTMLForm $this for chaining calls
*/
public function setName( $name ) {
$this->mName = $name;
return $this;
}
/**
* Prompt the whole form to be wrapped in a "<fieldset>", with
* this text as its "<legend>" element.
*
* @param string|bool $legend If false, no wrapper or legend will be displayed.
* If true, a wrapper will be displayed, but no legend.
* If a string, a wrapper will be displayed with that string as a legend.
* The string will be escaped before being output (this doesn't support HTML).
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setWrapperLegend( $legend ) {
$this->mWrapperLegend = $legend;
return $this;
}
/**
* For internal use only. Use is discouraged, and should only be used where
* support for gadgets/user scripts is warranted.
* @param array $attributes
* @internal
* @return HTMLForm $this for chaining calls
*/
public function setWrapperAttributes( $attributes ) {
$this->mWrapperAttributes = $attributes;
return $this;
}
/**
* Prompt the whole form to be wrapped in a "<fieldset>", with
* this message as its "<legend>" element.
* @since 1.19
*
* @param string|Message $msg Message key or Message object
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setWrapperLegendMsg( $msg ) {
if ( !$msg instanceof Message ) {
$msg = $this->msg( $msg );
}
$this->setWrapperLegend( $msg->text() );
return $this;
}
/**
* Set the prefix for various default messages
* @todo Currently only used for the "<fieldset>" legend on forms
* with multiple sections; should be used elsewhere?
*
* @param string $p
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setMessagePrefix( $p ) {
$this->mMessagePrefix = $p;
return $this;
}
/**
* Set the title for form submission
*
* @param Title $t Title of page the form is on/should be posted to
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setTitle( $t ) {
$this->mTitle = $t;
return $this;
}
/**
* @return Title
*/
public function getTitle() {
return $this->mTitle === false
? $this->getContext()->getTitle()
: $this->mTitle;
}
/**
* Set the method used to submit the form
*
* @param string $method
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setMethod( $method = 'post' ) {
$this->mMethod = strtolower( $method );
return $this;
}
/**
* @return string Always lowercase
*/
public function getMethod() {
return $this->mMethod;
}
/**
* Wraps the given $section into an user-visible fieldset.
* @stable to override
*
* @param string $legend Legend text for the fieldset
* @param string $section The section content in plain Html
* @param array $attributes Additional attributes for the fieldset
* @param bool $isRoot Section is at the root of the tree
* @return string The fieldset's Html
*/
protected function wrapFieldSetSection( $legend, $section, $attributes, $isRoot ) {
return Xml::fieldset( $legend, $section, $attributes ) . "\n";
}
/**
* @todo Document
* @stable to override
*
* @param array[]|HTMLFormField[] $fields Array of fields (either arrays or
* objects).
* @param string $sectionName ID attribute of the "<table>" tag for this
* section, ignored if empty.
* @param string $fieldsetIDPrefix ID prefix for the "<fieldset>" tag of
* each subsection, ignored if empty.
* @param bool &$hasUserVisibleFields Whether the section had user-visible fields.
* @throws LogicException When called on uninitialized field data, e.g. When
* HTMLForm::displayForm was called without calling HTMLForm::prepareForm
* first.
*
* @return string
*/
public function displaySection( $fields,
$sectionName = '',
$fieldsetIDPrefix = '',
&$hasUserVisibleFields = false
) {
if ( $this->mFieldData === null ) {
throw new LogicException( 'HTMLForm::displaySection() called on uninitialized field data. '
. 'You probably called displayForm() without calling prepareForm() first.' );
}
$displayFormat = $this->getDisplayFormat();
$html = [];
$subsectionHtml = '';
$hasLabel = false;
// Conveniently, PHP method names are case-insensitive.
// For grep: this can call getDiv, getRaw, getInline, getVForm, getOOUI
$getFieldHtmlMethod = $displayFormat === 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
foreach ( $fields as $key => $value ) {
if ( $value instanceof HTMLFormField ) {
$v = array_key_exists( $key, $this->mFieldData )
? $this->mFieldData[$key]
: $value->getDefault();
$retval = $value->$getFieldHtmlMethod( $v );
// check, if the form field should be added to
// the output.
if ( $value->hasVisibleOutput() ) {
$html[] = $retval;
$labelValue = trim( $value->getLabel() );
if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
$hasLabel = true;
}
$hasUserVisibleFields = true;
}
} elseif ( is_array( $value ) ) {
$subsectionHasVisibleFields = false;
$section =
$this->displaySection( $value,
"mw-htmlform-$key",
"$fieldsetIDPrefix$key-",
$subsectionHasVisibleFields );
$legend = null;
if ( $subsectionHasVisibleFields === true ) {
// Display the section with various niceties.
$hasUserVisibleFields = true;
$legend = $this->getLegend( $key );
$section = $this->getHeaderText( $key ) .
$section .
$this->getFooterText( $key );
$attributes = [];
if ( $fieldsetIDPrefix ) {
$attributes['id'] = Sanitizer::escapeIdForAttribute( "$fieldsetIDPrefix$key" );
}
$subsectionHtml .= $this->wrapFieldSetSection(
$legend, $section, $attributes, $fields === $this->mFieldTree
);
} else {
// Just return the inputs, nothing fancy.
$subsectionHtml .= $section;
}
}
}
$html = $this->formatSection( $html, $sectionName, $hasLabel );
if ( $subsectionHtml ) {
if ( $this->mSubSectionBeforeFields ) {
return $subsectionHtml . "\n" . $html;
} else {
return $html . "\n" . $subsectionHtml;
}
} else {
return $html;
}
}
/**
* Put a form section together from the individual fields' HTML, merging it and wrapping.
* @stable to override
* @param array $fieldsHtml
* @param string $sectionName
* @param bool $anyFieldHasLabel
* @return string HTML
*/
protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
if ( !$fieldsHtml ) {
// Do not generate any wrappers for empty sections. Sections may be empty if they only have
// subsections, but no fields. A legend will still be added in wrapFieldSetSection().
return '';
}
$displayFormat = $this->getDisplayFormat();
$html = implode( '', $fieldsHtml );
if ( $displayFormat === 'raw' ) {
return $html;
}
$classes = [];
if ( !$anyFieldHasLabel ) { // Avoid strange spacing when no labels exist
$classes[] = 'mw-htmlform-nolabel';
}
$attribs = [ 'class' => $classes ];
if ( $sectionName ) {
$attribs['id'] = Sanitizer::escapeIdForAttribute( $sectionName );
}
if ( $displayFormat === 'table' ) {
return Html::rawElement( 'table',
$attribs,
Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
} elseif ( $displayFormat === 'inline' ) {
return Html::rawElement( 'span', $attribs, "\n$html\n" );
} else {
return Html::rawElement( 'div', $attribs, "\n$html\n" );
}
}
/**
* Construct the form fields from the Descriptor array
*/
public function loadData() {
$fieldData = [];
foreach ( $this->mFlatFields as $fieldname => $field ) {
$request = $this->getRequest();
if ( $field->skipLoadData( $request ) ) {
continue;
} elseif ( !empty( $field->mParams['disabled'] ) ) {
$fieldData[$fieldname] = $field->getDefault();
} else {
$fieldData[$fieldname] = $field->loadDataFromRequest( $request );
}
}
# Filter data.
foreach ( $fieldData as $name => &$value ) {
$field = $this->mFlatFields[$name];
$value = $field->filter( $value, $this->mFlatFields );
}
$this->mFieldData = $fieldData;
}
/**
* Stop a reset button being shown for this form
*
* @param bool $suppressReset Set to false to re-enable the button again
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function suppressReset( $suppressReset = true ) {
$this->mShowReset = !$suppressReset;
return $this;
}
/**
* Overload this if you want to apply special filtration routines
* to the form as a whole, after it's submitted but before it's
* processed.
* @stable to override
*
* @param array $data
*
* @return array
*/
public function filterDataForSubmit( $data ) {
return $data;
}
/**
* Get a string to go in the "<legend>" of a section fieldset.
* Override this if you want something more complicated.
* @stable to override
*
* @param string $key
*
* @return string Plain text (not HTML-escaped)
*/
public function getLegend( $key ) {
return $this->msg( $this->mMessagePrefix ? "{$this->mMessagePrefix}-$key" : $key )->text();
}
/**
* Set the value for the action attribute of the form.
* When set to false (which is the default state), the set title is used.
*
* @since 1.19
*
* @param string|bool $action
*
* @return HTMLForm $this for chaining calls (since 1.20)
*/
public function setAction( $action ) {
$this->mAction = $action;
return $this;
}
/**
* Get the value for the action attribute of the form.
*
* @since 1.22
*
* @return string
*/
public function getAction() {
// If an action is alredy provided, return it
if ( $this->mAction !== false ) {
return $this->mAction;
}
$articlePath = $this->getConfig()->get( 'ArticlePath' );
// Check whether we are in GET mode and the ArticlePath contains a "?"
// meaning that getLocalURL() would return something like "index.php?title=...".
// As browser remove the query string before submitting GET forms,
// it means that the title would be lost. In such case use wfScript() instead
// and put title in an hidden field (see getHiddenFields()).
if ( strpos( $articlePath, '?' ) !== false && $this->getMethod() === 'get' ) {
return wfScript();
}
return $this->getTitle()->getLocalURL();
}
/**
* Set the value for the autocomplete attribute of the form. A typical value is "off".
* When set to null (which is the default state), the attribute get not set.
*
* @since 1.27
*
* @param string|null $autocomplete
*
* @return HTMLForm $this for chaining calls
*/
public function setAutocomplete( $autocomplete ) {
$this->mAutocomplete = $autocomplete;
return $this;
}
/**
* Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a
* name + parameters array) into a Message.
* @param mixed $value
* @return Message
*/
protected function getMessage( $value ) {
return Message::newFromSpecifier( $value )->setContext( $this );
}
/**
* Whether this form, with its current fields, requires the user agent to have JavaScript enabled
* for the client-side HTML5 form validation to work correctly. If this function returns true, a
* 'novalidate' attribute will be added on the `<form>` element. It will be removed if the user
* agent has JavaScript support, in htmlform.js.
*
* @return bool
* @since 1.29
*/
public function needsJSForHtml5FormValidation() {
foreach ( $this->mFlatFields as $fieldname => $field ) {
if ( $field->needsJSForHtml5FormValidation() ) {
return true;
}
}
return false;
}
}