27use InvalidArgumentException;
31use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
207 use ProtectedHookAccessorTrait;
211 'api' => HTMLApiField::class,
212 'text' => HTMLTextField::class,
213 'textwithbutton' => HTMLTextFieldWithButton::class,
214 'textarea' => HTMLTextAreaField::class,
215 'select' => HTMLSelectField::class,
216 'combobox' => HTMLComboboxField::class,
217 'radio' => HTMLRadioField::class,
218 'multiselect' => HTMLMultiSelectField::class,
219 'limitselect' => HTMLSelectLimitField::class,
220 'check' => HTMLCheckField::class,
221 'toggle' => HTMLCheckField::class,
222 'int' => HTMLIntField::class,
223 'file' => HTMLFileField::class,
224 'float' => HTMLFloatField::class,
225 'info' => HTMLInfoField::class,
226 'selectorother' => HTMLSelectOrOtherField::class,
227 'selectandother' => HTMLSelectAndOtherField::class,
228 'namespaceselect' => HTMLSelectNamespace::class,
229 'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
230 'tagfilter' => HTMLTagFilter::class,
231 'sizefilter' => HTMLSizeFilterField::class,
232 'submit' => HTMLSubmitField::class,
233 'hidden' => HTMLHiddenField::class,
234 'edittools' => HTMLEditTools::class,
235 'checkmatrix' => HTMLCheckMatrix::class,
236 'cloner' => HTMLFormFieldCloner::class,
237 'autocompleteselect' => HTMLAutoCompleteSelectField::class,
238 'language' => HTMLSelectLanguageField::class,
239 'date' => HTMLDateTimeField::class,
240 'time' => HTMLDateTimeField::class,
241 'datetime' => HTMLDateTimeField::class,
242 'expiry' => HTMLExpiryField::class,
243 'timezone' => HTMLTimezoneField::class,
247 'email' => HTMLTextField::class,
248 'password' => HTMLTextField::class,
249 'url' => HTMLTextField::class,
250 'title' => HTMLTitleTextField::class,
251 'user' => HTMLUserTextField::class,
252 'tagmultiselect' => HTMLTagMultiselectField::class,
253 'usersmultiselect' => HTMLUsersMultiselectField::class,
254 'titlesmultiselect' => HTMLTitlesMultiselectField::class,
255 'namespacesmultiselect' => HTMLNamespacesMultiselectField::class,
406 private $hiddenTitleAddedToForm =
false;
426 return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
428 return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
430 return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
432 $form =
new self( $descriptor, $context, $messagePrefix );
453 $this->mMessagePrefix = $messagePrefix;
467 $loadedDescriptor = [];
469 foreach ( $descriptor as $fieldname => $info ) {
470 $section = $info[
'section'] ??
'';
472 if ( isset( $info[
'type'] ) && $info[
'type'] ===
'file' ) {
473 $this->mUseMultipart =
true;
476 $field = static::loadInputFromParameters( $fieldname, $info, $this );
478 $setSection =& $loadedDescriptor;
480 foreach ( explode(
'/', $section ) as $newName ) {
481 $setSection[$newName] ??= [];
482 $setSection =& $setSection[$newName];
486 $setSection[$fieldname] = $field;
487 $this->mFlatFields[$fieldname] = $field;
490 $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
500 return isset( $this->mFlatFields[$fieldname] );
509 if ( !$this->
hasField( $fieldname ) ) {
510 throw new DomainException( __METHOD__ .
': no field named ' . $fieldname );
512 return $this->mFlatFields[$fieldname];
526 in_array( $format, $this->availableSubclassDisplayFormats,
true ) ||
527 in_array( $this->displayFormat, $this->availableSubclassDisplayFormats,
true )
529 throw new LogicException(
'Cannot change display format after creation, ' .
530 'use HTMLForm::factory() instead' );
533 if ( !in_array( $format, $this->availableDisplayFormats,
true ) ) {
534 throw new InvalidArgumentException(
'Display format must be one of ' .
537 $this->availableDisplayFormats,
538 $this->availableSubclassDisplayFormats
544 $this->displayFormat = $format;
575 if ( isset( $descriptor[
'class'] ) ) {
576 $class = $descriptor[
'class'];
577 } elseif ( isset( $descriptor[
'type'] ) ) {
578 $class = static::$typeMappings[$descriptor[
'type']];
579 $descriptor[
'class'] = $class;
585 throw new InvalidArgumentException(
"Descriptor with no class for $fieldname: "
586 . print_r( $descriptor,
true ) );
607 $class = static::getClassFromDescriptor( $fieldname, $descriptor );
609 $descriptor[
'fieldname'] = $fieldname;
611 $descriptor[
'parent'] = $parent;
614 # @todo This will throw a fatal error whenever someone try to use
615 # 'class' to feed a CSS class instead of 'cssclass'. Would be
616 # great to avoid the fatal error and show a nice error.
617 return new $class( $descriptor );
629 # Load data from the request.
631 $this->mFormIdentifier ===
null ||
632 $this->
getRequest()->getVal(
'wpFormIdentifier' ) === $this->mFormIdentifier ||
633 ( $this->mSingleForm && $this->
getMethod() ===
'get' )
637 $this->mFieldData = [];
650 if ( $this->mFormIdentifier ===
null ) {
659 } elseif ( $this->
getRequest()->wasPosted() ) {
660 $editToken = $this->
getRequest()->getVal(
'wpEditToken' );
661 if ( $this->
getUser()->isRegistered() || $editToken !==
null ) {
665 $tokenOkay = $this->
getUser()->matchEditToken( $editToken, $this->mTokenSalt );
671 if ( $tokenOkay && $identOkay ) {
672 $this->mWasSubmitted =
true;
690 if ( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) {
727 $hoistedErrors = Status::newGood();
728 if ( $this->mValidationErrorMessage ) {
729 foreach ( $this->mValidationErrorMessage as $error ) {
730 $hoistedErrors->fatal( ...$error );
733 $hoistedErrors->fatal(
'htmlform-invalid-input' );
736 $this->mWasSubmitted =
true;
738 # Check for cancelled submission
739 foreach ( $this->mFlatFields as $fieldname => $field ) {
740 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
743 if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
744 $this->mWasSubmitted =
false;
749 # Check for validation
750 $hasNonDefault =
false;
751 foreach ( $this->mFlatFields as $fieldname => $field ) {
752 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
755 $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
756 if ( $field->isDisabled( $this->mFieldData ) ) {
759 $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
760 if ( $res !==
true ) {
762 if ( $res !==
false && !$field->canDisplayErrors() ) {
763 if ( is_string( $res ) ) {
764 $hoistedErrors->fatal(
'rawmessage', $res );
766 $hoistedErrors->fatal( $res );
774 if ( !$hasNonDefault && $this->
getMethod() ===
'get' &&
775 ( $this->mFormIdentifier ===
null ||
776 $this->
getRequest()->getCheck(
'wpFormIdentifier' ) )
778 $this->mWasSubmitted =
false;
781 return $hoistedErrors;
785 if ( !is_callable( $callback ) ) {
786 throw new LogicException(
'HTMLForm: no submit callback provided. Use ' .
787 'setSubmitCallback() to set one.' );
792 $res = call_user_func( $callback, $data, $this );
793 if ( $res ===
false ) {
794 $this->mWasSubmitted =
false;
797 $res = Status::wrap( $res );
829 $this->mSubmitCallback = $cb;
844 $this->mValidationErrorMessage = $msg;
884 $this->mPre .= $html;
944 if ( $section ===
null ) {
945 $this->mHeader .= $html;
947 $this->mSectionHeaders[$section] ??=
'';
948 $this->mSectionHeaders[$section] .= $html;
964 if ( $section ===
null ) {
965 $this->mHeader = $html;
967 $this->mSectionHeaders[$section] = $html;
982 return $section ? $this->mSectionHeaders[$section] ??
'' :
$this->mHeader;
1035 if ( $section ===
null ) {
1036 $this->mFooter .= $html;
1038 $this->mSectionFooters[$section] ??=
'';
1039 $this->mSectionFooters[$section] .= $html;
1055 if ( $section ===
null ) {
1056 $this->mFooter = $html;
1058 $this->mSectionFooters[$section] = $html;
1072 return $section ? $this->mSectionFooters[$section] ??
'' :
$this->mFooter;
1123 $this->mPost .= $html;
1137 $this->mPost = $html;
1187 throw new \InvalidArgumentException(
1188 "Non-Codex HTMLForms do not support additional section information."
1192 $this->mSections = $sections;
1208 if ( !is_array( $value ) ) {
1210 $attribs += [
'name' => $name ];
1211 $this->mHiddenFields[] = [ $value, $attribs ];
1229 foreach ( $fields as $name => $value ) {
1230 if ( is_array( $value ) ) {
1234 $this->mHiddenFields[] = [ $value, [
'name' => $name ] ];
1264 if ( !is_array( $data ) ) {
1265 $args = func_get_args();
1266 if ( count( $args ) < 2 || count( $args ) > 4 ) {
1267 throw new InvalidArgumentException(
1268 'Incorrect number of arguments for deprecated calling style'
1273 'value' => $args[1],
1274 'id' => $args[2] ??
null,
1275 'attribs' => $args[3] ??
null,
1278 if ( !isset( $data[
'name'] ) ) {
1279 throw new InvalidArgumentException(
'A name is required' );
1281 if ( !isset( $data[
'value'] ) ) {
1282 throw new InvalidArgumentException(
'A value is required' );
1285 $this->mButtons[] = $data + [
1305 $this->mTokenSalt = $salt;
1332 private function getHiddenTitle(): string {
1333 if ( $this->hiddenTitleAddedToForm ) {
1341 $html .= Html::hidden(
'title', $this->
getTitle()->getPrefixedText() ) .
"\n";
1343 $this->hiddenTitleAddedToForm =
true;
1358 # For good measure (it is the default)
1359 $this->getOutput()->setPreventClickjacking(
true );
1360 $this->getOutput()->addModules(
'mediawiki.htmlform' );
1361 $this->getOutput()->addModuleStyles(
'mediawiki.htmlform.styles' );
1363 if ( $this->mCollapsible ) {
1365 $this->getOutput()->addModules(
'jquery.makeCollapsible' );
1368 $html = $this->getErrorsOrWarnings( $submitResult,
'error' )
1369 . $this->getErrorsOrWarnings( $submitResult,
'warning' )
1370 . $this->getHeaderText()
1371 . $this->getHiddenTitle()
1373 . $this->getHiddenFields()
1374 . $this->getButtons()
1375 . $this->getFooterText();
1377 return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1388 $this->mCollapsible =
true;
1389 $this->mCollapsed = $collapsedByDefault;
1399 # Use multipart/form-data
1400 $encType = $this->mUseMultipart
1401 ?
'multipart/form-data'
1402 :
'application/x-www-form-urlencoded';
1405 'class' =>
'mw-htmlform',
1406 'action' => $this->getAction(),
1407 'method' => $this->getMethod(),
1408 'enctype' => $encType,
1411 $attribs[
'id'] = $this->mId;
1413 if ( is_string( $this->mAutocomplete ) ) {
1414 $attribs[
'autocomplete'] = $this->mAutocomplete;
1416 if ( $this->mName ) {
1417 $attribs[
'name'] = $this->mName;
1419 if ( $this->needsJSForHtml5FormValidation() ) {
1420 $attribs[
'novalidate'] =
true;
1433 # Include a <fieldset> wrapper for style, if requested.
1434 if ( $this->mWrapperLegend !==
false ) {
1435 $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend :
false;
1436 $html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
1439 return Html::rawElement(
1441 $this->getFormAttributes(),
1455 $html .= $this->getHiddenTitle();
1457 if ( $this->mFormIdentifier !==
null ) {
1458 $html .= Html::hidden(
1460 $this->mFormIdentifier
1463 if ( $this->getMethod() ===
'post' ) {
1464 $html .= Html::hidden(
1466 $this->
getUser()->getEditToken( $this->mTokenSalt ),
1467 [
'id' =>
'wpEditToken' ]
1471 foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1472 $html .= Html::hidden( $attribs[
'name'], $value, $attribs ) .
"\n";
1486 if ( $this->mShowSubmit ) {
1489 if ( isset( $this->mSubmitID ) ) {
1490 $attribs[
'id'] = $this->mSubmitID;
1493 if ( isset( $this->mSubmitName ) ) {
1494 $attribs[
'name'] = $this->mSubmitName;
1497 if ( isset( $this->mSubmitTooltip ) ) {
1498 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1501 $attribs[
'class'] = [
'mw-htmlform-submit' ];
1503 $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) .
"\n";
1506 if ( $this->mShowCancel ) {
1507 $target = $this->getCancelTargetURL();
1513 $this->msg(
'cancel' )->text()
1517 foreach ( $this->mButtons as $button ) {
1520 'name' => $button[
'name'],
1521 'value' => $button[
'value']
1524 if ( isset( $button[
'label-message'] ) ) {
1525 $label = $this->getMessage( $button[
'label-message'] )->parse();
1526 } elseif ( isset( $button[
'label'] ) ) {
1527 $label = htmlspecialchars( $button[
'label'] );
1528 } elseif ( isset( $button[
'label-raw'] ) ) {
1529 $label = $button[
'label-raw'];
1531 $label = htmlspecialchars( $button[
'value'] );
1535 if ( $button[
'attribs'] ) {
1537 $attrs += $button[
'attribs'];
1540 if ( isset( $button[
'id'] ) ) {
1541 $attrs[
'id'] = $button[
'id'];
1544 $buttons .= Html::rawElement(
'button', $attrs, $label ) .
"\n";
1551 return Html::rawElement(
'span',
1552 [
'class' =>
'mw-htmlform-submit-buttons' ],
"\n$buttons" ) .
"\n";
1561 return $this->displaySection( $this->mFieldTree, $this->mTableId );
1574 if ( !in_array( $elementsType, [
'error',
'warning' ],
true ) ) {
1575 throw new DomainException( $elementsType .
' is not a valid type.' );
1577 $elementstr =
false;
1578 if ( $elements instanceof
Status ) {
1579 [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1580 $status = $elementsType ===
'error' ? $errorStatus : $warningStatus;
1581 if ( $status->isGood() ) {
1584 $elementstr = $status
1586 ->setContext( $this )
1587 ->setInterfaceMessageFlag(
true )
1590 } elseif ( $elementsType ===
'error' ) {
1591 if ( is_array( $elements ) ) {
1592 $elementstr = $this->formatErrors( $elements );
1593 } elseif ( $elements && $elements !==
true ) {
1594 $elementstr = (string)$elements;
1598 if ( !$elementstr ) {
1600 } elseif ( $elementsType ===
'error' ) {
1601 return Html::errorBox( $elementstr );
1603 return Html::warningBox( $elementstr );
1617 foreach ( $errors as $error ) {
1618 $errorstr .= Html::rawElement(
1621 $this->getMessage( $error )->parse()
1625 return Html::rawElement(
'ul', [], $errorstr );
1636 $this->mSubmitText = $t;
1648 $this->mSubmitFlags = [
'destructive',
'primary' ];
1662 if ( !$msg instanceof
Message ) {
1663 $msg = $this->msg( $msg );
1665 $this->setSubmitText( $msg->text() );
1675 return $this->mSubmitText ?: $this->msg(
'htmlform-submit' )->text();
1684 $this->mSubmitName = $name;
1695 $this->mSubmitTooltip = $name;
1709 $this->mSubmitID = $t;
1733 $this->mFormIdentifier = $ident;
1734 $this->mSingleForm = $single;
1750 $this->mShowSubmit = !$suppressSubmit;
1762 $this->mShowCancel = $show;
1774 $target = TitleValue::castPageToLinkTarget( $target );
1777 $this->mCancelTarget = $target;
1786 if ( is_string( $this->mCancelTarget ) ) {
1787 return $this->mCancelTarget;
1790 $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1791 return $target->getLocalURL();
1805 $this->mTableId = $id;
1826 $this->mName = $name;
1843 $this->mWrapperLegend = $legend;
1856 $this->mWrapperAttributes = $attributes;
1871 if ( !$msg instanceof
Message ) {
1872 $msg = $this->msg( $msg );
1874 $this->setWrapperLegend( $msg->text() );
1889 $this->mMessagePrefix = $p;
1903 $this->mTitle = Title::castFromPageReference( $t );
1912 return $this->mTitle ?: $this->
getContext()->getTitle();
1923 $this->mMethod = strtolower( $method );
1932 return $this->mMethod;
1946 return Xml::fieldset( $legend, $section, $attributes ) .
"\n";
1968 $fieldsetIDPrefix =
'',
1969 &$hasUserVisibleFields =
false
1971 if ( $this->mFieldData ===
null ) {
1972 throw new LogicException(
'HTMLForm::displaySection() called on uninitialized field data. '
1973 .
'You probably called displayForm() without calling prepareForm() first.' );
1977 $subsectionHtml =
'';
1980 foreach ( $fields as $key => $value ) {
1982 $v = array_key_exists( $key, $this->mFieldData )
1983 ? $this->mFieldData[$key]
1984 : $value->getDefault();
1986 $retval = $this->formatField( $value, $v ??
'' );
1990 if ( $value->hasVisibleOutput() ) {
1993 $labelValue = trim( $value->getLabel() );
1994 if ( $labelValue !==
"\u{00A0}" && $labelValue !==
' ' && $labelValue !==
'' ) {
1998 $hasUserVisibleFields =
true;
2000 } elseif ( is_array( $value ) ) {
2001 $subsectionHasVisibleFields =
false;
2003 $this->displaySection( $value,
2005 "$fieldsetIDPrefix$key-",
2006 $subsectionHasVisibleFields );
2008 if ( $subsectionHasVisibleFields ===
true ) {
2010 $hasUserVisibleFields =
true;
2012 $legend = $this->getLegend( $key );
2014 $section = $this->getHeaderText( $key ) .
2016 $this->getFooterText( $key );
2019 if ( $fieldsetIDPrefix ) {
2020 $attributes[
'id'] = Sanitizer::escapeIdForAttribute(
"$fieldsetIDPrefix$key" );
2022 $subsectionHtml .= $this->wrapFieldSetSection(
2023 $legend, $section, $attributes, $fields === $this->mFieldTree
2027 $subsectionHtml .= $section;
2032 $html = $this->formatSection( $html, $sectionName, $hasLabel );
2034 if ( $subsectionHtml ) {
2035 if ( $this->mSubSectionBeforeFields ) {
2036 return $subsectionHtml .
"\n" . $html;
2038 return $html .
"\n" . $subsectionHtml;
2054 $displayFormat = $this->getDisplayFormat();
2055 switch ( $displayFormat ) {
2059 return $field->
getDiv( $value );
2061 return $field->
getRaw( $value );
2065 throw new LogicException(
'Not implemented' );
2077 protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
2078 if ( !$fieldsHtml ) {
2084 $displayFormat = $this->getDisplayFormat();
2085 $html = implode(
'', $fieldsHtml );
2087 if ( $displayFormat ===
'raw' ) {
2092 $attribs = $anyFieldHasLabel ? [] : [
'class' =>
'mw-htmlform-nolabel' ];
2094 if ( $sectionName ) {
2095 $attribs[
'id'] = Sanitizer::escapeIdForAttribute( $sectionName );
2098 if ( $displayFormat ===
'table' ) {
2099 return Html::rawElement(
'table',
2101 Html::rawElement(
'tbody', [],
"\n$html\n" ) ) .
"\n";
2102 } elseif ( $displayFormat ===
'inline' ) {
2103 return Html::rawElement(
'span', $attribs,
"\n$html\n" );
2105 return Html::rawElement(
'div', $attribs,
"\n$html\n" );
2113 $this->prepareForm();
2123 foreach ( $this->mFlatFields as $fieldname => $field ) {
2124 if ( $field->skipLoadData( $request ) ) {
2127 if ( $field->mParams[
'disabled'] ??
false ) {
2128 $fieldData[$fieldname] = $field->getDefault();
2130 $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2136 foreach ( $fieldData as $name => &$value ) {
2137 $field = $this->mFlatFields[$name];
2138 if ( $field->isDisabled( $fieldData ) ) {
2139 $value = $field->getDefault();
2144 foreach ( $fieldData as $name => &$value ) {
2145 $field = $this->mFlatFields[$name];
2146 $value = $field->filter( $value, $fieldData );
2149 $this->mFieldData = $fieldData;
2176 return $this->msg( $this->mMessagePrefix ?
"{$this->mMessagePrefix}-$key" : $key )->text();
2190 $this->mAction = $action;
2204 if ( $this->mAction !==
false ) {
2205 return $this->mAction;
2214 if ( str_contains( $articlePath,
'?' ) && $this->getMethod() ===
'get' ) {
2218 return $this->
getTitle()->getLocalURL();
2232 $this->mAutocomplete = $autocomplete;
2257 foreach ( $this->mFlatFields as $field ) {
2258 if ( $field->needsJSForHtml5FormValidation() ) {
2267class_alias( HTMLForm::class,
'HTMLForm' );
if(!defined('MW_SETUP_CALLBACK'))
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
setContext(IContextSource $context)
Creates a text input field with a button assigned to the input field.
Implements a text input field for page titles.
Implements a text input field for user names.
A class containing constants representing the names of configuration variables.
const ArticlePath
Name constant for the ArticlePath setting, for use with Config::get()
const Script
Name constant for the Script setting, for use with Config::get()
Generic operation result class Has warning/error list, boolean status and arbitrary value.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Module of static functions for generating XML.
Interface for objects which can provide a MediaWiki context on request.