3 namespace Drupal\media_library\Form;
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;
25 * Creates a form to create media entities from uploaded files.
29 class MediaLibraryUploadForm extends FormBase {
32 * The element info manager.
34 * @var \Drupal\Core\Render\ElementInfoManagerInterface
36 protected $elementInfo;
39 * The entity type manager.
41 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
43 protected $entityTypeManager;
46 * Media types the current user has access to.
48 * @var \Drupal\media\MediaTypeInterface[]
53 * The media being processed.
55 * @var \Drupal\media\MediaInterface[]
57 protected $media = [];
60 * The files waiting for type selection.
62 * @var \Drupal\file\FileInterface[]
64 protected $files = [];
67 * Indicates whether the 'medium' image style exists.
71 protected $mediumStyleExists = FALSE;
74 * Constructs a new MediaLibraryUploadForm.
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.
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'));
90 public static function create(ContainerInterface $container) {
92 $container->get('entity_type.manager'),
93 $container->get('element_info')
100 public function getFormId() {
101 return 'media_library_upload_form';
107 public function buildForm(array $form, FormStateInterface $form_state) {
108 $form['#prefix'] = '<div id="media-library-upload-wrapper">';
109 $form['#suffix'] = '</div>';
111 $form['#attached']['library'][] = 'media_library/style';
113 $form['#attributes']['class'][] = 'media-library-upload';
115 if (empty($this->media) && empty($this->files)) {
116 $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
117 $upload_validators = $this->mergeUploadValidators($this->getTypes());
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,
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,
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;
138 '#type' => 'container',
140 foreach ($this->media as $i => $media) {
141 $source_field = $media->getSource()
142 ->getSourceFieldDefinition($media->bundle->entity)
146 '#type' => 'container',
149 'media-library-upload__media',
153 '#type' => 'container',
156 'media-library-upload__media-preview',
161 '#type' => 'container',
164 'media-library-upload__media-fields',
167 // Parents is set here as it is used in the form display.
168 '#parents' => ['media', $i, 'fields'],
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,
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';
185 if (isset($element['fields']['revision_log_message'])) {
186 $element['fields']['revision_log_message']['#access'] = FALSE;
188 $form['media'][$i] = $element;
192 '#type' => 'container',
194 foreach ($this->files as $i => $file) {
195 $types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
196 $form['files'][$i] = [
197 '#type' => 'container',
200 'media-library-upload__file',
204 '#markup' => '<strong class="media-library-upload__file-label">' . $this->t('Select a media type for %filename:', [
205 '%filename' => $file->getFilename(),
209 foreach ($types as $type) {
210 $form['files'][$i][$type->id()] = [
212 '#media_library_index' => $i,
213 '#media_library_type' => $type->id(),
214 '#value' => $type->label(),
215 '#submit' => ['::selectType'],
217 'callback' => '::updateFormCallback',
218 'wrapper' => 'media-library-upload-wrapper',
220 '#limit_validation_errors' => [['files', $i, $type->id()]],
226 '#type' => 'actions',
228 $form['actions']['submit'] = [
230 '#value' => $this->t('Save'),
232 'callback' => '::updateWidget',
233 'wrapper' => 'media-library-upload-wrapper',
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.'));
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);
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();
272 * AJAX callback to select a media type for a file.
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.
279 * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
280 * If the triggering element is missing required properties.
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.');
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();
295 * AJAX callback to update the field widget.
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.
302 * @return \Drupal\Core\Ajax\AjaxResponse
303 * A command to send the selection to the current field widget.
305 * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
306 * If the "media_library_widget_id" query parameter is not present.
308 public function updateWidget(array &$form, FormStateInterface $form_state) {
309 if ($form_state->getErrors()) {
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.');
316 $mids = array_map(function (MediaInterface $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());
327 * Processes an upload (managed_file) element.
329 * @param array $element
330 * The upload element.
331 * @param \Drupal\Core\Form\FormStateInterface $form_state
335 * The processed upload element.
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',
347 * Validates the upload element.
349 * @param array $element
350 * The upload element.
351 * @param \Drupal\Core\Form\FormStateInterface $form_state
355 * The processed upload element.
357 public function validateUploadElement(array $element, FormStateInterface $form_state) {
358 if ($form_state->getErrors()) {
359 $element['#value'] = [];
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'],
366 $form_state->setValue('upload', []);
367 $element['#value'] = [];
373 * Submit handler for the upload button, inside the managed_file element.
376 * The form render array.
377 * @param \Drupal\Core\Form\FormStateInterface $form_state
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));
391 $this->files[] = $file;
395 $form_state->setRebuild();
399 * Creates a new, unsaved media entity.
401 * @param \Drupal\file\FileInterface $file
402 * A file for the media source field.
403 * @param \Drupal\media\MediaTypeInterface $type
406 * @return \Drupal\media\MediaInterface
407 * An unsaved media entity.
410 * If a file operation failed when moving the upload.
412 protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) {
413 $media = $this->entityTypeManager->getStorage('media')->create([
414 'bundle' => $type->id(),
415 'name' => $file->getFilename(),
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");
422 $file = file_move($file, $location);
424 throw new \Exception("Unable to move file to '$location'");
426 $media->set($source_field, $file->id());
431 * AJAX callback for refreshing the entire form.
434 * The form render array.
435 * @param \Drupal\Core\Form\FormStateInterface $form_state
439 * The form render array.
441 public function updateFormCallback(array &$form, FormStateInterface $form_state) {
446 * Access callback to check that the user can create file based media.
448 * @param array $allowed_types
449 * (optional) The contextually allowed types.
451 * @return \Drupal\Core\Access\AccessResultInterface
454 * @todo Remove $allowed_types param in https://www.drupal.org/node/2956747
456 public function access(array $allowed_types = NULL) {
457 return AccessResultAllowed::allowedIf(count($this->getTypes($allowed_types)))->mergeCacheMaxAge(0);
461 * Returns media types which use files that the current user can create.
463 * @param array $allowed_types
464 * (optional) The contextually allowed types.
466 * @todo Move in https://www.drupal.org/node/2987924
468 * @return \Drupal\media\MediaTypeInterface[]
469 * A list of media types that are valid for this form.
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;
478 $types = $media_type_storage->loadMultiple($allowed_types);
479 $types = $this->filterTypesWithFileSource($types);
480 $types = $this->filterTypesWithCreateAccess($types);
481 $this->types = $types;
487 * Filters media types that accept a given file.
489 * @todo Move in https://www.drupal.org/node/2987924
491 * @param \Drupal\file\FileInterface $file
493 * @param \Drupal\media\MediaTypeInterface[] $types
494 * An array of available media types.
496 * @return \Drupal\media\MediaTypeInterface[]
497 * An array of media types that accept the file.
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);
509 * Filters an array of media types that accept file sources.
511 * @todo Move in https://www.drupal.org/node/2987924
513 * @param \Drupal\media\MediaTypeInterface[] $types
514 * An array of media types.
516 * @return \Drupal\media\MediaTypeInterface[]
517 * An array of media types that accept file sources.
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);
526 * Merges file upload validators for an array of media types.
528 * @todo Move in https://www.drupal.org/node/2987924
530 * @param \Drupal\media\MediaTypeInterface[] $types
531 * An array of media types.
534 * An array suitable for passing to file_save_upload() or the file field
535 * element's '#upload_validators' property.
537 protected function mergeUploadValidators(array $types) {
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]);
546 if (isset($validators['file_validate_extensions'])) {
547 $extensions = array_unique(array_merge($extensions, explode(' ', $validators['file_validate_extensions'][0])));
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();
555 'file_validate_extensions' => [implode(' ', $extensions)],
556 'file_validate_size' => [$max_size],
561 * Gets upload validators for a given media type.
563 * @todo Move in https://www.drupal.org/node/2987924
565 * @param \Drupal\media\MediaTypeInterface $type
569 * An array suitable for passing to file_save_upload() or the file field
570 * element's '#upload_validators' property.
572 protected function getUploadValidatorsForType(MediaTypeInterface $type) {
573 return $this->getFileItemForType($type)->getUploadValidators();
577 * Gets upload destination for a given media type.
579 * @todo Move in https://www.drupal.org/node/2987924
581 * @param \Drupal\media\MediaTypeInterface $type
585 * An unsanitized file directory URI with tokens replaced.
587 protected function getUploadLocationForType(MediaTypeInterface $type) {
588 return $this->getFileItemForType($type)->getUploadLocation();
592 * Creates a file item for a given media type.
594 * @todo Move in https://www.drupal.org/node/2987924
596 * @param \Drupal\media\MediaTypeInterface $type
599 * @return \Drupal\file\Plugin\Field\FieldType\FileItem
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);
609 * Filters an array of media types that can be created by the current user.
611 * @todo Move in https://www.drupal.org/node/2987924
613 * @param \Drupal\media\MediaTypeInterface[] $types
614 * An array of media types.
616 * @return \Drupal\media\MediaTypeInterface[]
617 * An array of media types that accept file sources.
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());