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,
407 private $hiddenTitleAddedToForm =
false;
427 return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
429 return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
431 return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
433 $form =
new self( $descriptor, $context, $messagePrefix );
454 $this->mMessagePrefix = $messagePrefix;
468 $loadedDescriptor = [];
470 foreach ( $descriptor as $fieldname => $info ) {
471 $section = $info[
'section'] ??
'';
473 if ( isset( $info[
'type'] ) && $info[
'type'] ===
'file' ) {
474 $this->mUseMultipart =
true;
477 $field = static::loadInputFromParameters( $fieldname, $info, $this );
479 $setSection =& $loadedDescriptor;
481 foreach ( explode(
'/', $section ) as $newName ) {
482 $setSection[$newName] ??= [];
483 $setSection =& $setSection[$newName];
487 $setSection[$fieldname] = $field;
488 $this->mFlatFields[$fieldname] = $field;
491 $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
501 return isset( $this->mFlatFields[$fieldname] );
510 if ( !$this->
hasField( $fieldname ) ) {
511 throw new DomainException( __METHOD__ .
': no field named ' . $fieldname );
513 return $this->mFlatFields[$fieldname];
527 in_array( $format, $this->availableSubclassDisplayFormats,
true ) ||
528 in_array( $this->displayFormat, $this->availableSubclassDisplayFormats,
true )
530 throw new LogicException(
'Cannot change display format after creation, ' .
531 'use HTMLForm::factory() instead' );
534 if ( !in_array( $format, $this->availableDisplayFormats,
true ) ) {
535 throw new InvalidArgumentException(
'Display format must be one of ' .
538 $this->availableDisplayFormats,
539 $this->availableSubclassDisplayFormats
545 $this->displayFormat = $format;
576 if ( isset( $descriptor[
'class'] ) ) {
577 $class = $descriptor[
'class'];
578 } elseif ( isset( $descriptor[
'type'] ) ) {
579 $class = static::$typeMappings[$descriptor[
'type']];
580 $descriptor[
'class'] = $class;
586 throw new InvalidArgumentException(
"Descriptor with no class for $fieldname: "
587 . print_r( $descriptor,
true ) );
608 $class = static::getClassFromDescriptor( $fieldname, $descriptor );
610 $descriptor[
'fieldname'] = $fieldname;
612 $descriptor[
'parent'] = $parent;
615 # @todo This will throw a fatal error whenever someone try to use
616 # 'class' to feed a CSS class instead of 'cssclass'. Would be
617 # great to avoid the fatal error and show a nice error.
618 return new $class( $descriptor );
630 # Load data from the request.
632 $this->mFormIdentifier ===
null ||
633 $this->
getRequest()->getVal(
'wpFormIdentifier' ) === $this->mFormIdentifier ||
634 ( $this->mSingleForm && $this->
getMethod() ===
'get' )
638 $this->mFieldData = [];
651 if ( $this->mFormIdentifier ===
null ) {
660 } elseif ( $this->
getRequest()->wasPosted() ) {
661 $editToken = $this->
getRequest()->getVal(
'wpEditToken' );
662 if ( $this->
getUser()->isRegistered() || $editToken !==
null ) {
666 $tokenOkay = $this->
getUser()->matchEditToken( $editToken, $this->mTokenSalt );
672 if ( $tokenOkay && $identOkay ) {
673 $this->mWasSubmitted =
true;
691 if ( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) {
728 $hoistedErrors = Status::newGood();
729 if ( $this->mValidationErrorMessage ) {
730 foreach ( $this->mValidationErrorMessage as $error ) {
731 $hoistedErrors->fatal( ...$error );
734 $hoistedErrors->fatal(
'htmlform-invalid-input' );
737 $this->mWasSubmitted =
true;
739 # Check for cancelled submission
740 foreach ( $this->mFlatFields as $fieldname => $field ) {
741 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
744 if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
745 $this->mWasSubmitted =
false;
750 # Check for validation
751 $hasNonDefault =
false;
752 foreach ( $this->mFlatFields as $fieldname => $field ) {
753 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
756 $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
757 if ( $field->isDisabled( $this->mFieldData ) ) {
760 $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
761 if ( $res !==
true ) {
763 if ( $res !==
false && !$field->canDisplayErrors() ) {
764 if ( is_string( $res ) ) {
765 $hoistedErrors->fatal(
'rawmessage', $res );
767 $hoistedErrors->fatal( $res );
775 if ( !$hasNonDefault && $this->
getMethod() ===
'get' &&
776 ( $this->mFormIdentifier ===
null ||
777 $this->
getRequest()->getCheck(
'wpFormIdentifier' ) )
779 $this->mWasSubmitted =
false;
782 return $hoistedErrors;
786 if ( !is_callable( $callback ) ) {
787 throw new LogicException(
'HTMLForm: no submit callback provided. Use ' .
788 'setSubmitCallback() to set one.' );
793 $res = call_user_func( $callback, $data, $this );
794 if ( $res ===
false ) {
795 $this->mWasSubmitted =
false;
798 $res = Status::wrap( $res );
830 $this->mSubmitCallback = $cb;
845 $this->mValidationErrorMessage = $msg;
885 $this->mPre .= $html;
945 if ( $section ===
null ) {
946 $this->mHeader .= $html;
948 $this->mSectionHeaders[$section] ??=
'';
949 $this->mSectionHeaders[$section] .= $html;
965 if ( $section ===
null ) {
966 $this->mHeader = $html;
968 $this->mSectionHeaders[$section] = $html;
983 return $section ? $this->mSectionHeaders[$section] ??
'' :
$this->mHeader;
1036 if ( $section ===
null ) {
1037 $this->mFooter .= $html;
1039 $this->mSectionFooters[$section] ??=
'';
1040 $this->mSectionFooters[$section] .= $html;
1056 if ( $section ===
null ) {
1057 $this->mFooter = $html;
1059 $this->mSectionFooters[$section] = $html;
1073 return $section ? $this->mSectionFooters[$section] ??
'' :
$this->mFooter;
1124 $this->mPost .= $html;
1138 $this->mPost = $html;
1188 throw new \InvalidArgumentException(
1189 "Non-Codex HTMLForms do not support additional section information."
1193 $this->mSections = $sections;
1209 if ( !is_array( $value ) ) {
1211 $attribs += [
'name' => $name ];
1212 $this->mHiddenFields[] = [ $value, $attribs ];
1230 foreach ( $fields as $name => $value ) {
1231 if ( is_array( $value ) ) {
1235 $this->mHiddenFields[] = [ $value, [
'name' => $name ] ];
1265 if ( !is_array( $data ) ) {
1266 $args = func_get_args();
1267 if ( count( $args ) < 2 || count( $args ) > 4 ) {
1268 throw new InvalidArgumentException(
1269 'Incorrect number of arguments for deprecated calling style'
1274 'value' => $args[1],
1275 'id' => $args[2] ??
null,
1276 'attribs' => $args[3] ??
null,
1279 if ( !isset( $data[
'name'] ) ) {
1280 throw new InvalidArgumentException(
'A name is required' );
1282 if ( !isset( $data[
'value'] ) ) {
1283 throw new InvalidArgumentException(
'A value is required' );
1286 $this->mButtons[] = $data + [
1306 $this->mTokenSalt = $salt;
1333 private function getHiddenTitle(): string {
1334 if ( $this->hiddenTitleAddedToForm ) {
1342 $html .= Html::hidden(
'title', $this->
getTitle()->getPrefixedText() ) .
"\n";
1344 $this->hiddenTitleAddedToForm =
true;
1359 # For good measure (it is the default)
1360 $this->getOutput()->setPreventClickjacking(
true );
1361 $this->getOutput()->addModules(
'mediawiki.htmlform' );
1362 $this->getOutput()->addModuleStyles(
'mediawiki.htmlform.styles' );
1364 if ( $this->mCollapsible ) {
1366 $this->getOutput()->addModules(
'jquery.makeCollapsible' );
1369 $html = $this->getErrorsOrWarnings( $submitResult,
'error' )
1370 . $this->getErrorsOrWarnings( $submitResult,
'warning' )
1371 . $this->getHeaderText()
1372 . $this->getHiddenTitle()
1374 . $this->getHiddenFields()
1375 . $this->getButtons()
1376 . $this->getFooterText();
1378 return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1389 $this->mCollapsible =
true;
1390 $this->mCollapsed = $collapsedByDefault;
1400 # Use multipart/form-data
1401 $encType = $this->mUseMultipart
1402 ?
'multipart/form-data'
1403 :
'application/x-www-form-urlencoded';
1406 'class' =>
'mw-htmlform',
1407 'action' => $this->getAction(),
1408 'method' => $this->getMethod(),
1409 'enctype' => $encType,
1412 $attribs[
'id'] = $this->mId;
1414 if ( is_string( $this->mAutocomplete ) ) {
1415 $attribs[
'autocomplete'] = $this->mAutocomplete;
1417 if ( $this->mName ) {
1418 $attribs[
'name'] = $this->mName;
1420 if ( $this->needsJSForHtml5FormValidation() ) {
1421 $attribs[
'novalidate'] =
true;
1434 # Include a <fieldset> wrapper for style, if requested.
1435 if ( $this->mWrapperLegend !==
false ) {
1436 $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend :
false;
1437 $html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
1440 return Html::rawElement(
1442 $this->getFormAttributes(),
1456 $html .= $this->getHiddenTitle();
1458 if ( $this->mFormIdentifier !==
null ) {
1459 $html .= Html::hidden(
1461 $this->mFormIdentifier
1464 if ( $this->getMethod() ===
'post' ) {
1465 $html .= Html::hidden(
1467 $this->
getUser()->getEditToken( $this->mTokenSalt ),
1468 [
'id' =>
'wpEditToken' ]
1472 foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1473 $html .= Html::hidden( $attribs[
'name'], $value, $attribs ) .
"\n";
1487 if ( $this->mShowSubmit ) {
1490 if ( isset( $this->mSubmitID ) ) {
1491 $attribs[
'id'] = $this->mSubmitID;
1494 if ( isset( $this->mSubmitName ) ) {
1495 $attribs[
'name'] = $this->mSubmitName;
1498 if ( isset( $this->mSubmitTooltip ) ) {
1499 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1502 $attribs[
'class'] = [
'mw-htmlform-submit' ];
1504 $buttons .= Xml::submitButton( $this->getSubmitText(), $attribs ) .
"\n";
1507 if ( $this->mShowReset ) {
1512 'value' => $this->msg(
'htmlform-reset' )->text(),
1517 if ( $this->mShowCancel ) {
1518 $target = $this->getCancelTargetURL();
1524 $this->msg(
'cancel' )->text()
1528 foreach ( $this->mButtons as $button ) {
1531 'name' => $button[
'name'],
1532 'value' => $button[
'value']
1535 if ( isset( $button[
'label-message'] ) ) {
1536 $label = $this->getMessage( $button[
'label-message'] )->parse();
1537 } elseif ( isset( $button[
'label'] ) ) {
1538 $label = htmlspecialchars( $button[
'label'] );
1539 } elseif ( isset( $button[
'label-raw'] ) ) {
1540 $label = $button[
'label-raw'];
1542 $label = htmlspecialchars( $button[
'value'] );
1546 if ( $button[
'attribs'] ) {
1548 $attrs += $button[
'attribs'];
1551 if ( isset( $button[
'id'] ) ) {
1552 $attrs[
'id'] = $button[
'id'];
1555 $buttons .= Html::rawElement(
'button', $attrs, $label ) .
"\n";
1562 return Html::rawElement(
'span',
1563 [
'class' =>
'mw-htmlform-submit-buttons' ],
"\n$buttons" ) .
"\n";
1572 return $this->displaySection( $this->mFieldTree, $this->mTableId );
1585 if ( !in_array( $elementsType, [
'error',
'warning' ],
true ) ) {
1586 throw new DomainException( $elementsType .
' is not a valid type.' );
1588 $elementstr =
false;
1589 if ( $elements instanceof
Status ) {
1590 [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1591 $status = $elementsType ===
'error' ? $errorStatus : $warningStatus;
1592 if ( $status->isGood() ) {
1595 $elementstr = $status
1597 ->setContext( $this )
1598 ->setInterfaceMessageFlag(
true )
1601 } elseif ( $elementsType ===
'error' ) {
1602 if ( is_array( $elements ) ) {
1603 $elementstr = $this->formatErrors( $elements );
1604 } elseif ( $elements && $elements !==
true ) {
1605 $elementstr = (string)$elements;
1609 if ( !$elementstr ) {
1611 } elseif ( $elementsType ===
'error' ) {
1612 return Html::errorBox( $elementstr );
1614 return Html::warningBox( $elementstr );
1628 foreach ( $errors as $error ) {
1629 $errorstr .= Html::rawElement(
1632 $this->getMessage( $error )->parse()
1636 return Html::rawElement(
'ul', [], $errorstr );
1647 $this->mSubmitText = $t;
1659 $this->mSubmitFlags = [
'destructive',
'primary' ];
1673 if ( !$msg instanceof
Message ) {
1674 $msg = $this->msg( $msg );
1676 $this->setSubmitText( $msg->text() );
1686 return $this->mSubmitText ?: $this->msg(
'htmlform-submit' )->text();
1695 $this->mSubmitName = $name;
1706 $this->mSubmitTooltip = $name;
1720 $this->mSubmitID = $t;
1744 $this->mFormIdentifier = $ident;
1745 $this->mSingleForm = $single;
1761 $this->mShowSubmit = !$suppressSubmit;
1773 $this->mShowCancel = $show;
1785 $target = TitleValue::castPageToLinkTarget( $target );
1788 $this->mCancelTarget = $target;
1797 if ( is_string( $this->mCancelTarget ) ) {
1798 return $this->mCancelTarget;
1801 $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1802 return $target->getLocalURL();
1816 $this->mTableId = $id;
1837 $this->mName = $name;
1854 $this->mWrapperLegend = $legend;
1867 $this->mWrapperAttributes = $attributes;
1882 if ( !$msg instanceof
Message ) {
1883 $msg = $this->msg( $msg );
1885 $this->setWrapperLegend( $msg->text() );
1900 $this->mMessagePrefix = $p;
1914 $this->mTitle = Title::castFromPageReference( $t );
1923 return $this->mTitle ?: $this->
getContext()->getTitle();
1934 $this->mMethod = strtolower( $method );
1943 return $this->mMethod;
1957 return Xml::fieldset( $legend, $section, $attributes ) .
"\n";
1979 $fieldsetIDPrefix =
'',
1980 &$hasUserVisibleFields =
false
1982 if ( $this->mFieldData ===
null ) {
1983 throw new LogicException(
'HTMLForm::displaySection() called on uninitialized field data. '
1984 .
'You probably called displayForm() without calling prepareForm() first.' );
1988 $subsectionHtml =
'';
1991 foreach ( $fields as $key => $value ) {
1993 $v = array_key_exists( $key, $this->mFieldData )
1994 ? $this->mFieldData[$key]
1995 : $value->getDefault();
1997 $retval = $this->formatField( $value, $v ??
'' );
2001 if ( $value->hasVisibleOutput() ) {
2004 $labelValue = trim( $value->getLabel() );
2005 if ( $labelValue !==
"\u{00A0}" && $labelValue !==
' ' && $labelValue !==
'' ) {
2009 $hasUserVisibleFields =
true;
2011 } elseif ( is_array( $value ) ) {
2012 $subsectionHasVisibleFields =
false;
2014 $this->displaySection( $value,
2016 "$fieldsetIDPrefix$key-",
2017 $subsectionHasVisibleFields );
2019 if ( $subsectionHasVisibleFields ===
true ) {
2021 $hasUserVisibleFields =
true;
2023 $legend = $this->getLegend( $key );
2025 $section = $this->getHeaderText( $key ) .
2027 $this->getFooterText( $key );
2030 if ( $fieldsetIDPrefix ) {
2031 $attributes[
'id'] = Sanitizer::escapeIdForAttribute(
"$fieldsetIDPrefix$key" );
2033 $subsectionHtml .= $this->wrapFieldSetSection(
2034 $legend, $section, $attributes, $fields === $this->mFieldTree
2038 $subsectionHtml .= $section;
2043 $html = $this->formatSection( $html, $sectionName, $hasLabel );
2045 if ( $subsectionHtml ) {
2046 if ( $this->mSubSectionBeforeFields ) {
2047 return $subsectionHtml .
"\n" . $html;
2049 return $html .
"\n" . $subsectionHtml;
2065 $displayFormat = $this->getDisplayFormat();
2066 switch ( $displayFormat ) {
2070 return $field->
getDiv( $value );
2072 return $field->
getRaw( $value );
2076 throw new LogicException(
'Not implemented' );
2088 protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
2089 if ( !$fieldsHtml ) {
2095 $displayFormat = $this->getDisplayFormat();
2096 $html = implode(
'', $fieldsHtml );
2098 if ( $displayFormat ===
'raw' ) {
2103 $attribs = $anyFieldHasLabel ? [] : [
'class' =>
'mw-htmlform-nolabel' ];
2105 if ( $sectionName ) {
2106 $attribs[
'id'] = Sanitizer::escapeIdForAttribute( $sectionName );
2109 if ( $displayFormat ===
'table' ) {
2110 return Html::rawElement(
'table',
2112 Html::rawElement(
'tbody', [],
"\n$html\n" ) ) .
"\n";
2113 } elseif ( $displayFormat ===
'inline' ) {
2114 return Html::rawElement(
'span', $attribs,
"\n$html\n" );
2116 return Html::rawElement(
'div', $attribs,
"\n$html\n" );
2124 $this->prepareForm();
2134 foreach ( $this->mFlatFields as $fieldname => $field ) {
2135 if ( $field->skipLoadData( $request ) ) {
2138 if ( $field->mParams[
'disabled'] ??
false ) {
2139 $fieldData[$fieldname] = $field->getDefault();
2141 $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2147 foreach ( $fieldData as $name => &$value ) {
2148 $field = $this->mFlatFields[$name];
2149 if ( $field->isDisabled( $fieldData ) ) {
2150 $value = $field->getDefault();
2155 foreach ( $fieldData as $name => &$value ) {
2156 $field = $this->mFlatFields[$name];
2157 $value = $field->filter( $value, $fieldData );
2160 $this->mFieldData = $fieldData;
2171 $this->mShowReset = !$suppressReset;
2200 return $this->msg( $this->mMessagePrefix ?
"{$this->mMessagePrefix}-$key" : $key )->text();
2214 $this->mAction = $action;
2228 if ( $this->mAction !==
false ) {
2229 return $this->mAction;
2238 if ( str_contains( $articlePath,
'?' ) && $this->getMethod() ===
'get' ) {
2242 return $this->
getTitle()->getLocalURL();
2256 $this->mAutocomplete = $autocomplete;
2281 foreach ( $this->mFlatFields as $field ) {
2282 if ( $field->needsJSForHtml5FormValidation() ) {
2291class_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.