13use InvalidArgumentException;
17use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
196 use ProtectedHookAccessorTrait;
200 'api' => HTMLApiField::class,
201 'text' => HTMLTextField::class,
202 'textwithbutton' => HTMLTextFieldWithButton::class,
203 'textarea' => HTMLTextAreaField::class,
204 'select' => HTMLSelectField::class,
205 'combobox' => HTMLComboboxField::class,
206 'radio' => HTMLRadioField::class,
207 'multiselect' => HTMLMultiSelectField::class,
208 'limitselect' => HTMLSelectLimitField::class,
209 'check' => HTMLCheckField::class,
210 'toggle' => HTMLCheckField::class,
211 'int' => HTMLIntField::class,
212 'file' => HTMLFileField::class,
213 'float' => HTMLFloatField::class,
214 'info' => HTMLInfoField::class,
215 'selectorother' => HTMLSelectOrOtherField::class,
216 'selectandother' => HTMLSelectAndOtherField::class,
217 'namespaceselect' => HTMLSelectNamespace::class,
218 'namespaceselectwithbutton' => HTMLSelectNamespaceWithButton::class,
219 'tagfilter' => HTMLTagFilter::class,
220 'sizefilter' => HTMLSizeFilterField::class,
221 'submit' => HTMLSubmitField::class,
222 'hidden' => HTMLHiddenField::class,
223 'edittools' => HTMLEditTools::class,
224 'checkmatrix' => HTMLCheckMatrix::class,
225 'cloner' => HTMLFormFieldCloner::class,
226 'autocompleteselect' => HTMLAutoCompleteSelectField::class,
227 'language' => HTMLSelectLanguageField::class,
228 'date' => HTMLDateTimeField::class,
229 'time' => HTMLDateTimeField::class,
230 'datetime' => HTMLDateTimeField::class,
231 'expiry' => HTMLExpiryField::class,
232 'timezone' => HTMLTimezoneField::class,
236 'email' => HTMLTextField::class,
237 'password' => HTMLTextField::class,
238 'url' => HTMLTextField::class,
239 'title' => HTMLTitleTextField::class,
240 'user' => HTMLUserTextField::class,
241 'tagmultiselect' => HTMLTagMultiselectField::class,
242 'orderedmultiselect' => HTMLOrderedMultiselectField::class,
243 'usersmultiselect' => HTMLUsersMultiselectField::class,
244 'titlesmultiselect' => HTMLTitlesMultiselectField::class,
245 'namespacesmultiselect' => HTMLNamespacesMultiselectField::class,
423 private $hiddenTitleAddedToForm =
false;
443 return new CodexHTMLForm( $descriptor, $context, $messagePrefix );
445 wfDeprecatedMsg(
"'vform' HTMLForm display format is deprecated",
'1.45' );
446 return new VFormHTMLForm( $descriptor, $context, $messagePrefix );
448 return new OOUIHTMLForm( $descriptor, $context, $messagePrefix );
450 $form =
new self( $descriptor, $context, $messagePrefix );
471 $this->mMessagePrefix = $messagePrefix;
485 $loadedDescriptor = [];
487 foreach ( $descriptor as $fieldname => $info ) {
488 $section = $info[
'section'] ??
'';
490 if ( isset( $info[
'type'] ) && $info[
'type'] ===
'file' ) {
491 $this->mUseMultipart =
true;
494 $field = static::loadInputFromParameters( $fieldname, $info, $this );
496 $setSection =& $loadedDescriptor;
498 foreach ( explode(
'/', $section ) as $newName ) {
499 $setSection[$newName] ??= [];
500 $setSection =& $setSection[$newName];
504 $setSection[$fieldname] = $field;
505 $this->mFlatFields[$fieldname] = $field;
508 $this->mFieldTree = array_merge_recursive( $this->mFieldTree, $loadedDescriptor );
518 return isset( $this->mFlatFields[$fieldname] );
527 if ( !$this->
hasField( $fieldname ) ) {
528 throw new DomainException( __METHOD__ .
': no field named ' . $fieldname );
530 return $this->mFlatFields[$fieldname];
544 in_array( $format, $this->availableSubclassDisplayFormats,
true ) ||
545 in_array( $this->displayFormat, $this->availableSubclassDisplayFormats,
true )
547 throw new LogicException(
'Cannot change display format after creation, ' .
548 'use HTMLForm::factory() instead' );
551 if ( !in_array( $format, $this->availableDisplayFormats,
true ) ) {
552 throw new InvalidArgumentException(
'Display format must be one of ' .
555 $this->availableDisplayFormats,
556 $this->availableSubclassDisplayFormats
562 $this->displayFormat = $format;
593 if ( isset( $descriptor[
'class'] ) ) {
594 $class = $descriptor[
'class'];
595 } elseif ( isset( $descriptor[
'type'] ) ) {
596 $class = static::$typeMappings[$descriptor[
'type']];
597 $descriptor[
'class'] = $class;
603 throw new InvalidArgumentException(
"Descriptor with no class for $fieldname: "
604 . print_r( $descriptor,
true ) );
625 $class = static::getClassFromDescriptor( $fieldname, $descriptor );
627 $descriptor[
'fieldname'] = $fieldname;
629 $descriptor[
'parent'] = $parent;
632 # @todo This will throw a fatal error whenever someone try to use
633 # 'class' to feed a CSS class instead of 'cssclass'. Would be
634 # great to avoid the fatal error and show a nice error.
635 return new $class( $descriptor );
647 # Load data from the request.
649 $this->mFormIdentifier ===
null ||
650 $this->
getRequest()->getVal(
'wpFormIdentifier' ) === $this->mFormIdentifier ||
651 ( $this->mSingleForm && $this->
getMethod() ===
'get' )
655 $this->mFieldData = [];
668 if ( $this->mFormIdentifier ===
null ) {
677 } elseif ( $this->
getRequest()->wasPosted() ) {
678 $editToken = $this->
getRequest()->getVal(
'wpEditToken' );
679 if ( $this->
getUser()->isRegistered() || $editToken !==
null ) {
684 CsrfTokenSet::DEFAULT_FIELD_NAME, $this->mTokenSalt
691 if ( $tokenOkay && $identOkay ) {
692 $this->mWasSubmitted =
true;
710 if ( $result ===
true || ( $result instanceof
Status && $result->
isGood() ) ) {
747 $hoistedErrors = Status::newGood();
748 if ( $this->mValidationErrorMessage ) {
749 foreach ( $this->mValidationErrorMessage as $error ) {
750 $hoistedErrors->fatal( ...$error );
753 $hoistedErrors->fatal(
'htmlform-invalid-input' );
756 $this->mWasSubmitted =
true;
758 # Check for cancelled submission
759 foreach ( $this->mFlatFields as $fieldname => $field ) {
760 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
763 if ( $field->cancelSubmit( $this->mFieldData[$fieldname], $this->mFieldData ) ) {
764 $this->mWasSubmitted =
false;
769 # Check for validation
770 $hasNonDefault =
false;
771 foreach ( $this->mFlatFields as $fieldname => $field ) {
772 if ( !array_key_exists( $fieldname, $this->mFieldData ) ) {
775 $hasNonDefault = $hasNonDefault || $this->mFieldData[$fieldname] !== $field->getDefault();
776 if ( $field->isDisabled( $this->mFieldData ) ) {
779 $res = $field->validate( $this->mFieldData[$fieldname], $this->mFieldData );
780 if ( $res !==
true ) {
782 if ( $res !==
false && !$field->canDisplayErrors() ) {
783 if ( is_string( $res ) ) {
784 $hoistedErrors->fatal(
'rawmessage', $res );
786 $hoistedErrors->fatal( $res );
794 if ( !$hasNonDefault && $this->
getMethod() ===
'get' &&
795 ( $this->mFormIdentifier ===
null ||
796 $this->
getRequest()->getCheck(
'wpFormIdentifier' ) )
798 $this->mWasSubmitted =
false;
801 return $hoistedErrors;
805 if ( !is_callable( $callback ) ) {
806 throw new LogicException(
'HTMLForm: no submit callback provided. Use ' .
807 'setSubmitCallback() to set one.' );
812 $res = $callback( $data, $this );
813 if ( $res ===
false ) {
814 $this->mWasSubmitted =
false;
817 $res = Status::wrap( $res );
849 $this->mSubmitCallback = $cb;
864 $this->mValidationErrorMessage = $msg;
892 $this->mPre .= $html;
917 if ( $section ===
null ) {
918 $this->mHeader .= $html;
920 $this->mSectionHeaders[$section] ??=
'';
921 $this->mSectionHeaders[$section] .= $html;
937 if ( $section ===
null ) {
938 $this->mHeader = $html;
940 $this->mSectionHeaders[$section] = $html;
955 return $section ? $this->mSectionHeaders[$section] ??
'' :
$this->mHeader;
968 if ( $section ===
null ) {
969 $this->mFooter .= $html;
971 $this->mSectionFooters[$section] ??=
'';
972 $this->mSectionFooters[$section] .= $html;
988 if ( $section ===
null ) {
989 $this->mFooter = $html;
991 $this->mSectionFooters[$section] = $html;
1005 return $section ? $this->mSectionFooters[$section] ??
'' :
$this->mFooter;
1017 $this->mPost .= $html;
1031 $this->mPost = $html;
1057 throw new \InvalidArgumentException(
1058 "Non-Codex HTMLForms do not support additional section information."
1062 $this->mSections = $sections;
1078 if ( !is_array( $value ) ) {
1080 $attribs += [
'name' => $name ];
1081 $this->mHiddenFields[] = [ $value, $attribs ];
1099 foreach ( $fields as $name => $value ) {
1100 if ( is_array( $value ) ) {
1104 $this->mHiddenFields[] = [ $value, [
'name' => $name ] ];
1134 if ( !is_array( $data ) ) {
1135 $args = func_get_args();
1136 if ( count( $args ) < 2 || count( $args ) > 4 ) {
1137 throw new InvalidArgumentException(
1138 'Incorrect number of arguments for deprecated calling style'
1143 'value' => $args[1],
1144 'id' => $args[2] ??
null,
1145 'attribs' => $args[3] ??
null,
1148 if ( !isset( $data[
'name'] ) ) {
1149 throw new InvalidArgumentException(
'A name is required' );
1151 if ( !isset( $data[
'value'] ) ) {
1152 throw new InvalidArgumentException(
'A value is required' );
1155 $this->mButtons[] = $data + [
1175 $this->mTokenSalt = $salt;
1201 private function getHiddenTitle(): string {
1202 if ( $this->hiddenTitleAddedToForm ) {
1210 $html .= Html::hidden(
'title', $this->
getTitle()->getPrefixedText() ) .
"\n";
1212 $this->hiddenTitleAddedToForm =
true;
1227 # For good measure (it is the default)
1228 $this->getOutput()->getMetadata()->setPreventClickjacking(
true );
1229 $this->getOutput()->addModules(
'mediawiki.htmlform' );
1230 $this->getOutput()->addModuleStyles( [
1231 'mediawiki.htmlform.styles',
1233 'mediawiki.codex.messagebox.styles'
1236 if ( $this->mCollapsible ) {
1238 $this->getOutput()->addModules(
'jquery.makeCollapsible' );
1241 $headerHtml = $this->getHeaderHtml();
1242 $footerHtml = $this->getFooterHtml();
1243 $html = $this->getErrorsOrWarnings( $submitResult,
'error' )
1244 . $this->getErrorsOrWarnings( $submitResult,
'warning' )
1246 . $this->getHiddenTitle()
1248 . $this->getHiddenFields()
1249 . $this->getButtons()
1252 return $this->mPre . $this->wrapForm( $html ) . $this->mPost;
1263 $this->mCollapsible =
true;
1264 $this->mCollapsed = $collapsedByDefault;
1274 # Use multipart/form-data
1275 $encType = $this->mUseMultipart
1276 ?
'multipart/form-data'
1277 :
'application/x-www-form-urlencoded';
1280 'class' =>
'mw-htmlform',
1281 'action' => $this->getAction(),
1282 'method' => $this->getMethod(),
1283 'enctype' => $encType,
1286 $attribs[
'id'] = $this->mId;
1288 if ( is_string( $this->mAutocomplete ) ) {
1289 $attribs[
'autocomplete'] = $this->mAutocomplete;
1291 if ( $this->mName ) {
1292 $attribs[
'name'] = $this->mName;
1294 if ( $this->needsJSForHtml5FormValidation() ) {
1295 $attribs[
'novalidate'] =
true;
1308 # Include a <fieldset> wrapper for style, if requested.
1309 if ( $this->mWrapperLegend !==
false ) {
1310 $legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend :
false;
1311 $html = Html::rawElement(
1313 $this->mWrapperAttributes,
1314 ( $legend ?
Html::element(
'legend', [], $legend ) :
'' ) . $html
1318 return Html::rawElement(
1320 $this->getFormAttributes(),
1334 $html .= $this->getHiddenTitle();
1336 if ( $this->mFormIdentifier !==
null ) {
1337 $html .= Html::hidden(
1339 $this->mFormIdentifier
1342 if ( $this->getMethod() ===
'post' ) {
1343 $html .= Html::hidden(
1345 $this->getUser()->getEditToken( $this->mTokenSalt ),
1346 [
'id' =>
'wpEditToken' ]
1350 foreach ( $this->mHiddenFields as [ $value, $attribs ] ) {
1351 $html .= Html::hidden( $attribs[
'name'], $value, $attribs ) .
"\n";
1365 if ( $this->mShowSubmit ) {
1368 if ( $this->mSubmitID !==
null ) {
1369 $attribs[
'id'] = $this->mSubmitID;
1372 if ( $this->mSubmitName !==
null ) {
1373 $attribs[
'name'] = $this->mSubmitName;
1376 if ( $this->mSubmitTooltip !==
null ) {
1377 $attribs += Linker::tooltipAndAccesskeyAttribs( $this->mSubmitTooltip );
1380 $attribs[
'class'] = [
'mw-htmlform-submit' ];
1382 $buttons .= Html::submitButton( $this->getSubmitText(), $attribs ) .
"\n";
1385 if ( $this->mShowCancel ) {
1386 $target = $this->getCancelTargetURL();
1392 $this->msg(
'cancel' )->text()
1396 foreach ( $this->mButtons as $button ) {
1399 'name' => $button[
'name'],
1400 'value' => $button[
'value']
1403 if ( isset( $button[
'label-message'] ) ) {
1404 $label = $this->getMessage( $button[
'label-message'] )->parse();
1405 } elseif ( isset( $button[
'label'] ) ) {
1406 $label = htmlspecialchars( $button[
'label'] );
1407 } elseif ( isset( $button[
'label-raw'] ) ) {
1408 $label = $button[
'label-raw'];
1410 $label = htmlspecialchars( $button[
'value'] );
1414 if ( $button[
'attribs'] ) {
1416 $attrs += $button[
'attribs'];
1419 if ( isset( $button[
'id'] ) ) {
1420 $attrs[
'id'] = $button[
'id'];
1423 $buttons .= Html::rawElement(
'button', $attrs, $label ) .
"\n";
1430 return Html::rawElement(
'span',
1431 [
'class' =>
'mw-htmlform-submit-buttons' ],
"\n$buttons" ) .
"\n";
1440 return $this->displaySection( $this->mFieldTree, $this->mTableId );
1453 if ( !in_array( $elementsType, [
'error',
'warning' ],
true ) ) {
1454 throw new DomainException( $elementsType .
' is not a valid type.' );
1456 $elementstr =
false;
1457 if ( $elements instanceof
Status ) {
1458 [ $errorStatus, $warningStatus ] = $elements->splitByErrorType();
1459 $status = $elementsType ===
'error' ? $errorStatus : $warningStatus;
1460 if ( $status->isGood() ) {
1463 $elementstr = $status
1465 ->setContext( $this )
1466 ->setInterfaceMessageFlag(
true )
1469 } elseif ( $elementsType ===
'error' ) {
1470 if ( is_array( $elements ) ) {
1471 $elementstr = $this->formatErrors( $elements );
1472 } elseif ( $elements && $elements !==
true ) {
1473 $elementstr = (string)$elements;
1477 if ( !$elementstr ) {
1479 } elseif ( $elementsType ===
'error' ) {
1480 return Html::errorBox( $elementstr );
1482 return Html::warningBox( $elementstr );
1496 foreach ( $errors as $error ) {
1497 $errorstr .= Html::rawElement(
1500 $this->getMessage( $error )->parse()
1504 return Html::rawElement(
'ul', [], $errorstr );
1515 $this->mSubmitText = $t;
1527 $this->mSubmitFlags = [
'destructive',
'primary' ];
1541 if ( !$msg instanceof
Message ) {
1542 $msg = $this->msg( $msg );
1544 $this->setSubmitText( $msg->text() );
1554 return $this->mSubmitText ?: $this->msg(
'htmlform-submit' )->text();
1563 $this->mSubmitName = $name;
1574 $this->mSubmitTooltip = $name;
1588 $this->mSubmitID = $t;
1612 $this->mFormIdentifier = $ident;
1613 $this->mSingleForm = $single;
1629 $this->mShowSubmit = !$suppressSubmit;
1641 $this->mShowCancel = $show;
1653 $target = TitleValue::castPageToLinkTarget( $target );
1656 $this->mCancelTarget = $target;
1665 if ( is_string( $this->mCancelTarget ) ) {
1666 return $this->mCancelTarget;
1669 $target = Title::castFromLinkTarget( $this->mCancelTarget ) ?: Title::newMainPage();
1670 return $target->getLocalURL();
1684 $this->mTableId = $id;
1705 $this->mName = $name;
1722 $this->mWrapperLegend = $legend;
1735 $this->mWrapperAttributes = $attributes;
1750 if ( !$msg instanceof
Message ) {
1751 $msg = $this->msg( $msg );
1753 $this->setWrapperLegend( $msg->text() );
1768 $this->mMessagePrefix = $p;
1782 $this->mTitle = Title::castFromPageReference( $t );
1791 return $this->mTitle ?: $this->getContext()->getTitle();
1802 $this->mMethod = strtolower( $method );
1811 return $this->mMethod;
1825 return Html::rawElement(
1852 $fieldsetIDPrefix =
'',
1853 &$hasUserVisibleFields =
false
1855 if ( $this->mFieldData ===
null ) {
1856 throw new LogicException(
'HTMLForm::displaySection() called on uninitialized field data. '
1857 .
'You probably called displayForm() without calling prepareForm() first.' );
1861 $subsectionHtml =
'';
1864 foreach ( $fields as $key => $value ) {
1866 $v = array_key_exists( $key, $this->mFieldData )
1867 ? $this->mFieldData[$key]
1868 : $value->getDefault();
1870 $retval = $this->formatField( $value, $v ??
'' );
1874 if ( $value->hasVisibleOutput() ) {
1877 $labelValue = trim( $value->getLabel() );
1878 if ( $labelValue !==
"\u{00A0}" && $labelValue !==
' ' && $labelValue !==
'' ) {
1882 $hasUserVisibleFields =
true;
1884 } elseif ( is_array( $value ) ) {
1885 $subsectionHasVisibleFields =
false;
1887 $this->displaySection( $value,
1889 "$fieldsetIDPrefix$key-",
1890 $subsectionHasVisibleFields );
1892 if ( $subsectionHasVisibleFields ===
true ) {
1894 $hasUserVisibleFields =
true;
1896 $legend = $this->getLegend( $key );
1898 $headerHtml = $this->getHeaderHtml( $key );
1899 $footerHtml = $this->getFooterHtml( $key );
1900 $section = $headerHtml .
1905 if ( $fieldsetIDPrefix ) {
1906 $attributes[
'id'] = Sanitizer::escapeIdForAttribute(
"$fieldsetIDPrefix$key" );
1908 $subsectionHtml .= $this->wrapFieldSetSection(
1909 $legend, $section, $attributes, $fields === $this->mFieldTree
1913 $subsectionHtml .= $section;
1918 $html = $this->formatSection( $html, $sectionName, $hasLabel );
1920 if ( $subsectionHtml ) {
1921 if ( $this->mSubSectionBeforeFields ) {
1922 return $subsectionHtml .
"\n" . $html;
1924 return $html .
"\n" . $subsectionHtml;
1940 $displayFormat = $this->getDisplayFormat();
1941 switch ( $displayFormat ) {
1945 return $field->
getDiv( $value );
1947 return $field->
getRaw( $value );
1951 throw new LogicException(
'Not implemented' );
1963 protected function formatSection( array $fieldsHtml, $sectionName, $anyFieldHasLabel ) {
1964 if ( !$fieldsHtml ) {
1970 $displayFormat = $this->getDisplayFormat();
1971 $html = implode(
'', $fieldsHtml );
1973 if ( $displayFormat ===
'raw' ) {
1978 $attribs = $anyFieldHasLabel ? [] : [
'class' =>
'mw-htmlform-nolabel' ];
1980 if ( $sectionName ) {
1981 $attribs[
'id'] = Sanitizer::escapeIdForAttribute( $sectionName );
1984 if ( $displayFormat ===
'table' ) {
1985 return Html::rawElement(
'table',
1987 Html::rawElement(
'tbody', [],
"\n$html\n" ) ) .
"\n";
1988 } elseif ( $displayFormat ===
'inline' ) {
1989 return Html::rawElement(
'span', $attribs,
"\n$html\n" );
1991 return Html::rawElement(
'div', $attribs,
"\n$html\n" );
1999 $this->prepareForm();
2007 $request = $this->getRequest();
2009 foreach ( $this->mFlatFields as $fieldname => $field ) {
2010 if ( $field->skipLoadData( $request ) ) {
2013 if ( $field->mParams[
'disabled'] ??
false ) {
2014 $fieldData[$fieldname] = $field->getDefault();
2016 $fieldData[$fieldname] = $field->loadDataFromRequest( $request );
2022 foreach ( $fieldData as $name => &$value ) {
2023 $field = $this->mFlatFields[$name];
2024 if ( $field->isDisabled( $fieldData ) ) {
2025 $value = $field->getDefault();
2030 foreach ( $fieldData as $name => &$value ) {
2031 $field = $this->mFlatFields[$name];
2032 $value = $field->filter( $value, $fieldData );
2035 $this->mFieldData = $fieldData;
2062 return $this->msg( $this->mMessagePrefix ?
"{$this->mMessagePrefix}-$key" : $key )->text();
2076 $this->mAction = $action;
2090 if ( $this->mAction !==
false ) {
2091 return $this->mAction;
2100 if ( str_contains( $articlePath,
'?' ) && $this->getMethod() ===
'get' ) {
2104 return $this->getTitle()->getLocalURL();
2118 $this->mAutocomplete = $autocomplete;
2143 foreach ( $this->mFlatFields as $field ) {
2144 if ( $field->needsJSForHtml5FormValidation() ) {
2153class_alias( HTMLForm::class,
'HTMLForm' );
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
if(!defined('MW_SETUP_CALLBACK'))
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
getCsrfTokenSet()
Get a repository to obtain and match CSRF tokens.
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.
Interface for objects which can provide a MediaWiki context on request.