Issue #3059955 by oknate, Hardik_Patel_12, paulocs, phenaproxima, seanB, NikolaAt, smustgrave, bnjmnm, yogeshmpawar, rensingh99, xjm, chr.fritsch, lauriii, raman.b, Wim Leers, nikunj.shah, AndySipple, harshitthakore, saurabh.tripathi.cs, AaronMcHale, irene_dobbs, alexpott, andrewmacpherson, dorficus, effulgentsia, himanshu_sindhwani, codersukanta, pankaj.singh, tripurari, snehalgaikwad, priyanka.sahni: It is possible to overflow the number of items allowed in Media Library

merge-requests/4095/head
Lauri Eskola 2023-06-20 19:48:39 +03:00
parent dd9c718324
commit ae4b7247b8
No known key found for this signature in database
GPG Key ID: 382FC0F5B0DF53F8
7 changed files with 292 additions and 23 deletions

View File

@ -309,6 +309,17 @@
$('.js-media-library-selected-count').html(selectItemsText);
}
function checkEnabled() {
updateSelectionCount(settings.media_library.selection_remaining);
if (
currentSelection.length === settings.media_library.selection_remaining
) {
disableItems($mediaItems.not(':checked'));
enableItems($mediaItems.filter(':checked'));
} else {
enableItems($mediaItems);
}
}
// Update the selection array and the hidden form field when a media item
// is selected.
$(once('media-item-change', $mediaItems)).on('change', (e) => {
@ -345,7 +356,7 @@
item.value = currentSelection.join();
});
});
checkEnabled();
// The hidden selection form field changes when the selection is updated.
$(
once(
@ -353,17 +364,7 @@
$form.find('#media-library-modal-selection'),
),
).on('change', (e) => {
updateSelectionCount(settings.media_library.selection_remaining);
// Prevent users from selecting more items than allowed.
if (
currentSelection.length === settings.media_library.selection_remaining
) {
disableItems($mediaItems.not(':checked'));
enableItems($mediaItems.filter(':checked'));
} else {
enableItems($mediaItems);
}
checkEnabled();
});
// Apply the current selection to the media library view. Changing the

View File

@ -6,6 +6,7 @@ use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\FocusFirstCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
@ -700,11 +701,22 @@ abstract class AddFormBase extends FormBase implements BaseFormIdInterface, Trus
return $media->id();
}, $this->getAddedMediaItems($form_state));
$selected_count = $this->getSelectedMediaItemCount($media_ids, $form_state);
$response = new AjaxResponse();
$response->addCommand(new UpdateSelectionCommand($media_ids));
$media_id_to_focus = array_pop($media_ids);
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
$response->addCommand(new InvokeCommand("#media-library-content [value=$media_id_to_focus]", 'focus'));
$available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots();
if ($available_slots > 0 && $selected_count > $available_slots) {
$warning = $this->formatPlural($selected_count - $available_slots, 'There are currently @total items selected. The maximum number of items for the field is @max. Please remove @count item from the selection.', 'There are currently @total items selected. The maximum number of items for the field is @max. Please remove @count items from the selection.', [
'@total' => $selected_count,
'@max' => $available_slots,
]);
$response->addCommand(new MessageCommand($warning, '#media-library-messages', ['type' => 'warning']));
}
return $response;
}
@ -750,17 +762,44 @@ abstract class AddFormBase extends FormBase implements BaseFormIdInterface, Trus
// The added media items get an ID when they are saved in ::submitForm().
// For that reason the added media items are keyed by delta in the form
// state and we have to do an array map to get each media ID.
$current_media_ids = array_map(function (MediaInterface $media) {
$media_ids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->getCurrentMediaItems($form_state));
// Allow the opener service to respond to the selection.
$state = $this->getMediaLibraryState($form_state);
$selected_count = $this->getSelectedMediaItemCount($media_ids, $form_state);
$available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots();
if ($available_slots > 0 && $selected_count > $available_slots) {
// Return to library where we display a warning about the overage.
return $this->updateLibrary($form, $form_state);
}
return $this->openerResolver->get($state)
->getSelectionResponse($state, $current_media_ids)
->getSelectionResponse($state, $media_ids)
->addCommand(new CloseDialogCommand());
}
/**
* Get the number of selected media.
*
* @param array $media_ids
* Array with the media IDs.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return int
* The number of media currently selected.
*/
private function getSelectedMediaItemCount(array $media_ids, FormStateInterface $form_state): int {
$selected_count = count($media_ids);
if ($current_selection = $form_state->getValue('current_selection')) {
$selected_count += count(explode(',', $current_selection));
}
return $selected_count;
}
/**
* Get the media library state from the form state.
*

View File

@ -165,8 +165,11 @@ class FileUploadForm extends AddFormBase {
// @todo Move validation in https://www.drupal.org/node/2988215
'#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
'#upload_validators' => $item->getUploadValidators(),
'#multiple' => $slots > 1 || $slots === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'#cardinality' => $slots,
'#multiple' => TRUE,
// Do not limit the number uploaded. There is validation based on the
// number selected in the media library that prevents overages.
// @see Drupal\media_library\Form\AddFormBase::updateLibrary()
'#cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'#remaining_slots' => $slots,
];

View File

@ -2,7 +2,9 @@
namespace Drupal\media_library\Plugin\views\field;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\MessageCommand;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
@ -46,6 +48,14 @@ class MediaLibrarySelectForm extends FieldPluginBase {
*/
public function viewsForm(array &$form, FormStateInterface $form_state) {
$form['#attributes']['class'] = ['js-media-library-views-form'];
// Add target for AJAX messages.
$form['media_library_messages'] = [
'#type' => 'container',
'#attributes' => [
'id' => 'media-library-messages',
],
'#weight' => -10,
];
// Add an attribute that identifies the media type displayed in the form.
if (isset($this->view->args[0])) {
@ -125,6 +135,19 @@ class MediaLibrarySelectForm extends FieldPluginBase {
// Allow the opener service to handle the selection.
$state = MediaLibraryState::fromRequest($request);
$current_selection = $form_state->getValue('media_library_select_form_selection');
$available_slots = $state->getAvailableSlots();
$selected_count = count(explode(',', $current_selection));
if ($available_slots > 0 && $selected_count > $available_slots) {
$response = new AjaxResponse();
$error = \Drupal::translation()->formatPlural($selected_count - $available_slots, 'There are currently @total items selected. The maximum number of items for the field is @max. Please remove @count item from the selection.', 'There are currently @total items selected. The maximum number of items for the field is @max. Please remove @count items from the selection.', [
'@total' => $selected_count,
'@max' => $available_slots,
]);
$response->addCommand(new MessageCommand($error, '#media-library-messages', ['type' => 'error']));
return $response;
}
return \Drupal::service('media_library.opener_resolver')
->get($state)
->getSelectionResponse($state, $selected_ids)

View File

@ -354,17 +354,21 @@ abstract class MediaLibraryTestBase extends WebDriverTestBase {
* @param string $expected_announcement
* (optional) The expected screen reader announcement once the modal is
* closed.
* @param bool $should_close
* (optional) TRUE if we expect the modal to be successfully closed.
*
* @todo Consider requiring screen reader assertion every time "Insert
* selected" is pressed in
* https://www.drupal.org/project/drupal/issues/3087227.
*/
protected function pressInsertSelected($expected_announcement = NULL) {
protected function pressInsertSelected($expected_announcement = NULL, bool $should_close = TRUE) {
$this->assertSession()
->elementExists('css', '.ui-dialog-buttonpane')
->pressButton('Insert selected');
$this->waitForNoText('Add or select media');
if ($should_close) {
$this->waitForNoText('Add or select media');
}
if ($expected_announcement) {
$this->waitForText($expected_announcement);
}
@ -400,6 +404,18 @@ abstract class MediaLibraryTestBase extends WebDriverTestBase {
}
}
/**
* De-selects an item in the media library modal.
*
* @param int $index
* The zero-based index of the item to unselect.
*/
protected function deselectMediaItem(int $index): void {
$checkboxes = $this->getCheckboxes();
$this->assertGreaterThan($index, count($checkboxes));
$checkboxes[$index]->uncheck();
}
/**
* Switches to the grid display of the widget view.
*/

View File

@ -0,0 +1,183 @@
<?php
namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests that uploads in the 'media_library_widget' works as expected.
*
* @group media_library
*
* @todo This test will occasionally fail with SQLite until
* https://www.drupal.org/node/3066447 is addressed.
*/
class WidgetOverflowTest extends MediaLibraryTestBase {
use TestFileCreationTrait;
/**
* The test image file.
*
* @var \Drupal\file\Entity\File
*/
protected $image;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
foreach ($this->getTestFiles('image') as $image) {
$extension = pathinfo($image->filename, PATHINFO_EXTENSION);
if ($extension === 'png') {
$this->image = $image;
}
}
if (!isset($this->image)) {
$this->fail('Expected test files not present.');
}
// Create a user that can only add media of type four.
$user = $this->drupalCreateUser([
'access administration pages',
'access content',
'create basic_page content',
'create type_one media',
'create type_three media',
'view media',
]);
$this->drupalLogin($user);
}
/**
* Uploads multiple test images into the media library.
*
* @param int $number
* The number of images to upload.
*/
private function uploadFiles(int $number): void {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = $this->container->get('file_system');
// Create a list of new files to upload.
$filenames = $remote_paths = [];
for ($i = 0; $i < $number; $i++) {
$path = $file_system->copy($this->image->uri, 'public://');
$path = $file_system->realpath($path);
$this->assertNotEmpty($path);
$this->assertFileExists($path);
$filenames[] = $file_system->basename($path);
$remote_paths[] = $this->getSession()
->getDriver()
->uploadFileAndGetRemoteFilePath($path);
}
$page = $this->getSession()->getPage();
$page->fillField('Add files', implode("\n", $remote_paths));
$this->assertMediaAdded();
$assert_session = $this->assertSession();
foreach ($filenames as $i => $filename) {
$assert_session->fieldValueEquals("media[$i][fields][name][0][value]", $filename);
$page->fillField("media[$i][fields][field_media_test_image][0][alt]", $filename);
}
}
/**
* Tests that the Media Library constrains the number of selected items.
*
* @param string|null $selected_operation
* The operation of the button to click. For example, if this is "insert",
* the "Save and insert" button will be pressed. If NULL, the "Save" button
* will be pressed.
*
* @dataProvider providerWidgetOverflow
*/
public function testWidgetOverflow(?string $selected_operation): void {
// If we want to press the "Save and insert" or "Save and select" buttons,
// we need to enable the advanced UI.
if ($selected_operation) {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
}
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet('node/add/basic_page');
// Upload 5 files into a media field that only allows 2.
$this->openMediaLibraryForField('field_twin_media');
$this->uploadFiles(5);
// Save the media items and ensure that the user is warned that they have
// selected too many items.
if ($selected_operation) {
$this->saveAnd($selected_operation);
}
else {
$this->pressSaveButton();
}
$this->waitForElementTextContains('.messages--warning', 'There are currently 5 items selected. The maximum number of items for the field is 2. Please remove 3 items from the selection.');
// If the user tries to insert the selected items anyway, they should get
// an error.
$this->pressInsertSelected(NULL, FALSE);
$this->waitForElementTextContains('.messages--error', 'There are currently 5 items selected. The maximum number of items for the field is 2. Please remove 3 items from the selection.');
$assert_session->elementNotExists('css', '.messages--warning');
// Once the extra items are deselected, all should be well.
$this->deselectMediaItem(2);
$this->deselectMediaItem(3);
$this->deselectMediaItem(4);
$this->pressInsertSelected('Added 2 media items.');
}
/**
* Tests that unlimited fields' selection count is not constrained.
*
* @param string|null $selected_operation
* The operation of the button to click. For example, if this is "insert",
* the "Save and insert" button will be pressed. If NULL, the "Save" button
* will be pressed.
*
* @dataProvider providerWidgetOverflow
*/
public function testUnlimitedCardinality(?string $selected_operation): void {
if ($selected_operation) {
$this->config('media_library.settings')->set('advanced_ui', TRUE)->save();
}
$assert_session = $this->assertSession();
// Visit a node create page and open the media library.
$this->drupalGet('node/add/basic_page');
$this->openMediaLibraryForField('field_unlimited_media');
$this->switchToMediaType('Three');
$this->uploadFiles(5);
if ($selected_operation) {
$this->saveAnd($selected_operation);
}
else {
$this->pressSaveButton();
}
if ($selected_operation !== 'insert') {
$this->pressInsertSelected();
}
// There should not be any warnings or errors.
$assert_session->elementNotExists('css', '.messages--error');
$assert_session->elementNotExists('css', '.messages--warning');
$this->waitForText('Added 5 media items.');
}
/**
* Data provider for ::testWidgetOverflow() and ::testUnlimitedCardinality().
*
* @return array[]
* Sets of arguments to pass to the test method.
*/
public function providerWidgetOverflow(): array {
return [
'Save' => [NULL],
'Save and insert' => ['insert'],
'Save and select' => ['select'],
];
}
}

View File

@ -6,7 +6,7 @@ use Drupal\media\Entity\Media;
use Drupal\Tests\TestFileCreationTrait;
/**
* Tests that uploads in the Media library's widget works as expected.
* Tests that uploads in the 'media_library_widget' works as expected.
*
* @group media_library
*
@ -23,7 +23,7 @@ class WidgetUploadTest extends MediaLibraryTestBase {
protected $defaultTheme = 'stark';
/**
* Tests that uploads in the Media library's widget works as expected.
* Tests that uploads in the 'media_library_widget' works as expected.
*/
public function testWidgetUpload() {
$assert_session = $this->assertSession();
@ -204,7 +204,8 @@ class WidgetUploadTest extends MediaLibraryTestBase {
// Assert we can now only upload one more media item.
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Four');
$this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
// Despite the 'One file only' text, we don't limit the number of uploads.
$this->assertTrue($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
$assert_session->pageTextContains('One file only.');
// Assert media type four should only allow jpg files by trying a png file
@ -547,7 +548,8 @@ class WidgetUploadTest extends MediaLibraryTestBase {
// Assert we can now only upload one more media item.
$this->openMediaLibraryForField('field_twin_media');
$this->switchToMediaType('Four');
$this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
// Despite the 'One file only' text, we don't limit the number of uploads.
$this->assertTrue($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
$assert_session->pageTextContains('One file only.');
// Assert media type four should only allow jpg files by trying a png file
@ -618,7 +620,9 @@ class WidgetUploadTest extends MediaLibraryTestBase {
$selection_area = $this->getSelectionArea();
$assert_session->checkboxChecked("Select $existing_media_name", $selection_area);
$selection_area->uncheckField("Select $existing_media_name");
$assert_session->hiddenFieldValueEquals('current_selection', '');
$page->waitFor(10, function () use ($page) {
return $page->find('hidden_field_selector', ['hidden_field', 'current_selection'])->getValue() === '';
});
// Close the details element so that clicking the Save and select works.
// @todo Fix dialog or test so this is not necessary to prevent random
// fails. https://www.drupal.org/project/drupal/issues/3055648