entityTypeManager = $entity_type_manager; $this->elementInfo = $element_info; $this->mediumStyleExists = !empty($entity_type_manager->getStorage('image_style')->load('medium')); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( $container->get('entity_type.manager'), $container->get('element_info') ); } /** * {@inheritdoc} */ public function getFormId() { return 'media_library_upload_form'; } /** * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { $form['#prefix'] = '
'; $form['#suffix'] = '
'; $form['#attached']['library'][] = 'media_library/style'; $form['#attributes']['class'][] = 'media-library-upload'; if (empty($this->media) && empty($this->files)) { $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []); $upload_validators = $this->mergeUploadValidators($this->getTypes()); $form['upload'] = [ '#type' => 'managed_file', '#title' => $this->t('Upload'), // @todo Move validation in https://www.drupal.org/node/2988215 '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']), '#upload_validators' => $upload_validators, ]; $form['upload_help'] = [ '#theme' => 'file_upload_help', '#description' => $this->t('Upload files here to add new media.'), '#upload_validators' => $upload_validators, ]; $remaining = (int) $this->getRequest()->query->get('media_library_remaining'); if ($remaining || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { $form['upload']['#multiple'] = $remaining > 1 || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; $form['upload']['#cardinality'] = $form['upload_help']['#cardinality'] = $remaining; } } else { $form['media'] = [ '#type' => 'container', ]; foreach ($this->media as $i => $media) { $source_field = $media->getSource() ->getSourceFieldDefinition($media->bundle->entity) ->getName(); $element = [ '#type' => 'container', '#attributes' => [ 'class' => [ 'media-library-upload__media', ], ], 'preview' => [ '#type' => 'container', '#attributes' => [ 'class' => [ 'media-library-upload__media-preview', ], ], ], 'fields' => [ '#type' => 'container', '#attributes' => [ 'class' => [ 'media-library-upload__media-fields', ], ], // Parents is set here as it is used in the form display. '#parents' => ['media', $i, 'fields'], ], ]; // @todo Make this configurable in https://www.drupal.org/node/2988223 if ($this->mediumStyleExists && $thumbnail_uri = $media->getSource()->getMetadata($media, 'thumbnail_uri')) { $element['preview']['thumbnail'] = [ '#theme' => 'image_style', '#style_name' => 'medium', '#uri' => $thumbnail_uri, ]; } EntityFormDisplay::collectRenderDisplay($media, 'media_library') ->buildForm($media, $element['fields'], $form_state); // We hide certain elements in the image widget with CSS. if (isset($element['fields'][$source_field])) { $element['fields'][$source_field]['#attributes']['class'][] = 'media-library-upload__source-field'; } if (isset($element['fields']['revision_log_message'])) { $element['fields']['revision_log_message']['#access'] = FALSE; } $form['media'][$i] = $element; } $form['files'] = [ '#type' => 'container', ]; foreach ($this->files as $i => $file) { $types = $this->filterTypesThatAcceptFile($file, $this->getTypes()); $form['files'][$i] = [ '#type' => 'container', '#attributes' => [ 'class' => [ 'media-library-upload__file', ], ], 'help' => [ '#markup' => '' . $this->t('Select a media type for %filename:', [ '%filename' => $file->getFilename(), ]) . '', ], ]; foreach ($types as $type) { $form['files'][$i][$type->id()] = [ '#type' => 'submit', '#media_library_index' => $i, '#media_library_type' => $type->id(), '#value' => $type->label(), '#submit' => ['::selectType'], '#ajax' => [ 'callback' => '::updateFormCallback', 'wrapper' => 'media-library-upload-wrapper', ], '#limit_validation_errors' => [['files', $i, $type->id()]], ]; } } $form['actions'] = [ '#type' => 'actions', ]; $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('Save'), '#ajax' => [ 'callback' => '::updateWidget', 'wrapper' => 'media-library-upload-wrapper', ], ]; } return $form; } /** * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state) { if (count($this->files)) { $form_state->setError($form['files'], $this->t('Please select a media type for all files.')); } foreach ($this->media as $i => $media) { $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library'); $form_display->extractFormValues($media, $form['media'][$i]['fields'], $form_state); $form_display->validateFormValues($media, $form['media'][$i]['fields'], $form_state); } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { foreach ($this->media as $i => $media) { EntityFormDisplay::collectRenderDisplay($media, 'media_library') ->extractFormValues($media, $form['media'][$i]['fields'], $form_state); $source_field = $media->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName(); /** @var \Drupal\file\FileInterface $file */ $file = $media->get($source_field)->entity; $file->setPermanent(); $file->save(); $media->save(); } } /** * AJAX callback to select a media type for a file. * * @param array $form * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * If the triggering element is missing required properties. */ public function selectType(array &$form, FormStateInterface $form_state) { $element = $form_state->getTriggeringElement(); if (!isset($element['#media_library_index']) || !isset($element['#media_library_type'])) { throw new BadRequestHttpException('The "#media_library_index" and "#media_library_type" properties on the triggering element are required for type selection.'); } $i = $element['#media_library_index']; $type = $element['#media_library_type']; $this->media[] = $this->createMediaEntity($this->files[$i], $this->getTypes()[$type]); unset($this->files[$i]); $form_state->setRebuild(); } /** * AJAX callback to update the field widget. * * @param array $form * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * * @return \Drupal\Core\Ajax\AjaxResponse * A command to send the selection to the current field widget. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * If the "media_library_widget_id" query parameter is not present. */ public function updateWidget(array &$form, FormStateInterface $form_state) { if ($form_state->getErrors()) { return $form; } $widget_id = $this->getRequest()->query->get('media_library_widget_id'); if (!$widget_id || !is_string($widget_id)) { throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.'); } $mids = array_map(function (MediaInterface $media) { return $media->id(); }, $this->media); // Pass the selection to the field widget based on the current widget ID. return (new AjaxResponse()) ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)])) ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown'])) ->addCommand(new CloseDialogCommand()); } /** * Processes an upload (managed_file) element. * * @param array $element * The upload element. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return array * The processed upload element. */ public function processUploadElement(array $element, FormStateInterface $form_state) { $element['upload_button']['#submit'] = ['::uploadButtonSubmit']; $element['upload_button']['#ajax'] = [ 'callback' => '::updateFormCallback', 'wrapper' => 'media-library-upload-wrapper', ]; return $element; } /** * Validates the upload element. * * @param array $element * The upload element. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return array * The processed upload element. */ public function validateUploadElement(array $element, FormStateInterface $form_state) { if ($form_state->getErrors()) { $element['#value'] = []; } $values = $form_state->getValue('upload', []); if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [ '@count' => $element['#cardinality'], ])); $form_state->setValue('upload', []); $element['#value'] = []; } return $element; } /** * Submit handler for the upload button, inside the managed_file element. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. */ public function uploadButtonSubmit(array $form, FormStateInterface $form_state) { $fids = $form_state->getValue('upload', []); $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids); /** @var \Drupal\file\FileInterface $file */ foreach ($files as $file) { $types = $this->filterTypesThatAcceptFile($file, $this->getTypes()); if (!empty($types)) { if (count($types) === 1) { $this->media[] = $this->createMediaEntity($file, reset($types)); } else { $this->files[] = $file; } } } $form_state->setRebuild(); } /** * Creates a new, unsaved media entity. * * @param \Drupal\file\FileInterface $file * A file for the media source field. * @param \Drupal\media\MediaTypeInterface $type * A media type. * * @return \Drupal\media\MediaInterface * An unsaved media entity. * * @throws \Exception * If a file operation failed when moving the upload. */ protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) { $media = $this->entityTypeManager->getStorage('media')->create([ 'bundle' => $type->id(), 'name' => $file->getFilename(), ]); $source_field = $type->getSource()->getSourceFieldDefinition($type)->getName(); $location = $this->getUploadLocationForType($media->bundle->entity); if (!file_prepare_directory($location, FILE_CREATE_DIRECTORY)) { throw new \Exception("The destination directory '$location' is not writable"); } $file = file_move($file, $location); if (!$file) { throw new \Exception("Unable to move file to '$location'"); } $media->set($source_field, $file->id()); return $media; } /** * AJAX callback for refreshing the entire form. * * @param array $form * The form render array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. * * @return array * The form render array. */ public function updateFormCallback(array &$form, FormStateInterface $form_state) { return $form; } /** * Access callback to check that the user can create file based media. * * @param array $allowed_types * (optional) The contextually allowed types. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. * * @todo Remove $allowed_types param in https://www.drupal.org/node/2956747 */ public function access(array $allowed_types = NULL) { return AccessResultAllowed::allowedIf(count($this->getTypes($allowed_types)))->mergeCacheMaxAge(0); } /** * Returns media types which use files that the current user can create. * * @param array $allowed_types * (optional) The contextually allowed types. * * @todo Move in https://www.drupal.org/node/2987924 * * @return \Drupal\media\MediaTypeInterface[] * A list of media types that are valid for this form. */ protected function getTypes(array $allowed_types = NULL) { // Cache results if possible. if (!isset($this->types)) { $media_type_storage = $this->entityTypeManager->getStorage('media_type'); if (!$allowed_types) { $allowed_types = _media_library_get_allowed_types() ?: NULL; } $types = $media_type_storage->loadMultiple($allowed_types); $types = $this->filterTypesWithFileSource($types); $types = $this->filterTypesWithCreateAccess($types); $this->types = $types; } return $this->types; } /** * Filters media types that accept a given file. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\file\FileInterface $file * A file entity. * @param \Drupal\media\MediaTypeInterface[] $types * An array of available media types. * * @return \Drupal\media\MediaTypeInterface[] * An array of media types that accept the file. */ protected function filterTypesThatAcceptFile(FileInterface $file, array $types) { $types = $this->filterTypesWithFileSource($types); return array_filter($types, function (MediaTypeInterface $type) use ($file) { $validators = $this->getUploadValidatorsForType($type); $errors = file_validate($file, $validators); return empty($errors); }); } /** * Filters an array of media types that accept file sources. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface[] $types * An array of media types. * * @return \Drupal\media\MediaTypeInterface[] * An array of media types that accept file sources. */ protected function filterTypesWithFileSource(array $types) { return array_filter($types, function (MediaTypeInterface $type) { return is_a($type->getSource()->getSourceFieldDefinition($type)->getClass(), FileFieldItemList::class, TRUE); }); } /** * Merges file upload validators for an array of media types. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface[] $types * An array of media types. * * @return array * An array suitable for passing to file_save_upload() or the file field * element's '#upload_validators' property. */ protected function mergeUploadValidators(array $types) { $max_size = 0; $extensions = []; $types = $this->filterTypesWithFileSource($types); foreach ($types as $type) { $validators = $this->getUploadValidatorsForType($type); if (isset($validators['file_validate_size'])) { $max_size = max($max_size, $validators['file_validate_size'][0]); } if (isset($validators['file_validate_extensions'])) { $extensions = array_unique(array_merge($extensions, explode(' ', $validators['file_validate_extensions'][0]))); } } // If no field defines a max size, default to the system wide setting. if ($max_size === 0) { $max_size = file_upload_max_size(); } return [ 'file_validate_extensions' => [implode(' ', $extensions)], 'file_validate_size' => [$max_size], ]; } /** * Gets upload validators for a given media type. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface $type * A media type. * * @return array * An array suitable for passing to file_save_upload() or the file field * element's '#upload_validators' property. */ protected function getUploadValidatorsForType(MediaTypeInterface $type) { return $this->getFileItemForType($type)->getUploadValidators(); } /** * Gets upload destination for a given media type. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface $type * A media type. * * @return string * An unsanitized file directory URI with tokens replaced. */ protected function getUploadLocationForType(MediaTypeInterface $type) { return $this->getFileItemForType($type)->getUploadLocation(); } /** * Creates a file item for a given media type. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface $type * A media type. * * @return \Drupal\file\Plugin\Field\FieldType\FileItem * The file item. */ protected function getFileItemForType(MediaTypeInterface $type) { $source = $type->getSource(); $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($type)); return new FileItem($source_data_definition); } /** * Filters an array of media types that can be created by the current user. * * @todo Move in https://www.drupal.org/node/2987924 * * @param \Drupal\media\MediaTypeInterface[] $types * An array of media types. * * @return \Drupal\media\MediaTypeInterface[] * An array of media types that accept file sources. */ protected function filterTypesWithCreateAccess(array $types) { $access_handler = $this->entityTypeManager->getAccessControlHandler('media'); return array_filter($types, function (MediaTypeInterface $type) use ($access_handler) { return $access_handler->createAccess($type->id()); }); } }