ac250b1077c49ed1f6908f7dea350c68eb39a6a2
[yaffs-website] / web / core / modules / media_library / src / Form / MediaLibraryUploadForm.php
1 <?php
2
3 namespace Drupal\media_library\Form;
4
5 use Drupal\Core\Access\AccessResultAllowed;
6 use Drupal\Core\Ajax\AjaxResponse;
7 use Drupal\Core\Ajax\CloseDialogCommand;
8 use Drupal\Core\Ajax\InvokeCommand;
9 use Drupal\Core\Entity\Entity\EntityFormDisplay;
10 use Drupal\Core\Entity\EntityTypeManagerInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
13 use Drupal\Core\Form\FormBase;
14 use Drupal\Core\Form\FormStateInterface;
15 use Drupal\Core\Render\ElementInfoManagerInterface;
16 use Drupal\file\FileInterface;
17 use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
18 use Drupal\file\Plugin\Field\FieldType\FileItem;
19 use Drupal\media\MediaInterface;
20 use Drupal\media\MediaTypeInterface;
21 use Symfony\Component\DependencyInjection\ContainerInterface;
22 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
23
24 /**
25  * Creates a form to create media entities from uploaded files.
26  *
27  * @internal
28  */
29 class MediaLibraryUploadForm extends FormBase {
30
31   /**
32    * The element info manager.
33    *
34    * @var \Drupal\Core\Render\ElementInfoManagerInterface
35    */
36   protected $elementInfo;
37
38   /**
39    * The entity type manager.
40    *
41    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
42    */
43   protected $entityTypeManager;
44
45   /**
46    * Media types the current user has access to.
47    *
48    * @var \Drupal\media\MediaTypeInterface[]
49    */
50   protected $types;
51
52   /**
53    * The media being processed.
54    *
55    * @var \Drupal\media\MediaInterface[]
56    */
57   protected $media = [];
58
59   /**
60    * The files waiting for type selection.
61    *
62    * @var \Drupal\file\FileInterface[]
63    */
64   protected $files = [];
65
66   /**
67    * Indicates whether the 'medium' image style exists.
68    *
69    * @var bool
70    */
71   protected $mediumStyleExists = FALSE;
72
73   /**
74    * Constructs a new MediaLibraryUploadForm.
75    *
76    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
77    *   The entity type manager.
78    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
79    *   The element info manager.
80    */
81   public function __construct(EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info) {
82     $this->entityTypeManager = $entity_type_manager;
83     $this->elementInfo = $element_info;
84     $this->mediumStyleExists = !empty($entity_type_manager->getStorage('image_style')->load('medium'));
85   }
86
87   /**
88    * {@inheritdoc}
89    */
90   public static function create(ContainerInterface $container) {
91     return new static(
92       $container->get('entity_type.manager'),
93       $container->get('element_info')
94     );
95   }
96
97   /**
98    * {@inheritdoc}
99    */
100   public function getFormId() {
101     return 'media_library_upload_form';
102   }
103
104   /**
105    * {@inheritdoc}
106    */
107   public function buildForm(array $form, FormStateInterface $form_state) {
108     $form['#prefix'] = '<div id="media-library-upload-wrapper">';
109     $form['#suffix'] = '</div>';
110
111     $form['#attached']['library'][] = 'media_library/style';
112
113     $form['#attributes']['class'][] = 'media-library-upload';
114
115     if (empty($this->media) && empty($this->files)) {
116       $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
117       $upload_validators = $this->mergeUploadValidators($this->getTypes());
118       $form['upload'] = [
119         '#type' => 'managed_file',
120         '#title' => $this->t('Upload'),
121         // @todo Move validation in https://www.drupal.org/node/2988215
122         '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
123         '#upload_validators' => $upload_validators,
124       ];
125       $form['upload_help'] = [
126         '#theme' => 'file_upload_help',
127         '#description' => $this->t('Upload files here to add new media.'),
128         '#upload_validators' => $upload_validators,
129       ];
130       $remaining = (int) $this->getRequest()->query->get('media_library_remaining');
131       if ($remaining || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
132         $form['upload']['#multiple'] = $remaining > 1 || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
133         $form['upload']['#cardinality'] = $form['upload_help']['#cardinality'] = $remaining;
134       }
135     }
136     else {
137       $form['media'] = [
138         '#type' => 'container',
139       ];
140       foreach ($this->media as $i => $media) {
141         $source_field = $media->getSource()
142           ->getSourceFieldDefinition($media->bundle->entity)
143           ->getName();
144
145         $element = [
146           '#type' => 'container',
147           '#attributes' => [
148             'class' => [
149               'media-library-upload__media',
150             ],
151           ],
152           'preview' => [
153             '#type' => 'container',
154             '#attributes' => [
155               'class' => [
156                 'media-library-upload__media-preview',
157               ],
158             ],
159           ],
160           'fields' => [
161             '#type' => 'container',
162             '#attributes' => [
163               'class' => [
164                 'media-library-upload__media-fields',
165               ],
166             ],
167             // Parents is set here as it is used in the form display.
168             '#parents' => ['media', $i, 'fields'],
169           ],
170         ];
171         // @todo Make this configurable in https://www.drupal.org/node/2988223
172         if ($this->mediumStyleExists && $thumbnail_uri = $media->getSource()->getMetadata($media, 'thumbnail_uri')) {
173           $element['preview']['thumbnail'] = [
174             '#theme' => 'image_style',
175             '#style_name' => 'medium',
176             '#uri' => $thumbnail_uri,
177           ];
178         }
179         EntityFormDisplay::collectRenderDisplay($media, 'media_library')
180           ->buildForm($media, $element['fields'], $form_state);
181         // We hide certain elements in the image widget with CSS.
182         if (isset($element['fields'][$source_field])) {
183           $element['fields'][$source_field]['#attributes']['class'][] = 'media-library-upload__source-field';
184         }
185         if (isset($element['fields']['revision_log_message'])) {
186           $element['fields']['revision_log_message']['#access'] = FALSE;
187         }
188         $form['media'][$i] = $element;
189       }
190
191       $form['files'] = [
192         '#type' => 'container',
193       ];
194       foreach ($this->files as $i => $file) {
195         $types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
196         $form['files'][$i] = [
197           '#type' => 'container',
198           '#attributes' => [
199             'class' => [
200               'media-library-upload__file',
201             ],
202           ],
203           'help' => [
204             '#markup' => '<strong class="media-library-upload__file-label">' . $this->t('Select a media type for %filename:', [
205               '%filename' => $file->getFilename(),
206             ]) . '</strong>',
207           ],
208         ];
209         foreach ($types as $type) {
210           $form['files'][$i][$type->id()] = [
211             '#type' => 'submit',
212             '#media_library_index' => $i,
213             '#media_library_type' => $type->id(),
214             '#value' => $type->label(),
215             '#submit' => ['::selectType'],
216             '#ajax' => [
217               'callback' => '::updateFormCallback',
218               'wrapper' => 'media-library-upload-wrapper',
219             ],
220             '#limit_validation_errors' => [['files', $i, $type->id()]],
221           ];
222         }
223       }
224
225       $form['actions'] = [
226         '#type' => 'actions',
227       ];
228       $form['actions']['submit'] = [
229         '#type' => 'submit',
230         '#value' => $this->t('Save'),
231         '#ajax' => [
232           'callback' => '::updateWidget',
233           'wrapper' => 'media-library-upload-wrapper',
234         ],
235       ];
236     }
237
238     return $form;
239   }
240
241   /**
242    * {@inheritdoc}
243    */
244   public function validateForm(array &$form, FormStateInterface $form_state) {
245     if (count($this->files)) {
246       $form_state->setError($form['files'], $this->t('Please select a media type for all files.'));
247     }
248     foreach ($this->media as $i => $media) {
249       $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
250       $form_display->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
251       $form_display->validateFormValues($media, $form['media'][$i]['fields'], $form_state);
252     }
253   }
254
255   /**
256    * {@inheritdoc}
257    */
258   public function submitForm(array &$form, FormStateInterface $form_state) {
259     foreach ($this->media as $i => $media) {
260       EntityFormDisplay::collectRenderDisplay($media, 'media_library')
261         ->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
262       $source_field = $media->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName();
263       /** @var \Drupal\file\FileInterface $file */
264       $file = $media->get($source_field)->entity;
265       $file->setPermanent();
266       $file->save();
267       $media->save();
268     }
269   }
270
271   /**
272    * AJAX callback to select a media type for a file.
273    *
274    * @param array $form
275    *   An associative array containing the structure of the form.
276    * @param \Drupal\Core\Form\FormStateInterface $form_state
277    *   The current state of the form.
278    *
279    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
280    *   If the triggering element is missing required properties.
281    */
282   public function selectType(array &$form, FormStateInterface $form_state) {
283     $element = $form_state->getTriggeringElement();
284     if (!isset($element['#media_library_index']) || !isset($element['#media_library_type'])) {
285       throw new BadRequestHttpException('The "#media_library_index" and "#media_library_type" properties on the triggering element are required for type selection.');
286     }
287     $i = $element['#media_library_index'];
288     $type = $element['#media_library_type'];
289     $this->media[] = $this->createMediaEntity($this->files[$i], $this->getTypes()[$type]);
290     unset($this->files[$i]);
291     $form_state->setRebuild();
292   }
293
294   /**
295    * AJAX callback to update the field widget.
296    *
297    * @param array $form
298    *   An associative array containing the structure of the form.
299    * @param \Drupal\Core\Form\FormStateInterface $form_state
300    *   The current state of the form.
301    *
302    * @return \Drupal\Core\Ajax\AjaxResponse
303    *   A command to send the selection to the current field widget.
304    *
305    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
306    *   If the "media_library_widget_id" query parameter is not present.
307    */
308   public function updateWidget(array &$form, FormStateInterface $form_state) {
309     if ($form_state->getErrors()) {
310       return $form;
311     }
312     $widget_id = $this->getRequest()->query->get('media_library_widget_id');
313     if (!$widget_id || !is_string($widget_id)) {
314       throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.');
315     }
316     $mids = array_map(function (MediaInterface $media) {
317       return $media->id();
318     }, $this->media);
319     // Pass the selection to the field widget based on the current widget ID.
320     return (new AjaxResponse())
321       ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)]))
322       ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']))
323       ->addCommand(new CloseDialogCommand());
324   }
325
326   /**
327    * Processes an upload (managed_file) element.
328    *
329    * @param array $element
330    *   The upload element.
331    * @param \Drupal\Core\Form\FormStateInterface $form_state
332    *   The form state.
333    *
334    * @return array
335    *   The processed upload element.
336    */
337   public function processUploadElement(array $element, FormStateInterface $form_state) {
338     $element['upload_button']['#submit'] = ['::uploadButtonSubmit'];
339     $element['upload_button']['#ajax'] = [
340       'callback' => '::updateFormCallback',
341       'wrapper' => 'media-library-upload-wrapper',
342     ];
343     return $element;
344   }
345
346   /**
347    * Validates the upload element.
348    *
349    * @param array $element
350    *   The upload element.
351    * @param \Drupal\Core\Form\FormStateInterface $form_state
352    *   The form state.
353    *
354    * @return array
355    *   The processed upload element.
356    */
357   public function validateUploadElement(array $element, FormStateInterface $form_state) {
358     if ($form_state->getErrors()) {
359       $element['#value'] = [];
360     }
361     $values = $form_state->getValue('upload', []);
362     if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
363       $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [
364         '@count' => $element['#cardinality'],
365       ]));
366       $form_state->setValue('upload', []);
367       $element['#value'] = [];
368     }
369     return $element;
370   }
371
372   /**
373    * Submit handler for the upload button, inside the managed_file element.
374    *
375    * @param array $form
376    *   The form render array.
377    * @param \Drupal\Core\Form\FormStateInterface $form_state
378    *   The form state.
379    */
380   public function uploadButtonSubmit(array $form, FormStateInterface $form_state) {
381     $fids = $form_state->getValue('upload', []);
382     $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
383     /** @var \Drupal\file\FileInterface $file */
384     foreach ($files as $file) {
385       $types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
386       if (!empty($types)) {
387         if (count($types) === 1) {
388           $this->media[] = $this->createMediaEntity($file, reset($types));
389         }
390         else {
391           $this->files[] = $file;
392         }
393       }
394     }
395     $form_state->setRebuild();
396   }
397
398   /**
399    * Creates a new, unsaved media entity.
400    *
401    * @param \Drupal\file\FileInterface $file
402    *   A file for the media source field.
403    * @param \Drupal\media\MediaTypeInterface $type
404    *   A media type.
405    *
406    * @return \Drupal\media\MediaInterface
407    *   An unsaved media entity.
408    *
409    * @throws \Exception
410    *   If a file operation failed when moving the upload.
411    */
412   protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) {
413     $media = $this->entityTypeManager->getStorage('media')->create([
414       'bundle' => $type->id(),
415       'name' => $file->getFilename(),
416     ]);
417     $source_field = $type->getSource()->getSourceFieldDefinition($type)->getName();
418     $location = $this->getUploadLocationForType($media->bundle->entity);
419     if (!file_prepare_directory($location, FILE_CREATE_DIRECTORY)) {
420       throw new \Exception("The destination directory '$location' is not writable");
421     }
422     $file = file_move($file, $location);
423     if (!$file) {
424       throw new \Exception("Unable to move file to '$location'");
425     }
426     $media->set($source_field, $file->id());
427     return $media;
428   }
429
430   /**
431    * AJAX callback for refreshing the entire form.
432    *
433    * @param array $form
434    *   The form render array.
435    * @param \Drupal\Core\Form\FormStateInterface $form_state
436    *   The form state.
437    *
438    * @return array
439    *   The form render array.
440    */
441   public function updateFormCallback(array &$form, FormStateInterface $form_state) {
442     return $form;
443   }
444
445   /**
446    * Access callback to check that the user can create file based media.
447    *
448    * @param array $allowed_types
449    *   (optional) The contextually allowed types.
450    *
451    * @return \Drupal\Core\Access\AccessResultInterface
452    *   The access result.
453    *
454    * @todo Remove $allowed_types param in https://www.drupal.org/node/2956747
455    */
456   public function access(array $allowed_types = NULL) {
457     return AccessResultAllowed::allowedIf(count($this->getTypes($allowed_types)))->mergeCacheMaxAge(0);
458   }
459
460   /**
461    * Returns media types which use files that the current user can create.
462    *
463    * @param array $allowed_types
464    *   (optional) The contextually allowed types.
465    *
466    * @todo Move in https://www.drupal.org/node/2987924
467    *
468    * @return \Drupal\media\MediaTypeInterface[]
469    *   A list of media types that are valid for this form.
470    */
471   protected function getTypes(array $allowed_types = NULL) {
472     // Cache results if possible.
473     if (!isset($this->types)) {
474       $media_type_storage = $this->entityTypeManager->getStorage('media_type');
475       if (!$allowed_types) {
476         $allowed_types = _media_library_get_allowed_types() ?: NULL;
477       }
478       $types = $media_type_storage->loadMultiple($allowed_types);
479       $types = $this->filterTypesWithFileSource($types);
480       $types = $this->filterTypesWithCreateAccess($types);
481       $this->types = $types;
482     }
483     return $this->types;
484   }
485
486   /**
487    * Filters media types that accept a given file.
488    *
489    * @todo Move in https://www.drupal.org/node/2987924
490    *
491    * @param \Drupal\file\FileInterface $file
492    *   A file entity.
493    * @param \Drupal\media\MediaTypeInterface[] $types
494    *   An array of available media types.
495    *
496    * @return \Drupal\media\MediaTypeInterface[]
497    *   An array of media types that accept the file.
498    */
499   protected function filterTypesThatAcceptFile(FileInterface $file, array $types) {
500     $types = $this->filterTypesWithFileSource($types);
501     return array_filter($types, function (MediaTypeInterface $type) use ($file) {
502       $validators = $this->getUploadValidatorsForType($type);
503       $errors = file_validate($file, $validators);
504       return empty($errors);
505     });
506   }
507
508   /**
509    * Filters an array of media types that accept file sources.
510    *
511    * @todo Move in https://www.drupal.org/node/2987924
512    *
513    * @param \Drupal\media\MediaTypeInterface[] $types
514    *   An array of media types.
515    *
516    * @return \Drupal\media\MediaTypeInterface[]
517    *   An array of media types that accept file sources.
518    */
519   protected function filterTypesWithFileSource(array $types) {
520     return array_filter($types, function (MediaTypeInterface $type) {
521       return is_a($type->getSource()->getSourceFieldDefinition($type)->getClass(), FileFieldItemList::class, TRUE);
522     });
523   }
524
525   /**
526    * Merges file upload validators for an array of media types.
527    *
528    * @todo Move in https://www.drupal.org/node/2987924
529    *
530    * @param \Drupal\media\MediaTypeInterface[] $types
531    *   An array of media types.
532    *
533    * @return array
534    *   An array suitable for passing to file_save_upload() or the file field
535    *   element's '#upload_validators' property.
536    */
537   protected function mergeUploadValidators(array $types) {
538     $max_size = 0;
539     $extensions = [];
540     $types = $this->filterTypesWithFileSource($types);
541     foreach ($types as $type) {
542       $validators = $this->getUploadValidatorsForType($type);
543       if (isset($validators['file_validate_size'])) {
544         $max_size = max($max_size, $validators['file_validate_size'][0]);
545       }
546       if (isset($validators['file_validate_extensions'])) {
547         $extensions = array_unique(array_merge($extensions, explode(' ', $validators['file_validate_extensions'][0])));
548       }
549     }
550     // If no field defines a max size, default to the system wide setting.
551     if ($max_size === 0) {
552       $max_size = file_upload_max_size();
553     }
554     return [
555       'file_validate_extensions' => [implode(' ', $extensions)],
556       'file_validate_size' => [$max_size],
557     ];
558   }
559
560   /**
561    * Gets upload validators for a given media type.
562    *
563    * @todo Move in https://www.drupal.org/node/2987924
564    *
565    * @param \Drupal\media\MediaTypeInterface $type
566    *   A media type.
567    *
568    * @return array
569    *   An array suitable for passing to file_save_upload() or the file field
570    *   element's '#upload_validators' property.
571    */
572   protected function getUploadValidatorsForType(MediaTypeInterface $type) {
573     return $this->getFileItemForType($type)->getUploadValidators();
574   }
575
576   /**
577    * Gets upload destination for a given media type.
578    *
579    * @todo Move in https://www.drupal.org/node/2987924
580    *
581    * @param \Drupal\media\MediaTypeInterface $type
582    *   A media type.
583    *
584    * @return string
585    *   An unsanitized file directory URI with tokens replaced.
586    */
587   protected function getUploadLocationForType(MediaTypeInterface $type) {
588     return $this->getFileItemForType($type)->getUploadLocation();
589   }
590
591   /**
592    * Creates a file item for a given media type.
593    *
594    * @todo Move in https://www.drupal.org/node/2987924
595    *
596    * @param \Drupal\media\MediaTypeInterface $type
597    *   A media type.
598    *
599    * @return \Drupal\file\Plugin\Field\FieldType\FileItem
600    *   The file item.
601    */
602   protected function getFileItemForType(MediaTypeInterface $type) {
603     $source = $type->getSource();
604     $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($type));
605     return new FileItem($source_data_definition);
606   }
607
608   /**
609    * Filters an array of media types that can be created by the current user.
610    *
611    * @todo Move in https://www.drupal.org/node/2987924
612    *
613    * @param \Drupal\media\MediaTypeInterface[] $types
614    *   An array of media types.
615    *
616    * @return \Drupal\media\MediaTypeInterface[]
617    *   An array of media types that accept file sources.
618    */
619   protected function filterTypesWithCreateAccess(array $types) {
620     $access_handler = $this->entityTypeManager->getAccessControlHandler('media');
621     return array_filter($types, function (MediaTypeInterface $type) use ($access_handler) {
622       return $access_handler->createAccess($type->id());
623     });
624   }
625
626 }