MediaWiki master
HTMLFormField.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\HTMLForm;
4
5use FormatJson;
6use HtmlArmor;
7use HTMLCheckField;
8use HTMLFormFieldCloner;
9use InvalidArgumentException;
17use StatusValue;
18
25abstract class HTMLFormField {
27 public $mParams;
28
30 protected $mValidationCallback;
32 protected $mName;
33 protected $mDir;
34 protected $mLabel; # String label, as HTML. Set on construction.
35 protected $mID;
36 protected $mClass = '';
37 protected $mVFormClass = '';
38 protected $mHelpClass = false;
39 protected $mDefault;
40 private $mNotices;
41
45 protected $mOptions = false;
50 protected $mCondState = [];
51 protected $mCondStateClass = [];
52
57 protected $mShowEmptyLabels = true;
58
62 public $mParent;
63
74 abstract public function getInputHTML( $value );
75
84 public function getInputOOUI( $value ) {
85 return false;
86 }
87
94 public function canDisplayErrors() {
95 return $this->hasVisibleOutput();
96 }
97
110 public function msg( $key, ...$params ) {
111 if ( $this->mParent ) {
112 return $this->mParent->msg( $key, ...$params );
113 }
114 return wfMessage( $key, ...$params );
115 }
116
124 public function hasVisibleOutput() {
125 return true;
126 }
127
134 public function getName() {
135 return $this->mName;
136 }
137
149 protected function getNearestField( $name, $backCompat = false ) {
150 // When the field is belong to a HTMLFormFieldCloner
151 $cloner = $this->mParams['cloner'] ?? null;
152 if ( $cloner instanceof HTMLFormFieldCloner ) {
153 $field = $cloner->findNearestField( $this, $name );
154 if ( $field ) {
155 return $field;
156 }
157 }
158
159 if ( $backCompat && str_starts_with( $name, 'wp' ) &&
160 !$this->mParent->hasField( $name )
161 ) {
162 // Don't break the existed use cases.
163 return $this->mParent->getField( substr( $name, 2 ) );
164 }
165 return $this->mParent->getField( $name );
166 }
167
179 protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
180 $field = $this->getNearestField( $name, $backCompat );
181 // When the field belongs to a HTMLFormFieldCloner
182 $cloner = $field->mParams['cloner'] ?? null;
183 if ( $cloner instanceof HTMLFormFieldCloner ) {
184 $value = $cloner->extractFieldData( $field, $alldata );
185 } else {
186 // Note $alldata is an empty array when first rendering a form with a formIdentifier.
187 // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
188 // field's default value
189 $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
190 }
191
192 // Check invert state for HTMLCheckField
193 if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
194 $value = !$value;
195 }
196
197 return $value;
198 }
199
210 protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
211 return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
212 }
213
220 protected function validateCondState( $params ) {
221 $origParams = $params;
222 $op = array_shift( $params );
223
224 $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException {
225 return new InvalidArgumentException(
226 "Invalid hide-if or disable-if specification for $this->mName: " .
227 $details . " in " . var_export( $origParams, true )
228 );
229 };
230
231 switch ( $op ) {
232 case 'NOT':
233 if ( count( $params ) !== 1 ) {
234 throw $makeException( "NOT takes exactly one parameter" );
235 }
236 // Fall-through intentionally
237
238 case 'AND':
239 case 'OR':
240 case 'NAND':
241 case 'NOR':
242 foreach ( $params as $i => $p ) {
243 if ( !is_array( $p ) ) {
244 $type = gettype( $p );
245 throw $makeException( "Expected array, found $type at index $i" );
246 }
247 $this->validateCondState( $p );
248 }
249 break;
250
251 case '===':
252 case '!==':
253 if ( count( $params ) !== 2 ) {
254 throw $makeException( "$op takes exactly two parameters" );
255 }
256 [ $name, $value ] = $params;
257 if ( !is_string( $name ) || !is_string( $value ) ) {
258 throw $makeException( "Parameters for $op must be strings" );
259 }
260 break;
261
262 default:
263 throw $makeException( "Unknown operation" );
264 }
265 }
266
274 protected function checkStateRecurse( array $alldata, array $params ) {
275 $op = array_shift( $params );
276 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
277 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
278
279 switch ( $op ) {
280 case 'AND':
281 case 'OR':
282 case 'NAND':
283 case 'NOR':
284 foreach ( $params as $p ) {
285 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
286 return !$valueRet[$op];
287 }
288 }
289 return $valueRet[$op];
290
291 case 'NOT':
292 return !$this->checkStateRecurse( $alldata, $params[0] );
293
294 case '===':
295 case '!==':
296 [ $field, $value ] = $params;
297 $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true );
298 switch ( $op ) {
299 case '===':
300 return ( $value === $testValue );
301 case '!==':
302 return ( $value !== $testValue );
303 }
304 }
305 }
306
315 protected function parseCondState( $params ) {
316 $op = array_shift( $params );
317
318 switch ( $op ) {
319 case 'AND':
320 case 'OR':
321 case 'NAND':
322 case 'NOR':
323 $ret = [ $op ];
324 foreach ( $params as $p ) {
325 $ret[] = $this->parseCondState( $p );
326 }
327 return $ret;
328
329 case 'NOT':
330 return [ 'NOT', $this->parseCondState( $params[0] ) ];
331
332 case '===':
333 case '!==':
334 [ $name, $value ] = $params;
335 $field = $this->getNearestField( $name, true );
336 return [ $op, $field->getName(), $value ];
337 }
338 }
339
345 protected function parseCondStateForClient() {
346 $parsed = [];
347 foreach ( $this->mCondState as $type => $params ) {
348 $parsed[$type] = $this->parseCondState( $params );
349 }
350 return $parsed;
351 }
352
361 public function isHidden( $alldata ) {
362 return isset( $this->mCondState['hide'] ) &&
363 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
364 }
365
374 public function isDisabled( $alldata ) {
375 return ( $this->mParams['disabled'] ?? false ) ||
376 $this->isHidden( $alldata ) ||
377 isset( $this->mCondState['disable'] ) &&
378 $this->checkStateRecurse( $alldata, $this->mCondState['disable'] );
379 }
380
392 public function cancelSubmit( $value, $alldata ) {
393 return false;
394 }
395
408 public function validate( $value, $alldata ) {
409 if ( $this->isHidden( $alldata ) ) {
410 return true;
411 }
412
413 if ( isset( $this->mParams['required'] )
414 && $this->mParams['required'] !== false
415 && ( $value === '' || $value === false || $value === null )
416 ) {
417 return $this->msg( 'htmlform-required' );
418 }
419
420 if ( !isset( $this->mValidationCallback ) ) {
421 return true;
422 }
423
424 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
425
426 if ( $p instanceof StatusValue ) {
427 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
428
429 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
430 }
431
432 return $p;
433 }
434
443 public function filter( $value, $alldata ) {
444 if ( isset( $this->mFilterCallback ) ) {
445 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
446 }
447
448 return $value;
449 }
450
458 protected function needsLabel() {
459 return true;
460 }
461
471 public function setShowEmptyLabel( $show ) {
472 $this->mShowEmptyLabels = $show;
473 }
474
486 protected function isSubmitAttempt( WebRequest $request ) {
487 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
488 return $request->wasPosted() && $request->getCheck( 'wpEditToken' )
489 // The identifier matching or not has been checked in HTMLForm::prepareForm()
490 || $request->getCheck( 'wpFormIdentifier' );
491 }
492
501 public function loadDataFromRequest( $request ) {
502 if ( $request->getCheck( $this->mName ) ) {
503 return $request->getText( $this->mName );
504 } else {
505 return $this->getDefault();
506 }
507 }
508
517 public function __construct( $params ) {
518 $this->mParams = $params;
519
520 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
521 $this->mParent = $params['parent'];
522 } else {
523 // Normally parent is added automatically by HTMLForm::factory.
524 // Several field types already assume unconditionally this is always set,
525 // so deprecate manually creating an HTMLFormField without a parent form set.
527 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
528 "1.40"
529 );
530 }
531
532 # Generate the label from a message, if possible
533 if ( isset( $params['label-message'] ) ) {
534 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
535 } elseif ( isset( $params['label'] ) ) {
536 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
537 // Apparently some things set &nbsp directly and in an odd format
538 $this->mLabel = "\u{00A0}";
539 } else {
540 $this->mLabel = htmlspecialchars( $params['label'] );
541 }
542 } elseif ( isset( $params['label-raw'] ) ) {
543 $this->mLabel = $params['label-raw'];
544 }
545
546 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
547
548 if ( isset( $params['dir'] ) ) {
549 $this->mDir = $params['dir'];
550 }
551
552 $this->mID = "mw-input-{$this->mName}";
553
554 if ( isset( $params['default'] ) ) {
555 $this->mDefault = $params['default'];
556 }
557
558 if ( isset( $params['id'] ) ) {
559 $this->mID = $params['id'];
560 }
561
562 if ( isset( $params['cssclass'] ) ) {
563 $this->mClass = $params['cssclass'];
564 }
565
566 if ( isset( $params['csshelpclass'] ) ) {
567 $this->mHelpClass = $params['csshelpclass'];
568 }
569
570 if ( isset( $params['validation-callback'] ) ) {
571 $this->mValidationCallback = $params['validation-callback'];
572 }
573
574 if ( isset( $params['filter-callback'] ) ) {
575 $this->mFilterCallback = $params['filter-callback'];
576 }
577
578 if ( isset( $params['hidelabel'] ) ) {
579 $this->mShowEmptyLabels = false;
580 }
581 if ( isset( $params['notices'] ) ) {
582 $this->mNotices = $params['notices'];
583 }
584
585 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
586 $this->validateCondState( $params['hide-if'] );
587 $this->mCondState['hide'] = $params['hide-if'];
588 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
589 }
590 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
591 isset( $params['disable-if'] ) && $params['disable-if']
592 ) {
593 $this->validateCondState( $params['disable-if'] );
594 $this->mCondState['disable'] = $params['disable-if'];
595 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
596 }
597 }
598
608 public function getTableRow( $value ) {
609 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
610 $inputHtml = $this->getInputHTML( $value );
611 $fieldType = $this->getClassName();
612 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
613 $cellAttributes = [];
614 $rowAttributes = [];
615 $rowClasses = '';
616
617 if ( !empty( $this->mParams['vertical-label'] ) ) {
618 $cellAttributes['colspan'] = 2;
619 $verticalLabel = true;
620 } else {
621 $verticalLabel = false;
622 }
623
624 $label = $this->getLabelHtml( $cellAttributes );
625
626 $field = Html::rawElement(
627 'td',
628 [ 'class' => 'mw-input' ] + $cellAttributes,
629 $inputHtml . "\n$errors"
630 );
631
632 if ( $this->mCondState ) {
633 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
634 $rowClasses .= implode( ' ', $this->mCondStateClass );
635 }
636
637 if ( $verticalLabel ) {
638 $html = Html::rawElement( 'tr',
639 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
640 $html .= Html::rawElement( 'tr',
641 $rowAttributes + [
642 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
643 ],
644 $field );
645 } else {
646 $html = Html::rawElement( 'tr',
647 $rowAttributes + [
648 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
649 ],
650 $label . $field );
651 }
652
653 return $html . $helptext;
654 }
655
666 public function getDiv( $value ) {
667 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
668 $inputHtml = $this->getInputHTML( $value );
669 $fieldType = $this->getClassName();
670 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
671 $cellAttributes = [];
672 $label = $this->getLabelHtml( $cellAttributes );
673
674 $outerDivClass = [
675 'mw-input',
676 'mw-htmlform-nolabel' => ( $label === '' )
677 ];
678
679 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
680
681 if ( $horizontalLabel ) {
682 $field = "\u{00A0}" . $inputHtml . "\n$errors";
683 } else {
684 $field = Html::rawElement(
685 'div',
686 // @phan-suppress-next-line PhanUselessBinaryAddRight
687 [ 'class' => $outerDivClass ] + $cellAttributes,
688 $inputHtml . "\n$errors"
689 );
690 }
691
692 $wrapperAttributes = [ 'class' => [
693 "mw-htmlform-field-$fieldType",
696 $errorClass,
697 ] ];
698 if ( $this->mCondState ) {
699 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
700 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
701 }
702 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
703 $helptext;
704 }
705
715 public function getOOUI( $value ) {
716 $inputField = $this->getInputOOUI( $value );
717
718 if ( !$inputField ) {
719 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
720 // generate the whole field, label and errors and all, then wrap it in a Widget.
721 // It might look weird, but it'll work OK.
722 return $this->getFieldLayoutOOUI(
723 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
724 [ 'align' => 'top' ]
725 );
726 }
727
728 $infusable = true;
729 if ( is_string( $inputField ) ) {
730 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
731 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
732 // JavaScript doesn't know how to rebuilt the contents.
733 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
734 $infusable = false;
735 }
736
737 $fieldType = $this->getClassName();
738 $help = $this->getHelpText();
739 $errors = $this->getErrorsRaw( $value );
740 foreach ( $errors as &$error ) {
741 $error = new \OOUI\HtmlSnippet( $error );
742 }
743
744 $config = [
745 'classes' => [ "mw-htmlform-field-$fieldType" ],
746 'align' => $this->getLabelAlignOOUI(),
747 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
748 'errors' => $errors,
749 'infusable' => $infusable,
750 'helpInline' => $this->isHelpInline(),
751 'notices' => $this->mNotices ?: [],
752 ];
753 if ( $this->mClass !== '' ) {
754 $config['classes'][] = $this->mClass;
755 }
756
757 $preloadModules = false;
758
759 if ( $infusable && $this->shouldInfuseOOUI() ) {
760 $preloadModules = true;
761 $config['classes'][] = 'mw-htmlform-autoinfuse';
762 }
763 if ( $this->mCondState ) {
764 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
765 }
766
767 // the element could specify, that the label doesn't need to be added
768 $label = $this->getLabel();
769 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
770 $config['label'] = new \OOUI\HtmlSnippet( $label );
771 }
772
773 if ( $this->mCondState ) {
774 $preloadModules = true;
775 $config['condState'] = $this->parseCondStateForClient();
776 }
777
778 $config['modules'] = $this->getOOUIModules();
779
780 if ( $preloadModules ) {
781 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
782 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
783 }
784
785 return $this->getFieldLayoutOOUI( $inputField, $config );
786 }
787
795 protected function getClassName() {
796 $name = explode( '\\', static::class );
797 return end( $name );
798 }
799
805 protected function getLabelAlignOOUI() {
806 return 'top';
807 }
808
815 protected function getFieldLayoutOOUI( $inputField, $config ) {
816 return new HTMLFormFieldLayout( $inputField, $config );
817 }
818
827 protected function shouldInfuseOOUI() {
828 // Always infuse fields with popup help text, since the interface for it is nicer with JS
829 return !$this->isHelpInline() && $this->getHelpMessages();
830 }
831
839 protected function getOOUIModules() {
840 return [];
841 }
842
853 public function getRaw( $value ) {
854 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
855 return "\n" . $errors .
856 $this->getLabelHtml() .
857 $this->getInputHTML( $value ) .
858 $this->getHelpTextHtmlRaw( $this->getHelpText() );
859 }
860
870 public function getVForm( $value ) {
871 // Ewwww
872 $this->mVFormClass = ' mw-ui-vform-field';
873 return $this->getDiv( $value );
874 }
875
883 public function getInline( $value ) {
884 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
885 return "\n" . $errors .
886 $this->getLabelHtml() .
887 "\u{00A0}" .
888 $this->getInputHTML( $value ) .
889 $this->getHelpTextHtmlDiv( $this->getHelpText() );
890 }
891
899 public function getHelpTextHtmlTable( $helptext ) {
900 if ( $helptext === null ) {
901 return '';
902 }
903
904 $rowAttributes = [];
905 if ( $this->mCondState ) {
906 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
907 $rowAttributes['class'] = $this->mCondStateClass;
908 }
909
910 $tdClasses = [ 'htmlform-tip' ];
911 if ( $this->mHelpClass !== false ) {
912 $tdClasses[] = $this->mHelpClass;
913 }
914 return Html::rawElement( 'tr', $rowAttributes,
915 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
916 );
917 }
918
927 public function getHelpTextHtmlDiv( $helptext ) {
928 if ( $helptext === null ) {
929 return '';
930 }
931
932 $wrapperAttributes = [
933 'class' => [ 'htmlform-tip' ],
934 ];
935 if ( $this->mHelpClass !== false ) {
936 $wrapperAttributes['class'][] = $this->mHelpClass;
937 }
938 if ( $this->mCondState ) {
939 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
940 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
941 }
942 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
943 }
944
952 public function getHelpTextHtmlRaw( $helptext ) {
953 return $this->getHelpTextHtmlDiv( $helptext );
954 }
955
956 private function getHelpMessages(): array {
957 if ( isset( $this->mParams['help-message'] ) ) {
958 return [ $this->mParams['help-message'] ];
959 } elseif ( isset( $this->mParams['help-messages'] ) ) {
960 return $this->mParams['help-messages'];
961 } elseif ( isset( $this->mParams['help'] ) ) {
962 return [ new HtmlArmor( $this->mParams['help'] ) ];
963 }
964
965 return [];
966 }
967
974 public function getHelpText() {
975 $html = [];
976
977 foreach ( $this->getHelpMessages() as $msg ) {
978 if ( $msg instanceof HtmlArmor ) {
979 $html[] = HtmlArmor::getHtml( $msg );
980 } else {
981 $msg = $this->getMessage( $msg );
982 if ( $msg->exists() ) {
983 $html[] = $msg->parse();
984 }
985 }
986 }
987
988 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
989 }
990
999 public function isHelpInline() {
1000 return $this->mParams['help-inline'] ?? true;
1001 }
1002
1015 public function getErrorsAndErrorClass( $value ) {
1016 $errors = $this->validate( $value, $this->mParent->mFieldData );
1017
1018 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1019 return [ '', '' ];
1020 }
1021
1022 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1023 }
1024
1032 public function getErrorsRaw( $value ) {
1033 $errors = $this->validate( $value, $this->mParent->mFieldData );
1034
1035 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1036 return [];
1037 }
1038
1039 if ( !is_array( $errors ) ) {
1040 $errors = [ $errors ];
1041 }
1042 foreach ( $errors as &$error ) {
1043 if ( $error instanceof Message ) {
1044 $error = $error->parse();
1045 }
1046 }
1047
1048 return $errors;
1049 }
1050
1055 public function getLabel() {
1056 return $this->mLabel ?? '';
1057 }
1058
1065 public function getLabelHtml( $cellAttributes = [] ) {
1066 # Don't output a for= attribute for labels with no associated input.
1067 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1068 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1069
1070 $labelValue = trim( $this->getLabel() );
1071 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1072
1073 $displayFormat = $this->mParent->getDisplayFormat();
1074 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1075
1076 if ( $displayFormat === 'table' ) {
1077 return Html::rawElement( 'td',
1078 [ 'class' => 'mw-label' ] + $cellAttributes,
1079 Html::rawElement( 'label', $for, $labelValue ) );
1080 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1081 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1082 return Html::rawElement( 'div',
1083 [ 'class' => 'mw-label' ] + $cellAttributes,
1084 Html::rawElement( 'label', $for, $labelValue ) );
1085 } else {
1086 return Html::rawElement( 'label', $for, $labelValue );
1087 }
1088 }
1089
1090 return '';
1091 }
1092
1097 public function getDefault() {
1098 return $this->mDefault ?? null;
1099 }
1100
1106 public function getTooltipAndAccessKey() {
1107 if ( empty( $this->mParams['tooltip'] ) ) {
1108 return [];
1109 }
1110
1111 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1112 }
1113
1119 public function getTooltipAndAccessKeyOOUI() {
1120 if ( empty( $this->mParams['tooltip'] ) ) {
1121 return [];
1122 }
1123
1124 return [
1125 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1126 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1127 ];
1128 }
1129
1137 public function getAttributes( array $list ) {
1138 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1139
1140 $ret = [];
1141 foreach ( $list as $key ) {
1142 if ( in_array( $key, $boolAttribs ) ) {
1143 if ( !empty( $this->mParams[$key] ) ) {
1144 $ret[$key] = '';
1145 }
1146 } elseif ( isset( $this->mParams[$key] ) ) {
1147 $ret[$key] = $this->mParams[$key];
1148 }
1149 }
1150
1151 return $ret;
1152 }
1153
1163 private function lookupOptionsKeys( $options, $needsParse ) {
1164 $ret = [];
1165 foreach ( $options as $key => $value ) {
1166 $msg = $this->msg( $key );
1167 $key = $needsParse ? $msg->parse() : $msg->plain();
1168 $ret[$key] = is_array( $value )
1169 ? $this->lookupOptionsKeys( $value, $needsParse )
1170 : strval( $value );
1171 }
1172 return $ret;
1173 }
1174
1182 public static function forceToStringRecursive( $array ) {
1183 if ( is_array( $array ) ) {
1184 return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1185 } else {
1186 return strval( $array );
1187 }
1188 }
1189
1196 public function getOptions() {
1197 if ( $this->mOptions === false ) {
1198 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1199 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1200 if ( $needsParse ) {
1201 $this->mOptionsLabelsNotFromMessage = true;
1202 }
1203 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1204 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1205 $this->mOptionsLabelsNotFromMessage = true;
1206 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1207 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1208 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1209 $this->mOptions = Html::listDropDownOptions( $message );
1210 } else {
1211 $this->mOptions = null;
1212 }
1213 }
1214
1215 return $this->mOptions;
1216 }
1217
1223 public function getOptionsOOUI() {
1224 $oldoptions = $this->getOptions();
1225
1226 if ( $oldoptions === null ) {
1227 return null;
1228 }
1229
1230 return Html::listDropDownOptionsOoui( $oldoptions );
1231 }
1232
1240 public static function flattenOptions( $options ) {
1241 $flatOpts = [];
1242
1243 foreach ( $options as $value ) {
1244 if ( is_array( $value ) ) {
1245 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1246 } else {
1247 $flatOpts[] = $value;
1248 }
1249 }
1250
1251 return $flatOpts;
1252 }
1253
1267 protected static function formatErrors( $errors ) {
1268 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1269 $errors = array_shift( $errors );
1270 }
1271
1272 if ( is_array( $errors ) ) {
1273 foreach ( $errors as &$error ) {
1274 $error = Html::rawElement( 'li', [],
1275 $error instanceof Message ? $error->parse() : $error
1276 );
1277 }
1278 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1279 } elseif ( $errors instanceof Message ) {
1280 $errors = $errors->parse();
1281 }
1282
1283 return Html::errorBox( $errors );
1284 }
1285
1292 protected function getMessage( $value ) {
1293 $message = Message::newFromSpecifier( $value );
1294
1295 if ( $this->mParent ) {
1296 $message->setContext( $this->mParent );
1297 }
1298
1299 return $message;
1300 }
1301
1309 public function skipLoadData( $request ) {
1310 return !empty( $this->mParams['nodata'] );
1311 }
1312
1321 // This is probably more restrictive than it needs to be, but better safe than sorry
1322 return (bool)$this->mCondState;
1323 }
1324}
1325
1327class_alias( HTMLFormField::class, 'HTMLFormField' );
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
JSON formatter wrapper class.
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.
getHelpTextHtmlDiv( $helptext)
Generate help text HTML in div format.
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.
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...
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.
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:206
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Some internal bits split of from Skin.php.
Definition Linker.php:65
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:441
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1059
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
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 the input, whatever that value is (even "0").
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.