MediaWiki master
HTMLFormField.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\HTMLForm;
4
5use HtmlArmor;
6use InvalidArgumentException;
17use StatusValue;
19
26abstract class HTMLFormField {
28 public $mParams;
29
31 protected $mValidationCallback;
33 protected $mFilterCallback;
35 protected $mName;
37 protected $mDir;
39 protected $mLabel;
41 protected $mID;
43 protected $mClass = '';
45 protected $mVFormClass = '';
47 protected $mHelpClass = false;
49 protected $mDefault;
51 private $mNotices;
52
56 protected $mOptions = false;
62 protected $mCondState = [];
64 protected $mCondStateClass = [];
65
70 protected $mShowEmptyLabels = true;
71
75 public $mParent;
76
87 abstract public function getInputHTML( $value );
88
97 public function getInputOOUI( $value ) {
98 return false;
99 }
100
112 public function getInputCodex( $value, $hasErrors ) {
113 // If not overridden, fall back to getInputHTML()
114 return $this->getInputHTML( $value );
115 }
116
123 public function canDisplayErrors() {
124 return $this->hasVisibleOutput();
125 }
126
139 public function msg( $key, ...$params ) {
140 if ( $this->mParent ) {
141 return $this->mParent->msg( $key, ...$params );
142 }
143 return wfMessage( $key, ...$params );
144 }
145
153 public function hasVisibleOutput() {
154 return true;
155 }
156
163 public function getName() {
164 return $this->mName;
165 }
166
178 protected function getNearestField( $name, $backCompat = false ) {
179 // When the field is belong to a HTMLFormFieldCloner
180 $cloner = $this->mParams['cloner'] ?? null;
181 if ( $cloner instanceof HTMLFormFieldCloner ) {
182 $field = $cloner->findNearestField( $this, $name );
183 if ( $field ) {
184 return $field;
185 }
186 }
187
188 if ( $backCompat && str_starts_with( $name, 'wp' ) &&
189 !$this->mParent->hasField( $name )
190 ) {
191 // Don't break the existed use cases.
192 return $this->mParent->getField( substr( $name, 2 ) );
193 }
194 return $this->mParent->getField( $name );
195 }
196
208 protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
209 $field = $this->getNearestField( $name, $backCompat );
210 // When the field belongs to a HTMLFormFieldCloner
211 $cloner = $field->mParams['cloner'] ?? null;
212 if ( $cloner instanceof HTMLFormFieldCloner ) {
213 $value = $cloner->extractFieldData( $field, $alldata );
214 } else {
215 // Note $alldata is an empty array when first rendering a form with a formIdentifier.
216 // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
217 // field's default value
218 $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
219 }
220
221 // Check invert state for HTMLCheckField
222 if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
223 $value = !$value;
224 }
225
226 return $value;
227 }
228
239 protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
240 return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
241 }
242
249 protected function validateCondState( $params ) {
250 $origParams = $params;
251 $op = array_shift( $params );
252
253 $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException {
254 return new InvalidArgumentException(
255 "Invalid hide-if or disable-if specification for $this->mName: " .
256 $details . " in " . var_export( $origParams, true )
257 );
258 };
259
260 switch ( $op ) {
261 case 'NOT':
262 if ( count( $params ) !== 1 ) {
263 throw $makeException( "NOT takes exactly one parameter" );
264 }
265 // Fall-through intentionally
266
267 case 'AND':
268 case 'OR':
269 case 'NAND':
270 case 'NOR':
271 foreach ( $params as $i => $p ) {
272 if ( !is_array( $p ) ) {
273 $type = get_debug_type( $p );
274 throw $makeException( "Expected array, found $type at index $i" );
275 }
276 $this->validateCondState( $p );
277 }
278 break;
279
280 case '===':
281 case '!==':
282 if ( count( $params ) !== 2 ) {
283 throw $makeException( "$op takes exactly two parameters" );
284 }
285 [ $name, $value ] = $params;
286 if ( !is_string( $name ) || !is_string( $value ) ) {
287 throw $makeException( "Parameters for $op must be strings" );
288 }
289 break;
290
291 default:
292 throw $makeException( "Unknown operation" );
293 }
294 }
295
303 protected function checkStateRecurse( array $alldata, array $params ) {
304 $op = array_shift( $params );
305 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
306 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
307
308 switch ( $op ) {
309 case 'AND':
310 case 'OR':
311 case 'NAND':
312 case 'NOR':
313 foreach ( $params as $p ) {
314 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
315 return !$valueRet[$op];
316 }
317 }
318 return $valueRet[$op];
319
320 case 'NOT':
321 return !$this->checkStateRecurse( $alldata, $params[0] );
322
323 case '===':
324 case '!==':
325 [ $field, $value ] = $params;
326 $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true );
327 switch ( $op ) {
328 case '===':
329 return ( $value === $testValue );
330 case '!==':
331 return ( $value !== $testValue );
332 }
333 }
334 }
335
344 protected function parseCondState( $params ) {
345 $op = array_shift( $params );
346
347 switch ( $op ) {
348 case 'AND':
349 case 'OR':
350 case 'NAND':
351 case 'NOR':
352 $ret = [ $op ];
353 foreach ( $params as $p ) {
354 $ret[] = $this->parseCondState( $p );
355 }
356 return $ret;
357
358 case 'NOT':
359 return [ 'NOT', $this->parseCondState( $params[0] ) ];
360
361 case '===':
362 case '!==':
363 [ $name, $value ] = $params;
364 $field = $this->getNearestField( $name, true );
365 return [ $op, $field->getName(), $value ];
366 }
367 }
368
374 protected function parseCondStateForClient() {
375 $parsed = [];
376 foreach ( $this->mCondState as $type => $params ) {
377 $parsed[$type] = $this->parseCondState( $params );
378 }
379 return $parsed;
380 }
381
390 public function isHidden( $alldata ) {
391 return isset( $this->mCondState['hide'] ) &&
392 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
393 }
394
403 public function isDisabled( $alldata ) {
404 return ( $this->mParams['disabled'] ?? false ) ||
405 $this->isHidden( $alldata ) ||
406 ( isset( $this->mCondState['disable'] )
407 && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) );
408 }
409
421 public function cancelSubmit( $value, $alldata ) {
422 return false;
423 }
424
437 public function validate( $value, $alldata ) {
438 if ( $this->isHidden( $alldata ) ) {
439 return true;
440 }
441
442 if ( isset( $this->mParams['required'] )
443 && $this->mParams['required'] !== false
444 && ( $value === '' || $value === false || $value === null )
445 ) {
446 return $this->msg( 'htmlform-required' );
447 }
448
449 if ( !isset( $this->mValidationCallback ) ) {
450 return true;
451 }
452
453 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
454
455 if ( $p instanceof StatusValue ) {
456 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
457
458 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
459 }
460
461 return $p;
462 }
463
472 public function filter( $value, $alldata ) {
473 if ( isset( $this->mFilterCallback ) ) {
474 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
475 }
476
477 return $value;
478 }
479
487 protected function needsLabel() {
488 return true;
489 }
490
500 public function setShowEmptyLabel( $show ) {
501 $this->mShowEmptyLabels = $show;
502 }
503
515 protected function isSubmitAttempt( WebRequest $request ) {
516 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
517 return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) )
518 // The identifier matching or not has been checked in HTMLForm::prepareForm()
519 || $request->getCheck( 'wpFormIdentifier' );
520 }
521
530 public function loadDataFromRequest( $request ) {
531 if ( $request->getCheck( $this->mName ) ) {
532 return $request->getText( $this->mName );
533 } else {
534 return $this->getDefault();
535 }
536 }
537
546 public function __construct( $params ) {
547 $this->mParams = $params;
548
549 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
550 $this->mParent = $params['parent'];
551 } else {
552 // Normally parent is added automatically by HTMLForm::factory.
553 // Several field types already assume unconditionally this is always set,
554 // so deprecate manually creating an HTMLFormField without a parent form set.
556 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
557 "1.40"
558 );
559 }
560
561 # Generate the label from a message, if possible
562 if ( isset( $params['label-message'] ) ) {
563 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
564 } elseif ( isset( $params['label'] ) ) {
565 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
566 // Apparently some things set &nbsp directly and in an odd format
567 $this->mLabel = "\u{00A0}";
568 } else {
569 $this->mLabel = htmlspecialchars( $params['label'] );
570 }
571 } elseif ( isset( $params['label-raw'] ) ) {
572 $this->mLabel = $params['label-raw'];
573 }
574
575 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
576
577 if ( isset( $params['dir'] ) ) {
578 $this->mDir = $params['dir'];
579 }
580
581 $this->mID = "mw-input-{$this->mName}";
582
583 if ( isset( $params['default'] ) ) {
584 $this->mDefault = $params['default'];
585 }
586
587 if ( isset( $params['id'] ) ) {
588 $this->mID = $params['id'];
589 }
590
591 if ( isset( $params['cssclass'] ) ) {
592 $this->mClass = $params['cssclass'];
593 }
594
595 if ( isset( $params['csshelpclass'] ) ) {
596 $this->mHelpClass = $params['csshelpclass'];
597 }
598
599 if ( isset( $params['validation-callback'] ) ) {
600 $this->mValidationCallback = $params['validation-callback'];
601 }
602
603 if ( isset( $params['filter-callback'] ) ) {
604 $this->mFilterCallback = $params['filter-callback'];
605 }
606
607 if ( isset( $params['hidelabel'] ) ) {
608 $this->mShowEmptyLabels = false;
609 }
610 if ( isset( $params['notices'] ) ) {
611 $this->mNotices = $params['notices'];
612 }
613
614 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
615 $this->validateCondState( $params['hide-if'] );
616 $this->mCondState['hide'] = $params['hide-if'];
617 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
618 }
619 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
620 isset( $params['disable-if'] ) && $params['disable-if']
621 ) {
622 $this->validateCondState( $params['disable-if'] );
623 $this->mCondState['disable'] = $params['disable-if'];
624 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
625 }
626 }
627
637 public function getTableRow( $value ) {
638 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
639 $inputHtml = $this->getInputHTML( $value );
640 $fieldType = $this->getClassName();
641 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
642 $cellAttributes = [];
643 $rowAttributes = [];
644 $rowClasses = '';
645
646 if ( !empty( $this->mParams['vertical-label'] ) ) {
647 $cellAttributes['colspan'] = 2;
648 $verticalLabel = true;
649 } else {
650 $verticalLabel = false;
651 }
652
653 $label = $this->getLabelHtml( $cellAttributes );
654
655 $field = Html::rawElement(
656 'td',
657 [ 'class' => 'mw-input' ] + $cellAttributes,
658 $inputHtml . "\n$errors"
659 );
660
661 if ( $this->mCondState ) {
662 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
663 $rowClasses .= implode( ' ', $this->mCondStateClass );
664 }
665
666 if ( $verticalLabel ) {
667 $html = Html::rawElement( 'tr',
668 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
669 $html .= Html::rawElement( 'tr',
670 $rowAttributes + [
671 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
672 ],
673 $field );
674 } else {
675 $html = Html::rawElement( 'tr',
676 $rowAttributes + [
677 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
678 ],
679 $label . $field );
680 }
681
682 return $html . $helptext;
683 }
684
695 public function getDiv( $value ) {
696 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
697 $inputHtml = $this->getInputHTML( $value );
698 $fieldType = $this->getClassName();
699 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
700 $cellAttributes = [];
701 $label = $this->getLabelHtml( $cellAttributes );
702
703 $outerDivClass = [
704 'mw-input',
705 'mw-htmlform-nolabel' => ( $label === '' )
706 ];
707
708 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
709
710 if ( $horizontalLabel ) {
711 $field = "\u{00A0}" . $inputHtml . "\n$errors";
712 } else {
713 $field = Html::rawElement(
714 'div',
715 // @phan-suppress-next-line PhanUselessBinaryAddRight
716 [ 'class' => $outerDivClass ] + $cellAttributes,
717 $inputHtml . "\n$errors"
718 );
719 }
720
721 $wrapperAttributes = [ 'class' => [
722 "mw-htmlform-field-$fieldType",
725 $errorClass,
726 ] ];
727 if ( $this->mCondState ) {
728 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
729 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
730 }
731 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
732 $helptext;
733 }
734
744 public function getOOUI( $value ) {
745 $inputField = $this->getInputOOUI( $value );
746
747 if ( !$inputField ) {
748 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
749 // generate the whole field, label and errors and all, then wrap it in a Widget.
750 // It might look weird, but it'll work OK.
751 return $this->getFieldLayoutOOUI(
752 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
753 [ 'align' => 'top' ]
754 );
755 }
756
757 $infusable = true;
758 if ( is_string( $inputField ) ) {
759 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
760 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
761 // JavaScript doesn't know how to rebuilt the contents.
762 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
763 $infusable = false;
764 }
765
766 $fieldType = $this->getClassName();
767 $help = $this->getHelpText();
768 $errors = $this->getErrorsRaw( $value );
769 foreach ( $errors as &$error ) {
770 $error = new \OOUI\HtmlSnippet( $error );
771 }
772
773 $config = [
774 'classes' => [ "mw-htmlform-field-$fieldType" ],
775 'align' => $this->getLabelAlignOOUI(),
776 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
777 'errors' => $errors,
778 'infusable' => $infusable,
779 'helpInline' => $this->isHelpInline(),
780 'notices' => $this->mNotices ?: [],
781 ];
782 if ( $this->mClass !== '' ) {
783 $config['classes'][] = $this->mClass;
784 }
785
786 $preloadModules = false;
787
788 if ( $infusable && $this->shouldInfuseOOUI() ) {
789 $preloadModules = true;
790 $config['classes'][] = 'mw-htmlform-autoinfuse';
791 }
792 if ( $this->mCondState ) {
793 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
794 }
795
796 // the element could specify, that the label doesn't need to be added
797 $label = $this->getLabel();
798 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
799 $config['label'] = new \OOUI\HtmlSnippet( $label );
800 }
801
802 if ( $this->mCondState ) {
803 $preloadModules = true;
804 $config['condState'] = $this->parseCondStateForClient();
805 }
806
807 $config['modules'] = $this->getOOUIModules();
808
809 if ( $preloadModules ) {
810 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
811 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
812 }
813
814 return $this->getFieldLayoutOOUI( $inputField, $config );
815 }
816
824 public function getCodex( $value ) {
825 $isDisabled = ( $this->mParams['disabled'] ?? false );
826
827 // Label
828 $labelDiv = '';
829 $labelValue = trim( $this->getLabel() );
830 // For weird historical reasons, a non-breaking space is treated as an empty label
831 // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version
832 if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' ) {
833 $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
834 $labelClasses = [ 'cdx-label' ];
835 if ( $isDisabled ) {
836 $labelClasses[] = 'cdx-label--disabled';
837 }
838 // <div class="cdx-label">
839 $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ],
840 // <label class="cdx-label__label" for="ID">
841 Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor,
842 // <span class="cdx-label__label__text">
843 Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ],
844 $labelValue
845 )
846 )
847 );
848 }
849
850 // Help text
851 // <div class="cdx-field__help-text">
852 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] );
853
854 // Validation message
855 // <div class="cdx-field__validation-message">
856 // $errors is a <div class="cdx-message">
857 // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead
858 $validationMessage = '';
859 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
860 if ( $errors !== '' ) {
861 $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ],
862 $errors
863 );
864 }
865
866 // Control
867 $inputHtml = $this->getInputCodex( $value, $errors !== '' );
868 // <div class="cdx-field__control cdx-field__control--has-help-text">
869 $controlClasses = [ 'cdx-field__control' ];
870 if ( $helptext ) {
871 $controlClasses[] = 'cdx-field__control--has-help-text';
872 }
873 $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml );
874
875 // <div class="cdx-field">
876 $fieldClasses = [
877 "mw-htmlform-field-{$this->getClassName()}",
879 $errorClass,
880 'cdx-field'
881 ];
882 if ( $isDisabled ) {
883 $fieldClasses[] = 'cdx-field--disabled';
884 }
885 $fieldAttributes = [];
886 // Set data attribute and CSS class for client side handling of hide-if / disable-if
887 if ( $this->mCondState ) {
888 $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
889 $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass );
890 }
891
892 return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes,
893 $labelDiv . $control . $helptext . $validationMessage
894 );
895 }
896
904 protected function getClassName() {
905 $name = explode( '\\', static::class );
906 return end( $name );
907 }
908
914 protected function getLabelAlignOOUI() {
915 return 'top';
916 }
917
924 protected function getFieldLayoutOOUI( $inputField, $config ) {
925 return new HTMLFormFieldLayout( $inputField, $config );
926 }
927
936 protected function shouldInfuseOOUI() {
937 // Always infuse fields with popup help text, since the interface for it is nicer with JS
938 return !$this->isHelpInline() && $this->getHelpMessages();
939 }
940
948 protected function getOOUIModules() {
949 return [];
950 }
951
962 public function getRaw( $value ) {
963 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
964 return "\n" . $errors .
965 $this->getLabelHtml() .
966 $this->getInputHTML( $value ) .
967 $this->getHelpTextHtmlRaw( $this->getHelpText() );
968 }
969
979 public function getVForm( $value ) {
980 // Ewwww
981 $this->mVFormClass = ' mw-ui-vform-field';
982 return $this->getDiv( $value );
983 }
984
992 public function getInline( $value ) {
993 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
994 return "\n" . $errors .
995 $this->getLabelHtml() .
996 "\u{00A0}" .
997 $this->getInputHTML( $value ) .
998 $this->getHelpTextHtmlDiv( $this->getHelpText() );
999 }
1000
1008 public function getHelpTextHtmlTable( $helptext ) {
1009 if ( $helptext === null ) {
1010 return '';
1011 }
1012
1013 $rowAttributes = [];
1014 if ( $this->mCondState ) {
1015 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1016 $rowAttributes['class'] = $this->mCondStateClass;
1017 }
1018
1019 $tdClasses = [ 'htmlform-tip' ];
1020 if ( $this->mHelpClass !== false ) {
1021 $tdClasses[] = $this->mHelpClass;
1022 }
1023 return Html::rawElement( 'tr', $rowAttributes,
1024 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
1025 );
1026 }
1027
1037 public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) {
1038 if ( $helptext === null ) {
1039 return '';
1040 }
1041
1042 $wrapperAttributes = [
1043 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ),
1044 ];
1045 if ( $this->mHelpClass !== false ) {
1046 $wrapperAttributes['class'][] = $this->mHelpClass;
1047 }
1048 if ( $this->mCondState ) {
1049 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1050 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
1051 }
1052 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
1053 }
1054
1062 public function getHelpTextHtmlRaw( $helptext ) {
1063 return $this->getHelpTextHtmlDiv( $helptext );
1064 }
1065
1066 private function getHelpMessages(): array {
1067 if ( isset( $this->mParams['help-message'] ) ) {
1068 return [ $this->mParams['help-message'] ];
1069 } elseif ( isset( $this->mParams['help-messages'] ) ) {
1070 return $this->mParams['help-messages'];
1071 } elseif ( isset( $this->mParams['help-raw'] ) ) {
1072 return [ new HtmlArmor( $this->mParams['help-raw'] ) ];
1073 } elseif ( isset( $this->mParams['help'] ) ) {
1074 // @deprecated since 1.43, use 'help-raw' key instead
1075 return [ new HtmlArmor( $this->mParams['help'] ) ];
1076 }
1077
1078 return [];
1079 }
1080
1087 public function getHelpText() {
1088 $html = [];
1089
1090 foreach ( $this->getHelpMessages() as $msg ) {
1091 if ( $msg instanceof HtmlArmor ) {
1092 $html[] = HtmlArmor::getHtml( $msg );
1093 } else {
1094 $msg = $this->getMessage( $msg );
1095 if ( $msg->exists() ) {
1096 $html[] = $msg->parse();
1097 }
1098 }
1099 }
1100
1101 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1102 }
1103
1112 public function isHelpInline() {
1113 return $this->mParams['help-inline'] ?? true;
1114 }
1115
1128 public function getErrorsAndErrorClass( $value ) {
1129 $errors = $this->validate( $value, $this->mParent->mFieldData );
1130
1131 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1132 return [ '', '' ];
1133 }
1134
1135 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1136 }
1137
1145 public function getErrorsRaw( $value ) {
1146 $errors = $this->validate( $value, $this->mParent->mFieldData );
1147
1148 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1149 return [];
1150 }
1151
1152 if ( !is_array( $errors ) ) {
1153 $errors = [ $errors ];
1154 }
1155 foreach ( $errors as &$error ) {
1156 if ( $error instanceof Message ) {
1157 $error = $error->parse();
1158 }
1159 }
1160
1161 return $errors;
1162 }
1163
1168 public function getLabel() {
1169 return $this->mLabel ?? '';
1170 }
1171
1178 public function getLabelHtml( $cellAttributes = [] ) {
1179 # Don't output a for= attribute for labels with no associated input.
1180 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1181 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1182
1183 $labelValue = trim( $this->getLabel() );
1184 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1185
1186 $displayFormat = $this->mParent->getDisplayFormat();
1187 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1188
1189 if ( $displayFormat === 'table' ) {
1190 return Html::rawElement( 'td',
1191 [ 'class' => 'mw-label' ] + $cellAttributes,
1192 Html::rawElement( 'label', $for, $labelValue ) );
1193 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1194 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1195 return Html::rawElement( 'div',
1196 [ 'class' => 'mw-label' ] + $cellAttributes,
1197 Html::rawElement( 'label', $for, $labelValue ) );
1198 } else {
1199 return Html::rawElement( 'label', $for, $labelValue );
1200 }
1201 }
1202
1203 return '';
1204 }
1205
1210 public function getDefault() {
1211 return $this->mDefault ?? null;
1212 }
1213
1219 public function getTooltipAndAccessKey() {
1220 if ( empty( $this->mParams['tooltip'] ) ) {
1221 return [];
1222 }
1223
1224 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1225 }
1226
1232 public function getTooltipAndAccessKeyOOUI() {
1233 if ( empty( $this->mParams['tooltip'] ) ) {
1234 return [];
1235 }
1236
1237 return [
1238 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1239 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1240 ];
1241 }
1242
1250 public function getAttributes( array $list ) {
1251 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1252
1253 $ret = [];
1254 foreach ( $list as $key ) {
1255 if ( in_array( $key, $boolAttribs ) ) {
1256 if ( !empty( $this->mParams[$key] ) ) {
1257 $ret[$key] = '';
1258 }
1259 } elseif ( isset( $this->mParams[$key] ) ) {
1260 $ret[$key] = $this->mParams[$key];
1261 }
1262 }
1263
1264 return $ret;
1265 }
1266
1276 private function lookupOptionsKeys( $options, $needsParse ) {
1277 $ret = [];
1278 foreach ( $options as $key => $value ) {
1279 $msg = $this->msg( $key );
1280 $msgAsText = $needsParse ? $msg->parse() : $msg->plain();
1281 if ( array_key_exists( $msgAsText, $ret ) ) {
1282 LoggerFactory::getInstance( 'error' )->error(
1283 'The option that uses the message key {msg_key_one} has the same translation as ' .
1284 'another option in {lang}. This means that {msg_key_one} will not be used as an option.',
1285 [
1286 'msg_key_one' => $key,
1287 'lang' => $this->mParent ?
1288 $this->mParent->getLanguageCode()->toBcp47Code() :
1289 RequestContext::getMain()->getLanguageCode()->toBcp47Code(),
1290 ]
1291 );
1292 continue;
1293 }
1294 $ret[$msgAsText] = is_array( $value )
1295 ? $this->lookupOptionsKeys( $value, $needsParse )
1296 : strval( $value );
1297 }
1298 return $ret;
1299 }
1300
1308 public static function forceToStringRecursive( $array ) {
1309 if ( is_array( $array ) ) {
1310 return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1311 } else {
1312 return strval( $array );
1313 }
1314 }
1315
1322 public function getOptions() {
1323 if ( $this->mOptions === false ) {
1324 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1325 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1326 if ( $needsParse ) {
1327 $this->mOptionsLabelsNotFromMessage = true;
1328 }
1329 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1330 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1331 $this->mOptionsLabelsNotFromMessage = true;
1332 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1333 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1334 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1335 $this->mOptions = Html::listDropdownOptions( $message );
1336 } else {
1337 $this->mOptions = null;
1338 }
1339 }
1340
1341 return $this->mOptions;
1342 }
1343
1349 public function getOptionsOOUI() {
1350 $oldoptions = $this->getOptions();
1351
1352 if ( $oldoptions === null ) {
1353 return null;
1354 }
1355
1356 return Html::listDropdownOptionsOoui( $oldoptions );
1357 }
1358
1366 public static function flattenOptions( $options ) {
1367 $flatOpts = [];
1368
1369 foreach ( $options as $value ) {
1370 if ( is_array( $value ) ) {
1371 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1372 } else {
1373 $flatOpts[] = $value;
1374 }
1375 }
1376
1377 return $flatOpts;
1378 }
1379
1393 protected static function formatErrors( $errors ) {
1394 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1395 $errors = array_shift( $errors );
1396 }
1397
1398 if ( is_array( $errors ) ) {
1399 foreach ( $errors as &$error ) {
1400 $error = Html::rawElement( 'li', [],
1401 $error instanceof Message ? $error->parse() : $error
1402 );
1403 }
1404 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1405 } elseif ( $errors instanceof Message ) {
1406 $errors = $errors->parse();
1407 }
1408
1409 return Html::errorBox( $errors );
1410 }
1411
1418 protected function getMessage( $value ) {
1419 $message = Message::newFromSpecifier( $value );
1420
1421 if ( $this->mParent ) {
1422 $message->setContext( $this->mParent );
1423 }
1424
1425 return $message;
1426 }
1427
1435 public function skipLoadData( $request ) {
1436 return !empty( $this->mParams['nodata'] );
1437 }
1438
1447 // This is probably more restrictive than it needs to be, but better safe than sorry
1448 return (bool)$this->mCondState;
1449 }
1450}
1451
1453class_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:469
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1086
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.