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 $mVFormClass = '';
48 protected $mHelpClass = false;
50 protected $mDefault;
52 private $mNotices;
53
57 protected $mOptions = false;
63 protected $mCondState = [];
65 protected $mCondStateClass = [];
66
71 protected $mShowEmptyLabels = true;
72
76 public $mParent;
77
88 abstract public function getInputHTML( $value );
89
98 public function getInputOOUI( $value ) {
99 return false;
100 }
101
113 public function getInputCodex( $value, $hasErrors ) {
114 // If not overridden, fall back to getInputHTML()
115 return $this->getInputHTML( $value );
116 }
117
124 public function canDisplayErrors() {
125 return $this->hasVisibleOutput();
126 }
127
142 public function msg( $key, ...$params ) {
143 if ( $this->mParent ) {
144 return $this->mParent->msg( $key, ...$params );
145 }
146 return wfMessage( $key, ...$params );
147 }
148
156 public function hasVisibleOutput() {
157 return true;
158 }
159
166 public function getName() {
167 return $this->mName;
168 }
169
181 protected function getNearestField( $name, $backCompat = false ) {
182 // When the field is belong to a HTMLFormFieldCloner
183 $cloner = $this->mParams['cloner'] ?? null;
184 if ( $cloner instanceof HTMLFormFieldCloner ) {
185 $field = $cloner->findNearestField( $this, $name );
186 if ( $field ) {
187 return $field;
188 }
189 }
190
191 if ( $backCompat && str_starts_with( $name, 'wp' ) &&
192 !$this->mParent->hasField( $name )
193 ) {
194 // Don't break the existed use cases.
195 return $this->mParent->getField( substr( $name, 2 ) );
196 }
197 return $this->mParent->getField( $name );
198 }
199
211 protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
212 $field = $this->getNearestField( $name, $backCompat );
213 // When the field belongs to a HTMLFormFieldCloner
214 $cloner = $field->mParams['cloner'] ?? null;
215 if ( $cloner instanceof HTMLFormFieldCloner ) {
216 $value = $cloner->extractFieldData( $field, $alldata );
217 } else {
218 // Note $alldata is an empty array when first rendering a form with a formIdentifier.
219 // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
220 // field's default value
221 $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
222 }
223
224 // Check invert state for HTMLCheckField
225 if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
226 $value = !$value;
227 }
228
229 return $value;
230 }
231
242 protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
243 return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
244 }
245
252 protected function validateCondState( $params ) {
253 $origParams = $params;
254 $op = array_shift( $params );
255
256 $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException {
257 return new InvalidArgumentException(
258 "Invalid hide-if or disable-if specification for $this->mName: " .
259 $details . " in " . var_export( $origParams, true )
260 );
261 };
262
263 switch ( $op ) {
264 case 'NOT':
265 if ( count( $params ) !== 1 ) {
266 throw $makeException( "NOT takes exactly one parameter" );
267 }
268 // Fall-through intentionally
269
270 case 'AND':
271 case 'OR':
272 case 'NAND':
273 case 'NOR':
274 foreach ( $params as $i => $p ) {
275 if ( !is_array( $p ) ) {
276 $type = get_debug_type( $p );
277 throw $makeException( "Expected array, found $type at index $i" );
278 }
279 $this->validateCondState( $p );
280 }
281 break;
282
283 case '===':
284 case '!==':
285 case 'CONTAINS':
286 if ( count( $params ) !== 2 ) {
287 throw $makeException( "$op takes exactly two parameters" );
288 }
289 [ $name, $value ] = $params;
290 if ( !is_string( $name ) || !is_string( $value ) ) {
291 throw $makeException( "Parameters for $op must be strings" );
292 }
293 break;
294
295 default:
296 throw $makeException( "Unknown operation" );
297 }
298 }
299
307 protected function checkStateRecurse( array $alldata, array $params ) {
308 $op = array_shift( $params );
309 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
310 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
311
312 switch ( $op ) {
313 case 'AND':
314 case 'OR':
315 case 'NAND':
316 case 'NOR':
317 foreach ( $params as $p ) {
318 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
319 return !$valueRet[$op];
320 }
321 }
322 return $valueRet[$op];
323
324 case 'NOT':
325 return !$this->checkStateRecurse( $alldata, $params[0] );
326
327 case '===':
328 case '!==':
329 case 'CONTAINS':
330 [ $field, $value ] = $params;
331 $testValue = $this->getNearestFieldValue( $alldata, $field, true, true );
332 switch ( $op ) {
333 case '===':
334 return ( $value === (string)$testValue );
335 case '!==':
336 return ( $value !== (string)$testValue );
337 case 'CONTAINS':
338 return in_array( $value, $testValue, true );
339 }
340 }
341 }
342
351 protected function parseCondState( $params ) {
352 $op = array_shift( $params );
353
354 switch ( $op ) {
355 case 'AND':
356 case 'OR':
357 case 'NAND':
358 case 'NOR':
359 $ret = [ $op ];
360 foreach ( $params as $p ) {
361 $ret[] = $this->parseCondState( $p );
362 }
363 return $ret;
364
365 case 'NOT':
366 return [ 'NOT', $this->parseCondState( $params[0] ) ];
367
368 case '===':
369 case '!==':
370 case 'CONTAINS':
371 [ $name, $value ] = $params;
372 $field = $this->getNearestField( $name, true );
373 return [ $op, $field->getName(), $value ];
374 }
375 }
376
382 protected function parseCondStateForClient() {
383 $parsed = [];
384 foreach ( $this->mCondState as $type => $params ) {
385 $parsed[$type] = $this->parseCondState( $params );
386 }
387 return $parsed;
388 }
389
398 public function isHidden( $alldata ) {
399 return isset( $this->mCondState['hide'] ) &&
400 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
401 }
402
411 public function isDisabled( $alldata ) {
412 return ( $this->mParams['disabled'] ?? false ) ||
413 $this->isHidden( $alldata ) ||
414 ( isset( $this->mCondState['disable'] )
415 && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) );
416 }
417
429 public function cancelSubmit( $value, $alldata ) {
430 return false;
431 }
432
445 public function validate( $value, $alldata ) {
446 if ( $this->isHidden( $alldata ) ) {
447 return true;
448 }
449
450 if ( isset( $this->mParams['required'] )
451 && $this->mParams['required'] !== false
452 && ( $value === '' || $value === false || $value === null )
453 ) {
454 return $this->msg( 'htmlform-required' );
455 }
456
457 if ( $this->mValidationCallback === null ) {
458 return true;
459 }
460
461 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
462
463 if ( $p instanceof StatusValue ) {
464 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
465
466 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
467 }
468
469 return $p;
470 }
471
480 public function filter( $value, $alldata ) {
481 if ( $this->mFilterCallback !== null ) {
482 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
483 }
484
485 return $value;
486 }
487
495 protected function needsLabel() {
496 return true;
497 }
498
508 public function setShowEmptyLabel( $show ) {
509 $this->mShowEmptyLabels = $show;
510 }
511
523 protected function isSubmitAttempt( WebRequest $request ) {
524 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
525 return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) )
526 // The identifier matching or not has been checked in HTMLForm::prepareForm()
527 || $request->getCheck( 'wpFormIdentifier' );
528 }
529
538 public function loadDataFromRequest( $request ) {
539 if ( $request->getCheck( $this->mName ) ) {
540 return $request->getText( $this->mName );
541 } else {
542 return $this->getDefault();
543 }
544 }
545
554 public function __construct( $params ) {
555 $this->mParams = $params;
556
557 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
558 $this->mParent = $params['parent'];
559 } else {
560 // Normally parent is added automatically by HTMLForm::factory.
561 // Several field types already assume unconditionally this is always set,
562 // so deprecate manually creating an HTMLFormField without a parent form set.
564 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
565 "1.40"
566 );
567 }
568
569 # Generate the label from a message, if possible
570 if ( isset( $params['label-message'] ) ) {
571 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
572 } elseif ( isset( $params['label'] ) ) {
573 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
574 // Apparently some things set &nbsp directly and in an odd format
575 $this->mLabel = "\u{00A0}";
576 } else {
577 $this->mLabel = htmlspecialchars( $params['label'] );
578 }
579 } elseif ( isset( $params['label-raw'] ) ) {
580 $this->mLabel = $params['label-raw'];
581 }
582
583 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
584
585 if ( isset( $params['dir'] ) ) {
586 $this->mDir = $params['dir'];
587 }
588
589 $this->mID = "mw-input-{$this->mName}";
590
591 if ( isset( $params['default'] ) ) {
592 $this->mDefault = $params['default'];
593 }
594
595 if ( isset( $params['id'] ) ) {
596 $this->mID = $params['id'];
597 }
598
599 if ( isset( $params['cssclass'] ) ) {
600 $this->mClass = $params['cssclass'];
601 }
602
603 if ( isset( $params['csshelpclass'] ) ) {
604 $this->mHelpClass = $params['csshelpclass'];
605 }
606
607 if ( isset( $params['validation-callback'] ) ) {
608 $this->mValidationCallback = $params['validation-callback'];
609 }
610
611 if ( isset( $params['filter-callback'] ) ) {
612 $this->mFilterCallback = $params['filter-callback'];
613 }
614
615 if ( isset( $params['hidelabel'] ) ) {
616 $this->mShowEmptyLabels = false;
617 }
618 if ( isset( $params['notices'] ) ) {
619 $this->mNotices = $params['notices'];
620 }
621
622 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
623 $this->validateCondState( $params['hide-if'] );
624 $this->mCondState['hide'] = $params['hide-if'];
625 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
626 }
627 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
628 isset( $params['disable-if'] ) && $params['disable-if']
629 ) {
630 $this->validateCondState( $params['disable-if'] );
631 $this->mCondState['disable'] = $params['disable-if'];
632 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
633 }
634 }
635
645 public function getTableRow( $value ) {
646 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
647 $inputHtml = $this->getInputHTML( $value );
648 $fieldType = $this->getClassName();
649 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
650 $cellAttributes = [];
651 $rowAttributes = [];
652 $rowClasses = '';
653
654 if ( !empty( $this->mParams['vertical-label'] ) ) {
655 $cellAttributes['colspan'] = 2;
656 $verticalLabel = true;
657 } else {
658 $verticalLabel = false;
659 }
660
661 $label = $this->getLabelHtml( $cellAttributes );
662
663 $field = Html::rawElement(
664 'td',
665 [ 'class' => 'mw-input' ] + $cellAttributes,
666 $inputHtml . "\n$errors"
667 );
668
669 if ( $this->mCondState ) {
670 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
671 $rowClasses .= implode( ' ', $this->mCondStateClass );
672 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
673 $rowClasses .= ' mw-htmlform-hide-if-hidden';
674 }
675 }
676
677 if ( $verticalLabel ) {
678 $html = Html::rawElement( 'tr',
679 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
680 $html .= Html::rawElement( 'tr',
681 $rowAttributes + [
682 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
683 ],
684 $field );
685 } else {
686 $html = Html::rawElement( 'tr',
687 $rowAttributes + [
688 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
689 ],
690 $label . $field );
691 }
692
693 return $html . $helptext;
694 }
695
706 public function getDiv( $value ) {
707 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
708 $inputHtml = $this->getInputHTML( $value );
709 $fieldType = $this->getClassName();
710 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
711 $cellAttributes = [];
712 $label = $this->getLabelHtml( $cellAttributes );
713
714 $outerDivClass = [
715 'mw-input',
716 'mw-htmlform-nolabel' => ( $label === '' )
717 ];
718
719 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
720
721 if ( $horizontalLabel ) {
722 $field = "\u{00A0}" . $inputHtml . "\n$errors";
723 } else {
724 $field = Html::rawElement(
725 'div',
726 // @phan-suppress-next-line PhanUselessBinaryAddRight
727 [ 'class' => $outerDivClass ] + $cellAttributes,
728 $inputHtml . "\n$errors"
729 );
730 }
731
732 $wrapperAttributes = [ 'class' => [
733 "mw-htmlform-field-$fieldType",
736 $errorClass,
737 ] ];
738 if ( $this->mCondState ) {
739 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
740 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
741 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
742 $wrapperAttributes['class'][] = 'mw-htmlform-hide-if-hidden';
743 }
744 }
745 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
746 $helptext;
747 }
748
758 public function getOOUI( $value ) {
759 $inputField = $this->getInputOOUI( $value );
760
761 if ( !$inputField ) {
762 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
763 // generate the whole field, label and errors and all, then wrap it in a Widget.
764 // It might look weird, but it'll work OK.
765 return $this->getFieldLayoutOOUI(
766 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
767 [ 'align' => 'top' ]
768 );
769 }
770
771 $infusable = true;
772 if ( is_string( $inputField ) ) {
773 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
774 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
775 // JavaScript doesn't know how to rebuilt the contents.
776 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
777 $infusable = false;
778 }
779
780 $fieldType = $this->getClassName();
781 $help = $this->getHelpText();
782 $errors = $this->getErrorsRaw( $value );
783 foreach ( $errors as &$error ) {
784 $error = new \OOUI\HtmlSnippet( $error );
785 }
786
787 $config = [
788 'classes' => [ "mw-htmlform-field-$fieldType" ],
789 'align' => $this->getLabelAlignOOUI(),
790 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
791 'errors' => $errors,
792 'infusable' => $infusable,
793 'helpInline' => $this->isHelpInline(),
794 'notices' => $this->mNotices ?: [],
795 ];
796 if ( $this->mClass !== '' ) {
797 $config['classes'][] = $this->mClass;
798 }
799
800 $preloadModules = false;
801
802 if ( $infusable && $this->shouldInfuseOOUI() ) {
803 $preloadModules = true;
804 $config['classes'][] = 'mw-htmlform-autoinfuse';
805 }
806 if ( $this->mCondState ) {
807 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
808 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
809 $config['classes'][] = 'mw-htmlform-hide-if-hidden';
810 }
811 }
812
813 // the element could specify, that the label doesn't need to be added
814 $label = $this->getLabel();
815 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
816 $config['label'] = new \OOUI\HtmlSnippet( $label );
817 }
818
819 if ( $this->mCondState ) {
820 $preloadModules = true;
821 $config['condState'] = $this->parseCondStateForClient();
822 }
823
824 $config['modules'] = $this->getOOUIModules();
825
826 if ( $preloadModules ) {
827 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
828 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
829 }
830
831 return $this->getFieldLayoutOOUI( $inputField, $config );
832 }
833
841 public function getCodex( $value ) {
842 $isDisabled = ( $this->mParams['disabled'] ?? false );
843
844 // Label
845 $labelDiv = '';
846 $labelValue = trim( $this->getLabel() );
847 // For weird historical reasons, a non-breaking space is treated as an empty label
848 // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version
849 if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' ) {
850 $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
851 $labelClasses = [ 'cdx-label' ];
852 if ( $isDisabled ) {
853 $labelClasses[] = 'cdx-label--disabled';
854 }
855 // <div class="cdx-label">
856 $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ],
857 // <label class="cdx-label__label" for="ID">
858 Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor,
859 // <span class="cdx-label__label__text">
860 Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ],
861 $labelValue
862 )
863 )
864 );
865 }
866
867 // Help text
868 // <div class="cdx-field__help-text">
869 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] );
870
871 // Validation message
872 // <div class="cdx-field__validation-message">
873 // $errors is a <div class="cdx-message">
874 // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead
875 $validationMessage = '';
876 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
877 if ( $errors !== '' ) {
878 $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ],
879 $errors
880 );
881 }
882
883 // Control
884 $inputHtml = $this->getInputCodex( $value, $errors !== '' );
885 // <div class="cdx-field__control cdx-field__control--has-help-text">
886 $controlClasses = [ 'cdx-field__control' ];
887 if ( $helptext ) {
888 $controlClasses[] = 'cdx-field__control--has-help-text';
889 }
890 $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml );
891
892 // <div class="cdx-field">
893 $fieldClasses = [
894 "mw-htmlform-field-{$this->getClassName()}",
896 $errorClass,
897 'cdx-field'
898 ];
899 if ( $isDisabled ) {
900 $fieldClasses[] = 'cdx-field--disabled';
901 }
902 $fieldAttributes = [];
903 // Set data attribute and CSS class for client side handling of hide-if / disable-if
904 if ( $this->mCondState ) {
905 $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
906 $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass );
907 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
908 $fieldClasses[] = 'mw-htmlform-hide-if-hidden';
909 }
910 }
911
912 return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes,
913 $labelDiv . $control . $helptext . $validationMessage
914 );
915 }
916
924 protected function getClassName() {
925 $name = explode( '\\', static::class );
926 return end( $name );
927 }
928
934 protected function getLabelAlignOOUI() {
935 return 'top';
936 }
937
944 protected function getFieldLayoutOOUI( $inputField, $config ) {
945 return new HTMLFormFieldLayout( $inputField, $config );
946 }
947
956 protected function shouldInfuseOOUI() {
957 // Always infuse fields with popup help text, since the interface for it is nicer with JS
958 return !$this->isHelpInline() && $this->getHelpMessages();
959 }
960
968 protected function getOOUIModules() {
969 return [];
970 }
971
982 public function getRaw( $value ) {
983 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
984 return "\n" . $errors .
985 $this->getLabelHtml() .
986 $this->getInputHTML( $value ) .
987 $this->getHelpTextHtmlRaw( $this->getHelpText() );
988 }
989
1000 public function getVForm( $value ) {
1001 wfDeprecated( __METHOD__, '1.45' );
1002 // Ewwww
1003 $this->mVFormClass = ' mw-ui-vform-field';
1004 return $this->getDiv( $value );
1005 }
1006
1014 public function getInline( $value ) {
1015 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
1016 return "\n" . $errors .
1017 $this->getLabelHtml() .
1018 "\u{00A0}" .
1019 $this->getInputHTML( $value ) .
1020 $this->getHelpTextHtmlDiv( $this->getHelpText() );
1021 }
1022
1030 public function getHelpTextHtmlTable( $helptext ) {
1031 if ( $helptext === null ) {
1032 return '';
1033 }
1034
1035 $rowAttributes = [];
1036 if ( $this->mCondState ) {
1037 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1038 $rowAttributes['class'] = $this->mCondStateClass;
1039 }
1040
1041 $tdClasses = [ 'htmlform-tip' ];
1042 if ( $this->mHelpClass !== false ) {
1043 $tdClasses[] = $this->mHelpClass;
1044 }
1045 return Html::rawElement( 'tr', $rowAttributes,
1046 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
1047 );
1048 }
1049
1059 public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) {
1060 if ( $helptext === null ) {
1061 return '';
1062 }
1063
1064 $wrapperAttributes = [
1065 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ),
1066 ];
1067 if ( $this->mHelpClass !== false ) {
1068 $wrapperAttributes['class'][] = $this->mHelpClass;
1069 }
1070 if ( $this->mCondState ) {
1071 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1072 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
1073 }
1074 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
1075 }
1076
1084 public function getHelpTextHtmlRaw( $helptext ) {
1085 return $this->getHelpTextHtmlDiv( $helptext );
1086 }
1087
1088 private function getHelpMessages(): array {
1089 if ( isset( $this->mParams['help-message'] ) ) {
1090 return [ $this->mParams['help-message'] ];
1091 } elseif ( isset( $this->mParams['help-messages'] ) ) {
1092 return $this->mParams['help-messages'];
1093 } elseif ( isset( $this->mParams['help-raw'] ) ) {
1094 return [ new HtmlArmor( $this->mParams['help-raw'] ) ];
1095 } elseif ( isset( $this->mParams['help'] ) ) {
1096 // @deprecated since 1.43, use 'help-raw' key instead
1097 return [ new HtmlArmor( $this->mParams['help'] ) ];
1098 }
1099
1100 return [];
1101 }
1102
1109 public function getHelpText() {
1110 $html = [];
1111
1112 foreach ( $this->getHelpMessages() as $msg ) {
1113 if ( $msg instanceof HtmlArmor ) {
1114 $html[] = HtmlArmor::getHtml( $msg );
1115 } else {
1116 $msg = $this->getMessage( $msg );
1117 if ( $msg->exists() ) {
1118 $html[] = $msg->parse();
1119 }
1120 }
1121 }
1122
1123 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1124 }
1125
1134 public function isHelpInline() {
1135 return $this->mParams['help-inline'] ?? true;
1136 }
1137
1150 public function getErrorsAndErrorClass( $value ) {
1151 $errors = $this->validate( $value, $this->mParent->mFieldData );
1152
1153 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1154 return [ '', '' ];
1155 }
1156
1157 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1158 }
1159
1167 public function getErrorsRaw( $value ) {
1168 $errors = $this->validate( $value, $this->mParent->mFieldData );
1169
1170 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1171 return [];
1172 }
1173
1174 if ( !is_array( $errors ) ) {
1175 $errors = [ $errors ];
1176 }
1177 foreach ( $errors as &$error ) {
1178 if ( $error instanceof Message ) {
1179 $error = $error->parse();
1180 }
1181 }
1182
1183 return $errors;
1184 }
1185
1190 public function getLabel() {
1191 return $this->mLabel ?? '';
1192 }
1193
1200 public function getLabelHtml( $cellAttributes = [] ) {
1201 # Don't output a for= attribute for labels with no associated input.
1202 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1203 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1204
1205 $labelValue = trim( $this->getLabel() );
1206 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1207
1208 $displayFormat = $this->mParent->getDisplayFormat();
1209 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1210
1211 if ( $displayFormat === 'table' ) {
1212 return Html::rawElement( 'td',
1213 [ 'class' => 'mw-label' ] + $cellAttributes,
1214 Html::rawElement( 'label', $for, $labelValue ) );
1215 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1216 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1217 return Html::rawElement( 'div',
1218 [ 'class' => 'mw-label' ] + $cellAttributes,
1219 Html::rawElement( 'label', $for, $labelValue ) );
1220 } else {
1221 return Html::rawElement( 'label', $for, $labelValue );
1222 }
1223 }
1224
1225 return '';
1226 }
1227
1232 public function getDefault() {
1233 return $this->mDefault ?? null;
1234 }
1235
1241 public function getTooltipAndAccessKey() {
1242 if ( empty( $this->mParams['tooltip'] ) ) {
1243 return [];
1244 }
1245
1246 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1247 }
1248
1254 public function getTooltipAndAccessKeyOOUI() {
1255 if ( empty( $this->mParams['tooltip'] ) ) {
1256 return [];
1257 }
1258
1259 return [
1260 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1261 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1262 ];
1263 }
1264
1272 public function getAttributes( array $list ) {
1273 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1274
1275 $ret = [];
1276 foreach ( $list as $key ) {
1277 if ( in_array( $key, $boolAttribs ) ) {
1278 if ( !empty( $this->mParams[$key] ) ) {
1279 $ret[$key] = '';
1280 }
1281 } elseif ( isset( $this->mParams[$key] ) ) {
1282 $ret[$key] = $this->mParams[$key];
1283 }
1284 }
1285
1286 return $ret;
1287 }
1288
1298 private function lookupOptionsKeys( $options, $needsParse ) {
1299 $ret = [];
1300 foreach ( $options as $key => $value ) {
1301 $msg = $this->msg( $key );
1302 $msgAsText = $needsParse ? $msg->parse() : $msg->plain();
1303 if ( array_key_exists( $msgAsText, $ret ) ) {
1304 LoggerFactory::getInstance( 'translation-problem' )->error(
1305 'The option that uses the message key {msg_key_one} has the same translation as ' .
1306 'another option in {lang}. This means that {msg_key_one} will not be used as an option.',
1307 [
1308 'msg_key_one' => $key,
1309 'lang' => $this->mParent ?
1310 $this->mParent->getLanguageCode()->toBcp47Code() :
1311 RequestContext::getMain()->getLanguageCode()->toBcp47Code(),
1312 ]
1313 );
1314 continue;
1315 }
1316 $ret[$msgAsText] = is_array( $value )
1317 ? $this->lookupOptionsKeys( $value, $needsParse )
1318 : strval( $value );
1319 }
1320 return $ret;
1321 }
1322
1330 public static function forceToStringRecursive( $array ) {
1331 if ( is_array( $array ) ) {
1332 return array_map( self::forceToStringRecursive( ... ), $array );
1333 } else {
1334 return strval( $array );
1335 }
1336 }
1337
1344 public function getOptions() {
1345 if ( $this->mOptions === false ) {
1346 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1347 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1348 if ( $needsParse ) {
1349 $this->mOptionsLabelsNotFromMessage = true;
1350 }
1351 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1352 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1353 $this->mOptionsLabelsNotFromMessage = true;
1354 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1355 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1356 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1357 $this->mOptions = Html::listDropdownOptions( $message );
1358 } else {
1359 $this->mOptions = null;
1360 }
1361 }
1362
1363 return $this->mOptions;
1364 }
1365
1371 public function getOptionsOOUI() {
1372 $oldoptions = $this->getOptions();
1373
1374 if ( $oldoptions === null ) {
1375 return null;
1376 }
1377
1378 return Html::listDropdownOptionsOoui( $oldoptions );
1379 }
1380
1388 public static function flattenOptions( $options ) {
1389 $flatOpts = [];
1390
1391 foreach ( $options as $value ) {
1392 if ( is_array( $value ) ) {
1393 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1394 } else {
1395 $flatOpts[] = $value;
1396 }
1397 }
1398
1399 return $flatOpts;
1400 }
1401
1415 protected static function formatErrors( $errors ) {
1416 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1417 $errors = array_shift( $errors );
1418 }
1419
1420 if ( is_array( $errors ) ) {
1421 foreach ( $errors as &$error ) {
1422 $error = Html::rawElement( 'li', [],
1423 $error instanceof Message ? $error->parse() : $error
1424 );
1425 }
1426 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1427 } elseif ( $errors instanceof Message ) {
1428 $errors = $errors->parse();
1429 }
1430
1431 return Html::errorBox( $errors );
1432 }
1433
1440 protected function getMessage( $value ) {
1441 $message = Message::newFromSpecifier( $value );
1442
1443 if ( $this->mParent ) {
1444 $message->setContext( $this->mParent );
1445 }
1446
1447 return $message;
1448 }
1449
1457 public function skipLoadData( $request ) {
1458 return !empty( $this->mParams['nodata'] );
1459 }
1460
1469 // This is probably more restrictive than it needs to be, but better safe than sorry
1470 return (bool)$this->mCondState;
1471 }
1472
1484 protected function escapeLabel( $label ) {
1485 return $this->mOptionsLabelsNotFromMessage
1486 ? $label : htmlspecialchars( $label, ENT_NOQUOTES );
1487 }
1488
1500 protected function makeLabelSnippet( $label ) {
1501 return $this->mOptionsLabelsNotFromMessage
1502 ? new \OOUI\HtmlSnippet( $label ) : $label;
1503 }
1504}
1505
1507class_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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:68
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.
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.
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.
getVForm( $value)
Get the complete field for the input, including help text, labels, and whatever.
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:195
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
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
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:492
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1122
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 {.