diff --git a/core/modules/media_library/config/optional/core.entity_form_display.media.remote_video.media_library.yml b/core/modules/media_library/config/optional/core.entity_form_display.media.remote_video.media_library.yml new file mode 100644 index 0000000000..6a1461cded --- /dev/null +++ b/core/modules/media_library/config/optional/core.entity_form_display.media.remote_video.media_library.yml @@ -0,0 +1,19 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_form_mode.media.media_library + - field.field.media.remote_video.field_media_oembed_video + - media.type.remote_video +id: media.remote_video.media_library +targetEntityType: media +bundle: remote_video +mode: media_library +content: { } +hidden: + created: true + field_media_oembed_video: true + name: true + path: true + status: true + uid: true diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css index e5ca9ab6d5..1821337ccb 100644 --- a/core/modules/media_library/css/media_library.theme.css +++ b/core/modules/media_library/css/media_library.theme.css @@ -83,19 +83,50 @@ border-left: 0; } +/* Generic media add form styles. */ .media-library-add-form--without-input { - margin-bottom: 1em; - border-bottom: 1px solid #c0c0c0; + padding: 16px; + border: 1px solid #bfbfbf; + border-radius: 2px; + background: #fcfcfa; } .media-library-add-form--without-input .form-item { margin: 0 0 1em; } +/* Style the media add upload form. */ +.media-library-add-form-upload.media-library-add-form--without-input .form-item-upload { + margin-bottom: 0; +} + .media-library-add-form .file-upload-help { margin: 8px 0 0; } +/* Style the media add oEmbed form. */ +.media-library-add-form-oembed-wrapper { + display: flex; +} +@media screen and (max-width: 37.5em) { + .media-library-add-form-oembed-wrapper { + display: block; + } +} + +.media-library-add-form-oembed.media-library-add-form--without-input .form-item-url { + margin-bottom: 0; +} + +.media-library-add-form-oembed-url { + width: 100%; +} + +/* We need to make our CSS more specific than .button. */ +.media-library-add-form-oembed-submit.button { + align-self: center; +} + .media-library-views-form__header .form-item { margin-right: 8px; } @@ -136,6 +167,7 @@ .media-library-item .field--name-thumbnail img { height: 180px; + object-fit: contain; object-position: center center; } @@ -303,8 +335,8 @@ /* Do not show the bottom border and padding for the last item. */ .media-library-add-form__media:last-child { - border-bottom: 0; padding-bottom: 0; + border-bottom: 0; } /* @todo Remove in https://www.drupal.org/project/drupal/issues/2987921 */ diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 6533258899..ac4db0a4d6 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -21,6 +21,7 @@ use Drupal\media\MediaTypeForm; use Drupal\media\MediaTypeInterface; use Drupal\media_library\Form\FileUploadForm; +use Drupal\media_library\Form\OEmbedForm; use Drupal\media_library\MediaLibraryState; use Drupal\views\Form\ViewsForm; use Drupal\views\Plugin\views\cache\CachePluginBase; @@ -48,6 +49,7 @@ function media_library_media_source_info_alter(array &$sources) { $sources['file']['forms']['media_library_add'] = FileUploadForm::class; $sources['image']['forms']['media_library_add'] = FileUploadForm::class; $sources['video_file']['forms']['media_library_add'] = FileUploadForm::class; + $sources['oembed:video']['forms']['media_library_add'] = OEmbedForm::class; } /** diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php index da783d36ab..2f078af9cb 100644 --- a/core/modules/media_library/src/Form/AddFormBase.php +++ b/core/modules/media_library/src/Form/AddFormBase.php @@ -308,10 +308,12 @@ protected function processInputValues(array $source_field_values, array $form, F * An unsaved media entity. */ protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $source_field_value) { - return $media_storage->create([ + $media = $media_storage->create([ 'bundle' => $media_type->id(), $source_field_name => $source_field_value, ]); + $media->setName($media->getName()); + return $media; } /** diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php index e89b67e7d1..eeeb24d23b 100644 --- a/core/modules/media_library/src/Form/FileUploadForm.php +++ b/core/modules/media_library/src/Form/FileUploadForm.php @@ -217,7 +217,7 @@ protected function createMediaFromValue(MediaTypeInterface $media_type, EntitySt throw new \RuntimeException("Unable to move file to '$upload_location'"); } - return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file)->setName($file->getFilename()); + return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file); } /** diff --git a/core/modules/media_library/src/Form/OEmbedForm.php b/core/modules/media_library/src/Form/OEmbedForm.php new file mode 100644 index 0000000000..26ab3c6600 --- /dev/null +++ b/core/modules/media_library/src/Form/OEmbedForm.php @@ -0,0 +1,165 @@ +urlResolver = $url_resolver; + $this->resourceFetcher = $resource_fetcher; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('media_library.ui_builder'), + $container->get('media.oembed.url_resolver'), + $container->get('media.oembed.resource_fetcher') + ); + } + + /** + * {@inheritdoc} + */ + protected function getMediaType(FormStateInterface $form_state) { + if ($this->mediaType) { + return $this->mediaType; + } + + $media_type = parent::getMediaType($form_state); + if (!$media_type->getSource() instanceof OEmbedInterface) { + throw new \InvalidArgumentException('Can only add media types which use an oEmbed source plugin.'); + } + return $media_type; + } + + /** + * {@inheritdoc} + */ + protected function buildInputElement(array $form, FormStateInterface $form_state) { + $form['#attributes']['class'][] = 'media-library-add-form-oembed'; + + $media_type = $this->getMediaType($form_state); + $providers = $media_type->getSource()->getProviders(); + + $form['container'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['media-library-add-form-oembed-wrapper'] + ], + ]; + + $form['container']['url'] = [ + '#type' => 'url', + '#title' => $this->t('Add @type via URL', [ + '@type' => $this->getMediaType($form_state)->label(), + ]), + '#description' => $this->t('Allowed providers: @providers.', [ + '@providers' => implode(', ', $providers), + ]), + '#required' => TRUE, + '#attributes' => [ + 'placeholder' => 'https://', + 'class' => ['media-library-add-form-oembed-url'] + ], + ]; + + $form['container']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Add'), + '#button_type' => 'primary', + '#validate' => ['::validateUrl'], + '#submit' => ['::addButtonSubmit'], + // @todo Move validation in https://www.drupal.org/node/2988215 + '#ajax' => [ + 'callback' => '::updateFormCallback', + 'wrapper' => 'media-library-wrapper', + ], + '#attributes' => [ + 'class' => ['media-library-add-form-oembed-submit'] + ], + ]; + return $form; + } + + /** + * Validates the oEmbed URL. + * + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + */ + public function validateUrl(array &$form, FormStateInterface $form_state) { + $url = $form_state->getValue('url'); + if ($url) { + try { + $resource_url = $this->urlResolver->getResourceUrl($url); + $this->resourceFetcher->fetchResource($resource_url); + } + catch (ResourceException $e) { + $form_state->setErrorByName('url', $e->getMessage()); + } + } + } + + /** + * Submit handler for the add button. + * + * @param array $form + * The form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function addButtonSubmit(array $form, FormStateInterface $form_state) { + $this->processInputValues([$form_state->getValue('url')], $form, $form_state); + } + +} diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.default.yml new file mode 100644 index 0000000000..8deaa648b7 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.default.yml @@ -0,0 +1,58 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_five.field_media_test_oembed_video + - media.type.type_five + module: + - path +id: media.type_five.default +targetEntityType: media +bundle: type_five +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_media_test_oembed_video: + type: oembed_textfield + weight: 0 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + region: content + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + path: + type: path + weight: 30 + region: content + settings: { } + third_party_settings: { } + status: + type: boolean_checkbox + settings: + display_label: true + weight: 100 + region: content + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.media_library.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.media_library.yml new file mode 100644 index 0000000000..4a41a6179b --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_five.media_library.yml @@ -0,0 +1,33 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_form_mode.media.media_library + - field.field.media.type_five.field_media_test_oembed_video + - media.type.type_five +id: media.type_five.media_library +targetEntityType: media +bundle: type_five +mode: media_library +content: + field_media_test_oembed_video: + weight: 1 + settings: + size: 60 + placeholder: '' + third_party_settings: { } + type: oembed_textfield + 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_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_five.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_five.default.yml new file mode 100644 index 0000000000..fe83091a08 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_five.default.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_five.field_media_test_oembed_video + - media.type.type_five + module: + - media +id: media.type_five.default +targetEntityType: media +bundle: type_five +mode: default +content: + created: + label: hidden + type: timestamp + weight: 0 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_media_test_oembed_video: + weight: 6 + label: hidden + settings: + max_width: 0 + max_height: 0 + third_party_settings: { } + type: oembed + region: content + thumbnail: + type: image + weight: 5 + label: hidden + settings: + image_style: thumbnail + image_link: '' + region: content + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: + name: true diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_five.field_media_test_oembed_video.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_five.field_media_test_oembed_video.yml new file mode 100644 index 0000000000..161c8c4eb5 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_five.field_media_test_oembed_video.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_test_oembed_video + - media.type.type_five +id: media.type_five.field_media_test_oembed_video +field_name: field_media_test_oembed_video +entity_type: media +bundle: type_five +label: 'Video URL' +description: '' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml index 69c0fc43f8..e0f4f7ff4a 100644 --- a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml @@ -21,6 +21,7 @@ settings: target_bundles: type_one: type_one type_three: type_three + type_five: type_five sort: field: _none auto_create: false diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_oembed_video.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_oembed_video.yml new file mode 100644 index 0000000000..9dcb13cfc1 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_oembed_video.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.field_media_test_oembed_video +field_name: field_media_test_oembed_video +entity_type: media +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_five.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_five.yml new file mode 100644 index 0000000000..6a58c9897c --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_five.yml @@ -0,0 +1,16 @@ +langcode: en +status: true +dependencies: { } +id: type_five +label: 'Type Five' +description: '' +source: 'oembed:video' +queue_thumbnail_downloads: false +new_revision: false +source_configuration: + thumbnails_directory: 'public://oembed_thumbnails' + providers: + - YouTube + - Vimeo + source_field: field_media_test_oembed_video +field_map: { } diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php index 34245daec4..c60ed2a54e 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php @@ -5,6 +5,8 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; use Drupal\media\Entity\Media; use Drupal\media_library\MediaLibraryState; +use Drupal\media_test_oembed\Controller\ResourceController; +use Drupal\Tests\media\Traits\OEmbedTestTrait; use Drupal\Tests\TestFileCreationTrait; use Drupal\user\Entity\Role; use Drupal\user\RoleInterface; @@ -17,6 +19,7 @@ class MediaLibraryTest extends WebDriverTestBase { use TestFileCreationTrait; + use OEmbedTestTrait; /** * {@inheritdoc} @@ -28,6 +31,7 @@ class MediaLibraryTest extends WebDriverTestBase { */ protected function setUp() { parent::setUp(); + $this->lockHttpClientToFixtures(); // Create a few example media items for use in selection. $media = [ @@ -752,4 +756,101 @@ public function testWidgetUpload() { $assert_session->pageTextContains($jpg_image->filename); } + /** + * Tests that oEmbed media can be added in the Media library's widget. + */ + public function testWidgetOEmbed() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $video_title = "Everyday I'm Drupalin' Drupal Rap (Rick Ross - Hustlin)"; + $video_url = 'https://www.youtube.com/watch?v=PWjcqE3QKBg'; + ResourceController::setResourceUrl($video_url, $this->getFixturesDirectory() . '/video_youtube.json'); + + // Visit a node create page. + $this->drupalGet('node/add/basic_page'); + + // Add to the unlimited media field. + $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Media library'); + + // Assert the default tab for media type one does not have an oEmbed form. + $assert_session->fieldNotExists('Add Type Five via URL'); + + // Assert other media types don't have the oEmbed form fields. + $page->clickLink('Type Three'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldNotExists('Add Type Five via URL'); + + // Assert we can add an oEmbed video to media type five. + $page->clickLink('Type Five'); + $assert_session->assertWaitOnAjaxRequest(); + $page->fillField('Add Type Five via URL', $video_url); + $assert_session->pageTextContains('Allowed providers: YouTube, Vimeo.'); + $page->pressButton('Add'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save'); + $assert_session->assertWaitOnAjaxRequest(); + + // Load the created media item. + $media_storage = $this->container->get('entity_type.manager')->getStorage('media'); + $media_items = $media_storage->loadMultiple(); + $added_media = array_pop($media_items); + + // Ensure the media item was saved to the library and automatically + // selected. The added media items should be in the first position of the + // add form. + $assert_session->pageTextContains('Media library'); + $assert_session->pageTextContains($video_title); + $assert_session->fieldValueEquals('media_library_select_form[0]', $added_media->id()); + $assert_session->checkboxChecked('media_library_select_form[0]'); + + // Assert the created oEmbed video is correctly added to the widget. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains('Media library'); + $assert_session->pageTextContains($video_title); + + // Open the media library again for the unlimited field and go to the tab + // for media type five. + $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Media library'); + $page->clickLink('Type Five'); + $assert_session->assertWaitOnAjaxRequest(); + + // Assert the video is available on the tab. + $assert_session->pageTextContains($video_title); + + // Assert we can only add supported URLs. + $page->fillField('Add Type Five via URL', 'https://www.youtube.com/'); + $page->pressButton('Add'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('No matching provider found.'); + // Assert we can not add a video ID that doesn't exist. We need to use a + // video ID that will not be filtered by the regex, because otherwise the + // message 'No matching provider found.' will be returned. + $page->fillField('Add Type Five via URL', 'https://www.youtube.com/watch?v=PWjcqE3QKBg1'); + $page->pressButton('Add'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Could not retrieve the oEmbed resource.'); + + // Assert we can add a oEmbed video with a custom name. + $page->fillField('Add Type Five via URL', $video_url); + $page->pressButton('Add'); + $assert_session->assertWaitOnAjaxRequest(); + $page->fillField('Name', 'Custom video title'); + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Media library'); + $assert_session->pageTextContains('Custom video title'); + + // Assert the created oEmbed video is correctly added to the widget. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains('Media library'); + $assert_session->pageTextContains('Custom video title'); + } + } diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php index 78d12f50ff..0d180bb880 100644 --- a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php +++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Form\FormState; use Drupal\KernelTests\KernelTestBase; use Drupal\media_library\Form\FileUploadForm; +use Drupal\media_library\Form\OEmbedForm; use Drupal\media_library\MediaLibraryState; use Drupal\Tests\media\Traits\MediaTypeCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait; @@ -72,20 +73,46 @@ public function testMediaTypeAddForm() { // Assert the form class is added to the media source. $this->assertSame(FileUploadForm::class, $image_source_definition['forms']['media_library_add']); - $this->assertArrayNotHasKey('media_library_add', $remote_video_source_definition['forms']); + $this->assertSame(OEmbedForm::class, $remote_video_source_definition['forms']['media_library_add']); // Assert the media library UI does not contains the add form when the user // does not have access. - $state = MediaLibraryState::create('test', ['image', 'remote_video'], 'image', -1); - $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state); - $this->assertEmpty($library_ui['content']['form']); + $this->assertEmpty($this->buildLibraryUi('image')['content']['form']); + $this->assertEmpty($this->buildLibraryUi('remote_video')['content']['form']); - // Create a user that has access to the media add form. + // Create a user that has access to create the image media type but not the + // remote video media type. $this->setCurrentUser($this->createUser([ 'create image media', ])); - $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state); - $this->assertSame('managed_file', $library_ui['content']['form']['upload']['#type']); + // Assert the media library UI only contains the add form for the image + // media type. + $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['upload']['#type']); + $this->assertEmpty($this->buildLibraryUi('remote_video')['content']['form']); + + // Create a user that has access to create both media types. + $this->setCurrentUser($this->createUser([ + 'create image media', + 'create remote_video media', + ])); + // Assert the media library UI only contains the add form for both media + // types. + $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['upload']['#type']); + $this->assertSame('url', $this->buildLibraryUi('remote_video')['content']['form']['container']['url']['#type']); + } + + /** + * Build the media library UI for a selected type. + * + * @param string $selected_type_id + * The selected media type ID. + * + * @return array + * The render array for the media library. + */ + protected function buildLibraryUi($selected_type_id) { + $state = MediaLibraryState::create('test', ['image', 'remote_video'], $selected_type_id, -1); + return \Drupal::service('media_library.ui_builder')->buildUi($state); } /**