MediaWiki  master
HTMLFormFieldCloner.php
Go to the documentation of this file.
1 <?php
2 
39  private static $counter = 0;
40 
46  protected $uniqueId;
47 
48  public function __construct( $params ) {
49  $this->uniqueId = static::class . ++self::$counter . 'x';
50  parent::__construct( $params );
51 
52  if ( empty( $this->mParams['fields'] ) || !is_array( $this->mParams['fields'] ) ) {
53  throw new MWException( 'HTMLFormFieldCloner called without any fields' );
54  }
55 
56  // Make sure the delete button, if explicitly specified, is sane
57  // @phan-suppress-next-line PhanTypeMismatchDimFetch Phan is very confused
58  if ( isset( $this->mParams['fields']['delete'] ) ) {
59  $class = 'mw-htmlform-cloner-delete-button';
60  $info = $this->mParams['fields']['delete'] + [
61  'formnovalidate' => true,
62  'cssclass' => $class
63  ];
64  unset( $info['name'], $info['class'] );
65 
66  if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
67  throw new MWException(
68  'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
69  );
70  }
71 
72  if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
73  $info['cssclass'] .= " $class";
74  }
75 
76  $this->mParams['fields']['delete'] = $info;
77  }
78  }
79 
87  protected function createFieldsForKey( $key ) {
88  $fields = [];
89  foreach ( $this->mParams['fields'] as $fieldname => $info ) {
90  $name = "{$this->mName}[$key][$fieldname]";
91  if ( isset( $info['name'] ) ) {
92  $info['name'] = "{$this->mName}[$key][{$info['name']}]";
93  } else {
94  $info['name'] = $name;
95  }
96  if ( isset( $info['id'] ) ) {
97  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--{$info['id']}" );
98  } else {
99  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" );
100  }
101  // Copy the hide-if rules to "child" fields, so that the JavaScript code handling them
102  // (resources/src/mediawiki/htmlform/hide-if.js) doesn't have to handle nested fields.
103  if ( $this->mHideIf ) {
104  if ( isset( $info['hide-if'] ) ) {
105  // Hide child field if either its rules say it's hidden, or parent's rules say it's hidden
106  $info['hide-if'] = [ 'OR', $info['hide-if'], $this->mHideIf ];
107  } else {
108  // Hide child field if parent's rules say it's hidden
109  $info['hide-if'] = $this->mHideIf;
110  }
111  }
112  $field = HTMLForm::loadInputFromParameters( $name, $info, $this->mParent );
113  $fields[$fieldname] = $field;
114  }
115  return $fields;
116  }
117 
126  protected function rekeyValuesArray( $key, $values ) {
127  $data = [];
128  foreach ( $values as $fieldname => $value ) {
129  $name = "{$this->mName}[$key][$fieldname]";
130  $data[$name] = $value;
131  }
132  return $data;
133  }
134 
135  protected function needsLabel() {
136  return false;
137  }
138 
139  public function loadDataFromRequest( $request ) {
140  // It's possible that this might be posted with no fields. Detect that
141  // by looking for an edit token.
142  if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
143  return $this->getDefault();
144  }
145 
146  $values = $request->getArray( $this->mName );
147  if ( $values === null ) {
148  $values = [];
149  }
150 
151  $ret = [];
152  foreach ( $values as $key => $value ) {
153  if ( $key === 'create' || isset( $value['delete'] ) ) {
154  $ret['nonjs'] = 1;
155  continue;
156  }
157 
158  // Add back in $request->getValues() so things that look for e.g.
159  // wpEditToken don't fail.
160  $data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
161 
162  $fields = $this->createFieldsForKey( $key );
163  $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
164  $row = [];
165  foreach ( $fields as $fieldname => $field ) {
166  if ( $field->skipLoadData( $subrequest ) ) {
167  continue;
168  } elseif ( !empty( $field->mParams['disabled'] ) ) {
169  $row[$fieldname] = $field->getDefault();
170  } else {
171  $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
172  }
173  }
174  $ret[] = $row;
175  }
176 
177  if ( isset( $values['create'] ) ) {
178  // Non-JS client clicked the "create" button.
179  $fields = $this->createFieldsForKey( $this->uniqueId );
180  $row = [];
181  foreach ( $fields as $fieldname => $field ) {
182  if ( !empty( $field->mParams['nodata'] ) ) {
183  continue;
184  } else {
185  $row[$fieldname] = $field->getDefault();
186  }
187  }
188  $ret[] = $row;
189  }
190 
191  return $ret;
192  }
193 
194  public function getDefault() {
195  $ret = parent::getDefault();
196 
197  // The default default is one entry with all subfields at their
198  // defaults.
199  if ( $ret === null ) {
200  $fields = $this->createFieldsForKey( $this->uniqueId );
201  $row = [];
202  foreach ( $fields as $fieldname => $field ) {
203  if ( !empty( $field->mParams['nodata'] ) ) {
204  continue;
205  } else {
206  $row[$fieldname] = $field->getDefault();
207  }
208  }
209  $ret = [ $row ];
210  }
211 
212  return $ret;
213  }
214 
219  public function cancelSubmit( $values, $alldata ) {
220  if ( isset( $values['nonjs'] ) ) {
221  return true;
222  }
223 
224  foreach ( $values as $key => $value ) {
225  $fields = $this->createFieldsForKey( $key );
226  foreach ( $fields as $fieldname => $field ) {
227  if ( !array_key_exists( $fieldname, $value ) ) {
228  continue;
229  }
230  if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
231  return true;
232  }
233  }
234  }
235 
236  return parent::cancelSubmit( $values, $alldata );
237  }
238 
243  public function validate( $values, $alldata ) {
244  if ( isset( $this->mParams['required'] )
245  && $this->mParams['required'] !== false
246  && !$values
247  ) {
248  return $this->msg( 'htmlform-cloner-required' );
249  }
250 
251  if ( isset( $values['nonjs'] ) ) {
252  // The submission was a non-JS create/delete click, so fail
253  // validation in case cancelSubmit() somehow didn't already handle
254  // it.
255  return false;
256  }
257 
258  foreach ( $values as $key => $value ) {
259  $fields = $this->createFieldsForKey( $key );
260  foreach ( $fields as $fieldname => $field ) {
261  if ( !array_key_exists( $fieldname, $value ) ) {
262  continue;
263  }
264  if ( $field->isHidden( $alldata ) ) {
265  continue;
266  }
267  $ok = $field->validate( $value[$fieldname], $alldata );
268  if ( $ok !== true ) {
269  return false;
270  }
271  }
272  }
273 
274  return parent::validate( $values, $alldata );
275  }
276 
284  protected function getInputHTMLForKey( $key, array $values ) {
285  $displayFormat = $this->mParams['format'] ?? $this->mParent->getDisplayFormat();
286 
287  // Conveniently, PHP method names are case-insensitive.
288  $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
289 
290  $html = '';
291  $hidden = '';
292  $hasLabel = false;
293 
294  $fields = $this->createFieldsForKey( $key );
295  foreach ( $fields as $fieldname => $field ) {
296  $v = array_key_exists( $fieldname, $values )
297  ? $values[$fieldname]
298  : $field->getDefault();
299 
300  if ( $field instanceof HTMLHiddenField ) {
301  // HTMLHiddenField doesn't generate its own HTML
302  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
303  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
304  } else {
305  $html .= $field->$getFieldHtmlMethod( $v );
306 
307  $labelValue = trim( $field->getLabel() );
308  if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
309  $hasLabel = true;
310  }
311  }
312  }
313 
314  if ( !isset( $fields['delete'] ) ) {
315  $field = $this->getDeleteButtonHtml( $key );
316 
317  if ( $displayFormat === 'table' ) {
318  $html .= $field->$getFieldHtmlMethod( $field->getDefault() );
319  } else {
320  $html .= $field->getInputHTML( $field->getDefault() );
321  }
322  }
323 
324  if ( $displayFormat !== 'raw' ) {
325  $classes = [
326  'mw-htmlform-cloner-row',
327  ];
328 
329  if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
330  $classes[] = 'mw-htmlform-nolabel';
331  }
332 
333  $attribs = [
334  'class' => implode( ' ', $classes ),
335  ];
336 
337  if ( $displayFormat === 'table' ) {
338  $html = Html::rawElement( 'table',
339  $attribs,
340  Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
341  } else {
342  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
343  }
344  }
345 
346  $html .= $hidden;
347 
348  if ( !empty( $this->mParams['row-legend'] ) ) {
349  $legend = $this->msg( $this->mParams['row-legend'] )->text();
350  $html = Xml::fieldset( $legend, $html );
351  }
352 
353  return $html;
354  }
355 
360  protected function getDeleteButtonHtml( $key ) : HTMLFormField {
361  $name = "{$this->mName}[$key][delete]";
362  $label = $this->mParams['delete-button-message'] ?? 'htmlform-cloner-delete';
363  $field = HTMLForm::loadInputFromParameters( $name, [
364  'type' => 'submit',
365  'formnovalidate' => true,
366  'name' => $name,
367  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
368  'cssclass' => 'mw-htmlform-cloner-delete-button',
369  'default' => $this->getMessage( $label )->text(),
370  ], $this->mParent );
371  return $field;
372  }
373 
374  protected function getCreateButtonHtml() : HTMLFormField {
375  $name = "{$this->mName}[create]";
376  $label = $this->mParams['create-button-message'] ?? 'htmlform-cloner-create';
377  return HTMLForm::loadInputFromParameters( $name, [
378  'type' => 'submit',
379  'formnovalidate' => true,
380  'name' => $name,
381  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
382  'cssclass' => 'mw-htmlform-cloner-create-button',
383  'default' => $this->getMessage( $label )->text(),
384  ], $this->mParent );
385  }
386 
387  public function getInputHTML( $values ) {
388  $html = '';
389 
390  foreach ( (array)$values as $key => $value ) {
391  if ( $key === 'nonjs' ) {
392  continue;
393  }
394  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
395  $this->getInputHTMLForKey( $key, $value )
396  );
397  }
398 
399  $template = $this->getInputHTMLForKey( $this->uniqueId, [] );
400  $html = Html::rawElement( 'ul', [
401  'id' => "mw-htmlform-cloner-list-{$this->mID}",
402  'class' => 'mw-htmlform-cloner-ul',
403  'data-template' => $template,
404  'data-unique-id' => $this->uniqueId,
405  ], $html );
406 
407  $field = $this->getCreateButtonHtml();
408  $html .= $field->getInputHTML( $field->getDefault() );
409 
410  return $html;
411  }
412 
420  protected function getInputOOUIForKey( $key, array $values ) {
421  $html = '';
422  $hidden = '';
423 
424  $fields = $this->createFieldsForKey( $key );
425  foreach ( $fields as $fieldname => $field ) {
426  $v = array_key_exists( $fieldname, $values )
427  ? $values[$fieldname]
428  : $field->getDefault();
429 
430  if ( $field instanceof HTMLHiddenField ) {
431  // HTMLHiddenField doesn't generate its own HTML
432  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
433  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
434  } else {
435  $html .= $field->getOOUI( $v );
436  }
437  }
438 
439  if ( !isset( $fields['delete'] ) ) {
440  $field = $this->getDeleteButtonHtml( $key );
441  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
442  $fieldHtml->setInfusable( true );
443 
444  $html .= $fieldHtml;
445  }
446 
447  $classes = [
448  'mw-htmlform-cloner-row',
449  ];
450 
451  $attribs = [
452  'class' => implode( ' ', $classes ),
453  ];
454 
455  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
456 
457  $html .= $hidden;
458 
459  if ( !empty( $this->mParams['row-legend'] ) ) {
460  $legend = $this->msg( $this->mParams['row-legend'] )->text();
461  $html = Xml::fieldset( $legend, $html );
462  }
463 
464  return $html;
465  }
466 
467  public function getInputOOUI( $values ) {
468  $html = '';
469 
470  foreach ( (array)$values as $key => $value ) {
471  if ( $key === 'nonjs' ) {
472  continue;
473  }
474  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
475  $this->getInputOOUIForKey( $key, $value )
476  );
477  }
478 
479  $template = $this->getInputOOUIForKey( $this->uniqueId, [] );
480  $html = Html::rawElement( 'ul', [
481  'id' => "mw-htmlform-cloner-list-{$this->mID}",
482  'class' => 'mw-htmlform-cloner-ul',
483  'data-template' => $template,
484  'data-unique-id' => $this->uniqueId,
485  ], $html );
486 
487  $field = $this->getCreateButtonHtml();
488  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
489  $fieldHtml->setInfusable( true );
490 
491  $html .= $fieldHtml;
492 
493  return $html;
494  }
495 }
DerivativeRequest
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
Definition: DerivativeRequest.php:34
HTMLFormFieldCloner\getDefault
getDefault()
Definition: HTMLFormFieldCloner.php:194
Sanitizer\escapeIdForAttribute
static escapeIdForAttribute( $id, $mode=self::ID_PRIMARY)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid HTM...
Definition: Sanitizer.php:1116
HTMLFormField\getMessage
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name,...
Definition: HTMLFormField.php:1155
HTMLFormFieldCloner\createFieldsForKey
createFieldsForKey( $key)
Create the HTMLFormFields that go inside this element, using the specified key.
Definition: HTMLFormFieldCloner.php:87
Xml\fieldset
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:613
HTMLFormFieldCloner\getDeleteButtonHtml
getDeleteButtonHtml( $key)
Definition: HTMLFormFieldCloner.php:360
HTMLFormFieldCloner\$counter
static $counter
Definition: HTMLFormFieldCloner.php:39
MWException
MediaWiki exception.
Definition: MWException.php:26
HTMLFormField
The parent class to generate form fields.
Definition: HTMLFormField.php:7
HTMLFormFieldCloner\getCreateButtonHtml
getCreateButtonHtml()
Definition: HTMLFormFieldCloner.php:374
HTMLForm\loadInputFromParameters
static loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent=null)
Initialise a new Object for the field.
Definition: HTMLForm.php:516
HTMLFormFieldCloner\getInputOOUI
getInputOOUI( $values)
Same as getInputHTML, but returns an OOUI object.
Definition: HTMLFormFieldCloner.php:467
HTMLFormFieldCloner\rekeyValuesArray
rekeyValuesArray( $key, $values)
Re-key the specified values array to match the names applied by createFieldsForKey().
Definition: HTMLFormFieldCloner.php:126
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
HTMLFormFieldCloner\__construct
__construct( $params)
Initialise the object.
Definition: HTMLFormFieldCloner.php:48
HTMLFormFieldCloner\getInputOOUIForKey
getInputOOUIForKey( $key, array $values)
Get the input OOUI HTML for the specified key.
Definition: HTMLFormFieldCloner.php:420
HTMLFormFieldCloner\$uniqueId
string $uniqueId
String uniquely identifying this cloner instance and unlikely to exist otherwise in the generated HTM...
Definition: HTMLFormFieldCloner.php:46
HTMLHiddenField
Definition: HTMLHiddenField.php:3
HTMLFormFieldCloner\getInputHTMLForKey
getInputHTMLForKey( $key, array $values)
Get the input HTML for the specified key.
Definition: HTMLFormFieldCloner.php:284
HTMLFormFieldCloner\needsLabel
needsLabel()
Should this field have a label, or is there no input element with the appropriate id for the label to...
Definition: HTMLFormFieldCloner.php:135
HTMLFormFieldCloner\validate
validate( $values, $alldata)
Override this function to add specific validation checks on the field input.Don't forget to call pare...
Definition: HTMLFormFieldCloner.php:243
HTMLFormFieldCloner\loadDataFromRequest
loadDataFromRequest( $request)
Get the value that this input has been set to from a posted form, or the input's default value if it ...
Definition: HTMLFormFieldCloner.php:139
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
HTMLFormFieldCloner\getInputHTML
getInputHTML( $values)
This function must be implemented to return the HTML to generate the input object itself.
Definition: HTMLFormFieldCloner.php:387
HTMLFormField\$mHideIf
$mHideIf
Definition: HTMLFormField.php:26
HTMLFormField\msg
msg( $key,... $params)
Get a translated interface message.
Definition: HTMLFormField.php:83
HTMLFormFieldCloner\cancelSubmit
cancelSubmit( $values, $alldata)
Override this function if the control can somehow trigger a form submission that shouldn't actually s...
Definition: HTMLFormFieldCloner.php:219
HTMLFormFieldCloner
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
Definition: HTMLFormFieldCloner.php:38