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;
18use StatusValue;
19
26abstract class HTMLFormField {
28 public $mParams;
29
31 protected $mValidationCallback;
33 protected $mName;
34 protected $mDir;
35 protected $mLabel; # String label, as HTML. Set on construction.
36 protected $mID;
37 protected $mClass = '';
38 protected $mVFormClass = '';
39 protected $mHelpClass = false;
40 protected $mDefault;
41 private $mNotices;
42
46 protected $mOptions = false;
51 protected $mCondState = [];
52 protected $mCondStateClass = [];
53
58 protected $mShowEmptyLabels = true;
59
63 public $mParent;
64
75 abstract public function getInputHTML( $value );
76
85 public function getInputOOUI( $value ) {
86 return false;
87 }
88
100 public function getInputCodex( $value, $hasErrors ) {
101 // If not overridden, fall back to getInputHTML()
102 return $this->getInputHTML( $value );
103 }
104
111 public function canDisplayErrors() {
112 return $this->hasVisibleOutput();
113 }
114
127 public function msg( $key, ...$params ) {
128 if ( $this->mParent ) {
129 return $this->mParent->msg( $key, ...$params );
130 }
131 return wfMessage( $key, ...$params );
132 }
133
141 public function hasVisibleOutput() {
142 return true;
143 }
144
151 public function getName() {
152 return $this->mName;
153 }
154
166 protected function getNearestField( $name, $backCompat = false ) {
167 // When the field is belong to a HTMLFormFieldCloner
168 $cloner = $this->mParams['cloner'] ?? null;
169 if ( $cloner instanceof HTMLFormFieldCloner ) {
170 $field = $cloner->findNearestField( $this, $name );
171 if ( $field ) {
172 return $field;
173 }
174 }
175
176 if ( $backCompat && str_starts_with( $name, 'wp' ) &&
177 !$this->mParent->hasField( $name )
178 ) {
179 // Don't break the existed use cases.
180 return $this->mParent->getField( substr( $name, 2 ) );
181 }
182 return $this->mParent->getField( $name );
183 }
184
196 protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
197 $field = $this->getNearestField( $name, $backCompat );
198 // When the field belongs to a HTMLFormFieldCloner
199 $cloner = $field->mParams['cloner'] ?? null;
200 if ( $cloner instanceof HTMLFormFieldCloner ) {
201 $value = $cloner->extractFieldData( $field, $alldata );
202 } else {
203 // Note $alldata is an empty array when first rendering a form with a formIdentifier.
204 // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
205 // field's default value
206 $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
207 }
208
209 // Check invert state for HTMLCheckField
210 if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
211 $value = !$value;
212 }
213
214 return $value;
215 }
216
227 protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
228 return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
229 }
230
237 protected function validateCondState( $params ) {
238 $origParams = $params;
239 $op = array_shift( $params );
240
241 $makeException = function ( string $details ) use ( $origParams ): InvalidArgumentException {
242 return new InvalidArgumentException(
243 "Invalid hide-if or disable-if specification for $this->mName: " .
244 $details . " in " . var_export( $origParams, true )
245 );
246 };
247
248 switch ( $op ) {
249 case 'NOT':
250 if ( count( $params ) !== 1 ) {
251 throw $makeException( "NOT takes exactly one parameter" );
252 }
253 // Fall-through intentionally
254
255 case 'AND':
256 case 'OR':
257 case 'NAND':
258 case 'NOR':
259 foreach ( $params as $i => $p ) {
260 if ( !is_array( $p ) ) {
261 $type = gettype( $p );
262 throw $makeException( "Expected array, found $type at index $i" );
263 }
264 $this->validateCondState( $p );
265 }
266 break;
267
268 case '===':
269 case '!==':
270 if ( count( $params ) !== 2 ) {
271 throw $makeException( "$op takes exactly two parameters" );
272 }
273 [ $name, $value ] = $params;
274 if ( !is_string( $name ) || !is_string( $value ) ) {
275 throw $makeException( "Parameters for $op must be strings" );
276 }
277 break;
278
279 default:
280 throw $makeException( "Unknown operation" );
281 }
282 }
283
291 protected function checkStateRecurse( array $alldata, array $params ) {
292 $op = array_shift( $params );
293 $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
294 $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
295
296 switch ( $op ) {
297 case 'AND':
298 case 'OR':
299 case 'NAND':
300 case 'NOR':
301 foreach ( $params as $p ) {
302 if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
303 return !$valueRet[$op];
304 }
305 }
306 return $valueRet[$op];
307
308 case 'NOT':
309 return !$this->checkStateRecurse( $alldata, $params[0] );
310
311 case '===':
312 case '!==':
313 [ $field, $value ] = $params;
314 $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true );
315 switch ( $op ) {
316 case '===':
317 return ( $value === $testValue );
318 case '!==':
319 return ( $value !== $testValue );
320 }
321 }
322 }
323
332 protected function parseCondState( $params ) {
333 $op = array_shift( $params );
334
335 switch ( $op ) {
336 case 'AND':
337 case 'OR':
338 case 'NAND':
339 case 'NOR':
340 $ret = [ $op ];
341 foreach ( $params as $p ) {
342 $ret[] = $this->parseCondState( $p );
343 }
344 return $ret;
345
346 case 'NOT':
347 return [ 'NOT', $this->parseCondState( $params[0] ) ];
348
349 case '===':
350 case '!==':
351 [ $name, $value ] = $params;
352 $field = $this->getNearestField( $name, true );
353 return [ $op, $field->getName(), $value ];
354 }
355 }
356
362 protected function parseCondStateForClient() {
363 $parsed = [];
364 foreach ( $this->mCondState as $type => $params ) {
365 $parsed[$type] = $this->parseCondState( $params );
366 }
367 return $parsed;
368 }
369
378 public function isHidden( $alldata ) {
379 return isset( $this->mCondState['hide'] ) &&
380 $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
381 }
382
391 public function isDisabled( $alldata ) {
392 return ( $this->mParams['disabled'] ?? false ) ||
393 $this->isHidden( $alldata ) ||
394 ( isset( $this->mCondState['disable'] )
395 && $this->checkStateRecurse( $alldata, $this->mCondState['disable'] ) );
396 }
397
409 public function cancelSubmit( $value, $alldata ) {
410 return false;
411 }
412
425 public function validate( $value, $alldata ) {
426 if ( $this->isHidden( $alldata ) ) {
427 return true;
428 }
429
430 if ( isset( $this->mParams['required'] )
431 && $this->mParams['required'] !== false
432 && ( $value === '' || $value === false || $value === null )
433 ) {
434 return $this->msg( 'htmlform-required' );
435 }
436
437 if ( !isset( $this->mValidationCallback ) ) {
438 return true;
439 }
440
441 $p = ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
442
443 if ( $p instanceof StatusValue ) {
444 $language = $this->mParent ? $this->mParent->getLanguage() : RequestContext::getMain()->getLanguage();
445
446 return $p->isGood() ? true : Status::wrap( $p )->getHTML( false, false, $language );
447 }
448
449 return $p;
450 }
451
460 public function filter( $value, $alldata ) {
461 if ( isset( $this->mFilterCallback ) ) {
462 $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
463 }
464
465 return $value;
466 }
467
475 protected function needsLabel() {
476 return true;
477 }
478
488 public function setShowEmptyLabel( $show ) {
489 $this->mShowEmptyLabels = $show;
490 }
491
503 protected function isSubmitAttempt( WebRequest $request ) {
504 // HTMLForm would add a hidden field of edit token for forms that require to be posted.
505 return ( $request->wasPosted() && $request->getCheck( 'wpEditToken' ) )
506 // The identifier matching or not has been checked in HTMLForm::prepareForm()
507 || $request->getCheck( 'wpFormIdentifier' );
508 }
509
518 public function loadDataFromRequest( $request ) {
519 if ( $request->getCheck( $this->mName ) ) {
520 return $request->getText( $this->mName );
521 } else {
522 return $this->getDefault();
523 }
524 }
525
534 public function __construct( $params ) {
535 $this->mParams = $params;
536
537 if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
538 $this->mParent = $params['parent'];
539 } else {
540 // Normally parent is added automatically by HTMLForm::factory.
541 // Several field types already assume unconditionally this is always set,
542 // so deprecate manually creating an HTMLFormField without a parent form set.
544 __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
545 "1.40"
546 );
547 }
548
549 # Generate the label from a message, if possible
550 if ( isset( $params['label-message'] ) ) {
551 $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
552 } elseif ( isset( $params['label'] ) ) {
553 if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
554 // Apparently some things set &nbsp directly and in an odd format
555 $this->mLabel = "\u{00A0}";
556 } else {
557 $this->mLabel = htmlspecialchars( $params['label'] );
558 }
559 } elseif ( isset( $params['label-raw'] ) ) {
560 $this->mLabel = $params['label-raw'];
561 }
562
563 $this->mName = $params['name'] ?? 'wp' . $params['fieldname'];
564
565 if ( isset( $params['dir'] ) ) {
566 $this->mDir = $params['dir'];
567 }
568
569 $this->mID = "mw-input-{$this->mName}";
570
571 if ( isset( $params['default'] ) ) {
572 $this->mDefault = $params['default'];
573 }
574
575 if ( isset( $params['id'] ) ) {
576 $this->mID = $params['id'];
577 }
578
579 if ( isset( $params['cssclass'] ) ) {
580 $this->mClass = $params['cssclass'];
581 }
582
583 if ( isset( $params['csshelpclass'] ) ) {
584 $this->mHelpClass = $params['csshelpclass'];
585 }
586
587 if ( isset( $params['validation-callback'] ) ) {
588 $this->mValidationCallback = $params['validation-callback'];
589 }
590
591 if ( isset( $params['filter-callback'] ) ) {
592 $this->mFilterCallback = $params['filter-callback'];
593 }
594
595 if ( isset( $params['hidelabel'] ) ) {
596 $this->mShowEmptyLabels = false;
597 }
598 if ( isset( $params['notices'] ) ) {
599 $this->mNotices = $params['notices'];
600 }
601
602 if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
603 $this->validateCondState( $params['hide-if'] );
604 $this->mCondState['hide'] = $params['hide-if'];
605 $this->mCondStateClass[] = 'mw-htmlform-hide-if';
606 }
607 if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
608 isset( $params['disable-if'] ) && $params['disable-if']
609 ) {
610 $this->validateCondState( $params['disable-if'] );
611 $this->mCondState['disable'] = $params['disable-if'];
612 $this->mCondStateClass[] = 'mw-htmlform-disable-if';
613 }
614 }
615
625 public function getTableRow( $value ) {
626 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
627 $inputHtml = $this->getInputHTML( $value );
628 $fieldType = $this->getClassName();
629 $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
630 $cellAttributes = [];
631 $rowAttributes = [];
632 $rowClasses = '';
633
634 if ( !empty( $this->mParams['vertical-label'] ) ) {
635 $cellAttributes['colspan'] = 2;
636 $verticalLabel = true;
637 } else {
638 $verticalLabel = false;
639 }
640
641 $label = $this->getLabelHtml( $cellAttributes );
642
643 $field = Html::rawElement(
644 'td',
645 [ 'class' => 'mw-input' ] + $cellAttributes,
646 $inputHtml . "\n$errors"
647 );
648
649 if ( $this->mCondState ) {
650 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
651 $rowClasses .= implode( ' ', $this->mCondStateClass );
652 }
653
654 if ( $verticalLabel ) {
655 $html = Html::rawElement( 'tr',
656 $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
657 $html .= Html::rawElement( 'tr',
658 $rowAttributes + [
659 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
660 ],
661 $field );
662 } else {
663 $html = Html::rawElement( 'tr',
664 $rowAttributes + [
665 'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
666 ],
667 $label . $field );
668 }
669
670 return $html . $helptext;
671 }
672
683 public function getDiv( $value ) {
684 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
685 $inputHtml = $this->getInputHTML( $value );
686 $fieldType = $this->getClassName();
687 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
688 $cellAttributes = [];
689 $label = $this->getLabelHtml( $cellAttributes );
690
691 $outerDivClass = [
692 'mw-input',
693 'mw-htmlform-nolabel' => ( $label === '' )
694 ];
695
696 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
697
698 if ( $horizontalLabel ) {
699 $field = "\u{00A0}" . $inputHtml . "\n$errors";
700 } else {
701 $field = Html::rawElement(
702 'div',
703 // @phan-suppress-next-line PhanUselessBinaryAddRight
704 [ 'class' => $outerDivClass ] + $cellAttributes,
705 $inputHtml . "\n$errors"
706 );
707 }
708
709 $wrapperAttributes = [ 'class' => [
710 "mw-htmlform-field-$fieldType",
713 $errorClass,
714 ] ];
715 if ( $this->mCondState ) {
716 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
717 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
718 }
719 return Html::rawElement( 'div', $wrapperAttributes, $label . $field ) .
720 $helptext;
721 }
722
732 public function getOOUI( $value ) {
733 $inputField = $this->getInputOOUI( $value );
734
735 if ( !$inputField ) {
736 // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
737 // generate the whole field, label and errors and all, then wrap it in a Widget.
738 // It might look weird, but it'll work OK.
739 return $this->getFieldLayoutOOUI(
740 new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
741 [ 'align' => 'top' ]
742 );
743 }
744
745 $infusable = true;
746 if ( is_string( $inputField ) ) {
747 // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
748 // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
749 // JavaScript doesn't know how to rebuilt the contents.
750 $inputField = new \OOUI\Widget( [ 'content' => new \OOUI\HtmlSnippet( $inputField ) ] );
751 $infusable = false;
752 }
753
754 $fieldType = $this->getClassName();
755 $help = $this->getHelpText();
756 $errors = $this->getErrorsRaw( $value );
757 foreach ( $errors as &$error ) {
758 $error = new \OOUI\HtmlSnippet( $error );
759 }
760
761 $config = [
762 'classes' => [ "mw-htmlform-field-$fieldType" ],
763 'align' => $this->getLabelAlignOOUI(),
764 'help' => ( $help !== null && $help !== '' ) ? new \OOUI\HtmlSnippet( $help ) : null,
765 'errors' => $errors,
766 'infusable' => $infusable,
767 'helpInline' => $this->isHelpInline(),
768 'notices' => $this->mNotices ?: [],
769 ];
770 if ( $this->mClass !== '' ) {
771 $config['classes'][] = $this->mClass;
772 }
773
774 $preloadModules = false;
775
776 if ( $infusable && $this->shouldInfuseOOUI() ) {
777 $preloadModules = true;
778 $config['classes'][] = 'mw-htmlform-autoinfuse';
779 }
780 if ( $this->mCondState ) {
781 $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
782 }
783
784 // the element could specify, that the label doesn't need to be added
785 $label = $this->getLabel();
786 if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
787 $config['label'] = new \OOUI\HtmlSnippet( $label );
788 }
789
790 if ( $this->mCondState ) {
791 $preloadModules = true;
792 $config['condState'] = $this->parseCondStateForClient();
793 }
794
795 $config['modules'] = $this->getOOUIModules();
796
797 if ( $preloadModules ) {
798 $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
799 $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
800 }
801
802 return $this->getFieldLayoutOOUI( $inputField, $config );
803 }
804
812 public function getCodex( $value ) {
813 $isDisabled = ( $this->mParams['disabled'] ?? false );
814
815 // Label
816 $labelDiv = '';
817 $labelValue = trim( $this->getLabel() );
818 // For weird historical reasons, a non-breaking space is treated as an empty label
819 // Check for both a literal nbsp ("\u{00A0}") and the HTML-encoded version
820 if ( $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' ) {
821 $labelFor = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
822 $labelClasses = [ 'cdx-label' ];
823 if ( $isDisabled ) {
824 $labelClasses[] = 'cdx-label--disabled';
825 }
826 // <div class="cdx-label">
827 $labelDiv = Html::rawElement( 'div', [ 'class' => $labelClasses ],
828 // <label class="cdx-label__label" for="ID">
829 Html::rawElement( 'label', [ 'class' => 'cdx-label__label' ] + $labelFor,
830 // <span class="cdx-label__label__text">
831 Html::rawElement( 'span', [ 'class' => 'cdx-label__label__text' ],
832 $labelValue
833 )
834 )
835 );
836 }
837
838 // Help text
839 // <div class="cdx-field__help-text">
840 $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText(), [ 'cdx-field__help-text' ] );
841
842 // Validation message
843 // <div class="cdx-field__validation-message">
844 // $errors is a <div class="cdx-message">
845 // FIXME right now this generates a block message (cdx-message--block), we want an inline message instead
846 $validationMessage = '';
847 [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
848 if ( $errors !== '' ) {
849 $validationMessage = Html::rawElement( 'div', [ 'class' => 'cdx-field__validation-message' ],
850 $errors
851 );
852 }
853
854 // Control
855 $inputHtml = $this->getInputCodex( $value, $errors !== '' );
856 // <div class="cdx-field__control cdx-field__control--has-help-text">
857 $controlClasses = [ 'cdx-field__control' ];
858 if ( $helptext ) {
859 $controlClasses[] = 'cdx-field__control--has-help-text';
860 }
861 $control = Html::rawElement( 'div', [ 'class' => $controlClasses ], $inputHtml );
862
863 // <div class="cdx-field">
864 $fieldClasses = [
865 "mw-htmlform-field-{$this->getClassName()}",
867 $errorClass,
868 'cdx-field'
869 ];
870 if ( $isDisabled ) {
871 $fieldClasses[] = 'cdx-field--disabled';
872 }
873 $fieldAttributes = [];
874 // Set data attribute and CSS class for client side handling of hide-if / disable-if
875 if ( $this->mCondState ) {
876 $fieldAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
877 $fieldClasses = array_merge( $fieldClasses, $this->mCondStateClass );
878 }
879
880 return Html::rawElement( 'div', [ 'class' => $fieldClasses ] + $fieldAttributes,
881 $labelDiv . $control . $helptext . $validationMessage
882 );
883 }
884
892 protected function getClassName() {
893 $name = explode( '\\', static::class );
894 return end( $name );
895 }
896
902 protected function getLabelAlignOOUI() {
903 return 'top';
904 }
905
912 protected function getFieldLayoutOOUI( $inputField, $config ) {
913 return new HTMLFormFieldLayout( $inputField, $config );
914 }
915
924 protected function shouldInfuseOOUI() {
925 // Always infuse fields with popup help text, since the interface for it is nicer with JS
926 return !$this->isHelpInline() && $this->getHelpMessages();
927 }
928
936 protected function getOOUIModules() {
937 return [];
938 }
939
950 public function getRaw( $value ) {
951 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
952 return "\n" . $errors .
953 $this->getLabelHtml() .
954 $this->getInputHTML( $value ) .
955 $this->getHelpTextHtmlRaw( $this->getHelpText() );
956 }
957
967 public function getVForm( $value ) {
968 // Ewwww
969 $this->mVFormClass = ' mw-ui-vform-field';
970 return $this->getDiv( $value );
971 }
972
980 public function getInline( $value ) {
981 [ $errors, ] = $this->getErrorsAndErrorClass( $value );
982 return "\n" . $errors .
983 $this->getLabelHtml() .
984 "\u{00A0}" .
985 $this->getInputHTML( $value ) .
986 $this->getHelpTextHtmlDiv( $this->getHelpText() );
987 }
988
996 public function getHelpTextHtmlTable( $helptext ) {
997 if ( $helptext === null ) {
998 return '';
999 }
1000
1001 $rowAttributes = [];
1002 if ( $this->mCondState ) {
1003 $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1004 $rowAttributes['class'] = $this->mCondStateClass;
1005 }
1006
1007 $tdClasses = [ 'htmlform-tip' ];
1008 if ( $this->mHelpClass !== false ) {
1009 $tdClasses[] = $this->mHelpClass;
1010 }
1011 return Html::rawElement( 'tr', $rowAttributes,
1012 Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext )
1013 );
1014 }
1015
1025 public function getHelpTextHtmlDiv( $helptext, $cssClasses = [] ) {
1026 if ( $helptext === null ) {
1027 return '';
1028 }
1029
1030 $wrapperAttributes = [
1031 'class' => array_merge( $cssClasses, [ 'htmlform-tip' ] ),
1032 ];
1033 if ( $this->mHelpClass !== false ) {
1034 $wrapperAttributes['class'][] = $this->mHelpClass;
1035 }
1036 if ( $this->mCondState ) {
1037 $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
1038 $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
1039 }
1040 return Html::rawElement( 'div', $wrapperAttributes, $helptext );
1041 }
1042
1050 public function getHelpTextHtmlRaw( $helptext ) {
1051 return $this->getHelpTextHtmlDiv( $helptext );
1052 }
1053
1054 private function getHelpMessages(): array {
1055 if ( isset( $this->mParams['help-message'] ) ) {
1056 return [ $this->mParams['help-message'] ];
1057 } elseif ( isset( $this->mParams['help-messages'] ) ) {
1058 return $this->mParams['help-messages'];
1059 } elseif ( isset( $this->mParams['help'] ) ) {
1060 return [ new HtmlArmor( $this->mParams['help'] ) ];
1061 }
1062
1063 return [];
1064 }
1065
1072 public function getHelpText() {
1073 $html = [];
1074
1075 foreach ( $this->getHelpMessages() as $msg ) {
1076 if ( $msg instanceof HtmlArmor ) {
1077 $html[] = HtmlArmor::getHtml( $msg );
1078 } else {
1079 $msg = $this->getMessage( $msg );
1080 if ( $msg->exists() ) {
1081 $html[] = $msg->parse();
1082 }
1083 }
1084 }
1085
1086 return $html ? implode( $this->msg( 'word-separator' )->escaped(), $html ) : null;
1087 }
1088
1097 public function isHelpInline() {
1098 return $this->mParams['help-inline'] ?? true;
1099 }
1100
1113 public function getErrorsAndErrorClass( $value ) {
1114 $errors = $this->validate( $value, $this->mParent->mFieldData );
1115
1116 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1117 return [ '', '' ];
1118 }
1119
1120 return [ self::formatErrors( $errors ), 'mw-htmlform-invalid-input' ];
1121 }
1122
1130 public function getErrorsRaw( $value ) {
1131 $errors = $this->validate( $value, $this->mParent->mFieldData );
1132
1133 if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1134 return [];
1135 }
1136
1137 if ( !is_array( $errors ) ) {
1138 $errors = [ $errors ];
1139 }
1140 foreach ( $errors as &$error ) {
1141 if ( $error instanceof Message ) {
1142 $error = $error->parse();
1143 }
1144 }
1145
1146 return $errors;
1147 }
1148
1153 public function getLabel() {
1154 return $this->mLabel ?? '';
1155 }
1156
1163 public function getLabelHtml( $cellAttributes = [] ) {
1164 # Don't output a for= attribute for labels with no associated input.
1165 # Kind of hacky here, possibly we don't want these to be <label>s at all.
1166 $for = $this->needsLabel() ? [ 'for' => $this->mID ] : [];
1167
1168 $labelValue = trim( $this->getLabel() );
1169 $hasLabel = $labelValue !== '' && $labelValue !== "\u{00A0}" && $labelValue !== '&#160;';
1170
1171 $displayFormat = $this->mParent->getDisplayFormat();
1172 $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1173
1174 if ( $displayFormat === 'table' ) {
1175 return Html::rawElement( 'td',
1176 [ 'class' => 'mw-label' ] + $cellAttributes,
1177 Html::rawElement( 'label', $for, $labelValue ) );
1178 } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1179 if ( $displayFormat === 'div' && !$horizontalLabel ) {
1180 return Html::rawElement( 'div',
1181 [ 'class' => 'mw-label' ] + $cellAttributes,
1182 Html::rawElement( 'label', $for, $labelValue ) );
1183 } else {
1184 return Html::rawElement( 'label', $for, $labelValue );
1185 }
1186 }
1187
1188 return '';
1189 }
1190
1195 public function getDefault() {
1196 return $this->mDefault ?? null;
1197 }
1198
1204 public function getTooltipAndAccessKey() {
1205 if ( empty( $this->mParams['tooltip'] ) ) {
1206 return [];
1207 }
1208
1209 return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1210 }
1211
1217 public function getTooltipAndAccessKeyOOUI() {
1218 if ( empty( $this->mParams['tooltip'] ) ) {
1219 return [];
1220 }
1221
1222 return [
1223 'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1224 'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1225 ];
1226 }
1227
1235 public function getAttributes( array $list ) {
1236 static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1237
1238 $ret = [];
1239 foreach ( $list as $key ) {
1240 if ( in_array( $key, $boolAttribs ) ) {
1241 if ( !empty( $this->mParams[$key] ) ) {
1242 $ret[$key] = '';
1243 }
1244 } elseif ( isset( $this->mParams[$key] ) ) {
1245 $ret[$key] = $this->mParams[$key];
1246 }
1247 }
1248
1249 return $ret;
1250 }
1251
1261 private function lookupOptionsKeys( $options, $needsParse ) {
1262 $ret = [];
1263 foreach ( $options as $key => $value ) {
1264 $msg = $this->msg( $key );
1265 $msgAsText = $needsParse ? $msg->parse() : $msg->plain();
1266 if ( array_key_exists( $msgAsText, $ret ) ) {
1267 LoggerFactory::getInstance( 'error' )->error(
1268 'The option that uses the message key {msg_key_one} has the same translation as ' .
1269 'another option in {lang}. This means that {msg_key_one} will not be used as an option.',
1270 [
1271 'msg_key_one' => $key,
1272 'lang' => $this->mParent ?
1273 $this->mParent->getLanguageCode()->toBcp47Code() :
1274 RequestContext::getMain()->getLanguageCode()->toBcp47Code(),
1275 ]
1276 );
1277 continue;
1278 }
1279 $ret[$msgAsText] = is_array( $value )
1280 ? $this->lookupOptionsKeys( $value, $needsParse )
1281 : strval( $value );
1282 }
1283 return $ret;
1284 }
1285
1293 public static function forceToStringRecursive( $array ) {
1294 if ( is_array( $array ) ) {
1295 return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1296 } else {
1297 return strval( $array );
1298 }
1299 }
1300
1307 public function getOptions() {
1308 if ( $this->mOptions === false ) {
1309 if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1310 $needsParse = $this->mParams['options-messages-parse'] ?? false;
1311 if ( $needsParse ) {
1312 $this->mOptionsLabelsNotFromMessage = true;
1313 }
1314 $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1315 } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1316 $this->mOptionsLabelsNotFromMessage = true;
1317 $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1318 } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1319 $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1320 $this->mOptions = Html::listDropdownOptions( $message );
1321 } else {
1322 $this->mOptions = null;
1323 }
1324 }
1325
1326 return $this->mOptions;
1327 }
1328
1334 public function getOptionsOOUI() {
1335 $oldoptions = $this->getOptions();
1336
1337 if ( $oldoptions === null ) {
1338 return null;
1339 }
1340
1341 return Html::listDropdownOptionsOoui( $oldoptions );
1342 }
1343
1351 public static function flattenOptions( $options ) {
1352 $flatOpts = [];
1353
1354 foreach ( $options as $value ) {
1355 if ( is_array( $value ) ) {
1356 $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1357 } else {
1358 $flatOpts[] = $value;
1359 }
1360 }
1361
1362 return $flatOpts;
1363 }
1364
1378 protected static function formatErrors( $errors ) {
1379 if ( is_array( $errors ) && count( $errors ) === 1 ) {
1380 $errors = array_shift( $errors );
1381 }
1382
1383 if ( is_array( $errors ) ) {
1384 foreach ( $errors as &$error ) {
1385 $error = Html::rawElement( 'li', [],
1386 $error instanceof Message ? $error->parse() : $error
1387 );
1388 }
1389 $errors = Html::rawElement( 'ul', [], implode( "\n", $errors ) );
1390 } elseif ( $errors instanceof Message ) {
1391 $errors = $errors->parse();
1392 }
1393
1394 return Html::errorBox( $errors );
1395 }
1396
1403 protected function getMessage( $value ) {
1404 $message = Message::newFromSpecifier( $value );
1405
1406 if ( $this->mParent ) {
1407 $message->setContext( $this->mParent );
1408 }
1409
1410 return $message;
1411 }
1412
1420 public function skipLoadData( $request ) {
1421 return !empty( $this->mParams['nodata'] );
1422 }
1423
1432 // This is probably more restrictive than it needs to be, but better safe than sorry
1433 return (bool)$this->mCondState;
1434 }
1435}
1436
1438class_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
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.
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...
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: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
Create PSR-3 logger objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition Message.php:454
parse()
Fully parse the text from wikitext to HTML.
Definition Message.php:1097
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.