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  if ( isset( $this->mParams['fields']['delete'] ) ) {
58  $class = 'mw-htmlform-cloner-delete-button';
59  $info = $this->mParams['fields']['delete'] + [
60  'formnovalidate' => true,
61  'cssclass' => $class
62  ];
63  unset( $info['name'], $info['class'] );
64 
65  if ( !isset( $info['type'] ) || $info['type'] !== 'submit' ) {
66  throw new MWException(
67  'HTMLFormFieldCloner delete field, if specified, must be of type "submit"'
68  );
69  }
70 
71  if ( !in_array( $class, explode( ' ', $info['cssclass'] ) ) ) {
72  $info['cssclass'] .= " $class";
73  }
74 
75  $this->mParams['fields']['delete'] = $info;
76  }
77  }
78 
86  protected function createFieldsForKey( $key ) {
87  $fields = [];
88  foreach ( $this->mParams['fields'] as $fieldname => $info ) {
89  $name = "{$this->mName}[$key][$fieldname]";
90  if ( isset( $info['name'] ) ) {
91  $info['name'] = "{$this->mName}[$key][{$info['name']}]";
92  } else {
93  $info['name'] = $name;
94  }
95  if ( isset( $info['id'] ) ) {
96  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--{$info['id']}" );
97  } else {
98  $info['id'] = Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--$fieldname" );
99  }
100  // Copy the hide-if rules to "child" fields, so that the JavaScript code handling them
101  // (resources/src/mediawiki/htmlform/hide-if.js) doesn't have to handle nested fields.
102  if ( $this->mHideIf ) {
103  if ( isset( $info['hide-if'] ) ) {
104  // Hide child field if either its rules say it's hidden, or parent's rules say it's hidden
105  $info['hide-if'] = [ 'OR', $info['hide-if'], $this->mHideIf ];
106  } else {
107  // Hide child field if parent's rules say it's hidden
108  $info['hide-if'] = $this->mHideIf;
109  }
110  }
111  $field = HTMLForm::loadInputFromParameters( $name, $info, $this->mParent );
112  $fields[$fieldname] = $field;
113  }
114  return $fields;
115  }
116 
125  protected function rekeyValuesArray( $key, $values ) {
126  $data = [];
127  foreach ( $values as $fieldname => $value ) {
128  $name = "{$this->mName}[$key][$fieldname]";
129  $data[$name] = $value;
130  }
131  return $data;
132  }
133 
134  protected function needsLabel() {
135  return false;
136  }
137 
138  public function loadDataFromRequest( $request ) {
139  // It's possible that this might be posted with no fields. Detect that
140  // by looking for an edit token.
141  if ( !$request->getCheck( 'wpEditToken' ) && $request->getArray( $this->mName ) === null ) {
142  return $this->getDefault();
143  }
144 
145  $values = $request->getArray( $this->mName );
146  if ( $values === null ) {
147  $values = [];
148  }
149 
150  $ret = [];
151  foreach ( $values as $key => $value ) {
152  if ( $key === 'create' || isset( $value['delete'] ) ) {
153  $ret['nonjs'] = 1;
154  continue;
155  }
156 
157  // Add back in $request->getValues() so things that look for e.g.
158  // wpEditToken don't fail.
159  $data = $this->rekeyValuesArray( $key, $value ) + $request->getValues();
160 
161  $fields = $this->createFieldsForKey( $key );
162  $subrequest = new DerivativeRequest( $request, $data, $request->wasPosted() );
163  $row = [];
164  foreach ( $fields as $fieldname => $field ) {
165  if ( $field->skipLoadData( $subrequest ) ) {
166  continue;
167  } elseif ( !empty( $field->mParams['disabled'] ) ) {
168  $row[$fieldname] = $field->getDefault();
169  } else {
170  $row[$fieldname] = $field->loadDataFromRequest( $subrequest );
171  }
172  }
173  $ret[] = $row;
174  }
175 
176  if ( isset( $values['create'] ) ) {
177  // Non-JS client clicked the "create" button.
178  $fields = $this->createFieldsForKey( $this->uniqueId );
179  $row = [];
180  foreach ( $fields as $fieldname => $field ) {
181  if ( !empty( $field->mParams['nodata'] ) ) {
182  continue;
183  } else {
184  $row[$fieldname] = $field->getDefault();
185  }
186  }
187  $ret[] = $row;
188  }
189 
190  return $ret;
191  }
192 
193  public function getDefault() {
194  $ret = parent::getDefault();
195 
196  // The default default is one entry with all subfields at their
197  // defaults.
198  if ( $ret === null ) {
199  $fields = $this->createFieldsForKey( $this->uniqueId );
200  $row = [];
201  foreach ( $fields as $fieldname => $field ) {
202  if ( !empty( $field->mParams['nodata'] ) ) {
203  continue;
204  } else {
205  $row[$fieldname] = $field->getDefault();
206  }
207  }
208  $ret = [ $row ];
209  }
210 
211  return $ret;
212  }
213 
214  public function cancelSubmit( $values, $alldata ) {
215  if ( isset( $values['nonjs'] ) ) {
216  return true;
217  }
218 
219  foreach ( $values as $key => $value ) {
220  $fields = $this->createFieldsForKey( $key );
221  foreach ( $fields as $fieldname => $field ) {
222  if ( !array_key_exists( $fieldname, $value ) ) {
223  continue;
224  }
225  if ( $field->cancelSubmit( $value[$fieldname], $alldata ) ) {
226  return true;
227  }
228  }
229  }
230 
231  return parent::cancelSubmit( $values, $alldata );
232  }
233 
234  public function validate( $values, $alldata ) {
235  if ( isset( $this->mParams['required'] )
236  && $this->mParams['required'] !== false
237  && !$values
238  ) {
239  return $this->msg( 'htmlform-cloner-required' );
240  }
241 
242  if ( isset( $values['nonjs'] ) ) {
243  // The submission was a non-JS create/delete click, so fail
244  // validation in case cancelSubmit() somehow didn't already handle
245  // it.
246  return false;
247  }
248 
249  foreach ( $values as $key => $value ) {
250  $fields = $this->createFieldsForKey( $key );
251  foreach ( $fields as $fieldname => $field ) {
252  if ( !array_key_exists( $fieldname, $value ) ) {
253  continue;
254  }
255  if ( $field->isHidden( $alldata ) ) {
256  continue;
257  }
258  $ok = $field->validate( $value[$fieldname], $alldata );
259  if ( $ok !== true ) {
260  return false;
261  }
262  }
263  }
264 
265  return parent::validate( $values, $alldata );
266  }
267 
275  protected function getInputHTMLForKey( $key, array $values ) {
276  $displayFormat = $this->mParams['format'] ?? $this->mParent->getDisplayFormat();
277 
278  // Conveniently, PHP method names are case-insensitive.
279  $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
280 
281  $html = '';
282  $hidden = '';
283  $hasLabel = false;
284 
285  $fields = $this->createFieldsForKey( $key );
286  foreach ( $fields as $fieldname => $field ) {
287  $v = array_key_exists( $fieldname, $values )
288  ? $values[$fieldname]
289  : $field->getDefault();
290 
291  if ( $field instanceof HTMLHiddenField ) {
292  // HTMLHiddenField doesn't generate its own HTML
293  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
294  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
295  } else {
296  $html .= $field->$getFieldHtmlMethod( $v );
297 
298  $labelValue = trim( $field->getLabel() );
299  if ( $labelValue !== "\u{00A0}" && $labelValue !== '&#160;' && $labelValue !== '' ) {
300  $hasLabel = true;
301  }
302  }
303  }
304 
305  if ( !isset( $fields['delete'] ) ) {
306  $field = $this->getDeleteButtonHtml( $key );
307 
308  if ( $displayFormat === 'table' ) {
309  $html .= $field->$getFieldHtmlMethod( $field->getDefault() );
310  } else {
311  $html .= $field->getInputHTML( $field->getDefault() );
312  }
313  }
314 
315  if ( $displayFormat !== 'raw' ) {
316  $classes = [
317  'mw-htmlform-cloner-row',
318  ];
319 
320  if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
321  $classes[] = 'mw-htmlform-nolabel';
322  }
323 
324  $attribs = [
325  'class' => implode( ' ', $classes ),
326  ];
327 
328  if ( $displayFormat === 'table' ) {
329  $html = Html::rawElement( 'table',
330  $attribs,
331  Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
332  } else {
333  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
334  }
335  }
336 
337  $html .= $hidden;
338 
339  if ( !empty( $this->mParams['row-legend'] ) ) {
340  $legend = $this->msg( $this->mParams['row-legend'] )->text();
341  $html = Xml::fieldset( $legend, $html );
342  }
343 
344  return $html;
345  }
346 
351  protected function getDeleteButtonHtml( $key ) : HTMLFormField {
352  $name = "{$this->mName}[$key][delete]";
353  $label = $this->mParams['delete-button-message'] ?? 'htmlform-cloner-delete';
355  'type' => 'submit',
356  'formnovalidate' => true,
357  'name' => $name,
358  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
359  'cssclass' => 'mw-htmlform-cloner-delete-button',
360  'default' => $this->getMessage( $label )->text(),
361  ], $this->mParent );
362  return $field;
363  }
364 
365  protected function getCreateButtonHtml() : HTMLFormField {
366  $name = "{$this->mName}[create]";
367  $label = $this->mParams['create-button-message'] ?? 'htmlform-cloner-create';
369  'type' => 'submit',
370  'formnovalidate' => true,
371  'name' => $name,
372  'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
373  'cssclass' => 'mw-htmlform-cloner-create-button',
374  'default' => $this->getMessage( $label )->text(),
375  ], $this->mParent );
376  }
377 
378  public function getInputHTML( $values ) {
379  $html = '';
380 
381  foreach ( (array)$values as $key => $value ) {
382  if ( $key === 'nonjs' ) {
383  continue;
384  }
385  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
386  $this->getInputHTMLForKey( $key, $value )
387  );
388  }
389 
390  $template = $this->getInputHTMLForKey( $this->uniqueId, [] );
391  $html = Html::rawElement( 'ul', [
392  'id' => "mw-htmlform-cloner-list-{$this->mID}",
393  'class' => 'mw-htmlform-cloner-ul',
394  'data-template' => $template,
395  'data-unique-id' => $this->uniqueId,
396  ], $html );
397 
398  $field = $this->getCreateButtonHtml();
399  $html .= $field->getInputHTML( $field->getDefault() );
400 
401  return $html;
402  }
403 
411  protected function getInputOOUIForKey( $key, array $values ) {
412  $html = '';
413  $hidden = '';
414 
415  $fields = $this->createFieldsForKey( $key );
416  foreach ( $fields as $fieldname => $field ) {
417  $v = array_key_exists( $fieldname, $values )
418  ? $values[$fieldname]
419  : $field->getDefault();
420 
421  if ( $field instanceof HTMLHiddenField ) {
422  // HTMLHiddenField doesn't generate its own HTML
423  list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
424  $hidden .= Html::hidden( $name, $value, $params ) . "\n";
425  } else {
426  $html .= $field->getOOUI( $v );
427  }
428  }
429 
430  if ( !isset( $fields['delete'] ) ) {
431  $field = $this->getDeleteButtonHtml( $key );
432  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
433  $fieldHtml->setInfusable( true );
434 
435  $html .= $fieldHtml;
436  }
437 
438  $classes = [
439  'mw-htmlform-cloner-row',
440  ];
441 
442  $attribs = [
443  'class' => implode( ' ', $classes ),
444  ];
445 
446  $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
447 
448  $html .= $hidden;
449 
450  if ( !empty( $this->mParams['row-legend'] ) ) {
451  $legend = $this->msg( $this->mParams['row-legend'] )->text();
452  $html = Xml::fieldset( $legend, $html );
453  }
454 
455  return $html;
456  }
457 
458  public function getInputOOUI( $values ) {
459  $html = '';
460 
461  foreach ( (array)$values as $key => $value ) {
462  if ( $key === 'nonjs' ) {
463  continue;
464  }
465  $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
466  $this->getInputOOUIForKey( $key, $value )
467  );
468  }
469 
470  $template = $this->getInputOOUIForKey( $this->uniqueId, [] );
471  $html = Html::rawElement( 'ul', [
472  'id' => "mw-htmlform-cloner-list-{$this->mID}",
473  'class' => 'mw-htmlform-cloner-ul',
474  'data-template' => $template,
475  'data-unique-id' => $this->uniqueId,
476  ], $html );
477 
478  $field = $this->getCreateButtonHtml();
479  $fieldHtml = $field->getInputOOUI( $field->getDefault() );
480  $fieldHtml->setInfusable( true );
481 
482  $html .= $fieldHtml;
483 
484  return $html;
485  }
486 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1982
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1982
msg()
Get a translated interface message.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
Similar to FauxRequest, but only fakes URL parameters and method (POST or GET) and use the base reque...
$value
cancelSubmit( $values, $alldata)
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
getInputOOUIForKey( $key, array $values)
Get the input OOUI HTML for the specified key.
static loadInputFromParameters( $fieldname, $descriptor, HTMLForm $parent=null)
Initialise a new Object for the field.
Definition: HTMLForm.php:493
getInputHTMLForKey( $key, array $values)
Get the input HTML for the specified key.
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:1287
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:609
A container for HTMLFormFields that allows for multiple copies of the set of fields to be displayed t...
$params
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition: hooks.txt:1982
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
createFieldsForKey( $key)
Create the HTMLFormFields that go inside this element, using the specified key.
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
validate( $values, $alldata)
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition: hooks.txt:780
rekeyValuesArray( $key, $values)
Re-key the specified values array to match the names applied by createFieldsForKey().
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
string $uniqueId
String uniquely identifying this cloner instance and unlikely to exist otherwise in the generated HTM...
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:797
getMessage( $value)
Turns a *-message parameter (which could be a MessageSpecifier, or a message name, or a name + parameters array) into a Message.
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2633