Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Field / WidgetBase.php
1 <?php
2
3 namespace Drupal\Core\Field;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\NestedArray;
7 use Drupal\Component\Utility\SortArray;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Render\Element;
10 use Symfony\Component\Validator\ConstraintViolationInterface;
11 use Symfony\Component\Validator\ConstraintViolationListInterface;
12
13 /**
14  * Base class for 'Field widget' plugin implementations.
15  *
16  * @ingroup field_widget
17  */
18 abstract class WidgetBase extends PluginSettingsBase implements WidgetInterface {
19
20   use AllowedTagsXssTrait;
21
22   /**
23    * The field definition.
24    *
25    * @var \Drupal\Core\Field\FieldDefinitionInterface
26    */
27   protected $fieldDefinition;
28
29   /**
30    * The widget settings.
31    *
32    * @var array
33    */
34   protected $settings;
35
36   /**
37    * Constructs a WidgetBase object.
38    *
39    * @param string $plugin_id
40    *   The plugin_id for the widget.
41    * @param mixed $plugin_definition
42    *   The plugin implementation definition.
43    * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
44    *   The definition of the field to which the widget is associated.
45    * @param array $settings
46    *   The widget settings.
47    * @param array $third_party_settings
48    *   Any third party settings.
49    */
50   public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
51     parent::__construct([], $plugin_id, $plugin_definition);
52     $this->fieldDefinition = $field_definition;
53     $this->settings = $settings;
54     $this->thirdPartySettings = $third_party_settings;
55   }
56
57   /**
58    * {@inheritdoc}
59    */
60   public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
61     $field_name = $this->fieldDefinition->getName();
62     $parents = $form['#parents'];
63
64     // Store field information in $form_state.
65     if (!static::getWidgetState($parents, $field_name, $form_state)) {
66       $field_state = [
67         'items_count' => count($items),
68         'array_parents' => [],
69       ];
70       static::setWidgetState($parents, $field_name, $form_state, $field_state);
71     }
72
73     // Collect widget elements.
74     $elements = [];
75
76     // If the widget is handling multiple values (e.g Options), or if we are
77     // displaying an individual element, just get a single form element and make
78     // it the $delta value.
79     if ($this->handlesMultipleValues() || isset($get_delta)) {
80       $delta = isset($get_delta) ? $get_delta : 0;
81       $element = [
82         '#title' => $this->fieldDefinition->getLabel(),
83         '#description' => FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())),
84       ];
85       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
86
87       if ($element) {
88         if (isset($get_delta)) {
89           // If we are processing a specific delta value for a field where the
90           // field module handles multiples, set the delta in the result.
91           $elements[$delta] = $element;
92         }
93         else {
94           // For fields that handle their own processing, we cannot make
95           // assumptions about how the field is structured, just merge in the
96           // returned element.
97           $elements = $element;
98         }
99       }
100     }
101     // If the widget does not handle multiple values itself, (and we are not
102     // displaying an individual element), process the multiple value form.
103     else {
104       $elements = $this->formMultipleElements($items, $form, $form_state);
105     }
106
107     // Populate the 'array_parents' information in $form_state->get('field')
108     // after the form is built, so that we catch changes in the form structure
109     // performed in alter() hooks.
110     $elements['#after_build'][] = [get_class($this), 'afterBuild'];
111     $elements['#field_name'] = $field_name;
112     $elements['#field_parents'] = $parents;
113     // Enforce the structure of submitted values.
114     $elements['#parents'] = array_merge($parents, [$field_name]);
115     // Most widgets need their internal structure preserved in submitted values.
116     $elements += ['#tree' => TRUE];
117
118     return [
119       // Aid in theming of widgets by rendering a classified container.
120       '#type' => 'container',
121       // Assign a different parent, to keep the main id for the widget itself.
122       '#parents' => array_merge($parents, [$field_name . '_wrapper']),
123       '#attributes' => [
124         'class' => [
125           'field--type-' . Html::getClass($this->fieldDefinition->getType()),
126           'field--name-' . Html::getClass($field_name),
127           'field--widget-' . Html::getClass($this->getPluginId()),
128         ],
129       ],
130       'widget' => $elements,
131     ];
132   }
133
134   /**
135    * Special handling to create form elements for multiple values.
136    *
137    * Handles generic features for multiple fields:
138    * - number of widgets
139    * - AHAH-'add more' button
140    * - table display and drag-n-drop value reordering
141    */
142   protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
143     $field_name = $this->fieldDefinition->getName();
144     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
145     $parents = $form['#parents'];
146
147     // Determine the number of widgets to display.
148     switch ($cardinality) {
149       case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
150         $field_state = static::getWidgetState($parents, $field_name, $form_state);
151         $max = $field_state['items_count'];
152         $is_multiple = TRUE;
153         break;
154
155       default:
156         $max = $cardinality - 1;
157         $is_multiple = ($cardinality > 1);
158         break;
159     }
160
161     $title = $this->fieldDefinition->getLabel();
162     $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
163
164     $elements = [];
165
166     for ($delta = 0; $delta <= $max; $delta++) {
167       // Add a new empty item if it doesn't exist yet at this delta.
168       if (!isset($items[$delta])) {
169         $items->appendItem();
170       }
171
172       // For multiple fields, title and description are handled by the wrapping
173       // table.
174       if ($is_multiple) {
175         $element = [
176           '#title' => $this->t('@title (value @number)', ['@title' => $title, '@number' => $delta + 1]),
177           '#title_display' => 'invisible',
178           '#description' => '',
179         ];
180       }
181       else {
182         $element = [
183           '#title' => $title,
184           '#title_display' => 'before',
185           '#description' => $description,
186         ];
187       }
188
189       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
190
191       if ($element) {
192         // Input field for the delta (drag-n-drop reordering).
193         if ($is_multiple) {
194           // We name the element '_weight' to avoid clashing with elements
195           // defined by widget.
196           $element['_weight'] = [
197             '#type' => 'weight',
198             '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
199             '#title_display' => 'invisible',
200             // Note: this 'delta' is the FAPI #type 'weight' element's property.
201             '#delta' => $max,
202             '#default_value' => $items[$delta]->_weight ?: $delta,
203             '#weight' => 100,
204           ];
205         }
206
207         $elements[$delta] = $element;
208       }
209     }
210
211     if ($elements) {
212       $elements += [
213         '#theme' => 'field_multiple_value_form',
214         '#field_name' => $field_name,
215         '#cardinality' => $cardinality,
216         '#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
217         '#required' => $this->fieldDefinition->isRequired(),
218         '#title' => $title,
219         '#description' => $description,
220         '#max_delta' => $max,
221       ];
222
223       // Add 'add more' button, if not working with a programmed form.
224       if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
225         $id_prefix = implode('-', array_merge($parents, [$field_name]));
226         $wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
227         $elements['#prefix'] = '<div id="' . $wrapper_id . '">';
228         $elements['#suffix'] = '</div>';
229
230         $elements['add_more'] = [
231           '#type' => 'submit',
232           '#name' => strtr($id_prefix, '-', '_') . '_add_more',
233           '#value' => t('Add another item'),
234           '#attributes' => ['class' => ['field-add-more-submit']],
235           '#limit_validation_errors' => [array_merge($parents, [$field_name])],
236           '#submit' => [[get_class($this), 'addMoreSubmit']],
237           '#ajax' => [
238             'callback' => [get_class($this), 'addMoreAjax'],
239             'wrapper' => $wrapper_id,
240             'effect' => 'fade',
241           ],
242         ];
243       }
244     }
245
246     return $elements;
247   }
248
249   /**
250    * After-build handler for field elements in a form.
251    *
252    * This stores the final location of the field within the form structure so
253    * that flagErrors() can assign validation errors to the right form element.
254    */
255   public static function afterBuild(array $element, FormStateInterface $form_state) {
256     $parents = $element['#field_parents'];
257     $field_name = $element['#field_name'];
258
259     $field_state = static::getWidgetState($parents, $field_name, $form_state);
260     $field_state['array_parents'] = $element['#array_parents'];
261     static::setWidgetState($parents, $field_name, $form_state, $field_state);
262
263     return $element;
264   }
265
266   /**
267    * Submission handler for the "Add another item" button.
268    */
269   public static function addMoreSubmit(array $form, FormStateInterface $form_state) {
270     $button = $form_state->getTriggeringElement();
271
272     // Go one level up in the form, to the widgets container.
273     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
274     $field_name = $element['#field_name'];
275     $parents = $element['#field_parents'];
276
277     // Increment the items count.
278     $field_state = static::getWidgetState($parents, $field_name, $form_state);
279     $field_state['items_count']++;
280     static::setWidgetState($parents, $field_name, $form_state, $field_state);
281
282     $form_state->setRebuild();
283   }
284
285   /**
286    * Ajax callback for the "Add another item" button.
287    *
288    * This returns the new page content to replace the page content made obsolete
289    * by the form submission.
290    */
291   public static function addMoreAjax(array $form, FormStateInterface $form_state) {
292     $button = $form_state->getTriggeringElement();
293
294     // Go one level up in the form, to the widgets container.
295     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
296
297     // Ensure the widget allows adding additional items.
298     if ($element['#cardinality'] != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
299       return;
300     }
301
302     // Add a DIV around the delta receiving the Ajax effect.
303     $delta = $element['#max_delta'];
304     $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
305     $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
306
307     return $element;
308   }
309
310   /**
311    * Generates the form element for a single copy of the widget.
312    */
313   protected function formSingleElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
314     $element += [
315       '#field_parents' => $form['#parents'],
316       // Only the first widget should be required.
317       '#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
318       '#delta' => $delta,
319       '#weight' => $delta,
320     ];
321
322     $element = $this->formElement($items, $delta, $element, $form, $form_state);
323
324     if ($element) {
325       // Allow modules to alter the field widget form element.
326       $context = [
327         'form' => $form,
328         'widget' => $this,
329         'items' => $items,
330         'delta' => $delta,
331         'default' => $this->isDefaultValueWidget($form_state),
332       ];
333       \Drupal::moduleHandler()->alter(['field_widget_form', 'field_widget_' . $this->getPluginId() . '_form'], $element, $form_state, $context);
334     }
335
336     return $element;
337   }
338
339   /**
340    * {@inheritdoc}
341    */
342   public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
343     $field_name = $this->fieldDefinition->getName();
344
345     // Extract the values from $form_state->getValues().
346     $path = array_merge($form['#parents'], [$field_name]);
347     $key_exists = NULL;
348     $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
349
350     if ($key_exists) {
351       // Account for drag-and-drop reordering if needed.
352       if (!$this->handlesMultipleValues()) {
353         // Remove the 'value' of the 'add more' button.
354         unset($values['add_more']);
355
356         // The original delta, before drag-and-drop reordering, is needed to
357         // route errors to the correct form element.
358         foreach ($values as $delta => &$value) {
359           $value['_original_delta'] = $delta;
360         }
361
362         usort($values, function ($a, $b) {
363           return SortArray::sortByKeyInt($a, $b, '_weight');
364         });
365       }
366
367       // Let the widget massage the submitted values.
368       $values = $this->massageFormValues($values, $form, $form_state);
369
370       // Assign the values and remove the empty ones.
371       $items->setValue($values);
372       $items->filterEmptyItems();
373
374       // Put delta mapping in $form_state, so that flagErrors() can use it.
375       $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
376       foreach ($items as $delta => $item) {
377         $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
378         unset($item->_original_delta, $item->_weight);
379       }
380       static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
381     }
382   }
383
384   /**
385    * {@inheritdoc}
386    */
387   public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
388     $field_name = $this->fieldDefinition->getName();
389
390     $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
391
392     if ($violations->count()) {
393       // Locate the correct element in the form.
394       $element = NestedArray::getValue($form_state->getCompleteForm(), $field_state['array_parents']);
395
396       // Do not report entity-level validation errors if Form API errors have
397       // already been reported for the field.
398       // @todo Field validation should not be run on fields with FAPI errors to
399       //   begin with. See https://www.drupal.org/node/2070429.
400       $element_path = implode('][', $element['#parents']);
401       if ($reported_errors = $form_state->getErrors()) {
402         foreach (array_keys($reported_errors) as $error_path) {
403           if (strpos($error_path, $element_path) === 0) {
404             return;
405           }
406         }
407       }
408
409       // Only set errors if the element is visible.
410       if (Element::isVisibleElement($element)) {
411         $handles_multiple = $this->handlesMultipleValues();
412
413         $violations_by_delta = [];
414         foreach ($violations as $violation) {
415           // Separate violations by delta.
416           $property_path = explode('.', $violation->getPropertyPath());
417           $delta = array_shift($property_path);
418           $violations_by_delta[$delta][] = $violation;
419           $violation->arrayPropertyPath = $property_path;
420         }
421
422         /** @var \Symfony\Component\Validator\ConstraintViolationInterface[] $delta_violations */
423         foreach ($violations_by_delta as $delta => $delta_violations) {
424           // Pass violations to the main element:
425           // - if this is a multiple-value widget,
426           // - or if the violations are at the ItemList level.
427           if ($handles_multiple || !is_numeric($delta)) {
428             $delta_element = $element;
429           }
430           // Otherwise, pass errors by delta to the corresponding sub-element.
431           else {
432             $original_delta = $field_state['original_deltas'][$delta];
433             $delta_element = $element[$original_delta];
434           }
435           foreach ($delta_violations as $violation) {
436             // @todo: Pass $violation->arrayPropertyPath as property path.
437             $error_element = $this->errorElement($delta_element, $violation, $form, $form_state);
438             if ($error_element !== FALSE) {
439               $form_state->setError($error_element, $violation->getMessage());
440             }
441           }
442         }
443       }
444     }
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public static function getWidgetState(array $parents, $field_name, FormStateInterface $form_state) {
451     return NestedArray::getValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name));
452   }
453
454   /**
455    * {@inheritdoc}
456    */
457   public static function setWidgetState(array $parents, $field_name, FormStateInterface $form_state, array $field_state) {
458     NestedArray::setValue($form_state->getStorage(), static::getWidgetStateParents($parents, $field_name), $field_state);
459   }
460
461   /**
462    * Returns the location of processing information within $form_state.
463    *
464    * @param array $parents
465    *   The array of #parents where the widget lives in the form.
466    * @param string $field_name
467    *   The field name.
468    *
469    * @return array
470    *   The location of processing information within $form_state.
471    */
472   protected static function getWidgetStateParents(array $parents, $field_name) {
473     // Field processing data is placed at
474     // $form_state->get(['field_storage', '#parents', ...$parents..., '#fields', $field_name]),
475     // to avoid clashes between field names and $parents parts.
476     return array_merge(['field_storage', '#parents'], $parents, ['#fields', $field_name]);
477   }
478
479   /**
480    * {@inheritdoc}
481    */
482   public function settingsForm(array $form, FormStateInterface $form_state) {
483     return [];
484   }
485
486   /**
487    * {@inheritdoc}
488    */
489   public function settingsSummary() {
490     return [];
491   }
492
493   /**
494    * {@inheritdoc}
495    */
496   public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
497     return $element;
498   }
499
500   /**
501    * {@inheritdoc}
502    */
503   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
504     return $values;
505   }
506
507   /**
508    * Returns the array of field settings.
509    *
510    * @return array
511    *   The array of settings.
512    */
513   protected function getFieldSettings() {
514     return $this->fieldDefinition->getSettings();
515   }
516
517   /**
518    * Returns the value of a field setting.
519    *
520    * @param string $setting_name
521    *   The setting name.
522    *
523    * @return mixed
524    *   The setting value.
525    */
526   protected function getFieldSetting($setting_name) {
527     return $this->fieldDefinition->getSetting($setting_name);
528   }
529
530   /**
531    * Returns whether the widget handles multiple values.
532    *
533    * @return bool
534    *   TRUE if a single copy of formElement() can handle multiple field values,
535    *   FALSE if multiple values require separate copies of formElement().
536    */
537   protected function handlesMultipleValues() {
538     $definition = $this->getPluginDefinition();
539     return $definition['multiple_values'];
540   }
541
542   /**
543    * {@inheritdoc}
544    */
545   public static function isApplicable(FieldDefinitionInterface $field_definition) {
546     // By default, widgets are available for all fields.
547     return TRUE;
548   }
549
550   /**
551    * Returns whether the widget used for default value form.
552    *
553    * @param \Drupal\Core\Form\FormStateInterface $form_state
554    *   The current state of the form.
555    *
556    * @return bool
557    *   TRUE if a widget used to input default value, FALSE otherwise.
558    */
559   protected function isDefaultValueWidget(FormStateInterface $form_state) {
560     return (bool) $form_state->get('default_value_widget');
561   }
562
563   /**
564    * Returns the filtered field description.
565    *
566    * @return \Drupal\Core\Field\FieldFilteredMarkup
567    *   The filtered field description, with tokens replaced.
568    */
569   protected function getFilteredDescription() {
570     return FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription()));
571   }
572
573 }