Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / file / src / Plugin / Field / FieldWidget / FileWidget.php
1 <?php
2
3 namespace Drupal\file\Plugin\Field\FieldWidget;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Field\FieldDefinitionInterface;
7 use Drupal\Core\Field\FieldItemListInterface;
8 use Drupal\Core\Field\FieldStorageDefinitionInterface;
9 use Drupal\Core\Field\WidgetBase;
10 use Drupal\Core\Form\FormStateInterface;
11 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
12 use Drupal\Core\Render\Element;
13 use Drupal\Core\Render\ElementInfoManagerInterface;
14 use Drupal\file\Element\ManagedFile;
15 use Drupal\file\Entity\File;
16 use Symfony\Component\DependencyInjection\ContainerInterface;
17 use Symfony\Component\Validator\ConstraintViolationListInterface;
18
19 /**
20  * Plugin implementation of the 'file_generic' widget.
21  *
22  * @FieldWidget(
23  *   id = "file_generic",
24  *   label = @Translation("File"),
25  *   field_types = {
26  *     "file"
27  *   }
28  * )
29  */
30 class FileWidget extends WidgetBase implements ContainerFactoryPluginInterface {
31
32   /**
33    * {@inheritdoc}
34    */
35   public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) {
36     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
37     $this->elementInfo = $element_info;
38   }
39
40   /**
41    * {@inheritdoc}
42    */
43   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
44     return new static($plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('element_info'));
45   }
46
47   /**
48    * {@inheritdoc}
49    */
50   public static function defaultSettings() {
51     return [
52       'progress_indicator' => 'throbber',
53     ] + parent::defaultSettings();
54   }
55
56   /**
57    * {@inheritdoc}
58    */
59   public function settingsForm(array $form, FormStateInterface $form_state) {
60     $element['progress_indicator'] = [
61       '#type' => 'radios',
62       '#title' => t('Progress indicator'),
63       '#options' => [
64         'throbber' => t('Throbber'),
65         'bar' => t('Bar with progress meter'),
66       ],
67       '#default_value' => $this->getSetting('progress_indicator'),
68       '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
69       '#weight' => 16,
70       '#access' => file_progress_implementation(),
71     ];
72     return $element;
73   }
74
75   /**
76    * {@inheritdoc}
77    */
78   public function settingsSummary() {
79     $summary = [];
80     $summary[] = t('Progress indicator: @progress_indicator', ['@progress_indicator' => $this->getSetting('progress_indicator')]);
81     return $summary;
82   }
83
84   /**
85    * Overrides \Drupal\Core\Field\WidgetBase::formMultipleElements().
86    *
87    * Special handling for draggable multiple widgets and 'add more' button.
88    */
89   protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
90     $field_name = $this->fieldDefinition->getName();
91     $parents = $form['#parents'];
92
93     // Load the items for form rebuilds from the field state as they might not
94     // be in $form_state->getValues() because of validation limitations. Also,
95     // they are only passed in as $items when editing existing entities.
96     $field_state = static::getWidgetState($parents, $field_name, $form_state);
97     if (isset($field_state['items'])) {
98       $items->setValue($field_state['items']);
99     }
100
101     // Determine the number of widgets to display.
102     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
103     switch ($cardinality) {
104       case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
105         $max = count($items);
106         $is_multiple = TRUE;
107         break;
108
109       default:
110         $max = $cardinality - 1;
111         $is_multiple = ($cardinality > 1);
112         break;
113     }
114
115     $title = $this->fieldDefinition->getLabel();
116     $description = $this->getFilteredDescription();
117
118     $elements = [];
119
120     $delta = 0;
121     // Add an element for every existing item.
122     foreach ($items as $item) {
123       $element = [
124         '#title' => $title,
125         '#description' => $description,
126       ];
127       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
128
129       if ($element) {
130         // Input field for the delta (drag-n-drop reordering).
131         if ($is_multiple) {
132           // We name the element '_weight' to avoid clashing with elements
133           // defined by widget.
134           $element['_weight'] = [
135             '#type' => 'weight',
136             '#title' => t('Weight for row @number', ['@number' => $delta + 1]),
137             '#title_display' => 'invisible',
138             // Note: this 'delta' is the FAPI #type 'weight' element's property.
139             '#delta' => $max,
140             '#default_value' => $item->_weight ?: $delta,
141             '#weight' => 100,
142           ];
143         }
144
145         $elements[$delta] = $element;
146         $delta++;
147       }
148     }
149
150     $empty_single_allowed = ($cardinality == 1 && $delta == 0);
151     $empty_multiple_allowed = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta < $cardinality) && !$form_state->isProgrammed();
152
153     // Add one more empty row for new uploads except when this is a programmed
154     // multiple form as it is not necessary.
155     if ($empty_single_allowed || $empty_multiple_allowed) {
156       // Create a new empty item.
157       $items->appendItem();
158       $element = [
159         '#title' => $title,
160         '#description' => $description,
161       ];
162       $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
163       if ($element) {
164         $element['#required'] = ($element['#required'] && $delta == 0);
165         $elements[$delta] = $element;
166       }
167     }
168
169     if ($is_multiple) {
170       // The group of elements all-together need some extra functionality after
171       // building up the full list (like draggable table rows).
172       $elements['#file_upload_delta'] = $delta;
173       $elements['#type'] = 'details';
174       $elements['#open'] = TRUE;
175       $elements['#theme'] = 'file_widget_multiple';
176       $elements['#theme_wrappers'] = ['details'];
177       $elements['#process'] = [[get_class($this), 'processMultiple']];
178       $elements['#title'] = $title;
179
180       $elements['#description'] = $description;
181       $elements['#field_name'] = $field_name;
182       $elements['#language'] = $items->getLangcode();
183       // The field settings include defaults for the field type. However, this
184       // widget is a base class for other widgets (e.g., ImageWidget) that may
185       // act on field types without these expected settings.
186       $field_settings = $this->getFieldSettings() + ['display_field' => NULL];
187       $elements['#display_field'] = (bool) $field_settings['display_field'];
188
189       // Add some properties that will eventually be added to the file upload
190       // field. These are added here so that they may be referenced easily
191       // through a hook_form_alter().
192       $elements['#file_upload_title'] = t('Add a new file');
193       $elements['#file_upload_description'] = [
194         '#theme' => 'file_upload_help',
195         '#description' => '',
196         '#upload_validators' => $elements[0]['#upload_validators'],
197         '#cardinality' => $cardinality,
198       ];
199     }
200
201     return $elements;
202   }
203
204   /**
205    * {@inheritdoc}
206    */
207   public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
208     $field_settings = $this->getFieldSettings();
209
210     // The field settings include defaults for the field type. However, this
211     // widget is a base class for other widgets (e.g., ImageWidget) that may act
212     // on field types without these expected settings.
213     $field_settings += [
214       'display_default' => NULL,
215       'display_field' => NULL,
216       'description_field' => NULL,
217     ];
218
219     $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
220     $defaults = [
221       'fids' => [],
222       'display' => (bool) $field_settings['display_default'],
223       'description' => '',
224     ];
225
226     // Essentially we use the managed_file type, extended with some
227     // enhancements.
228     $element_info = $this->elementInfo->getInfo('managed_file');
229     $element += [
230       '#type' => 'managed_file',
231       '#upload_location' => $items[$delta]->getUploadLocation(),
232       '#upload_validators' => $items[$delta]->getUploadValidators(),
233       '#value_callback' => [get_class($this), 'value'],
234       '#process' => array_merge($element_info['#process'], [[get_class($this), 'process']]),
235       '#progress_indicator' => $this->getSetting('progress_indicator'),
236       // Allows this field to return an array instead of a single value.
237       '#extended' => TRUE,
238       // Add properties needed by value() and process() methods.
239       '#field_name' => $this->fieldDefinition->getName(),
240       '#entity_type' => $items->getEntity()->getEntityTypeId(),
241       '#display_field' => (bool) $field_settings['display_field'],
242       '#display_default' => $field_settings['display_default'],
243       '#description_field' => $field_settings['description_field'],
244       '#cardinality' => $cardinality,
245     ];
246
247     $element['#weight'] = $delta;
248
249     // Field stores FID value in a single mode, so we need to transform it for
250     // form element to recognize it correctly.
251     if (!isset($items[$delta]->fids) && isset($items[$delta]->target_id)) {
252       $items[$delta]->fids = [$items[$delta]->target_id];
253     }
254     $element['#default_value'] = $items[$delta]->getValue() + $defaults;
255
256     $default_fids = $element['#extended'] ? $element['#default_value']['fids'] : $element['#default_value'];
257     if (empty($default_fids)) {
258       $file_upload_help = [
259         '#theme' => 'file_upload_help',
260         '#description' => $element['#description'],
261         '#upload_validators' => $element['#upload_validators'],
262         '#cardinality' => $cardinality,
263       ];
264       $element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help);
265       $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE;
266       if ($cardinality != 1 && $cardinality != -1) {
267         $element['#element_validate'] = [[get_class($this), 'validateMultipleCount']];
268       }
269     }
270
271     return $element;
272   }
273
274   /**
275    * {@inheritdoc}
276    */
277   public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
278     // Since file upload widget now supports uploads of more than one file at a
279     // time it always returns an array of fids. We have to translate this to a
280     // single fid, as field expects single value.
281     $new_values = [];
282     foreach ($values as &$value) {
283       foreach ($value['fids'] as $fid) {
284         $new_value = $value;
285         $new_value['target_id'] = $fid;
286         unset($new_value['fids']);
287         $new_values[] = $new_value;
288       }
289     }
290
291     return $new_values;
292   }
293
294   /**
295    * {@inheritdoc}
296    */
297   public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
298     parent::extractFormValues($items, $form, $form_state);
299
300     // Update reference to 'items' stored during upload to take into account
301     // changes to values like 'alt' etc.
302     // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit()
303     $field_name = $this->fieldDefinition->getName();
304     $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
305     $field_state['items'] = $items->getValue();
306     static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
307   }
308
309   /**
310    * Form API callback. Retrieves the value for the file_generic field element.
311    *
312    * This method is assigned as a #value_callback in formElement() method.
313    */
314   public static function value($element, $input, FormStateInterface $form_state) {
315     if ($input) {
316       // Checkboxes lose their value when empty.
317       // If the display field is present make sure its unchecked value is saved.
318       if (empty($input['display'])) {
319         $input['display'] = $element['#display_field'] ? 0 : 1;
320       }
321     }
322
323     // We depend on the managed file element to handle uploads.
324     $return = ManagedFile::valueCallback($element, $input, $form_state);
325
326     // Ensure that all the required properties are returned even if empty.
327     $return += [
328       'fids' => [],
329       'display' => 1,
330       'description' => '',
331     ];
332
333     return $return;
334   }
335
336   /**
337    * Form element validation callback for upload element on file widget. Checks
338    * if user has uploaded more files than allowed.
339    *
340    * This validator is used only when cardinality not set to 1 or unlimited.
341    */
342   public static function validateMultipleCount($element, FormStateInterface $form_state, $form) {
343     $values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
344
345     $array_parents = $element['#array_parents'];
346     array_pop($array_parents);
347     $previously_uploaded_count = count(Element::children(NestedArray::getValue($form, $array_parents))) - 1;
348
349     $field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($element['#entity_type']);
350     $field_storage = $field_storage_definitions[$element['#field_name']];
351     $newly_uploaded_count = count($values['fids']);
352     $total_uploaded_count = $newly_uploaded_count + $previously_uploaded_count;
353     if ($total_uploaded_count > $field_storage->getCardinality()) {
354       $keep = $newly_uploaded_count - $total_uploaded_count + $field_storage->getCardinality();
355       $removed_files = array_slice($values['fids'], $keep);
356       $removed_names = [];
357       foreach ($removed_files as $fid) {
358         $file = File::load($fid);
359         $removed_names[] = $file->getFilename();
360       }
361       $args = [
362         '%field' => $field_storage->getName(),
363         '@max' => $field_storage->getCardinality(),
364         '@count' => $total_uploaded_count,
365         '%list' => implode(', ', $removed_names),
366       ];
367       $message = t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
368       \Drupal::messenger()->addWarning($message);
369       $values['fids'] = array_slice($values['fids'], 0, $keep);
370       NestedArray::setValue($form_state->getValues(), $element['#parents'], $values);
371     }
372   }
373
374   /**
375    * Form API callback: Processes a file_generic field element.
376    *
377    * Expands the file_generic type to include the description and display
378    * fields.
379    *
380    * This method is assigned as a #process callback in formElement() method.
381    */
382   public static function process($element, FormStateInterface $form_state, $form) {
383     $item = $element['#value'];
384     $item['fids'] = $element['fids']['#value'];
385
386     // Add the display field if enabled.
387     if ($element['#display_field']) {
388       $element['display'] = [
389         '#type' => empty($item['fids']) ? 'hidden' : 'checkbox',
390         '#title' => t('Include file in display'),
391         '#attributes' => ['class' => ['file-display']],
392       ];
393       if (isset($item['display'])) {
394         $element['display']['#value'] = $item['display'] ? '1' : '';
395       }
396       else {
397         $element['display']['#value'] = $element['#display_default'];
398       }
399     }
400     else {
401       $element['display'] = [
402         '#type' => 'hidden',
403         '#value' => '1',
404       ];
405     }
406
407     // Add the description field if enabled.
408     if ($element['#description_field'] && $item['fids']) {
409       $config = \Drupal::config('file.settings');
410       $element['description'] = [
411         '#type' => $config->get('description.type'),
412         '#title' => t('Description'),
413         '#value' => isset($item['description']) ? $item['description'] : '',
414         '#maxlength' => $config->get('description.length'),
415         '#description' => t('The description may be used as the label of the link to the file.'),
416       ];
417     }
418
419     // Adjust the Ajax settings so that on upload and remove of any individual
420     // file, the entire group of file fields is updated together.
421     if ($element['#cardinality'] != 1) {
422       $parents = array_slice($element['#array_parents'], 0, -1);
423       $new_options = [
424         'query' => [
425           'element_parents' => implode('/', $parents),
426         ],
427       ];
428       $field_element = NestedArray::getValue($form, $parents);
429       $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
430       foreach (Element::children($element) as $key) {
431         if (isset($element[$key]['#ajax'])) {
432           $element[$key]['#ajax']['options'] = $new_options;
433           $element[$key]['#ajax']['wrapper'] = $new_wrapper;
434         }
435       }
436       unset($element['#prefix'], $element['#suffix']);
437     }
438
439     // Add another submit handler to the upload and remove buttons, to implement
440     // functionality needed by the field widget. This submit handler, along with
441     // the rebuild logic in file_field_widget_form() requires the entire field,
442     // not just the individual item, to be valid.
443     foreach (['upload_button', 'remove_button'] as $key) {
444       $element[$key]['#submit'][] = [get_called_class(), 'submit'];
445       $element[$key]['#limit_validation_errors'] = [array_slice($element['#parents'], 0, -1)];
446     }
447
448     return $element;
449   }
450
451   /**
452    * Form API callback: Processes a group of file_generic field elements.
453    *
454    * Adds the weight field to each row so it can be ordered and adds a new Ajax
455    * wrapper around the entire group so it can be replaced all at once.
456    *
457    * This method on is assigned as a #process callback in formMultipleElements()
458    * method.
459    */
460   public static function processMultiple($element, FormStateInterface $form_state, $form) {
461     $element_children = Element::children($element, TRUE);
462     $count = count($element_children);
463
464     // Count the number of already uploaded files, in order to display new
465     // items in \Drupal\file\Element\ManagedFile::uploadAjaxCallback().
466     if (!$form_state->isRebuilding()) {
467       $count_items_before = 0;
468       foreach ($element_children as $children) {
469         if (!empty($element[$children]['#default_value']['fids'])) {
470           $count_items_before++;
471         }
472       }
473
474       $form_state->set('file_upload_delta_initial', $count_items_before);
475     }
476
477     foreach ($element_children as $delta => $key) {
478       if ($key != $element['#file_upload_delta']) {
479         $description = static::getDescriptionFromElement($element[$key]);
480         $element[$key]['_weight'] = [
481           '#type' => 'weight',
482           '#title' => $description ? t('Weight for @title', ['@title' => $description]) : t('Weight for new file'),
483           '#title_display' => 'invisible',
484           '#delta' => $count,
485           '#default_value' => $delta,
486         ];
487       }
488       else {
489         // The title needs to be assigned to the upload field so that validation
490         // errors include the correct widget label.
491         $element[$key]['#title'] = $element['#title'];
492         $element[$key]['_weight'] = [
493           '#type' => 'hidden',
494           '#default_value' => $delta,
495         ];
496       }
497     }
498
499     // Add a new wrapper around all the elements for Ajax replacement.
500     $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
501     $element['#suffix'] = '</div>';
502
503     return $element;
504   }
505
506   /**
507    * Retrieves the file description from a field field element.
508    *
509    * This helper static method is used by processMultiple() method.
510    *
511    * @param array $element
512    *   An associative array with the element being processed.
513    *
514    * @return array|false
515    *   A description of the file suitable for use in the administrative
516    *   interface.
517    */
518   protected static function getDescriptionFromElement($element) {
519     // Use the actual file description, if it's available.
520     if (!empty($element['#default_value']['description'])) {
521       return $element['#default_value']['description'];
522     }
523     // Otherwise, fall back to the filename.
524     if (!empty($element['#default_value']['filename'])) {
525       return $element['#default_value']['filename'];
526     }
527     // This is probably a newly uploaded file; no description is available.
528     return FALSE;
529   }
530
531   /**
532    * Form submission handler for upload/remove button of formElement().
533    *
534    * This runs in addition to and after file_managed_file_submit().
535    *
536    * @see file_managed_file_submit()
537    */
538   public static function submit($form, FormStateInterface $form_state) {
539     // During the form rebuild, formElement() will create field item widget
540     // elements using re-indexed deltas, so clear out FormState::$input to
541     // avoid a mismatch between old and new deltas. The rebuilt elements will
542     // have #default_value set appropriately for the current state of the field,
543     // so nothing is lost in doing this.
544     $button = $form_state->getTriggeringElement();
545     $parents = array_slice($button['#parents'], 0, -2);
546     NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
547
548     // Go one level up in the form, to the widgets container.
549     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
550     $field_name = $element['#field_name'];
551     $parents = $element['#field_parents'];
552
553     $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2));
554     foreach ($submitted_values as $delta => $submitted_value) {
555       if (empty($submitted_value['fids'])) {
556         unset($submitted_values[$delta]);
557       }
558     }
559
560     // If there are more files uploaded via the same widget, we have to separate
561     // them, as we display each file in its own widget.
562     $new_values = [];
563     foreach ($submitted_values as $delta => $submitted_value) {
564       if (is_array($submitted_value['fids'])) {
565         foreach ($submitted_value['fids'] as $fid) {
566           $new_value = $submitted_value;
567           $new_value['fids'] = [$fid];
568           $new_values[] = $new_value;
569         }
570       }
571       else {
572         $new_value = $submitted_value;
573       }
574     }
575
576     // Re-index deltas after removing empty items.
577     $submitted_values = array_values($new_values);
578
579     // Update form_state values.
580     NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values);
581
582     // Update items.
583     $field_state = static::getWidgetState($parents, $field_name, $form_state);
584     $field_state['items'] = $submitted_values;
585     static::setWidgetState($parents, $field_name, $form_state, $field_state);
586   }
587
588   /**
589    * {@inheritdoc}
590    */
591   public function flagErrors(FieldItemListInterface $items, ConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
592     // Never flag validation errors for the remove button.
593     $clicked_button = end($form_state->getTriggeringElement()['#parents']);
594     if ($clicked_button !== 'remove_button') {
595       parent::flagErrors($items, $violations, $form, $form_state);
596     }
597   }
598
599 }