]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/htmlform/fields/HTMLFormFieldCloner.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / htmlform / fields / HTMLFormFieldCloner.php
1 <?php
2
3 /**
4  * A container for HTMLFormFields that allows for multiple copies of the set of
5  * fields to be displayed to and entered by the user.
6  *
7  * Recognized parameters, besides the general ones, include:
8  *   fields - HTMLFormField descriptors for the subfields this cloner manages.
9  *     The format is just like for the HTMLForm. A field with key 'delete' is
10  *     special: it must have type = submit and will serve to delete the group
11  *     of fields.
12  *   required - If specified, at least one group of fields must be submitted.
13  *   format - HTMLForm display format to use when displaying the subfields:
14  *     'table', 'div', or 'raw'.
15  *   row-legend - If non-empty, each group of subfields will be enclosed in a
16  *     fieldset. The value is the name of a message key to use as the legend.
17  *   create-button-message - Message to use as the text of the button to
18  *     add an additional group of fields.
19  *   delete-button-message - Message to use as the text of automatically-
20  *     generated 'delete' button. Ignored if 'delete' is included in 'fields'.
21  *
22  * In the generated HTML, the subfields will be named along the lines of
23  * "clonerName[index][fieldname]", with ids "clonerId--index--fieldid". 'index'
24  * may be a number or an arbitrary string, and may likely change when the page
25  * is resubmitted. Cloners may be nested, resulting in field names along the
26  * lines of "cloner1Name[index1][cloner2Name][index2][fieldname]" and
27  * corresponding ids.
28  *
29  * Use of cloner may result in submissions of the page that are not submissions
30  * of the HTMLForm, when non-JavaScript clients use the create or remove buttons.
31  *
32  * The result is an array, with values being arrays mapping subfield names to
33  * their values. On non-HTMLForm-submission page loads, there may also be
34  * additional (string) keys present with other types of values.
35  *
36  * @since 1.23
37  */
38 class HTMLFormFieldCloner extends HTMLFormField {
39         private static $counter = 0;
40
41         /**
42          * @var string String uniquely identifying this cloner instance and
43          * unlikely to exist otherwise in the generated HTML, while still being
44          * valid as part of an HTML id.
45          */
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
79         /**
80          * Create the HTMLFormFields that go inside this element, using the
81          * specified key.
82          *
83          * @param string $key Array key under which these fields should be named
84          * @return HTMLFormField[]
85          */
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
117         /**
118          * Re-key the specified values array to match the names applied by
119          * createFieldsForKey().
120          *
121          * @param string $key Array key under which these fields should be named
122          * @param array $values Values array from the request
123          * @return array
124          */
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
268         /**
269          * Get the input HTML for the specified key.
270          *
271          * @param string $key Array key under which the fields should be named
272          * @param array $values
273          * @return string
274          */
275         protected function getInputHTMLForKey( $key, array $values ) {
276                 $displayFormat = isset( $this->mParams['format'] )
277                         ? $this->mParams['format']
278                         : $this->mParent->getDisplayFormat();
279
280                 // Conveniently, PHP method names are case-insensitive.
281                 $getFieldHtmlMethod = $displayFormat == 'table' ? 'getTableRow' : ( 'get' . $displayFormat );
282
283                 $html = '';
284                 $hidden = '';
285                 $hasLabel = false;
286
287                 $fields = $this->createFieldsForKey( $key );
288                 foreach ( $fields as $fieldname => $field ) {
289                         $v = array_key_exists( $fieldname, $values )
290                                 ? $values[$fieldname]
291                                 : $field->getDefault();
292
293                         if ( $field instanceof HTMLHiddenField ) {
294                                 // HTMLHiddenField doesn't generate its own HTML
295                                 list( $name, $value, $params ) = $field->getHiddenFieldData( $v );
296                                 $hidden .= Html::hidden( $name, $value, $params ) . "\n";
297                         } else {
298                                 $html .= $field->$getFieldHtmlMethod( $v );
299
300                                 $labelValue = trim( $field->getLabel() );
301                                 if ( $labelValue != '&#160;' && $labelValue !== '' ) {
302                                         $hasLabel = true;
303                                 }
304                         }
305                 }
306
307                 if ( !isset( $fields['delete'] ) ) {
308                         $name = "{$this->mName}[$key][delete]";
309                         $label = isset( $this->mParams['delete-button-message'] )
310                                 ? $this->mParams['delete-button-message']
311                                 : 'htmlform-cloner-delete';
312                         $field = HTMLForm::loadInputFromParameters( $name, [
313                                 'type' => 'submit',
314                                 'formnovalidate' => true,
315                                 'name' => $name,
316                                 'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--$key--delete" ),
317                                 'cssclass' => 'mw-htmlform-cloner-delete-button',
318                                 'default' => $this->getMessage( $label )->text(),
319                         ], $this->mParent );
320                         $v = $field->getDefault();
321
322                         if ( $displayFormat === 'table' ) {
323                                 $html .= $field->$getFieldHtmlMethod( $v );
324                         } else {
325                                 $html .= $field->getInputHTML( $v );
326                         }
327                 }
328
329                 if ( $displayFormat !== 'raw' ) {
330                         $classes = [
331                                 'mw-htmlform-cloner-row',
332                         ];
333
334                         if ( !$hasLabel ) { // Avoid strange spacing when no labels exist
335                                 $classes[] = 'mw-htmlform-nolabel';
336                         }
337
338                         $attribs = [
339                                 'class' => implode( ' ', $classes ),
340                         ];
341
342                         if ( $displayFormat === 'table' ) {
343                                 $html = Html::rawElement( 'table',
344                                         $attribs,
345                                         Html::rawElement( 'tbody', [], "\n$html\n" ) ) . "\n";
346                         } else {
347                                 $html = Html::rawElement( 'div', $attribs, "\n$html\n" );
348                         }
349                 }
350
351                 $html .= $hidden;
352
353                 if ( !empty( $this->mParams['row-legend'] ) ) {
354                         $legend = $this->msg( $this->mParams['row-legend'] )->text();
355                         $html = Xml::fieldset( $legend, $html );
356                 }
357
358                 return $html;
359         }
360
361         public function getInputHTML( $values ) {
362                 $html = '';
363
364                 foreach ( (array)$values as $key => $value ) {
365                         if ( $key === 'nonjs' ) {
366                                 continue;
367                         }
368                         $html .= Html::rawElement( 'li', [ 'class' => 'mw-htmlform-cloner-li' ],
369                                 $this->getInputHTMLForKey( $key, $value )
370                         );
371                 }
372
373                 $template = $this->getInputHTMLForKey( $this->uniqueId, [] );
374                 $html = Html::rawElement( 'ul', [
375                         'id' => "mw-htmlform-cloner-list-{$this->mID}",
376                         'class' => 'mw-htmlform-cloner-ul',
377                         'data-template' => $template,
378                         'data-unique-id' => $this->uniqueId,
379                 ], $html );
380
381                 $name = "{$this->mName}[create]";
382                 $label = isset( $this->mParams['create-button-message'] )
383                         ? $this->mParams['create-button-message']
384                         : 'htmlform-cloner-create';
385                 $field = HTMLForm::loadInputFromParameters( $name, [
386                         'type' => 'submit',
387                         'formnovalidate' => true,
388                         'name' => $name,
389                         'id' => Sanitizer::escapeIdForAttribute( "{$this->mID}--create" ),
390                         'cssclass' => 'mw-htmlform-cloner-create-button',
391                         'default' => $this->getMessage( $label )->text(),
392                 ], $this->mParent );
393                 $html .= $field->getInputHTML( $field->getDefault() );
394
395                 return $html;
396         }
397 }