diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php new file mode 100644 index 0000000000..7e36a33dcd --- /dev/null +++ b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php @@ -0,0 +1,51 @@ + '70%', + ]; + + /** + * OpenDialogUrlCommand constructor. + * + * @param \Drupal\Core\Url|string $url + * The URL to open in the modal dialog. + * @param array $dialog_options + * (optional) Additional client-side options for the modal dialog. + */ + public function __construct($url, array $dialog_options = []) { + $this->url = $url instanceof Url ? $url->toString() : $url; + if ($dialog_options) { + $this->dialogOptions = $dialog_options; + } + } + + /** + * {@inheritdoc} + */ + public function render() { + return [ + 'command' => 'openDialogUrl', + 'url' => $this->url, + 'dialogOptions' => $this->dialogOptions, + ]; + } + +} diff --git a/core/misc/dialog/dialog.ajax.es6.js b/core/misc/dialog/dialog.ajax.es6.js index 798673a1d8..6c8b44a809 100644 --- a/core/misc/dialog/dialog.ajax.es6.js +++ b/core/misc/dialog/dialog.ajax.es6.js @@ -95,6 +95,27 @@ }; /** + * Command to open an arbitrary URL in a modal dialog. + * + * @param {Drupal.Ajax} ajax + * The Drupal Ajax object. + * @param {object} response + * Object holding the server response. + * @param {number} [status] + * The HTTP status code. + */ + Drupal.AjaxCommands.prototype.openDialogUrl = function (ajax, response, status) { + new Drupal.Ajax(null, null, { + url: response.url, + dialogType: 'modal', + dialog: response.dialogOptions, + progress: { + type: 'throbber' + } + }).execute(); + }; + + /** * Command to open a dialog. * * @param {Drupal.Ajax} ajax diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js index 8277df3e51..15e11f39c4 100644 --- a/core/misc/dialog/dialog.ajax.js +++ b/core/misc/dialog/dialog.ajax.js @@ -59,6 +59,17 @@ } }; + Drupal.AjaxCommands.prototype.openDialogUrl = function (ajax, response, status) { + new Drupal.Ajax(null, null, { + url: response.url, + dialogType: 'modal', + dialog: response.dialogOptions, + progress: { + type: 'throbber' + } + }).execute(); + }; + Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) { if (!response.selector) { return false; diff --git a/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml new file mode 100644 index 0000000000..e2ed5cefbd --- /dev/null +++ b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml @@ -0,0 +1,34 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_form_mode.media.add_file + - field.field.media.file.field_media_file + - media.type.file + module: + - file +id: media.file.add_file +targetEntityType: media +bundle: file +mode: add_file +content: + field_media_file: + settings: + progress_indicator: throbber + third_party_settings: { } + type: file_generic + weight: 1 + region: content + name: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } +hidden: + created: true + path: true + status: true + uid: true diff --git a/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml new file mode 100644 index 0000000000..75be602501 --- /dev/null +++ b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml @@ -0,0 +1,36 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_form_mode.media.add_file + - field.field.media.image.field_media_image + - image.style.thumbnail + - media.type.image + module: + - image +id: media.image.add_file +targetEntityType: media +bundle: image +mode: add_file +content: + field_media_image: + settings: + progress_indicator: throbber + preview_image_style: thumbnail + third_party_settings: { } + type: image_image + weight: 1 + region: content + name: + type: string_textfield + weight: 0 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } +hidden: + created: true + path: true + status: true + uid: true diff --git a/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml new file mode 100644 index 0000000000..ff7eccdeb5 --- /dev/null +++ b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.add_file +label: 'File Upload' +targetEntityType: media +cache: true diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml index dad518985a..1176376da8 100644 --- a/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -40,6 +40,14 @@ field.formatter.settings.media_thumbnail: type: field.formatter.settings.image label: 'Media thumbnail field display format settings' +field.widget.settings.media_file: + type: mapping + label: 'File widget settings' + mapping: + open: + type: boolean + label: 'Open by default' + media.source.*: type: mapping label: 'Media source settings' diff --git a/core/modules/media/media.module b/core/modules/media/media.module index dc2d898e3c..f89ae4a730 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -6,12 +6,15 @@ */ use Drupal\Core\Access\AccessResult; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\field\FieldConfigInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; +use Drupal\file\FileInterface; +use Drupal\media\Form\AddFileForm; /** * Implements hook_help(). @@ -96,3 +99,39 @@ function template_preprocess_media(array &$variables) { $variables['content'][$key] = $variables['elements'][$key]; } } + +/** + * Validate uploaded file for media field. + * + * @param \Drupal\file\FileInterface $file + * File that should be validated. + * + * @return array + * Result of validation. + */ +function media_validate_file_upload(FileInterface $file) { + $media_types = \Drupal::service('media.type_guesser.file')->getTypesForFile($file); + + if (empty($media_types)) { + return [ + t('%filename could not be matched to any media types.', [ + '%filename' => $file->getFilename(), + ]), + ]; + } + return []; +} + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(). + */ +function media_field_widget_file_generic_form_alter(array &$element, FormStateInterface $form_state, array $context) { + AddFileForm::alterFileWidget($element, $form_state); +} + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(). + */ +function media_field_widget_image_image_form_alter(array &$element, FormStateInterface $form_state, array $context) { + AddFileForm::alterFileWidget($element, $form_state); +} diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml index 9fbadeff29..fb7a582f4c 100644 --- a/core/modules/media/media.routing.yml +++ b/core/modules/media/media.routing.yml @@ -19,3 +19,16 @@ entity.media.revision: requirements: _access_media_revision: 'view' media: \d+ + +entity.media.add_file: + path: '/media/add-file/{media_type}/{file}/{selector}' + defaults: + _entity_form: 'media.add_file' + options: + parameters: + media_type: + type: 'entity:media_type' + file: + type: 'entity:file' + requirements: + _entity_create_access: 'media' diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml index f22f90a124..7896a10ce1 100644 --- a/core/modules/media/media.services.yml +++ b/core/modules/media/media.services.yml @@ -8,3 +8,8 @@ services: arguments: ['@entity_type.manager'] tags: - { name: access_check, applies_to: _access_media_revision } + + media.type_guesser.file: + class: Drupal\media\FileGuesser + # TODO: Add a cache backend. + arguments: ['@entity_type.manager'] diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index f8f8d6bdd4..8e11abdc11 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -38,6 +38,7 @@ * "add" = "Drupal\media\MediaForm", * "edit" = "Drupal\media\MediaForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", + * "add_file" = "Drupal\media\Form\AddFileForm", * }, * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\media\MediaViewsData", diff --git a/core/modules/media/src/FileGuesser.php b/core/modules/media/src/FileGuesser.php new file mode 100644 index 0000000000..9a23fe5b35 --- /dev/null +++ b/core/modules/media/src/FileGuesser.php @@ -0,0 +1,153 @@ +entityTypeManager = $entity_type_manager; + $this->cacheBackend = $cache_backend; + $this->useCaches = isset($cache_backend); + } + + /** + * {@inheritdoc} + */ + public function guessForFile($file, array $limit = []) { + $media_types = $this->getTypesForFile($file, $limit); + return key($media_types); + } + + /** + * {@inheritdoc} + */ + public function guessForExtension($extension, array $limit = []) { + $media_types = $this->getTypesByExtension($extension, $limit); + return key($media_types); + } + + /** + * {@inheritdoc} + */ + public function getTypesForFile($file, array $limit = []) { + if (is_numeric($file)) { + $file = $this->entityTypeManager->getStorage('file')->load($file); + } + + if ($file instanceof FileInterface) { + $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION); + + return $this->getTypesByExtension($extension, $limit); + } + else { + throw new \InvalidArgumentException('Expected file ID or entity'); + } + } + + /** + * {@inheritdoc} + */ + public function getTypesByExtension($extension, array $limit = []) { + $cache = $this->cacheGet($extension); + if ($cache) { + return $cache->data; + } + + $media_types = $this->getAllowedMediaTypes(); + if ($limit) { + $media_types = array_intersect_key($media_types, array_flip($limit)); + } + + $filter = function (MediaTypeInterface $media_type) { + $item_class = $media_type + ->getSource() + ->getSourceFieldDefinition($media_type) + ->getItemDefinition() + ->getClass(); + + return is_a($item_class, FileItem::class, TRUE); + }; + $media_types = array_filter($media_types, $filter); + + $extension = Unicode::strtolower($extension); + + $matches = []; + /** @var \Drupal\media\MediaTypeInterface $media_type */ + foreach ($media_types as $id => $media_type) { + $extensions = $media_type + ->getSource() + ->getSourceFieldDefinition($media_type) + ->getSetting('file_extensions'); + + $allowed_extensions = preg_split('/\s+/', Unicode::strtolower($extensions)); + + if (in_array($extension, $allowed_extensions)) { + $matches[$id] = $media_type->label(); + } + } + $this->cacheSet($extension, $matches); + + return $matches; + } + + /** + * Returns all media types that can be created by the current user. + * + * @return \Drupal\media\MediaTypeInterface[] + * The media types. + */ + protected function getAllowedMediaTypes() { + if (isset($this->allowedTypes)) { + return $this->allowedTypes; + } + + $storage = $this->entityTypeManager->getStorage('media_type'); + + $media_types = array_filter( + $storage->getQuery()->execute(), + [ + $this->entityTypeManager->getAccessControlHandler('media'), + 'createAccess', + ] + ); + $this->allowedTypes = $storage->loadMultiple($media_types); + + return $this->allowedTypes; + } + +} diff --git a/core/modules/media/src/FileGuesserInterface.php b/core/modules/media/src/FileGuesserInterface.php new file mode 100644 index 0000000000..2cbddee69b --- /dev/null +++ b/core/modules/media/src/FileGuesserInterface.php @@ -0,0 +1,69 @@ +fileGuesser = $file_guesser; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('media.type_guesser.file'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time') + ); + } + + /** + * AJAX callback associated with the 'media type' input selector. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The forms 'fields' array. + */ + public static function onTypeSelect(array &$form, FormStateInterface $form_state) { + return $form['fields']; + } + + /** + * {@inheritdoc} + */ + protected function init(FormStateInterface $form_state) { + if ($form_state->hasValue('bundle')) { + $entity = $this->entityTypeManager + ->getStorage('media') + ->create([ + 'bundle' => $form_state->getValue('bundle'), + ]); + + $this->setEntity($entity); + } + return parent::init($form_state); + } + + /** + * {@inheritdoc} + */ + protected function prepareEntity() { + parent::prepareEntity(); + + /** @var \Drupal\media\MediaInterface $entity */ + $entity = $this->getEntity(); + + $source_field = $entity->getSource() + ->getSourceFieldDefinition($entity->bundle->entity) + ->getName(); + + /** @var \Drupal\file\FileInterface $file */ + $file = $this->getRouteMatch()->getParameter('file'); + $entity->set($source_field, $file); + + // Move the file to the location specified by the source field. + /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $item */ + $item = $entity->get($source_field)->first(); + $destination = $item->getUploadLocation(); + file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $destination .= '/' . $file->getFilename(); + if (!file_exists($destination)) { + // TODO: Use an injected file_system service for this. + $destination = file_unmanaged_move($file->getFileUri(), $destination); + } + $file->setFileUri($destination); + $file->save(); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $entity = parent::validateForm($form, $form_state); + + // If the bundle has changed, reinitialize the entity form. This will call + // init(), which will replace the current entity with a new one of the + // selected type. + $chosen_bundle = $form_state->getValue('bundle'); + if ($chosen_bundle && $chosen_bundle !== $this->getEntity()->bundle()) { + $storage = &$form_state->getStorage(); + unset($storage['entity_form_initialized']); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form['fields'] = parent::form($form, $form_state); + $form['fields']['#prefix'] = '