MediaWiki master
HTMLFormField.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\HTMLForm;
4
5use HtmlArmor;
6use InvalidArgumentException;
17use 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 if ( count( $params ) !== 2 ) {
286 throw $makeException( "$op takes exactly two parameters" );
287 }
288 [ $name, $value ] = $params;
289 if ( !is_string( $name ) || !is_string( $value ) ) {
290 throw $makeException( "Parameters for $op must be strings" );
291 }
292 break;
293
294 default:
295 throw $makeException( "Unknown operation" );
296 }
297 }
298
306 protected function checkStateRecurse( array $alldata, array $params ) {
307 $op = array_shift( $params );
308 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
309 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
310
311 switch ( $op ) {
312 case 'AND':
313 case 'OR':
314 case 'NAND':
315 case 'NOR':
316 foreach ( $params as $p ) {
317 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
318 return !$valueRet[$op];
319 }
320 }
321 return $valueRet[$op];
322
323 case 'NOT':
324 return !$this->checkStateRecurse( $alldata, $params[0] );
325
326 case '===':
327 case '!==':
328 [ $field, $value ] = $params;
329 $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true );
330 switch ( $op ) {
331 case '===':
332 return ( $value === $testValue );
333 case '!==':
334 return ( $value !== $testValue );
335 }
336 }
337 }
338
347 protected function parseCondState( $params ) {
348 $op = array_shift( $params );
349
350 switch ( $op ) {
351 case 'AND':
352 case 'OR':
353 case 'NAND':
354 case 'NOR':
355 $ret = [ $op ];
356 foreach ( $params as $p ) {
357 $ret[] = $this->parseCondState( $p );
358 }
359 return $ret;
360
361 case 'NOT':
362 return [ 'NOT', $this->parseCondState( $params[0] ) ];
363
364 case '===':
365 case '!==':
366 [ $name, $value ] = $params;
367 $field = $this->getNearestField( $name, true );
368 return [ $op, $field->getName(), $value ];
369 }
370 }
371
377 protected function parseCondStateForClient() {
378 $parsed = [];
379 foreach ( $this->mCondState as $type => $params ) {
380 $parsed[$type] = $this->parseCondState( $params );
381 }
382 return $parsed;
383 }
384
393 public function isHidden( $alldata ) {
394 return isset( $this->mCondState['hide'] ) &&
395 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
396 }
397
406 public function isDisabled( $alldata ) {
407 return ( $this->mParams['disabled'] ?? false ) ||
408 $this->isHidden( $alldata ) ||
409 ( isset( $this->mCondState['disable'] )
410 && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) );
411 }
412
424 public function cancelSubmit( $value, $alldata ) {
425 return false;
426 }
427
440 public function validate( $value, $alldata ) {
441 if ( $this->isHidden( $alldata ) ) {
442 return true;
443 }
444
445 if ( isset( $this->mParams['required'] )
446 && $this->mParams['required'] !== false
447 && ( $value === '' || $value === false || $value === null )
448 ) {
449 return $this->msg( 'htmlform-required' );
450 }
451
452 if ( $this->mValidationCallback === null ) {
453 return true;
454 }
455
456 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
457
458 if ( $p instanceof StatusValue ) {
459 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
460
461 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
462 }
463
464 return $p;
465 }
466
475 public function filter( $value, $alldata ) {
476 if ( $this->mFilterCallback !== null ) {
477 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
478 }
479
480 return $value;
481 }
482
490 protected function needsLabel() {
491 return true;
492 }
493
503 public function setShowEmptyLabel( $show ) {
504 $this->mShowEmptyLabels = $show;
505 }
506
518 protected function isSubmitAttempt( WebRequest $request ) {
519 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
520 return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) )
521 // The identifier matching or not has been checked in HTMLForm::prepareForm()
522 || $request->getCheck( 'wpFormIdentifier' );
523 }
524
533 public function loadDataFromRequest( $request ) {
534 if ( $request->getCheck( $this->mName ) ) {
535 return $request->getText( $this->mName );
536 } else {
537 return $this->getDefault();
538 }
539 }
540
549 public function __construct( $params ) {
550 $this->mParams = $params;
551
552 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
553 $this->mParent = $params['parent'];
554 } else {
555 // Normally parent is added automatically by HTMLForm::factory.
556 // Several field types already assume unconditionally this is always set,
557 // so deprecate manually creating an HTMLFormField without a parent form set.
559 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
560 "1.40"
561 );
562 }
563
564 # Generate the label from a message, if possible
565 if ( isset( $params['label-message'] ) ) {
566 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
567 } elseif ( isset( $params['label'] ) ) {
568 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
569 // Apparently some things set &nbsp directly and in an odd format
570 $this->mLabel = "\u{00A0}";
571 } else {
572 $this->mLabel = htmlspecialchars( $params['label'] );
573 }
574 } elseif ( isset( $params['label-raw'] ) ) {
575 $this->mLabel = $params['label-raw'];
576 }
577
578 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
579
580 if ( isset( $params['dir'] ) ) {
581 $this->mDir = $params['dir'];
582 }
583
584 $this->mID = "mw-input-{$this->mName}";
585
586 if ( isset( $params['default'] ) ) {
587 $this->mDefault = $params['default'];
588 }
589
590 if ( isset( $params['id'] ) ) {
591 $this->mID = $params['id'];
592 }
593
594 if ( isset( $params['cssclass'] ) ) {
595 $this->mClass = $params['cssclass'];
596 }
597
598 if ( isset( $params['csshelpclass'] ) ) {
599 $this->mHelpClass = $params['csshelpclass'];
600 }
601
602 if ( isset( $params['validation-callback'] ) ) {
603 $this->mValidationCallback = $params['validation-callback'];
604 }
605
606 if ( isset( $params['filter-callback'] ) ) {
607 $this->mFilterCallback = $params['filter-callback'];
608 }
609
610 if ( isset( $params['hidelabel'] ) ) {
611 $this->mShowEmptyLabels = false;
612 }
613 if ( isset( $params['notices'] ) ) {
614 $this->mNotices = $params['notices'];
615 }
616
617 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
618 $this->validateCondState( $params['hide-if'] );
619 $this->mCondState['hide'] = $params['hide-if'];
620 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
621 }
622 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
623 isset( $params['disable-if'] ) && $params['disable-if']
624 ) {
625 $this->validateCondState( $params['disable-if'] );
626 $this->mCondState['disable'] = $params['disable-if'];
627 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
628 }
629 }
630
640 public function getTableRow( $value ) {
641 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
642 $inputHtml = $this->getInputHTML( $value );
643 $fieldType = $this->getClassName();
644 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
645 $cellAttributes = [];
646 $rowAttributes = [];
647 $rowClasses = '';
648
649 if ( !empty( $this->mParams['vertical-label'] ) ) {
650 $cellAttributes['colspan'] = 2;
651 $verticalLabel = true;
652 } else {
653 $verticalLabel = false;
654 }
655
656 $label = $this->getLabelHtml( $cellAttributes );
657
658 $field = Html::rawElement(
659 'td',
660 [ 'class' => 'mw-input' ] + $cellAttributes,
661 $inputHtml . "\n$errors"
662 );
663
664 if ( $this->mCondState ) {
665 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
666 $rowClasses .= implode( ' ', $this->mCondStateClass );
667 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
668 $rowClasses .= ' mw-htmlform-hide-if-hidden';
669 }
670 }
671
672 if ( $verticalLabel ) {
673 $html = Html::rawElement( 'tr',
674 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
675 $html .= Html::rawElement( 'tr',
676 $rowAttributes + [
677 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
678 ],
679 $field );
680 } else {
681 $html = Html::rawElement( 'tr',
682 $rowAttributes + [
683 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
684 ],
685 $label . $field );
686 }
687
688 return $html . $helptext;
689 }
690
701 public function getDiv( $value ) {
702 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
703 $inputHtml = $this->getInputHTML( $value );
704 $fieldType = $this->getClassName();
705 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
706 $cellAttributes = [];
707 $label = $this->getLabelHtml( $cellAttributes );
708
709 $outerDivClass = [
710 'mw-input',
711 'mw-htmlform-nolabel' => ( $label === '' )
712 ];
713
714 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
715
716 if ( $horizontalLabel ) {
717 $field = "\u{00A0}" . $inputHtml . "\n$errors";
718 } else {
719 $field = Html::rawElement(
720 'div',
721 // @phan-suppress-next-line PhanUselessBinaryAddRight
722 [ 'class' => $outerDivClass ] + $cellAttributes,
723 $inputHtml . "\n$errors"
724 );
725 }
726
727 $wrapperAttributes = [ 'class' => [
728 "mw-htmlform-field-$fieldType",
731 $errorClass,
732 ] ];
733 if ( $this->mCondState ) {
734 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
735 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
736 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
737 $wrapperAttributes['class'][] = 'mw-htmlform-hide-if-hidden';
738 }
739 }
740 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
741 $helptext;
742 }
743
753 public function getOOUI( $value ) {
754 $inputField = $this->getInputOOUI( $value );
755
756 if ( !$inputField ) {
757 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
758 // generate the whole field, label and errors and all, then wrap it in a Widget.
759 // It might look weird, but it'll work OK.
760 return $this->getFieldLayoutOOUI(
761 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
762 [ 'align' => 'top' ]
763 );
764 }
765
766 $infusable = true;
767 if ( is_string( $inputField ) ) {
768 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
769 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
770 // JavaScript doesn't know how to rebuilt the contents.
771 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
772 $infusable = false;
773 }
774
775 $fieldType = $this->getClassName();
776 $help = $this->getHelpText();
777 $errors = $this->getErrorsRaw( $value );
778 foreach ( $errors as &$error ) {
779 $error = new \OOUI\HtmlSnippet( $error );
780 }
781
782 $config = [
783 'classes' => [ "mw-htmlform-field-$fieldType" ],
784 'align' => $this->getLabelAlignOOUI(),
785 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
786 'errors' => $errors,
787 'infusable' => $infusable,
788 'helpInline' => $this->isHelpInline(),
789 'notices' => $this->mNotices ?: [],
790 ];
791 if ( $this->mClass !== '' ) {
792 $config['classes'][] = $this->mClass;
793 }
794
795 $preloadModules = false;
796
797 if ( $infusable && $this->shouldInfuseOOUI() ) {
798 $preloadModules = true;
799 $config['classes'][] = 'mw-htmlform-autoinfuse';
800 }
801 if ( $this->mCondState ) {
802 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
803 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
804 $config['classes'][] = 'mw-htmlform-hide-if-hidden';
805 }
806 }
807
808 // the element could specify, that the label doesn't need to be added
809 $label = $this->getLabel();
810 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
811 $config['label'] = new \OOUI\HtmlSnippet( $label );
812 }
813
814 if ( $this->mCondState ) {
815 $preloadModules = true;
816 $config['condState'] = $this->parseCondStateForClient();
817 }
818
819 $config['modules'] = $this->getOOUIModules();
820
821 if ( $preloadModules ) {
822 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
823 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
824 }
825
826 return $this->getFieldLayoutOOUI( $inputField, $config );
827 }
828
836 public function getCodex( $value ) {
837 $isDisabled = ( $this->mParams['disabled'] ?? false );
838
839 // Label
840 $labelDiv = '';
841 $labelValue = trim( $this->getLabel() );
842 // For weird historical reasons, a non-breaking space is treated as an empty label
843 // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version
844 if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' ) {
845 $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
846 $labelClasses = [ 'cdx-label' ];
847 if ( $isDisabled ) {
848 $labelClasses[] = 'cdx-label--disabled';
849 }
850 // <div class="cdx-label">
851 $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ],
852 // <label class="cdx-label__label" for="ID">
853 Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor,
854 // <span class="cdx-label__label__text">
855 Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ],
856 $labelValue
857 )
858 )
859 );
860 }
861
862 // Help text
863 // <div class="cdx-field__help-text">
864 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] );
865
866 // Validation message
867 // <div class="cdx-field__validation-message">
868 // $errors is a <div class="cdx-message">
869 // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead
870 $validationMessage = '';
871 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
872 if ( $errors !== '' ) {
873 $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ],
874 $errors
875 );
876 }
877
878 // Control
879 $inputHtml = $this->getInputCodex( $value, $errors !== '' );
880 // <div class="cdx-field__control cdx-field__control--has-help-text">
881 $controlClasses = [ 'cdx-field__control' ];
882 if ( $helptext ) {
883 $controlClasses[] = 'cdx-field__control--has-help-text';
884 }
885 $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml );
886
887 // <div class="cdx-field">
888 $fieldClasses = [
889 "mw-htmlform-field-{$this->getClassName()}",
891 $errorClass,
892 'cdx-field'
893 ];
894 if ( $isDisabled ) {
895 $fieldClasses[] = 'cdx-field--disabled';
896 }
897 $fieldAttributes = [];
898 // Set data attribute and CSS class for client side handling of hide-if / disable-if
899 if ( $this->mCondState ) {
900 $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
901 $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass );
902 if ( $this->isHidden( $this->mParent->mFieldData ) ) {
903 $fieldClasses[] = 'mw-htmlform-hide-if-hidden';
904 }
905 }
906
907 return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes,
908 $labelDiv . $control . $helptext . $validationMessage
909 );
910 }
911
919 protected function getClassName() {
920 $name = explode( '\\', static::class );
921 return end( $name );
922 }
923
929 protected function getLabelAlignOOUI() {
930 return 'top';
931 }
932
939 protected function getFieldLayoutOOUI( $inputField, $config ) {
940 return new HTMLFormFieldLayout( $inputField, $config );
941 }
942
951 protected function shouldInfuseOOUI() {
952 // Always infuse fields with popup help text, since the interface for it is nicer with JS
953 return !$this->isHelpInline() && $this->getHelpMessages();
954 }
955
963 protected function getOOUIModules() {
964 return [];
965 }
966
977 public function getRaw( $value ) {
978 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
979 return "\n" . $errors .
980 $this->getLabelHtml() .
981 $this->getInputHTML( $value ) .
982 $this->getHelpTextHtmlRaw( $this->getHelpText() );
983 }
984
994 public function getVForm( $value ) {
995 // Ewwww
996 $this->mVFormClass = ' mw-ui-vform-field';
997 return $this->getDiv( $value );
998 }
999
1007 public function getInline( $value ) {
1008 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
1009 return "\n" . $errors .
1010 $this->getLabelHtml() .
1011 "\u{00A0}" .
1012 $this->getInputHTML( $value ) .
1013 $this->getHelpTextHtmlDiv( $this->getHelpText() );
1014 }
1015
1023 public function getHelpTextHtmlTable( $helptext ) {
1024 if ( $helptext === null ) {
1025 return '';
1026 }
1027
1028 $rowAttributes = [];
1029 if ( $this->mCondState ) {
1030 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1031 $rowAttributes['class'] = $this->mCondStateClass;
1032 }
1033
1034 $tdClasses = [ 'htmlform-tip' ];
1035 if ( $this->mHelpClass !== false ) {
1036 $tdClasses[] = $this->mHelpClass;
1037 }
1038 return Html::rawElement( 'tr', $rowAttributes,
1039 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
1040 );
1041 }
1042
1052 public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) {
1053 if ( $helptext === null ) {
1054 return '';
1055 }
1056
1057 $wrapperAttributes = [
1058 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ),
1059 ];
1060 if ( $this->mHelpClass !== false ) {
1061 $wrapperAttributes['class'][] = $this->mHelpClass;
1062 }
1063 if ( $this->mCondState ) {
1064 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1065 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
1066 }
1067 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
1068 }
1069
1077 public function getHelpTextHtmlRaw( $helptext ) {
1078 return $this->getHelpTextHtmlDiv( $helptext );
1079 }
1080
1081 private function getHelpMessages(): array {
1082 if ( isset( $this->mParams['help-message'] ) ) {
1083 return [ $this->mParams['help-message'] ];
1084 } elseif ( isset( $this->mParams['help-messages'] ) ) {
1085 return $this->mParams['help-messages'];
1086 } elseif ( isset( $this->mParams['help-raw'] ) ) {
1087 return [ new HtmlArmor( $this->mParams['help-raw'] ) ];
1088 } elseif ( isset( $this->mParams['help'] ) ) {
1089 // @deprecated since 1.43, use 'help-raw' key instead
1090 return [ new HtmlArmor( $this->mParams['help'] ) ];
1091 }
1092
1093 return [];
1094 }
1095
1102 public function getHelpText() {
1103 $html = [];
1104
1105 foreach ( $this->getHelpMessages() as $msg ) {
1106 if ( $msg instanceof HtmlArmor ) {
1107 $html[] = HtmlArmor::getHtml( $msg );
1108 } else {
1109 $msg = $this->getMessage( $msg );
1110 if ( $msg->exists() ) {
1111 $html[] = $msg->parse();
1112 }
1113 }
1114 }
1115
1116 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1117 }
1118
1127 public function isHelpInline() {
1128 return $this->mParams['help-inline'] ?? true;
1129 }
1130
1143 public function getErrorsAndErrorClass( $value ) {
1144 $errors = $this->validate( $value, $this->mParent->mFieldData );
1145
1146 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1147 return [ '', '' ];
1148 }
1149
1150 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1151 }
1152
1160 public function getErrorsRaw( $value ) {
1161 $errors = $this->validate( $value, $this->mParent->mFieldData );
1162
1163 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1164 return [];
1165 }
1166
1167 if ( !is_array( $errors ) ) {
1168 $errors = [ $errors ];
1169 }
1170 foreach ( $errors as &$error ) {
1171 if ( $error instanceof Message ) {
1172 $error = $error->parse();
1173 }
1174 }
1175
1176 return $errors;
1177 }
1178
1183 public function getLabel() {
1184 return $this->mLabel ?? '';
1185 }
1186
1193 public function getLabelHtml( $cellAttributes = [] ) {
1194 # Don't output a for= attribute for labels with no associated input.
1195 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1196 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1197
1198 $labelValue = trim( $this->getLabel() );
1199 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1200
1201 $displayFormat = $this->mParent->getDisplayFormat();
1202 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1203
1204 if ( $displayFormat === 'table' ) {
1205 return Html::rawElement( 'td',
1206 [ 'class' => 'mw-label' ] + $cellAttributes,
1207 Html::rawElement( 'label', $for, $labelValue ) );
1208 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1209 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1210 return Html::rawElement( 'div',
1211 [ 'class' => 'mw-label' ] + $cellAttributes,
1212 Html::rawElement( 'label', $for, $labelValue ) );
1213 } else {
1214 return Html::rawElement( 'label', $for, $labelValue );
1215 }
1216 }
1217
1218 return '';
1219 }
1220
1225 public function getDefault() {
1226 return $this->mDefault ?? null;
1227 }
1228
1234 public function getTooltipAndAccessKey() {
1235 if ( empty( $this->mParams['tooltip'] ) ) {
1236 return [];
1237 }
1238
1239 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1240 }
1241
1247 public function getTooltipAndAccessKeyOOUI() {
1248 if ( empty( $this->mParams['tooltip'] ) ) {
1249 return [];
1250 }
1251
1252 return [
1253 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1254 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1255 ];
1256 }
1257
1265 public function getAttributes( array $list ) {
1266 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1267
1268 $ret = [];
1269 foreach ( $list as $key ) {
1270 if ( in_array( $key, $boolAttribs ) ) {
1271 if ( !empty( $this->mParams[$key] ) ) {
1272 $ret[$key] = '';
1273 }
1274 } elseif ( isset( $this->mParams[$key] ) ) {
1275 $ret[$key] = $this->mParams[$key];
1276 }
1277 }
1278
1279 return $ret;
1280 }
1281
1291 private function lookupOptionsKeys( $options, $needsParse ) {
1292 $ret = [];
1293 foreach ( $options as $key => $value ) {
1294 $msg = $this->msg( $key );
1295 $msgAsText = $needsParse ? $msg->parse() : $msg->plain();
1296 if ( array_key_exists( $msgAsText, $ret ) ) {
1297 LoggerFactory::getInstance( 'translation-problem' )->error(
1298 'The option that uses the message key {msg_key_one} has the same translation as ' .
1299 'another option in {lang}. This means that {msg_key_one} will not be used as an option.',
1300 [
1301 'msg_key_one' => $key,
1302 'lang' => $this->mParent ?
1303 $this->mParent->getLanguageCode()->toBcp47Code() :
1304 RequestContext::getMain()->getLanguageCode()->toBcp47Code(),
1305 ]
1306 );
1307 continue;
1308 }
1309 $ret[$msgAsText] = is_array( $value )
1310 ? $this->lookupOptionsKeys( $value, $needsParse )
1311 : strval( $value );
1312 }
1313 return $ret;
1314 }
1315
1323 public static function forceToStringRecursive( $array ) {
1324 if ( is_array( $array ) ) {
1325 return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1326 } else {
1327 return strval( $array );
1328 }
1329 }
1330
1337 public function getOptions() {
1338 if ( $this->mOptions === false ) {
1339 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1340 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1341 if ( $needsParse ) {
1342 $this->mOptionsLabelsNotFromMessage = true;
1343 }
1344 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1345 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1346 $this->mOptionsLabelsNotFromMessage = true;
1347 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1348 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1349 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1350 $this->mOptions = Html::listDropdownOptions( $message );
1351 } else {
1352 $this->mOptions = null;
1353 }
1354 }
1355
1356 return $this->mOptions;
1357 }
1358
1364 public function getOptionsOOUI() {
1365 $oldoptions = $this->getOptions();
1366
1367 if ( $oldoptions === null ) {
1368 return null;
1369 }
1370
1371 return Html::listDropdownOptionsOoui( $oldoptions );
1372 }
1373
1381 public static function flattenOptions( $options ) {
1382 $flatOpts = [];
1383
1384 foreach ( $options as $value ) {
1385 if ( is_array( $value ) ) {
1386 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1387 } else {
1388 $flatOpts[] = $value;
1389 }
1390 }
1391
1392 return $flatOpts;
1393 }
1394
1408 protected static function formatErrors( $errors ) {
1409 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1410 $errors = array_shift( $errors );
1411 }
1412
1413 if ( is_array( $errors ) ) {
1414 foreach ( $errors as &$error ) {
1415 $error = Html::rawElement( 'li', [],
1416 $error instanceof Message ? $error->parse() : $error
1417 );
1418 }
1419 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1420 } elseif ( $errors instanceof Message ) {
1421 $errors = $errors->parse();
1422 }
1423
1424 return Html::errorBox( $errors );
1425 }
1426
1433 protected function getMessage( $value ) {
1434 $message = Message::newFromSpecifier( $value );
1435
1436 if ( $this->mParent ) {
1437 $message->setContext( $this->mParent );
1438 }
1439
1440 return $message;
1441 }
1442
1450 public function skipLoadData( $request ) {
1451 return !empty( $this->mParams['nodata'] );
1452 }
1453
1462 // This is probably more restrictive than it needs to be, but better safe than sorry
1463 return (bool)$this->mCondState;
1464 }
1465}
1466
1468class_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.
array $params
The job parameters.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
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.
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.
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:209
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
JSON formatter wrapper class.
Some internal bits split of from Skin.php.
Definition Linker.php:63
Create PSR-3 logger objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:155
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:471
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1091
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:54
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Value object representing a message parameter that consists of a list of values.