MediaWiki  master
HTMLFormField.php
Go to the documentation of this file.
1 <?php
2 
4 
11 abstract class HTMLFormField {
13  public $mParams;
14 
16  protected $mFilterCallback;
17  protected $mName;
18  protected $mDir;
19  protected $mLabel; # String label, as HTML. Set on construction.
20  protected $mID;
21  protected $mClass = '';
22  protected $mVFormClass = '';
23  protected $mHelpClass = false;
24  protected $mDefault;
28  protected $mOptions = false;
29  protected $mOptionsLabelsNotFromMessage = false;
33  protected $mCondState = [];
34  protected $mCondStateClass = [];
35 
40  protected $mShowEmptyLabels = true;
41 
45  public $mParent;
46 
57  abstract public function getInputHTML( $value );
58 
67  public function getInputOOUI( $value ) {
68  return false;
69  }
70 
77  public function canDisplayErrors() {
78  return $this->hasVisibleOutput();
79  }
80 
93  public function msg( $key, ...$params ) {
94  if ( $this->mParent ) {
95  return $this->mParent->msg( $key, ...$params );
96  }
97  return wfMessage( $key, ...$params );
98  }
99 
107  public function hasVisibleOutput() {
108  return true;
109  }
110 
117  public function getName() {
118  return $this->mName;
119  }
120 
132  protected function getNearestField( $name, $backCompat = false ) {
133  // When the field is belong to a HTMLFormFieldCloner
134  if ( isset( $this->mParams['cloner'] ) ) {
135  $field = $this->mParams['cloner']->findNearestField( $this, $name );
136  if ( $field ) {
137  return $field;
138  }
139  }
140 
141  if ( $backCompat && str_starts_with( $name, 'wp' ) &&
142  !$this->mParent->hasField( $name )
143  ) {
144  // Don't break the existed use cases.
145  return $this->mParent->getField( substr( $name, 2 ) );
146  }
147  return $this->mParent->getField( $name );
148  }
149 
161  protected function getNearestFieldValue( $alldata, $name, $asDisplay = false, $backCompat = false ) {
162  $field = $this->getNearestField( $name, $backCompat );
163  // When the field belongs to a HTMLFormFieldCloner
164  if ( isset( $field->mParams['cloner'] ) ) {
165  $value = $field->mParams['cloner']->extractFieldData( $field, $alldata );
166  } else {
167  // Note $alldata is an empty array when first rendering a form with a formIdentifier.
168  // In that case, $alldata[$field->mParams['fieldname']] is unset and we use the
169  // field's default value
170  $value = $alldata[$field->mParams['fieldname']] ?? $field->getDefault();
171  }
172 
173  // Check invert state for HTMLCheckField
174  if ( $asDisplay && $field instanceof HTMLCheckField && ( $field->mParams['invert'] ?? false ) ) {
175  $value = !$value;
176  }
177 
178  return $value;
179  }
180 
191  protected function getNearestFieldByName( $alldata, $name, $asDisplay = false ) {
192  return (string)$this->getNearestFieldValue( $alldata, $name, $asDisplay );
193  }
194 
202  protected function validateCondState( $params ) {
203  $origParams = $params;
204  $op = array_shift( $params );
205 
206  try {
207  switch ( $op ) {
208  case 'NOT':
209  if ( count( $params ) !== 1 ) {
210  throw new MWException( "NOT takes exactly one parameter" );
211  }
212  // Fall-through intentionally
213 
214  case 'AND':
215  case 'OR':
216  case 'NAND':
217  case 'NOR':
218  foreach ( $params as $i => $p ) {
219  if ( !is_array( $p ) ) {
220  $type = gettype( $p );
221  throw new MWException( "Expected array, found $type at index $i" );
222  }
223  $this->validateCondState( $p );
224  }
225  break;
226 
227  case '===':
228  case '!==':
229  if ( count( $params ) !== 2 ) {
230  throw new MWException( "$op takes exactly two parameters" );
231  }
232  [ $name, $value ] = $params;
233  if ( !is_string( $name ) || !is_string( $value ) ) {
234  throw new MWException( "Parameters for $op must be strings" );
235  }
236  break;
237 
238  default:
239  throw new MWException( "Unknown operation" );
240  }
241  } catch ( MWException $ex ) {
242  throw new MWException(
243  "Invalid hide-if or disable-if specification for $this->mName: " .
244  $ex->getMessage() . " in " . var_export( $origParams, true ),
245  0, $ex
246  );
247  }
248  }
249 
258  protected function checkStateRecurse( array $alldata, array $params ) {
259  $op = array_shift( $params );
260  $valueChk = [ 'AND' => false, 'OR' => true, 'NAND' => false, 'NOR' => true ];
261  $valueRet = [ 'AND' => true, 'OR' => false, 'NAND' => false, 'NOR' => true ];
262 
263  switch ( $op ) {
264  case 'AND':
265  case 'OR':
266  case 'NAND':
267  case 'NOR':
268  foreach ( $params as $p ) {
269  if ( $valueChk[$op] === $this->checkStateRecurse( $alldata, $p ) ) {
270  return !$valueRet[$op];
271  }
272  }
273  return $valueRet[$op];
274 
275  case 'NOT':
276  return !$this->checkStateRecurse( $alldata, $params[0] );
277 
278  case '===':
279  case '!==':
280  [ $field, $value ] = $params;
281  $testValue = (string)$this->getNearestFieldValue( $alldata, $field, true, true );
282  switch ( $op ) {
283  case '===':
284  return ( $value === $testValue );
285  case '!==':
286  return ( $value !== $testValue );
287  }
288  }
289  }
290 
299  protected function parseCondState( $params ) {
300  $op = array_shift( $params );
301 
302  switch ( $op ) {
303  case 'AND':
304  case 'OR':
305  case 'NAND':
306  case 'NOR':
307  $ret = [ $op ];
308  foreach ( $params as $p ) {
309  $ret[] = $this->parseCondState( $p );
310  }
311  return $ret;
312 
313  case 'NOT':
314  return [ 'NOT', $this->parseCondState( $params[0] ) ];
315 
316  case '===':
317  case '!==':
318  [ $name, $value ] = $params;
319  $field = $this->getNearestField( $name, true );
320  return [ $op, $field->getName(), $value ];
321  }
322  }
323 
329  protected function parseCondStateForClient() {
330  $parsed = [];
331  foreach ( $this->mCondState as $type => $params ) {
332  $parsed[$type] = $this->parseCondState( $params );
333  }
334  return $parsed;
335  }
336 
345  public function isHidden( $alldata ) {
346  if ( !( $this->mCondState && isset( $this->mCondState['hide'] ) ) ) {
347  return false;
348  }
349 
350  return $this->checkStateRecurse( $alldata, $this->mCondState['hide'] );
351  }
352 
361  public function isDisabled( $alldata ) {
362  if ( $this->mParams['disabled'] ?? false ) {
363  return true;
364  }
365  $hidden = $this->isHidden( $alldata );
366  if ( !$this->mCondState || !isset( $this->mCondState['disable'] ) ) {
367  return $hidden;
368  }
369 
370  return $hidden || $this->checkStateRecurse( $alldata, $this->mCondState['disable'] );
371  }
372 
384  public function cancelSubmit( $value, $alldata ) {
385  return false;
386  }
387 
400  public function validate( $value, $alldata ) {
401  if ( $this->isHidden( $alldata ) ) {
402  return true;
403  }
404 
405  if ( isset( $this->mParams['required'] )
406  && $this->mParams['required'] !== false
407  && ( $value === '' || $value === false || $value === null )
408  ) {
409  return $this->msg( 'htmlform-required' );
410  }
411 
412  if ( isset( $this->mValidationCallback ) ) {
413  return ( $this->mValidationCallback )( $value, $alldata, $this->mParent );
414  }
415 
416  return true;
417  }
418 
427  public function filter( $value, $alldata ) {
428  if ( isset( $this->mFilterCallback ) ) {
429  $value = ( $this->mFilterCallback )( $value, $alldata, $this->mParent );
430  }
431 
432  return $value;
433  }
434 
442  protected function needsLabel() {
443  return true;
444  }
445 
455  public function setShowEmptyLabel( $show ) {
456  $this->mShowEmptyLabels = $show;
457  }
458 
470  protected function isSubmitAttempt( WebRequest $request ) {
471  // HTMLForm would add a hidden field of edit token for forms that require to be posted.
472  return $request->wasPosted() && $request->getCheck( 'wpEditToken' )
473  // The identifier matching or not has been checked in HTMLForm::prepareForm()
474  || $request->getCheck( 'wpFormIdentifier' );
475  }
476 
485  public function loadDataFromRequest( $request ) {
486  if ( $request->getCheck( $this->mName ) ) {
487  return $request->getText( $this->mName );
488  } else {
489  return $this->getDefault();
490  }
491  }
492 
502  public function __construct( $params ) {
503  $this->mParams = $params;
504 
505  if ( isset( $params['parent'] ) && $params['parent'] instanceof HTMLForm ) {
506  $this->mParent = $params['parent'];
507  } else {
508  // Normally parent is added automatically by HTMLForm::factory.
509  // Several field types already assume unconditionally this is always set,
510  // so deprecate manually creating an HTMLFormField without a parent form set.
512  __METHOD__ . ": Constructing an HTMLFormField without a 'parent' parameter",
513  "1.40"
514  );
515  }
516 
517  # Generate the label from a message, if possible
518  if ( isset( $params['label-message'] ) ) {
519  $this->mLabel = $this->getMessage( $params['label-message'] )->parse();
520  } elseif ( isset( $params['label'] ) ) {
521  if ( $params['label'] === '&#160;' || $params['label'] === "\u{00A0}" ) {
522  // Apparently some things set &nbsp directly and in an odd format
523  $this->mLabel = "\u{00A0}";
524  } else {
525  $this->mLabel = htmlspecialchars( $params['label'] );
526  }
527  } elseif ( isset( $params['label-raw'] ) ) {
528  $this->mLabel = $params['label-raw'];
529  }
530 
531  $this->mName = "wp{$params['fieldname']}";
532  if ( isset( $params['name'] ) ) {
533  $this->mName = $params['name'];
534  }
535 
536  if ( isset( $params['dir'] ) ) {
537  $this->mDir = $params['dir'];
538  }
539 
540  $this->mID = "mw-input-{$this->mName}";
541 
542  if ( isset( $params['default'] ) ) {
543  $this->mDefault = $params['default'];
544  }
545 
546  if ( isset( $params['id'] ) ) {
547  $this->mID = $params['id'];
548  }
549 
550  if ( isset( $params['cssclass'] ) ) {
551  $this->mClass = $params['cssclass'];
552  }
553 
554  if ( isset( $params['csshelpclass'] ) ) {
555  $this->mHelpClass = $params['csshelpclass'];
556  }
557 
558  if ( isset( $params['validation-callback'] ) ) {
559  $this->mValidationCallback = $params['validation-callback'];
560  }
561 
562  if ( isset( $params['filter-callback'] ) ) {
563  $this->mFilterCallback = $params['filter-callback'];
564  }
565 
566  if ( isset( $params['hidelabel'] ) ) {
567  $this->mShowEmptyLabels = false;
568  }
569 
570  if ( isset( $params['hide-if'] ) && $params['hide-if'] ) {
571  $this->validateCondState( $params['hide-if'] );
572  $this->mCondState['hide'] = $params['hide-if'];
573  $this->mCondStateClass[] = 'mw-htmlform-hide-if';
574  }
575  if ( !( isset( $params['disabled'] ) && $params['disabled'] ) &&
576  isset( $params['disable-if'] ) && $params['disable-if']
577  ) {
578  $this->validateCondState( $params['disable-if'] );
579  $this->mCondState['disable'] = $params['disable-if'];
580  $this->mCondStateClass[] = 'mw-htmlform-disable-if';
581  }
582  }
583 
593  public function getTableRow( $value ) {
594  [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
595  $inputHtml = $this->getInputHTML( $value );
596  $fieldType = $this->getClassName();
597  $helptext = $this->getHelpTextHtmlTable( $this->getHelpText() );
598  $cellAttributes = [];
599  $rowAttributes = [];
600  $rowClasses = '';
601 
602  if ( !empty( $this->mParams['vertical-label'] ) ) {
603  $cellAttributes['colspan'] = 2;
604  $verticalLabel = true;
605  } else {
606  $verticalLabel = false;
607  }
608 
609  $label = $this->getLabelHtml( $cellAttributes );
610 
611  $field = Html::rawElement(
612  'td',
613  [ 'class' => 'mw-input' ] + $cellAttributes,
614  $inputHtml . "\n$errors"
615  );
616 
617  if ( $this->mCondState ) {
618  $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
619  $rowClasses .= implode( ' ', $this->mCondStateClass );
620  }
621 
622  if ( $verticalLabel ) {
623  $html = Html::rawElement( 'tr',
624  $rowAttributes + [ 'class' => "mw-htmlform-vertical-label $rowClasses" ], $label );
625  $html .= Html::rawElement( 'tr',
626  $rowAttributes + [
627  'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
628  ],
629  $field );
630  } else {
631  $html = Html::rawElement( 'tr',
632  $rowAttributes + [
633  'class' => "mw-htmlform-field-$fieldType {$this->mClass} $errorClass $rowClasses"
634  ],
635  $label . $field );
636  }
637 
638  return $html . $helptext;
639  }
640 
651  public function getDiv( $value ) {
652  [ $errors, $errorClass ] = $this->getErrorsAndErrorClass( $value );
653  $inputHtml = $this->getInputHTML( $value );
654  $fieldType = $this->getClassName();
655  $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
656  $cellAttributes = [];
657  $label = $this->getLabelHtml( $cellAttributes );
658 
659  $outerDivClass = [
660  'mw-input',
661  'mw-htmlform-nolabel' => ( $label === '' )
662  ];
663 
664  $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
665 
666  if ( $horizontalLabel ) {
667  $field = "\u{00A0}" . $inputHtml . "\n$errors";
668  } else {
669  $field = Html::rawElement(
670  'div',
671  // @phan-suppress-next-line PhanUselessBinaryAddRight
672  [ 'class' => $outerDivClass ] + $cellAttributes,
673  $inputHtml . "\n$errors"
674  );
675  }
676  $divCssClasses = [ "mw-htmlform-field-$fieldType",
677  $this->mClass, $this->mVFormClass, $errorClass ];
678 
679  $wrapperAttributes = [
680  'class' => $divCssClasses,
681  ];
682  if ( $this->mCondState ) {
683  $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
684  $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
685  }
686  $html = Html::rawElement( 'div', $wrapperAttributes, $label . $field );
687  $html .= $helptext;
688 
689  return $html;
690  }
691 
701  public function getOOUI( $value ) {
702  $inputField = $this->getInputOOUI( $value );
703 
704  if ( !$inputField ) {
705  // This field doesn't have an OOUI implementation yet at all. Fall back to getDiv() to
706  // generate the whole field, label and errors and all, then wrap it in a Widget.
707  // It might look weird, but it'll work OK.
708  return $this->getFieldLayoutOOUI(
709  new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $this->getDiv( $value ) ) ] ),
710  [ 'align' => 'top' ]
711  );
712  }
713 
714  $infusable = true;
715  if ( is_string( $inputField ) ) {
716  // We have an OOUI implementation, but it's not proper, and we got a load of HTML.
717  // Cheat a little and wrap it in a widget. It won't be infusable, though, since client-side
718  // JavaScript doesn't know how to rebuilt the contents.
719  $inputField = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $inputField ) ] );
720  $infusable = false;
721  }
722 
723  $fieldType = $this->getClassName();
724  $help = $this->getHelpText();
725  $errors = $this->getErrorsRaw( $value );
726  foreach ( $errors as &$error ) {
727  $error = new OOUI\HtmlSnippet( $error );
728  }
729 
730  $config = [
731  'classes' => [ "mw-htmlform-field-$fieldType" ],
732  'align' => $this->getLabelAlignOOUI(),
733  'help' => ( $help !== null && $help !== '' ) ? new OOUI\HtmlSnippet( $help ) : null,
734  'errors' => $errors,
735  'infusable' => $infusable,
736  'helpInline' => $this->isHelpInline(),
737  ];
738  if ( $this->mClass !== '' ) {
739  $config['classes'][] = $this->mClass;
740  }
741 
742  $preloadModules = false;
743 
744  if ( $infusable && $this->shouldInfuseOOUI() ) {
745  $preloadModules = true;
746  $config['classes'][] = 'mw-htmlform-autoinfuse';
747  }
748  if ( $this->mCondState ) {
749  $config['classes'] = array_merge( $config['classes'], $this->mCondStateClass );
750  }
751 
752  // the element could specify, that the label doesn't need to be added
753  $label = $this->getLabel();
754  if ( $label && $label !== "\u{00A0}" && $label !== '&#160;' ) {
755  $config['label'] = new OOUI\HtmlSnippet( $label );
756  }
757 
758  if ( $this->mCondState ) {
759  $preloadModules = true;
760  $config['condState'] = $this->parseCondStateForClient();
761  }
762 
763  $config['modules'] = $this->getOOUIModules();
764 
765  if ( $preloadModules ) {
766  $this->mParent->getOutput()->addModules( 'mediawiki.htmlform.ooui' );
767  $this->mParent->getOutput()->addModules( $this->getOOUIModules() );
768  }
769 
770  return $this->getFieldLayoutOOUI( $inputField, $config );
771  }
772 
780  protected function getClassName() {
781  $name = explode( '\\', static::class );
782  return end( $name );
783  }
784 
790  protected function getLabelAlignOOUI() {
791  return 'top';
792  }
793 
800  protected function getFieldLayoutOOUI( $inputField, $config ) {
801  return new HTMLFormFieldLayout( $inputField, $config );
802  }
803 
812  protected function shouldInfuseOOUI() {
813  // Always infuse fields with popup help text, since the interface for it is nicer with JS
814  return $this->getHelpText() !== null && !$this->isHelpInline();
815  }
816 
824  protected function getOOUIModules() {
825  return [];
826  }
827 
838  public function getRaw( $value ) {
839  [ $errors, ] = $this->getErrorsAndErrorClass( $value );
840  $inputHtml = $this->getInputHTML( $value );
841  $helptext = $this->getHelpTextHtmlRaw( $this->getHelpText() );
842  $cellAttributes = [];
843  $label = $this->getLabelHtml( $cellAttributes );
844 
845  $html = "\n$errors";
846  $html .= $label;
847  $html .= $inputHtml;
848  $html .= $helptext;
849 
850  return $html;
851  }
852 
862  public function getVForm( $value ) {
863  // Ewwww
864  $this->mVFormClass = ' mw-ui-vform-field';
865  return $this->getDiv( $value );
866  }
867 
875  public function getInline( $value ) {
876  [ $errors, ] = $this->getErrorsAndErrorClass( $value );
877  $inputHtml = $this->getInputHTML( $value );
878  $helptext = $this->getHelpTextHtmlDiv( $this->getHelpText() );
879  $cellAttributes = [];
880  $label = $this->getLabelHtml( $cellAttributes );
881 
882  $html = "\n" . $errors .
883  $label . "\u{00A0}" .
884  $inputHtml .
885  $helptext;
886 
887  return $html;
888  }
889 
897  public function getHelpTextHtmlTable( $helptext ) {
898  if ( $helptext === null ) {
899  return '';
900  }
901 
902  $rowAttributes = [];
903  if ( $this->mCondState ) {
904  $rowAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
905  $rowAttributes['class'] = $this->mCondStateClass;
906  }
907 
908  $tdClasses = [ 'htmlform-tip' ];
909  if ( $this->mHelpClass !== false ) {
910  $tdClasses[] = $this->mHelpClass;
911  }
912  $row = Html::rawElement( 'td', [ 'colspan' => 2, 'class' => $tdClasses ], $helptext );
913  $row = Html::rawElement( 'tr', $rowAttributes, $row );
914 
915  return $row;
916  }
917 
926  public function getHelpTextHtmlDiv( $helptext ) {
927  if ( $helptext === null ) {
928  return '';
929  }
930 
931  $wrapperAttributes = [
932  'class' => [ 'htmlform-tip' ],
933  ];
934  if ( $this->mHelpClass !== false ) {
935  $wrapperAttributes['class'][] = $this->mHelpClass;
936  }
937  if ( $this->mCondState ) {
938  $wrapperAttributes['data-cond-state'] = FormatJson::encode( $this->parseCondStateForClient() );
939  $wrapperAttributes['class'] = array_merge( $wrapperAttributes['class'], $this->mCondStateClass );
940  }
941  $div = Html::rawElement( 'div', $wrapperAttributes, $helptext );
942 
943  return $div;
944  }
945 
953  public function getHelpTextHtmlRaw( $helptext ) {
954  return $this->getHelpTextHtmlDiv( $helptext );
955  }
956 
963  public function getHelpText() {
964  $helptext = null;
965 
966  if ( isset( $this->mParams['help-message'] ) ) {
967  $this->mParams['help-messages'] = [ $this->mParams['help-message'] ];
968  }
969 
970  if ( isset( $this->mParams['help-messages'] ) ) {
971  foreach ( $this->mParams['help-messages'] as $msg ) {
972  $msg = $this->getMessage( $msg );
973 
974  if ( $msg->exists() ) {
975  if ( $helptext === null ) {
976  $helptext = '';
977  } else {
978  $helptext .= $this->msg( 'word-separator' )->escaped(); // some space
979  }
980  $helptext .= $msg->parse(); // Append message
981  }
982  }
983  } elseif ( isset( $this->mParams['help'] ) ) {
984  $helptext = $this->mParams['help'];
985  }
986 
987  return $helptext;
988  }
989 
998  public function isHelpInline() {
999  return $this->mParams['help-inline'] ?? true;
1000  }
1001 
1014  public function getErrorsAndErrorClass( $value ) {
1015  $errors = $this->validate( $value, $this->mParent->mFieldData );
1016 
1017  if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1018  $errors = '';
1019  $errorClass = '';
1020  } else {
1021  $errors = self::formatErrors( $errors );
1022  $errorClass = 'mw-htmlform-invalid-input';
1023  }
1024 
1025  return [ $errors, $errorClass ];
1026  }
1027 
1035  public function getErrorsRaw( $value ) {
1036  $errors = $this->validate( $value, $this->mParent->mFieldData );
1037 
1038  if ( is_bool( $errors ) || !$this->mParent->wasSubmitted() ) {
1039  $errors = [];
1040  }
1041 
1042  if ( !is_array( $errors ) ) {
1043  $errors = [ $errors ];
1044  }
1045  foreach ( $errors as &$error ) {
1046  if ( $error instanceof Message ) {
1047  $error = $error->parse();
1048  }
1049  }
1050 
1051  return $errors;
1052  }
1053 
1058  public function getLabel() {
1059  return $this->mLabel ?? '';
1060  }
1061 
1068  public function getLabelHtml( $cellAttributes = [] ) {
1069  # Don't output a for= attribute for labels with no associated input.
1070  # Kind of hacky here, possibly we don't want these to be <label>s at all.
1071  $for = [];
1072 
1073  if ( $this->needsLabel() ) {
1074  $for['for'] = $this->mID;
1075  }
1076 
1077  $labelValue = trim( $this->getLabel() );
1078  $hasLabel = false;
1079  if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
1080  $hasLabel = true;
1081  }
1082 
1083  $displayFormat = $this->mParent->getDisplayFormat();
1084  $html = '';
1085  $horizontalLabel = $this->mParams['horizontal-label'] ?? false;
1086 
1087  if ( $displayFormat === 'table' ) {
1088  $html =
1089  Html::rawElement( 'td',
1090  [ 'class' => 'mw-label' ] + $cellAttributes,
1091  Html::rawElement( 'label', $for, $labelValue ) );
1092  } elseif ( $hasLabel || $this->mShowEmptyLabels ) {
1093  if ( $displayFormat === 'div' && !$horizontalLabel ) {
1094  $html =
1095  Html::rawElement( 'div',
1096  [ 'class' => 'mw-label' ] + $cellAttributes,
1097  Html::rawElement( 'label', $for, $labelValue ) );
1098  } else {
1099  $html = Html::rawElement( 'label', $for, $labelValue );
1100  }
1101  }
1102 
1103  return $html;
1104  }
1105 
1110  public function getDefault() {
1111  return $this->mDefault ?? null;
1112  }
1113 
1119  public function getTooltipAndAccessKey() {
1120  if ( empty( $this->mParams['tooltip'] ) ) {
1121  return [];
1122  }
1123 
1124  return Linker::tooltipAndAccesskeyAttribs( $this->mParams['tooltip'] );
1125  }
1126 
1132  public function getTooltipAndAccessKeyOOUI() {
1133  if ( empty( $this->mParams['tooltip'] ) ) {
1134  return [];
1135  }
1136 
1137  return [
1138  'title' => Linker::titleAttrib( $this->mParams['tooltip'] ),
1139  'accessKey' => Linker::accesskey( $this->mParams['tooltip'] ),
1140  ];
1141  }
1142 
1150  public function getAttributes( array $list ) {
1151  static $boolAttribs = [ 'disabled', 'required', 'autofocus', 'multiple', 'readonly' ];
1152 
1153  $ret = [];
1154  foreach ( $list as $key ) {
1155  if ( in_array( $key, $boolAttribs ) ) {
1156  if ( !empty( $this->mParams[$key] ) ) {
1157  $ret[$key] = '';
1158  }
1159  } elseif ( isset( $this->mParams[$key] ) ) {
1160  $ret[$key] = $this->mParams[$key];
1161  }
1162  }
1163 
1164  return $ret;
1165  }
1166 
1176  private function lookupOptionsKeys( $options, $needsParse ) {
1177  $ret = [];
1178  foreach ( $options as $key => $value ) {
1179  $msg = $this->msg( $key );
1180  $key = $needsParse ? $msg->parse() : $msg->plain();
1181  $ret[$key] = is_array( $value )
1182  ? $this->lookupOptionsKeys( $value, $needsParse )
1183  : strval( $value );
1184  }
1185  return $ret;
1186  }
1187 
1195  public static function forceToStringRecursive( $array ) {
1196  if ( is_array( $array ) ) {
1197  return array_map( [ __CLASS__, 'forceToStringRecursive' ], $array );
1198  } else {
1199  return strval( $array );
1200  }
1201  }
1202 
1209  public function getOptions() {
1210  if ( $this->mOptions === false ) {
1211  if ( array_key_exists( 'options-messages', $this->mParams ) ) {
1212  $needsParse = $this->mParams['options-messages-parse'] ?? false;
1213  if ( $needsParse ) {
1214  $this->mOptionsLabelsNotFromMessage = true;
1215  }
1216  $this->mOptions = $this->lookupOptionsKeys( $this->mParams['options-messages'], $needsParse );
1217  } elseif ( array_key_exists( 'options', $this->mParams ) ) {
1218  $this->mOptionsLabelsNotFromMessage = true;
1219  $this->mOptions = self::forceToStringRecursive( $this->mParams['options'] );
1220  } elseif ( array_key_exists( 'options-message', $this->mParams ) ) {
1221  $message = $this->getMessage( $this->mParams['options-message'] )->inContentLanguage()->plain();
1222  $this->mOptions = Xml::listDropDownOptions( $message );
1223  } else {
1224  $this->mOptions = null;
1225  }
1226  }
1227 
1228  return $this->mOptions;
1229  }
1230 
1236  public function getOptionsOOUI() {
1237  $oldoptions = $this->getOptions();
1238 
1239  if ( $oldoptions === null ) {
1240  return null;
1241  }
1242 
1243  return Xml::listDropDownOptionsOoui( $oldoptions );
1244  }
1245 
1253  public static function flattenOptions( $options ) {
1254  $flatOpts = [];
1255 
1256  foreach ( $options as $value ) {
1257  if ( is_array( $value ) ) {
1258  $flatOpts = array_merge( $flatOpts, self::flattenOptions( $value ) );
1259  } else {
1260  $flatOpts[] = $value;
1261  }
1262  }
1263 
1264  return $flatOpts;
1265  }
1266 
1280  protected static function formatErrors( $errors ) {
1281  if ( is_array( $errors ) && count( $errors ) === 1 ) {
1282  $errors = array_shift( $errors );
1283  }
1284 
1285  if ( is_array( $errors ) ) {
1286  $lines = [];
1287  foreach ( $errors as $error ) {
1288  if ( $error instanceof Message ) {
1289  $lines[] = Html::rawElement( 'li', [], $error->parse() );
1290  } else {
1291  $lines[] = Html::rawElement( 'li', [], $error );
1292  }
1293  }
1294 
1295  $errors = Html::rawElement( 'ul', [], implode( "\n", $lines ) );
1296  } else {
1297  if ( $errors instanceof Message ) {
1298  $errors = $errors->parse();
1299  }
1300  }
1301 
1302  return Html::errorBox( $errors );
1303  }
1304 
1311  protected function getMessage( $value ) {
1312  $message = Message::newFromSpecifier( $value );
1313 
1314  if ( $this->mParent ) {
1315  $message->setContext( $this->mParent );
1316  }
1317 
1318  return $message;
1319  }
1320 
1328  public function skipLoadData( $request ) {
1329  return !empty( $this->mParams['nodata'] );
1330  }
1331 
1339  public function needsJSForHtml5FormValidation() {
1340  if ( $this->mCondState ) {
1341  // This is probably more restrictive than it needs to be, but better safe than sorry
1342  return true;
1343  }
1344  return false;
1345  }
1346 }
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.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
A checkbox field.
The parent class to generate form fields.
getName()
Get the field name that will be used for submission.
getHelpTextHtmlDiv( $helptext)
Generate help text HTML in div format.
needsJSForHtml5FormValidation()
Whether this field requires the user agent to have JavaScript enabled for the client-side HTML5 form ...
parseCondStateForClient()
Parse the cond-state array for client-side.
HTMLForm null $mParent
getLabelAlignOOUI()
Get label alignment when generating field for OOUI.
getInputOOUI( $value)
Same as getInputHTML, but returns an OOUI object.
getHelpTextHtmlRaw( $helptext)
Generate help text HTML formatted for raw output.
isHelpInline()
Determine if the help text should be displayed inline.
isDisabled( $alldata)
Test whether this field is supposed to be disabled, based on the values of the other form fields.
getFieldLayoutOOUI( $inputField, $config)
Get a FieldLayout (or subclass thereof) to wrap this field in when using OOUI output.
validateCondState( $params)
Validate the cond-state params, the existence check of fields should be done later.
isHidden( $alldata)
Test whether this field is supposed to be hidden, based on the values of the other form fields.
filter( $value, $alldata)
bool $mShowEmptyLabels
If true will generate an empty div element with no label.
getNearestField( $name, $backCompat=false)
Get the closest field matching a given name.
shouldInfuseOOUI()
Whether the field should be automatically infused.
__construct( $params)
Initialise the object.
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
getClassName()
Gets the non namespaced class name.
getOOUIModules()
Get the list of extra ResourceLoader modules which must be loaded client-side before it's possible to...
getNearestFieldByName( $alldata, $name, $asDisplay=false)
Fetch a field value from $alldata for the closest field matching a given name.
skipLoadData( $request)
Skip this field when collecting data.
array $mCondState
Array to hold params for 'hide-if' or 'disable-if' statements.
getVForm( $value)
Get the complete field 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.
getErrorsAndErrorClass( $value)
Determine form errors to display and their classes.
validate( $value, $alldata)
Override this function to add specific validation checks on the field input.
getOOUI( $value)
Get the OOUI version of the div.
canDisplayErrors()
True if this field type is able to display errors; false if validation errors need to be displayed in...
checkStateRecurse(array $alldata, array $params)
Helper function for isHidden and isDisabled to handle recursive data structures.
getHelpTextHtmlTable( $helptext)
Generate help text HTML in table format.
array array[] $mParams
static flattenOptions( $options)
flatten an array of options to a single array, for instance, a set of "<options>" inside "<optgroups>...
getRaw( $value)
Get the complete raw fields for the input, including help text, labels, and whatever.
getTooltipAndAccessKeyOOUI()
Returns the attributes required for the tooltip and accesskey, for OOUI widgets' config.
getErrorsRaw( $value)
Determine form errors to display, returning them in an array.
getHelpText()
Determine the help text to display.
getOptions()
Fetch the array of options from the field's parameters.
parseCondState( $params)
Parse the cond-state array to use the field name for submission, since the key in the form descriptor...
getTableRow( $value)
Get the complete table row for the input, including help text, labels, and whatever.
getInline( $value)
Get the complete field as an inline element.
isSubmitAttempt(WebRequest $request)
Can we assume that the request is an attempt to submit a HTMLForm, as opposed to an attempt to just v...
msg( $key,... $params)
Get a translated interface message.
array null false $mOptions
getLabelHtml( $cellAttributes=[])
loadDataFromRequest( $request)
Get the value that this input has been set to from a posted form, or the input's default value if it ...
getAttributes(array $list)
Returns the given attributes from the parameters.
getNearestFieldValue( $alldata, $name, $asDisplay=false, $backCompat=false)
Fetch a field value from $alldata for the closest field matching a given name.
setShowEmptyLabel( $show)
Tell the field whether to generate a separate label element if its label is blank.
needsLabel()
Should this field have a label, or is there no input element with the appropriate id for the label to...
getDiv( $value)
Get the complete div for the input, including help text, labels, and whatever.
cancelSubmit( $value, $alldata)
Override this function if the control can somehow trigger a form submission that shouldn't actually s...
getOptionsOOUI()
Get options and make them into arrays suitable for OOUI.
hasVisibleOutput()
If this field has a user-visible output or not.
getTooltipAndAccessKey()
Returns the attributes required for the tooltip and accesskey, for Html::element() etc.
static formatErrors( $errors)
Formats one or more errors as accepted by field validation-callback.
static forceToStringRecursive( $array)
Recursively forces values in an array to strings, because issues arise with integer 0 as a value.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition: HTMLForm.php:151
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
MediaWiki exception.
Definition: MWException.php:30
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:143
static newFromSpecifier( $value)
Transform a MessageSpecifier or a primitive value used interchangeably with specifiers (a message key...
Definition: Message.php:425
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Definition: WebRequest.php:47
getCheck( $name)
Return true if the named value is set in the input, whatever that value is (even "0").
Definition: WebRequest.php:687
wasPosted()
Returns true if the present request was reached by a POST operation, false otherwise (GET,...
Definition: WebRequest.php:816
static listDropDownOptions( $list, $params=[])
Build options for a drop-down box from a textual list.
Definition: Xml.php:547
static listDropDownOptionsOoui( $options)
Convert options for a drop-down box into a format accepted by OOUI\DropdownInputWidget etc.
Definition: Xml.php:597
return true
Definition: router.php:90
if(!file_exists( $CREDITS)) $lines