MediaWiki master
HTMLFormField.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\HTMLForm;
4
5use InvalidArgumentException;
16use StatusValue;
20
27abstract class HTMLFormField {
29 public $mParams;
30
32 protected $mValidationCallback;
34 protected $mFilterCallback;
36 protected $mName;
38 protected $mDir;
40 protected $mLabel;
42 protected $mID;
44 protected $mClass = '';
46 protected $mHelpClass = false;
48 protected $mDefault;
50 private $mNotices;
51
55 protected $mOptions = false;
61 protected $mCondState = [];
63 protected $mCondStateClass = [];
64
69 protected $mShowEmptyLabels = true;
70
74 public $mParent;
75
86 abstract public function getInputHTML( $value );
87
96 public function getInputOOUI( $value ) {
97 return false;
98 }
99
111 public function getInputCodex( $value, $hasErrors ) {
112 // If not overridden, fall back to getInputHTML()
113 return $this->getInputHTML( $value );
114 }
115
122 public function canDisplayErrors() {
123 return $this->hasVisibleOutput();
124 }
125
140 public function msg( $key, ...$params ) {
141 if ( $this->mParent ) {
142 return $this->mParent->msg( $key, ...$params );
143 }
144 return wfMessage( $key, ...$params );
145 }
146
154 public function hasVisibleOutput() {
155 return true;
156 }
157
164 public function getName() {
165 return $this->mName;
166 }
167
179 protected function getNearestField( $name, $backCompat = false ) {
180 // When the field is belong to a HTMLFormFieldCloner
181 $cloner = $this->mParams['cloner'] ?? null;
182 if ( $cloner instanceof HTMLFormFieldCloner ) {
183 $field = $cloner->findNearestField( $this, $name );
184 if ( $field ) {
185 return $field;
186 }
187 }
188
189 if ( $backCompat && str_starts_with( $name, 'wp' ) &&
190 !$this->mParent->hasField( $name )
191 ) {
192 // Don't break the existed use cases.
193 return $this->mParent->getField( substr( $name, 2 ) );
194 }
195 return $this->mParent->getField( $name );
196 }
197
209 protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
210 $field = $this->getNearestField( $name, $backCompat );
211 // When the field belongs to a HTMLFormFieldCloner
212 $cloner = $field->mParams['cloner'] ?? null;
213 if ( $cloner instanceof HTMLFormFieldCloner ) {
214 $value = $cloner->extractFieldData( $field, $alldata );
215 } else {
216 // Note $alldata is an empty array when first rendering a form with a formIdentifier.
217 // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
218 // field's default value
219 $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
220 }
221
222 // Check invert state for HTMLCheckField
223 if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
224 $value = !$value;
225 }
226
227 return $value;
228 }
229
240 protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
241 return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
242 }
243
250 protected function validateCondState( $params ) {
251 $origParams = $params;
252 $op = array_shift( $params );
253
254 $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException {
255 return new InvalidArgumentException(
256 "Invalid hide-if or disable-if specification for $this->mName: " .
257 $details . " in " . var_export( $origParams, true )
258 );
259 };
260
261 switch ( $op ) {
262 case 'NOT':
263 if ( count( $params ) !== 1 ) {
264 throw $makeException( "NOT takes exactly one parameter" );
265 }
266 // Fall-through intentionally
267
268 case 'AND':
269 case 'OR':
270 case 'NAND':
271 case 'NOR':
272 foreach ( $params as $i => $p ) {
273 if ( !is_array( $p ) ) {
274 $type = get_debug_type( $p );
275 throw $makeException( "Expected array, found $type at index $i" );
276 }
277 $this->validateCondState( $p );
278 }
279 break;
280
281 case '===':
282 case '!==':
283 case 'CONTAINS':
284 if ( count( $params ) !== 2 ) {
285 throw $makeException( "$op takes exactly two parameters" );
286 }
287 [ $name, $value ] = $params;
288 if ( !is_string( $name ) || !is_string( $value ) ) {
289 throw $makeException( "Parameters for $op must be strings" );
290 }
291 break;
292
293 default:
294 throw $makeException( "Unknown operation" );
295 }
296 }
297
305 protected function checkStateRecurse( array $alldata, array $params ) {
306 $op = array_shift( $params );
307 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
308 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
309
310 switch ( $op ) {
311 case 'AND':
312 case 'OR':
313 case 'NAND':
314 case 'NOR':
315 foreach ( $params as $p ) {
316 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
317 return !$valueRet[$op];
318 }
319 }
320 return $valueRet[$op];
321
322 case 'NOT':
323 return !$this->checkStateRecurse( $alldata, $params[0] );
324
325 case '===':
326 case '!==':
327 case 'CONTAINS':
328 [ $field, $value ] = $params;
329 $testValue = $this->getNearestFieldValue( $alldata, $field, true, true );
330 switch ( $op ) {
331 case '===':
332 return ( $value === (string)$testValue );
333 case '!==':
334 return ( $value !== (string)$testValue );
335 case 'CONTAINS':
336 return in_array( $value, $testValue, true );
337 }
338 }
339 }
340
349 protected function parseCondState( $params ) {
350 $op = array_shift( $params );
351
352 switch ( $op ) {
353 case 'AND':
354 case 'OR':
355 case 'NAND':
356 case 'NOR':
357 $ret = [ $op ];
358 foreach ( $params as $p ) {
359 $ret[] = $this->parseCondState( $p );
360 }
361 return $ret;
362
363 case 'NOT':
364 return [ 'NOT', $this->parseCondState( $params[0] ) ];
365
366 case '===':
367 case '!==':
368 case 'CONTAINS':
369 [ $name, $value ] = $params;
370 $field = $this->getNearestField( $name, true );
371 return [ $op, $field->getName(), $value ];
372 }
373 }
374
380 protected function parseCondStateForClient() {
381 $parsed = [];
382 foreach ( $this->mCondState as $type => $params ) {
383 $parsed[$type] = $this->parseCondState( $params );
384 }
385 return $parsed;
386 }
387
396 public function isHidden( $alldata ) {
397 return isset( $this->mCondState['hide'] ) &&
398 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
399 }
400
409 public function isDisabled( $alldata ) {
410 return ( $this->mParams['disabled'] ?? false ) ||
411 $this->isHidden( $alldata ) ||
412 ( isset( $this->mCondState['disable'] )
413 && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) );
414 }
415
427 public function cancelSubmit( $value, $alldata ) {
428 return false;
429 }
430
443 public function validate( $value, $alldata ) {
444 if ( $this->isHidden( $alldata ) ) {
445 return true;
446 }
447
448 if ( isset( $this->mParams['required'] )
449 && $this->mParams['required'] !== false
450 && ( $value === '' || $value === false || $value === null )
451 ) {
452 return $this->msg( 'htmlform-required' );
453 }
454
455 if ( $this->mValidationCallback === null ) {
456 return true;
457 }
458
459 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
460
461 if ( $p instanceof StatusValue ) {
462 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
463
464 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
465 }
466
467 return $p;
468 }
469
478 public function filter( $value, $alldata ) {
479 if ( $this->mFilterCallback !== null ) {
480 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
481 }
482
483 return $value;
484 }
485
493 protected function needsLabel() {
494 return true;
495 }
496
506 public function setShowEmptyLabel( $show ) {
507 $this->mShowEmptyLabels = $show;
508 }
509
521 protected function isSubmitAttempt( WebRequest $request ) {
522 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
523 return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) )
524 // The identifier matching or not has been checked in HTMLForm::prepareForm()
525 || $request->getCheck( 'wpFormIdentifier' );
526 }
527
536 public function loadDataFromRequest( $request ) {
537 if ( $request->getCheck( $this->mName ) ) {
538 return $request->getText( $this->mName );
539 } else {
540 return $this->getDefault();
541 }
542 }
543
552 public function __construct( $params ) {
553 $this->mParams = $params;
554
555 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
556 $this->mParent = $params['parent'];
557 } else {
558 // Normally parent is added automatically by HTMLForm::factory.
559 // Several field types already assume unconditionally this is always set,
560 // so deprecate manually creating an HTMLFormField without a parent form set.
562 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
563 "1.40"
564 );
565 }
566
567 # Generate the label from a message, if possible
568 if ( isset( $params['label-message'] ) ) {
569 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
570 } elseif ( isset( $params['label'] ) ) {
571 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
572 // Apparently some things set &nbsp directly and in an odd format
573 $this->mLabel = "\u{00A0}";
574 } else {
575 $this->mLabel = htmlspecialchars( $params['label'] );
576 }
577 } elseif ( isset( $params['label-raw'] ) ) {
578 $this->mLabel = $params['label-raw'];
579 }
580
581 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
582
583 if ( isset( $params['dir'] ) ) {
584 $this->mDir = $params['dir'];
585 }
586
587 $this->mID = "mw-input-{$this->mName}";
588
589 if ( isset( $params['default'] ) ) {
590 $this->mDefault = $params['default'];
591 }
592
593 if ( isset( $params['id'] ) ) {
594 $this->mID = $params['id'];
595 }
596
597 if ( isset( $params['cssclass'] ) ) {
598 $this->mClass = $params['cssclass'];
599 }
600
601 if ( isset( $params['csshelpclass'] ) ) {
602 $this->mHelpClass = $params['csshelpclass'];
603 }
604
605 if ( isset( $params['validation-callback'] ) ) {
606 $this->mValidationCallback = $params['validation-callback'];
607 }
608
609 if ( isset( $params['filter-callback'] ) ) {
610 $this->mFilterCallback = $params['filter-callback'];
611 }
612
613 if ( isset( $params['hidelabel'] ) ) {
614 $this->mShowEmptyLabels = false;
615 }
616 if ( isset( $params['notices'] ) ) {
617 $this->mNotices = $params['notices'];
618 }
619
620 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
621 $this->validateCondState( $params['hide-if'] );
622 $this->mCondState['hide'] = $params['hide-if'];
623 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
624 }
625 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
626 isset( $params['disable-if'] ) && $params['disable-if']
627 ) {
628 $this->validateCondState( $params['disable-if'] );
629 $this->mCondState['disable'] = $params['disable-if'];
630 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
631 }
632 }
633
643 public function getTableRow( $value ) {
644 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
645 $inputHtml = $this->getInputHTML( $value );
646 $fieldType = $this->getClassName();
647 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
648 $cellAttributes = [];
649 $rowAttributes = [];
650 $rowClasses = '';
651
652 if ( !empty( $this->mParams['vertical-label'] ) ) {
653 $cellAttributes['colspan'] = 2;
654 $verticalLabel = true;
655 } else {
656 $verticalLabel = false;
657 }
658
659 $label = $this->getLabelHtml( $cellAttributes );
660
661 $field = Html::rawElement(
662 'td',
663 [ 'class' => 'mw-input' ] + $cellAttributes,
664 $inputHtml . "\n$errors"
665 );
666
667 if ( $this->mCondState ) {
668 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
669 $rowClasses .= implode( ' ', $this->mCondStateClass );
670 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
671 $rowClasses .= ' mw-htmlform-hide-if-hidden';
672 }
673 }
674
675 if ( $verticalLabel ) {
676 $html = Html::rawElement( 'tr',
677 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
678 $html .= Html::rawElement( 'tr',
679 $rowAttributes + [
680 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
681 ],
682 $field );
683 } else {
684 $html = Html::rawElement( 'tr',
685 $rowAttributes + [
686 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
687 ],
688 $label . $field );
689 }
690
691 return $html . $helptext;
692 }
693
704 public function getDiv( $value ) {
705 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
706 $inputHtml = $this->getInputHTML( $value );
707 $fieldType = $this->getClassName();
708 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
709 $cellAttributes = [];
710 $label = $this->getLabelHtml( $cellAttributes );
711
712 $outerDivClass = [
713 'mw-input',
714 'mw-htmlform-nolabel' => ( $label === '' )
715 ];
716
717 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
718
719 if ( $horizontalLabel ) {
720 $field = "\u{00A0}" . $inputHtml . "\n$errors";
721 } else {
722 $field = Html::rawElement(
723 'div',
724 // @phan-suppress-next-line PhanUselessBinaryAddRight
725 [ 'class' => $outerDivClass ] + $cellAttributes,
726 $inputHtml . "\n$errors"
727 );
728 }
729
730 $wrapperAttributes = [ 'class' => [
731 "mw-htmlform-field-$fieldType",
733 $errorClass,
734 ] ];
735 if ( $this->mCondState ) {
736 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
737 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
738 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
739 $wrapperAttributes['class'][] = 'mw-htmlform-hide-if-hidden';
740 }
741 }
742 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
743 $helptext;
744 }
745
755 public function getOOUI( $value ) {
756 if ( $this->getDescriptionMessages() !== [] ) {
757 throw new InvalidArgumentException(
758 "OOUIHTMLForm does not support the descriptions for fields. Please use Codex"
759 );
760 }
761 $inputField = $this->getInputOOUI( $value );
762
763 if ( !$inputField ) {
764 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
765 // generate the whole field, label and errors and all, then wrap it in a Widget.
766 // It might look weird, but it'll work OK.
767 return $this->getFieldLayoutOOUI(
768 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
769 [ 'align' => 'top' ]
770 );
771 }
772
773 $infusable = true;
774 if ( is_string( $inputField ) ) {
775 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
776 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
777 // JavaScript doesn't know how to rebuilt the contents.
778 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
779 $infusable = false;
780 }
781
782 $fieldType = $this->getClassName();
783 $help = $this->getHelpText();
784 $errors = $this->getErrorsRaw( $value );
785 foreach ( $errors as &$error ) {
786 $error = new \OOUI\HtmlSnippet( $error );
787 }
788
789 $config = [
790 'classes' => [ "mw-htmlform-field-$fieldType" ],
791 'align' => $this->getLabelAlignOOUI(),
792 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
793 'errors' => $errors,
794 'infusable' => $infusable,
795 'helpInline' => $this->isHelpInline(),
796 'notices' => $this->mNotices ?: [],
797 ];
798 if ( $this->mClass !== '' ) {
799 $config['classes'][] = $this->mClass;
800 }
801
802 $preloadModules = false;
803
804 if ( $infusable && $this->shouldInfuseOOUI() ) {
805 $preloadModules = true;
806 $config['classes'][] = 'mw-htmlform-autoinfuse';
807 }
808 if ( $this->mCondState ) {
809 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
810 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
811 $config['classes'][] = 'mw-htmlform-hide-if-hidden';
812 }
813 }
814
815 // the element could specify, that the label doesn't need to be added
816 $label = $this->getLabel();
817 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
818 $config['label'] = new \OOUI\HtmlSnippet( $label );
819 }
820
821 if ( $this->mCondState ) {
822 $preloadModules = true;
823 $config['condState'] = $this->parseCondStateForClient();
824 }
825
826 $config['modules'] = $this->getOOUIModules();
827
828 if ( $preloadModules ) {
829 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
830 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
831 }
832
833 return $this->getFieldLayoutOOUI( $inputField, $config );
834 }
835
843 public function getCodex( $value ) {
844 $isDisabled = ( $this->mParams['disabled'] ?? false );
845
846 // Label
847 $labelDiv = '';
848 $labelValue = trim( $this->getLabel() );
849 // For weird historical reasons, a non-breaking space is treated as an empty label
850 // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version
851 if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' ) {
852 $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
853 $labelClasses = [ 'cdx-label' ];
854 if ( $isDisabled ) {
855 $labelClasses[] = 'cdx-label--disabled';
856 }
857 $descriptionHtml = $this->getDescriptionHtmlSpan(
858 $this->getDescriptionText(),
859 [ 'cdx-label__description' ]
860 );
861 $optionalHtml = '';
862 if ( $this->showOptionalFlag() ) {
863 $messageKey = $this->mParams['optional-message'] ?? 'htmlform-optional-flag';
864 $optionalHtml = Html::rawElement(
865 'span',
866 [ 'class' => 'cdx-label__label__optional-flag' ],
867 ' ' . $this->getMessage( $messageKey )->parse(),
868 );
869 }
870 // <div class="cdx-label">
871 $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ],
872 // <label class="cdx-label__label" for="ID">
873 Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor,
874 // <span class="cdx-label__label__text">
875 Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ],
876 $labelValue,
877 ) . $optionalHtml,
878 ) . $descriptionHtml,
879 );
880 }
881
882 // Help text
883 // <div class="cdx-field__help-text">
884 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] );
885
886 // Validation message
887 // <div class="cdx-field__validation-message">
888 // $errors is a <div class="cdx-message">
889 // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead
890 $validationMessage = '';
891 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
892 if ( $errors !== '' ) {
893 $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ],
894 $errors
895 );
896 }
897
898 // Control
899 $inputHtml = $this->getInputCodex( $value, $errors !== '' );
900 // <div class="cdx-field__control cdx-field__control--has-help-text">
901 $controlClasses = [ 'cdx-field__control' ];
902 if ( $helptext ) {
903 $controlClasses[] = 'cdx-field__control--has-help-text';
904 }
905 $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml );
906
907 // <div class="cdx-field">
908 $fieldClasses = [
909 "mw-htmlform-field-{$this->getClassName()}",
911 $errorClass,
912 'cdx-field'
913 ];
914 if ( $isDisabled ) {
915 $fieldClasses[] = 'cdx-field--disabled';
916 }
917 $fieldAttributes = [];
918 // Set data attribute and CSS class for client side handling of hide-if / disable-if
919 if ( $this->mCondState ) {
920 $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
921 $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass );
922 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
923 $fieldClasses[] = 'mw-htmlform-hide-if-hidden';
924 }
925 }
926
927 return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes,
928 $labelDiv . $control . $helptext . $validationMessage
929 );
930 }
931
932 private function showOptionalFlag(): bool {
933 $shouldShowOptionalFlag = $this->mParams['show-optional-flag'] ?? false;
934 if ( !$shouldShowOptionalFlag ) {
935 return false;
936 }
937
938 $isRequired = $this->mParams['required'] ?? false;
939 if ( $isRequired ) {
940 // field is both required AND set to show a label-suffix "(optional)". Something is wrong
941 throw new \InvalidArgumentException( 'A field cannot be both optional and required.' );
942 }
943 return true;
944 }
945
953 protected function getClassName() {
954 $name = explode( '\\', static::class );
955 return end( $name );
956 }
957
963 protected function getLabelAlignOOUI() {
964 return 'top';
965 }
966
973 protected function getFieldLayoutOOUI( $inputField, $config ) {
974 return new HTMLFormFieldLayout( $inputField, $config );
975 }
976
985 protected function shouldInfuseOOUI() {
986 // Always infuse fields with popup help text, since the interface for it is nicer with JS
987 return !$this->isHelpInline() && $this->getHelpMessages();
988 }
989
997 protected function getOOUIModules() {
998 return [];
999 }
1000
1011 public function getRaw( $value ) {
1012 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
1013 return "\n" . $errors .
1014 $this->getLabelHtml() .
1015 $this->getInputHTML( $value ) .
1016 $this->getHelpTextHtmlRaw( $this->getHelpText() );
1017 }
1018
1026 public function getInline( $value ) {
1027 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
1028 return "\n" . $errors .
1029 $this->getLabelHtml() .
1030 "\u{00A0}" .
1031 $this->getInputHTML( $value ) .
1032 $this->getHelpTextHtmlDiv( $this->getHelpText() );
1033 }
1034
1042 public function getHelpTextHtmlTable( $helptext ) {
1043 if ( $helptext === null ) {
1044 return '';
1045 }
1046
1047 $rowAttributes = [];
1048 if ( $this->mCondState ) {
1049 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1050 $rowAttributes['class'] = $this->mCondStateClass;
1051 }
1052
1053 $tdClasses = [ 'htmlform-tip' ];
1054 if ( $this->mHelpClass !== false ) {
1055 $tdClasses[] = $this->mHelpClass;
1056 }
1057 return Html::rawElement( 'tr', $rowAttributes,
1058 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
1059 );
1060 }
1061
1071 public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) {
1072 if ( $helptext === null ) {
1073 return '';
1074 }
1075
1076 $wrapperAttributes = [
1077 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ),
1078 ];
1079 if ( $this->mHelpClass !== false ) {
1080 $wrapperAttributes['class'][] = $this->mHelpClass;
1081 }
1082 if ( $this->mCondState ) {
1083 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1084 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
1085 }
1086 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
1087 }
1088
1089 public function getDescriptionHtmlSpan( ?string $descriptionHtml, array $cssClasses = [] ): string {
1090 if ( $descriptionHtml === null ) {
1091 return '';
1092 }
1093
1094 return Html::rawElement( 'span', [ 'class' => $cssClasses ], $descriptionHtml );
1095 }
1096
1104 public function getHelpTextHtmlRaw( $helptext ) {
1105 return $this->getHelpTextHtmlDiv( $helptext );
1106 }
1107
1108 private function getHelpMessages(): array {
1109 if ( isset( $this->mParams['help-message'] ) ) {
1110 return [ $this->mParams['help-message'] ];
1111 } elseif ( isset( $this->mParams['help-messages'] ) ) {
1112 return $this->mParams['help-messages'];
1113 } elseif ( isset( $this->mParams['help-raw'] ) ) {
1114 return [ new HtmlArmor( $this->mParams['help-raw'] ) ];
1115 } elseif ( isset( $this->mParams['help'] ) ) {
1116 // @deprecated since 1.43, use 'help-raw' key instead
1117 return [ new HtmlArmor( $this->mParams['help'] ) ];
1118 }
1119
1120 return [];
1121 }
1122
1129 public function getHelpText() {
1130 $html = [];
1131
1132 foreach ( $this->getHelpMessages() as $msg ) {
1133 if ( $msg instanceof HtmlArmor ) {
1134 $html[] = HtmlArmor::getHtml( $msg );
1135 } else {
1136 $msg = $this->getMessage( $msg );
1137 if ( $msg->exists() ) {
1138 $html[] = $msg->parse();
1139 }
1140 }
1141 }
1142
1143 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1144 }
1145
1146 private function getDescriptionMessages(): array {
1147 if ( isset( $this->mParams['description-message'] ) ) {
1148 return [ $this->mParams['description-message'] ];
1149 }
1150
1151 if ( isset( $this->mParams['description-messages'] ) ) {
1152 return $this->mParams['description-messages'];
1153 }
1154
1155 if ( isset( $this->mParams['description-raw'] ) ) {
1156 return [ new HtmlArmor( $this->mParams['description-raw'] ) ];
1157 }
1158
1159 return [];
1160 }
1161
1168 public function getDescriptionText(): ?string {
1169 $html = [];
1170
1171 foreach ( $this->getDescriptionMessages() as $msg ) {
1172 if ( $msg instanceof HtmlArmor ) {
1173 $html[] = HtmlArmor::getHtml( $msg );
1174 } else {
1175 $msg = $this->getMessage( $msg );
1176 if ( $msg->exists() ) {
1177 $html[] = $msg->parse();
1178 }
1179 }
1180 }
1181
1182 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1183 }
1184
1193 public function isHelpInline() {
1194 return $this->mParams['help-inline'] ?? true;
1195 }
1196
1209 public function getErrorsAndErrorClass( $value ) {
1210 $errors = $this->validate( $value, $this->mParent->mFieldData );
1211
1212 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1213 return [ '', '' ];
1214 }
1215
1216 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1217 }
1218
1226 public function getErrorsRaw( $value ) {
1227 $errors = $this->validate( $value, $this->mParent->mFieldData );
1228
1229 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1230 return [];
1231 }
1232
1233 if ( !is_array( $errors ) ) {
1234 $errors = [ $errors ];
1235 }
1236 foreach ( $errors as &$error ) {
1237 if ( $error instanceof Message ) {
1238 $error = $error->parse();
1239 }
1240 }
1241
1242 return $errors;
1243 }
1244
1249 public function getLabel() {
1250 return $this->mLabel ?? '';
1251 }
1252
1259 public function getLabelHtml( $cellAttributes = [] ) {
1260 # Don't output a for= attribute for labels with no associated input.
1261 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1262 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1263
1264 $labelValue = trim( $this->getLabel() );
1265 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1266
1267 $displayFormat = $this->mParent->getDisplayFormat();
1268 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1269
1270 if ( $displayFormat === 'table' ) {
1271 return Html::rawElement( 'td',
1272 [ 'class' => 'mw-label' ] + $cellAttributes,
1273 Html::rawElement( 'label', $for, $labelValue ) );
1274 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1275 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1276 return Html::rawElement( 'div',
1277 [ 'class' => 'mw-label' ] + $cellAttributes,
1278 Html::rawElement( 'label', $for, $labelValue ) );
1279 } else {
1280 return Html::rawElement( 'label', $for, $labelValue );
1281 }
1282 }
1283
1284 return '';
1285 }
1286
1291 public function getDefault() {
1292 return $this->mDefault ?? null;
1293 }
1294
1300 public function getTooltipAndAccessKey() {
1301 if ( empty( $this->mParams['tooltip'] ) ) {
1302 return [];
1303 }
1304
1305 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1306 }
1307
1313 public function getTooltipAndAccessKeyOOUI() {
1314 if ( empty( $this->mParams['tooltip'] ) ) {
1315 return [];
1316 }
1317
1318 return [
1319 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1320 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1321 ];
1322 }
1323
1331 public function getAttributes( array $list ) {
1332 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1333
1334 $ret = [];
1335 foreach ( $list as $key ) {
1336 if ( in_array( $key, $boolAttribs ) ) {
1337 if ( !empty( $this->mParams[$key] ) ) {
1338 $ret[$key] = '';
1339 }
1340 } elseif ( isset( $this->mParams[$key] ) ) {
1341 $ret[$key] = $this->mParams[$key];
1342 }
1343 }
1344
1345 return $ret;
1346 }
1347
1357 private function lookupOptionsKeys( $options, $needsParse ) {
1358 $ret = [];
1359 foreach ( $options as $key => $value ) {
1360 $msg = $this->msg( $key );
1361 $msgAsText = $needsParse ? $msg->parse() : $msg->plain();
1362 if ( array_key_exists( $msgAsText, $ret ) ) {
1363 LoggerFactory::getInstance( 'translation-problem' )->error(
1364 'The option that uses the message key {msg_key_one} has the same translation as ' .
1365 'another option in {lang}. This means that {msg_key_one} will not be used as an option.',
1366 [
1367 'msg_key_one' => $key,
1368 'lang' => $this->mParent ?
1369 $this->mParent->getLanguageCode()->toBcp47Code() :
1370 RequestContext::getMain()->getLanguageCode()->toBcp47Code(),
1371 ]
1372 );
1373 continue;
1374 }
1375 $ret[$msgAsText] = is_array( $value )
1376 ? $this->lookupOptionsKeys( $value, $needsParse )
1377 : strval( $value );
1378 }
1379 return $ret;
1380 }
1381
1389 public static function forceToStringRecursive( $array ) {
1390 if ( is_array( $array ) ) {
1391 return array_map( self::forceToStringRecursive( ... ), $array );
1392 } else {
1393 return strval( $array );
1394 }
1395 }
1396
1403 public function getOptions() {
1404 if ( $this->mOptions === false ) {
1405 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1406 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1407 if ( $needsParse ) {
1408 $this->mOptionsLabelsNotFromMessage = true;
1409 }
1410 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1411 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1412 $this->mOptionsLabelsNotFromMessage = true;
1413 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1414 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1415 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1416 $this->mOptions = Html::listDropdownOptions( $message );
1417 } else {
1418 $this->mOptions = null;
1419 }
1420 }
1421
1422 return $this->mOptions;
1423 }
1424
1430 public function getOptionsOOUI() {
1431 $oldoptions = $this->getOptions();
1432
1433 if ( $oldoptions === null ) {
1434 return null;
1435 }
1436
1437 return Html::listDropdownOptionsOoui( $oldoptions );
1438 }
1439
1447 public static function flattenOptions( $options ) {
1448 $flatOpts = [];
1449
1450 foreach ( $options as $value ) {
1451 if ( is_array( $value ) ) {
1452 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1453 } else {
1454 $flatOpts[] = $value;
1455 }
1456 }
1457
1458 return $flatOpts;
1459 }
1460
1474 protected static function formatErrors( $errors ) {
1475 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1476 $errors = array_shift( $errors );
1477 }
1478
1479 if ( is_array( $errors ) ) {
1480 foreach ( $errors as &$error ) {
1481 $error = Html::rawElement( 'li', [],
1482 $error instanceof Message ? $error->parse() : $error
1483 );
1484 }
1485 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1486 } elseif ( $errors instanceof Message ) {
1487 $errors = $errors->parse();
1488 }
1489
1490 return Html::errorBox( $errors );
1491 }
1492
1499 protected function getMessage( $value ) {
1500 $message = Message::newFromSpecifier( $value );
1501
1502 if ( $this->mParent ) {
1503 $message->setContext( $this->mParent );
1504 }
1505
1506 return $message;
1507 }
1508
1516 public function skipLoadData( $request ) {
1517 return !empty( $this->mParams['nodata'] );
1518 }
1519
1528 // This is probably more restrictive than it needs to be, but better safe than sorry
1529 return (bool)$this->mCondState;
1530 }
1531
1543 protected function escapeLabel( $label ) {
1544 return $this->mOptionsLabelsNotFromMessage
1545 ? $label : htmlspecialchars( $label, ENT_NOQUOTES );
1546 }
1547
1559 protected function makeLabelSnippet( $label ) {
1560 return $this->mOptionsLabelsNotFromMessage
1561 ? new \OOUI\HtmlSnippet( $label ) : $label;
1562 }
1563}
1564
1566class_alias( HTMLFormField::class, 'HTMLFormField' );
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:69
Group all the pieces relevant to the context of a request into one instance.
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
The parent class to generate form fields.
parseCondStateForClient()
Parse the cond-state array for client-side.
getTooltipAndAccessKey()
Returns the attributes required for the tooltip and accesskey, for Html::element() etc.
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
getOptionsOOUI()
Get options and make them into arrays suitable for OOUI.
array $mCondState
Array to hold params for 'hide-if' or 'disable-if' statements.
getOOUI( $value)
Get the OOUI version of the div.
getDescriptionText()
Determine the help text to display.
makeLabelSnippet( $label)
The keys in the array returned by getOptions() can be either HTML or plain text depending on $this->m...
getName()
Get the field name that will be used for submission.
getNearestFieldValue( $alldata, $name, $asDisplay=false, $backCompat=false)
Fetch a field value from $alldata for the closest field matching a given name.
getHelpTextHtmlRaw( $helptext)
Generate help text HTML formatted for raw output.
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
getTooltipAndAccessKeyOOUI()
Returns the attributes required for the tooltip and accesskey, for OOUI widgets' config.
getNearestFieldByName( $alldata, $name, $asDisplay=false)
Fetch a field value from $alldata for the closest field matching a given name.
bool $mShowEmptyLabels
If true will generate an empty div element with no label.
getErrorsAndErrorClass( $value)
Determine form errors to display and their classes.
isHidden( $alldata)
Test whether this field is supposed to be hidden, based on the values of the other form fields.
isHelpInline()
Determine if the help text should be displayed inline.
getOOUIModules()
Get the list of extra ResourceLoader modules which must be loaded client-side before it's possible to...
loadDataFromRequest( $request)
Get the value that this input has been set to from a posted form, or the input's default value if it ...
__construct( $params)
Initialise the object.
getClassName()
Gets the non namespaced class name.
skipLoadData( $request)
Skip this field when collecting data.
static forceToStringRecursive( $array)
Recursively forces values in an array to strings, because issues arise with integer 0 as a value.
hasVisibleOutput()
If this field has a user-visible output or not.
getLabelAlignOOUI()
Get label alignment when generating field for OOUI.
validateCondState( $params)
Validate the cond-state params, the existence check of fields should be done later.
needsLabel()
Should this field have a label, or is there no input element with the appropriate id for the label to...
getOptions()
Fetch the array of options from the field's parameters.
cancelSubmit( $value, $alldata)
Override this function if the control can somehow trigger a form submission that shouldn't actually s...
getDiv( $value)
Get the complete div for the input, including help text, labels, and whatever.
getAttributes(array $list)
Returns the given attributes from the parameters.
getFieldLayoutOOUI( $inputField, $config)
Get a FieldLayout (or subclass thereof) to wrap this field in when using OOUI output.
getHelpText()
Determine the help text to display.
getCodex( $value)
Get the Codex version of the div.
parseCondState( $params)
Parse the cond-state array to use the field name for submission, since the key in the form descriptor...
getNearestField( $name, $backCompat=false)
Get the closest field matching a given name.
getTableRow( $value)
Get the complete table row for the input, including help text, labels, and whatever.
msg( $key,... $params)
Get a translated interface message.
getRaw( $value)
Get the complete raw fields for the input, including help text, labels, and whatever.
getInputHTML( $value)
This function must be implemented to return the HTML to generate the input object itself.
getErrorsRaw( $value)
Determine form errors to display, returning them in an array.
shouldInfuseOOUI()
Whether the field should be automatically infused.
needsJSForHtml5FormValidation()
Whether this field requires the user agent to have JavaScript enabled for the client-side HTML5 form ...
canDisplayErrors()
True if this field type is able to display errors; false if validation errors need to be displayed in...
getInputOOUI( $value)
Same as getInputHTML, but returns an OOUI object.
isSubmitAttempt(WebRequest $request)
Can we assume that the request is an attempt to submit a HTMLForm, as opposed to an attempt to just v...
string $mLabel
String label, as HTML.
getInputCodex( $value, $hasErrors)
Same as getInputHTML, but for Codex.
isDisabled( $alldata)
Test whether this field is supposed to be disabled, based on the values of the other form fields.
getDescriptionHtmlSpan(?string $descriptionHtml, array $cssClasses=[])
validate( $value, $alldata)
Override this function to add specific validation checks on the field input.
escapeLabel( $label)
The keys in the array returned by getOptions() can be either HTML or plain text depending on $this->m...
checkStateRecurse(array $alldata, array $params)
Helper function for isHidden and isDisabled to handle recursive data structures.
getHelpTextHtmlDiv( $helptext, $cssClasses=[])
Generate help text HTML in div format.
getHelpTextHtmlTable( $helptext)
Generate help text HTML in table format.
static formatErrors( $errors)
Formats one or more errors as accepted by field validation-callback.
setShowEmptyLabel( $show)
Tell the field whether to generate a separate label element if its label is blank.
getInline( $value)
Get the complete field as an inline element.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:207
This class is a collection of static functions that serve two purposes:
Definition Html.php:44
JSON formatter wrapper class.
Some internal bits split of from Skin.php.
Definition Linker.php:47
Create PSR-3 logger objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1125
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
getCheck( $name)
Return true if the named value is set in this web request's $_GET, $_POST or path router vars,...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:18
Value object representing a message parameter with one of the types from {.