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,
393 private $hiddenTitleAddedToForm =
false;
413 return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
415 return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
417 return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
419 $form =
new self( $descriptor, $context, $messagePrefix );
440 $this->mMessagePrefix = $messagePrefix;
445 && $this->displayFormat ===
'table'
447 $this->displayFormat =
'div';
463 $loadedDescriptor = [];
465 foreach ( $descriptor as $fieldname => $info ) {
466 $section = $info[
'section'] ??
'';
468 if ( isset( $info[
'type'] ) && $info[
'type'] ===
'file' ) {
469 $this->mUseMultipart =
true;
472 $field = static::loadInputFromParameters( $fieldname, $info, $this );
474 $setSection =& $loadedDescriptor;
476 foreach ( explode(
'/', $section ) as $newName ) {
477 $setSection[$newName] ??= [];
478 $setSection =& $setSection[$newName];
482 $setSection[$fieldname] = $field;
483 $this->mFlatFields[$fieldname] = $field;
486 $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
496 return isset( $this->mFlatFields[$fieldname] );
505 if ( !$this->
hasField( $fieldname ) ) {
506 throw new DomainException( __METHOD__ .
': no field named ' . $fieldname );
508 return $this->mFlatFields[$fieldname];
522 in_array( $format, $this->availableSubclassDisplayFormats,
true ) ||
523 in_array( $this->displayFormat, $this->availableSubclassDisplayFormats,
true )
525 throw new LogicException(
'Cannot change display format after creation, ' .
526 'use HTMLForm::factory() instead' );
529 if ( !in_array( $format, $this->availableDisplayFormats,
true ) ) {
530 throw new InvalidArgumentException(
'Display format must be one of ' .
533 $this->availableDisplayFormats,
534 $this->availableSubclassDisplayFormats
542 $format ===
'table' ) {
546 $this->displayFormat = $format;
577 if ( isset( $descriptor[
'class'] ) ) {
578 $class = $descriptor[
'class'];
579 } elseif ( isset( $descriptor[
'type'] ) ) {
580 $class = static::$typeMappings[$descriptor[
'type']];
581 $descriptor[
'class'] = $class;
587 throw new InvalidArgumentException(
"Descriptor with no class for $fieldname: "
588 . print_r( $descriptor,
true ) );
609 $class = static::getClassFromDescriptor( $fieldname, $descriptor );
611 $descriptor[
'fieldname'] = $fieldname;
613 $descriptor[
'parent'] = $parent;
616 # @todo This will throw a fatal error whenever someone try to use
617 # 'class' to feed a CSS class instead of 'cssclass'. Would be
618 # great to avoid the fatal error and show a nice error.
619 return new $class( $descriptor );
631 # Load data from the request.
633 $this->mFormIdentifier ===
null ||
634 $this->
getRequest()->getVal(
'wpFormIdentifier' ) === $this->mFormIdentifier ||
635 $this->mSingleForm && $this->
getMethod() ===
'get'
639 $this->mFieldData = [];
652 if ( $this->mFormIdentifier ===
null ) {
661 } elseif ( $this->
getRequest()->wasPosted() ) {
662 $editToken = $this->
getRequest()->getVal(
'wpEditToken' );
663 if ( $this->
getUser()->isRegistered() || $editToken !==
null ) {
667 $tokenOkay = $this->
getUser()->matchEditToken( $editToken, $this->mTokenSalt );
673 if ( $tokenOkay && $identOkay ) {
674 $this->mWasSubmitted =
true;
692 if ( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) {
729 $hoistedErrors = Status::newGood();
730 if ( $this->mValidationErrorMessage ) {
731 foreach ( $this->mValidationErrorMessage as $error ) {
732 $hoistedErrors->fatal( ...$error );
735 $hoistedErrors->fatal(
'htmlform-invalid-input' );
738 $this->mWasSubmitted =
true;
740 # Check for cancelled submission
741 foreach ( $this->mFlatFields as $fieldname => $field ) {
742 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
745 if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
746 $this->mWasSubmitted =
false;
751 # Check for validation
752 $hasNonDefault =
false;
753 foreach ( $this->mFlatFields as $fieldname => $field ) {
754 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
757 $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
758 if ( $field->isDisabled( $this->mFieldData ) ) {
761 $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
762 if ( $res !==
true ) {
764 if ( $res !==
false && !$field->canDisplayErrors() ) {
765 if ( is_string( $res ) ) {
766 $hoistedErrors->fatal(
'rawmessage', $res );
768 $hoistedErrors->fatal( $res );
776 if ( !$hasNonDefault && $this->
getMethod() ===
'get' &&
777 ( $this->mFormIdentifier ===
null ||
778 $this->
getRequest()->getCheck(
'wpFormIdentifier' ) )
780 $this->mWasSubmitted =
false;
783 return $hoistedErrors;
787 if ( !is_callable( $callback ) ) {
788 throw new LogicException(
'HTMLForm: no submit callback provided. Use ' .
789 'setSubmitCallback() to set one.' );
794 $res = call_user_func( $callback, $data, $this );
795 if ( $res ===
false ) {
796 $this->mWasSubmitted =
false;
799 $res = Status::wrap( $res );
831 $this->mSubmitCallback = $cb;
846 $this->mValidationErrorMessage = $msg;
886 $this->mPre .= $html;
946 if ( $section ===
null ) {
947 $this->mHeader .= $html;
949 $this->mSectionHeaders[$section] ??=
'';
950 $this->mSectionHeaders[$section] .= $html;
966 if ( $section ===
null ) {
967 $this->mHeader = $html;
969 $this->mSectionHeaders[$section] = $html;
984 return $section ? $this->mSectionHeaders[$section] ??
'' :
$this->mHeader;
1037 if ( $section ===
null ) {
1038 $this->mFooter .= $html;
1040 $this->mSectionFooters[$section] ??=
'';
1041 $this->mSectionFooters[$section] .= $html;
1057 if ( $section ===
null ) {
1058 $this->mFooter = $html;
1060 $this->mSectionFooters[$section] = $html;
1074 return $section ? $this->mSectionFooters[$section] ??
'' :
$this->mFooter;
1125 $this->mPost .= $html;
1139 $this->mPost = $html;
1189 if ( !is_array( $value ) ) {
1191 $attribs += [
'name' => $name ];
1192 $this->mHiddenFields[] = [ $value, $attribs ];
1210 foreach ( $fields as $name => $value ) {
1211 if ( is_array( $value ) ) {
1215 $this->mHiddenFields[] = [ $value, [
'name' => $name ] ];
1245 if ( !is_array( $data ) ) {
1246 $args = func_get_args();
1247 if ( count( $args ) < 2 || count( $args ) > 4 ) {
1248 throw new InvalidArgumentException(
1249 'Incorrect number of arguments for deprecated calling style'
1254 'value' => $args[1],
1255 'id' => $args[2] ??
null,
1256 'attribs' => $args[3] ??
null,
1259 if ( !isset( $data[
'name'] ) ) {
1260 throw new InvalidArgumentException(
'A name is required' );
1262 if ( !isset( $data[
'value'] ) ) {
1263 throw new InvalidArgumentException(
'A value is required' );
1266 $this->mButtons[] = $data + [
1286 $this->mTokenSalt = $salt;
1313 private function getHiddenTitle(): string {
1314 if ( $this->hiddenTitleAddedToForm ) {
1322 $html .= Html::hidden(
'title', $this->
getTitle()->getPrefixedText() ) .
"\n";
1324 $this->hiddenTitleAddedToForm =
true;
1339 # For good measure (it is the default)
1340 $this->getOutput()->setPreventClickjacking(
true );
1341 $this->getOutput()->addModules(
'mediawiki.htmlform' );
1342 $this->getOutput()->addModuleStyles(
'mediawiki.htmlform.styles' );
1344 if ( $this->mCollapsible ) {
1346 $this->getOutput()->addModules(
'jquery.makeCollapsible' );
1349 $html = $this->getErrorsOrWarnings( $submitResult,
'error' )
1350 . $this->getErrorsOrWarnings( $submitResult,
'warning' )
1351 . $this->getHeaderText()
1352 . $this->getHiddenTitle()
1354 . $this->getHiddenFields()
1355 . $this->getButtons()
1356 . $this->getFooterText();
1358 return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1369 $this->mCollapsible =
true;
1370 $this->mCollapsed = $collapsedByDefault;
1380 # Use multipart/form-data
1381 $encType = $this->mUseMultipart
1382 ?
'multipart/form-data'
1383 :
'application/x-www-form-urlencoded';
1386 'class' =>
'mw-htmlform',
1387 'action' => $this->getAction(),
1388 'method' => $this->getMethod(),
1389 'enctype' => $encType,
1392 $attribs[
'id'] = $this->mId;
1394 if ( is_string( $this->mAutocomplete ) ) {
1395 $attribs[
'autocomplete'] = $this->mAutocomplete;
1397 if ( $this->mName ) {
1398 $attribs[
'name'] = $this->mName;
1400 if ( $this->needsJSForHtml5FormValidation() ) {
1401 $attribs[
'novalidate'] =
true;
1414 # Include a <fieldset> wrapper for style, if requested.
1415 if ( $this->mWrapperLegend !==
false ) {
1416 $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend :
false;
1417 $html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
1420 return Html::rawElement(
1422 $this->getFormAttributes(),
1436 $html .= $this->getHiddenTitle();
1438 if ( $this->mFormIdentifier !==
null ) {
1439 $html .= Html::hidden(
1441 $this->mFormIdentifier
1444 if ( $this->getMethod() ===
'post' ) {
1445 $html .= Html::hidden(
1447 $this->
getUser()->getEditToken( $this->mTokenSalt ),
1448 [
'id' =>
'wpEditToken' ]
1452 foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1453 $html .= Html::hidden( $attribs[
'name'], $value, $attribs ) .
"\n";
1467 if ( $this->mShowSubmit ) {
1470 if ( isset( $this->mSubmitID ) ) {
1471 $attribs[
'id'] = $this->mSubmitID;
1474 if ( isset( $this->mSubmitName ) ) {
1475 $attribs[
'name'] = $this->mSubmitName;
1478 if ( isset( $this->mSubmitTooltip ) ) {
1479 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1482 $attribs[
'class'] = [
'mw-htmlform-submit' ];
1484 $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) .
"\n";
1487 if ( $this->mShowReset ) {
1492 'value' => $this->msg(
'htmlform-reset' )->text(),
1497 if ( $this->mShowCancel ) {
1498 $target = $this->getCancelTargetURL();
1504 $this->msg(
'cancel' )->text()
1508 foreach ( $this->mButtons as $button ) {
1511 'name' => $button[
'name'],
1512 'value' => $button[
'value']
1515 if ( isset( $button[
'label-message'] ) ) {
1516 $label = $this->getMessage( $button[
'label-message'] )->parse();
1517 } elseif ( isset( $button[
'label'] ) ) {
1518 $label = htmlspecialchars( $button[
'label'] );
1519 } elseif ( isset( $button[
'label-raw'] ) ) {
1520 $label = $button[
'label-raw'];
1522 $label = htmlspecialchars( $button[
'value'] );
1526 if ( $button[
'attribs'] ) {
1528 $attrs += $button[
'attribs'];
1531 if ( isset( $button[
'id'] ) ) {
1532 $attrs[
'id'] = $button[
'id'];
1535 $buttons .= Html::rawElement(
'button', $attrs, $label ) .
"\n";
1542 return Html::rawElement(
'span',
1543 [
'class' =>
'mw-htmlform-submit-buttons' ],
"\n$buttons" ) .
"\n";
1552 return $this->displaySection( $this->mFieldTree, $this->mTableId );
1565 if ( !in_array( $elementsType, [
'error',
'warning' ],
true ) ) {
1566 throw new DomainException( $elementsType .
' is not a valid type.' );
1568 $elementstr =
false;
1569 if ( $elements instanceof
Status ) {
1570 [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1571 $status = $elementsType ===
'error' ? $errorStatus : $warningStatus;
1572 if ( $status->isGood() ) {
1575 $elementstr = $status
1577 ->setContext( $this )
1578 ->setInterfaceMessageFlag(
true )
1581 } elseif ( $elementsType ===
'error' ) {
1582 if ( is_array( $elements ) ) {
1583 $elementstr = $this->formatErrors( $elements );
1584 } elseif ( $elements && $elements !==
true ) {
1585 $elementstr = (string)$elements;
1589 if ( !$elementstr ) {
1591 } elseif ( $elementsType ===
'error' ) {
1592 return Html::errorBox( $elementstr );
1594 return Html::warningBox( $elementstr );
1608 foreach ( $errors as $error ) {
1609 $errorstr .= Html::rawElement(
1612 $this->getMessage( $error )->parse()
1616 return Html::rawElement(
'ul', [], $errorstr );
1627 $this->mSubmitText = $t;
1639 $this->mSubmitFlags = [
'destructive',
'primary' ];
1653 if ( !$msg instanceof
Message ) {
1654 $msg = $this->msg( $msg );
1656 $this->setSubmitText( $msg->text() );
1666 return $this->mSubmitText ?: $this->msg(
'htmlform-submit' )->text();
1675 $this->mSubmitName = $name;
1686 $this->mSubmitTooltip = $name;
1700 $this->mSubmitID = $t;
1724 $this->mFormIdentifier = $ident;
1725 $this->mSingleForm = $single;
1741 $this->mShowSubmit = !$suppressSubmit;
1753 $this->mShowCancel = $show;
1765 $target = TitleValue::castPageToLinkTarget( $target );
1768 $this->mCancelTarget = $target;
1777 if ( is_string( $this->mCancelTarget ) ) {
1778 return $this->mCancelTarget;
1781 $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1782 return $target->getLocalURL();
1796 $this->mTableId = $id;
1817 $this->mName = $name;
1834 $this->mWrapperLegend = $legend;
1847 $this->mWrapperAttributes = $attributes;
1862 if ( !$msg instanceof
Message ) {
1863 $msg = $this->msg( $msg );
1865 $this->setWrapperLegend( $msg->text() );
1880 $this->mMessagePrefix = $p;
1894 $this->mTitle = Title::castFromPageReference( $t );
1903 return $this->mTitle ?: $this->
getContext()->getTitle();
1914 $this->mMethod = strtolower( $method );
1923 return $this->mMethod;
1937 return Xml::fieldset( $legend, $section, $attributes ) .
"\n";
1959 $fieldsetIDPrefix =
'',
1960 &$hasUserVisibleFields =
false
1962 if ( $this->mFieldData ===
null ) {
1963 throw new LogicException(
'HTMLForm::displaySection() called on uninitialized field data. '
1964 .
'You probably called displayForm() without calling prepareForm() first.' );
1968 $subsectionHtml =
'';
1971 foreach ( $fields as $key => $value ) {
1973 $v = array_key_exists( $key, $this->mFieldData )
1974 ? $this->mFieldData[$key]
1975 : $value->getDefault();
1977 $retval = $this->formatField( $value, $v ??
'' );
1981 if ( $value->hasVisibleOutput() ) {
1984 $labelValue = trim( $value->getLabel() );
1985 if ( $labelValue !==
"\u{00A0}" && $labelValue !==
' ' && $labelValue !==
'' ) {
1989 $hasUserVisibleFields =
true;
1991 } elseif ( is_array( $value ) ) {
1992 $subsectionHasVisibleFields =
false;
1994 $this->displaySection( $value,
1996 "$fieldsetIDPrefix$key-",
1997 $subsectionHasVisibleFields );
1999 if ( $subsectionHasVisibleFields ===
true ) {
2001 $hasUserVisibleFields =
true;
2003 $legend = $this->getLegend( $key );
2005 $section = $this->getHeaderText( $key ) .
2007 $this->getFooterText( $key );
2010 if ( $fieldsetIDPrefix ) {
2011 $attributes[
'id'] = Sanitizer::escapeIdForAttribute(
"$fieldsetIDPrefix$key" );
2013 $subsectionHtml .= $this->wrapFieldSetSection(
2014 $legend, $section, $attributes, $fields === $this->mFieldTree
2018 $subsectionHtml .= $section;
2023 $html = $this->formatSection( $html, $sectionName, $hasLabel );
2025 if ( $subsectionHtml ) {
2026 if ( $this->mSubSectionBeforeFields ) {
2027 return $subsectionHtml .
"\n" . $html;
2029 return $html .
"\n" . $subsectionHtml;
2045 $displayFormat = $this->getDisplayFormat();
2046 switch ( $displayFormat ) {
2050 return $field->
getDiv( $value );
2052 return $field->
getRaw( $value );
2056 throw new LogicException(
'Not implemented' );
2068 protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
2069 if ( !$fieldsHtml ) {
2075 $displayFormat = $this->getDisplayFormat();
2076 $html = implode(
'', $fieldsHtml );
2078 if ( $displayFormat ===
'raw' ) {
2083 $attribs = $anyFieldHasLabel ? [] : [
'class' =>
'mw-htmlform-nolabel' ];
2085 if ( $sectionName ) {
2086 $attribs[
'id'] = Sanitizer::escapeIdForAttribute( $sectionName );
2089 if ( $displayFormat ===
'table' ) {
2090 return Html::rawElement(
'table',
2092 Html::rawElement(
'tbody', [],
"\n$html\n" ) ) .
"\n";
2093 } elseif ( $displayFormat ===
'inline' ) {
2094 return Html::rawElement(
'span', $attribs,
"\n$html\n" );
2096 return Html::rawElement(
'div', $attribs,
"\n$html\n" );
2104 $this->prepareForm();
2114 foreach ( $this->mFlatFields as $fieldname => $field ) {
2115 if ( $field->skipLoadData( $request ) ) {
2118 if ( $field->mParams[
'disabled'] ??
false ) {
2119 $fieldData[$fieldname] = $field->getDefault();
2121 $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2127 foreach ( $fieldData as $name => &$value ) {
2128 $field = $this->mFlatFields[$name];
2129 if ( $field->isDisabled( $fieldData ) ) {
2130 $value = $field->getDefault();
2135 foreach ( $fieldData as $name => &$value ) {
2136 $field = $this->mFlatFields[$name];
2137 $value = $field->filter( $value, $fieldData );
2140 $this->mFieldData = $fieldData;
2151 $this->mShowReset = !$suppressReset;
2180 return $this->msg( $this->mMessagePrefix ?
"{$this->mMessagePrefix}-$key" : $key )->text();
2194 $this->mAction = $action;
2208 if ( $this->mAction !==
false ) {
2209 return $this->mAction;
2218 if ( str_contains( $articlePath,
'?' ) && $this->getMethod() ===
'get' ) {
2222 return $this->
getTitle()->getLocalURL();
2236 $this->mAutocomplete = $autocomplete;
2261 foreach ( $this->mFlatFields as $field ) {
2262 if ( $field->needsJSForHtml5FormValidation() ) {
2271class_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 HTMLFormAllowTableFormat
Name constant for the HTMLFormAllowTableFormat 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.