diff --git a/core/modules/image/image.post_update.php b/core/modules/image/image.post_update.php index 1909fdbabd..b9d3529626 100644 --- a/core/modules/image/image.post_update.php +++ b/core/modules/image/image.post_update.php @@ -14,3 +14,10 @@ function image_removed_post_updates() { 'image_post_update_scale_and_crop_effect_add_anchor' => '9.0.0', ]; } + +/** + * Fix problem with image dimensions when using multiple upload. + */ +function image_post_update_multiple_upload_fix_with_dimensions() { + \Drupal::messenger()->addMessage(t('Fixed problem with incorrect processing of image dimensions when using multiple upload. To eliminate this problem for already existing records see https://www.drupal.org/project/drupal/issues/2967586'), 'status'); +} diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php index 47aa51fa15..552da6b78e 100644 --- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php +++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php @@ -10,6 +10,7 @@ use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldWidget\FileWidget; use Drupal\image\Entity\ImageStyle; +use Drupal\Component\Utility\NestedArray; /** * Plugin implementation of the 'image_image' widget. @@ -185,6 +186,33 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen return $element; } + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + // Since file upload widget now supports uploads of more than one file at a + // time it always returns an array of fids. We have to translate this to a + // single fid, as field expects single value. In this process, we have to + // unset any image dimension for files after the first, that would + // otherwise be copied from the ones of the first file. + $new_values = []; + foreach ($values as &$value) { + $first = TRUE; + foreach ($value['fids'] as $fid) { + $new_value = $value; + $new_value['target_id'] = $fid; + unset($new_value['fids']); + if (!$first) { + unset($new_value['width'], $new_value['height']); + } + $new_values[] = $new_value; + $first = FALSE; + } + } + + return $new_values; + } + /** * Form API callback: Processes a image_image field element. * @@ -285,6 +313,70 @@ public static function process($element, FormStateInterface $form_state, $form) return parent::process($element, $form_state, $form); } + /** + * Form submission handler for upload/remove button of formElement(). + * + * This runs in addition to and after file_managed_file_submit(). + * + * @see file_managed_file_submit() + */ + public static function submit($form, FormStateInterface $form_state) { + // During the form rebuild, formElement() will create field item widget + // elements using re-indexed deltas, so clear out FormState::$input to + // avoid a mismatch between old and new deltas. The rebuilt elements will + // have #default_value set appropriately for the current state of the field, + // so nothing is lost in doing this. + $button = $form_state->getTriggeringElement(); + $parents = array_slice($button['#parents'], 0, -2); + NestedArray::setValue($form_state->getUserInput(), $parents, NULL); + + // Go one level up in the form, to the widgets container. + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + + $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2)); + foreach ($submitted_values as $delta => $submitted_value) { + if (empty($submitted_value['fids'])) { + unset($submitted_values[$delta]); + } + } + + // If there are more files uploaded via the same widget, we have to + // separate them, as we display each file in its own widget. In this + // process, we have to unset any image dimension for files after the first, + // that would otherwise be copied from the ones of the first file. + $new_values = []; + foreach ($submitted_values as $delta => $submitted_value) { + if (is_array($submitted_value['fids'])) { + $first = TRUE; + foreach ($submitted_value['fids'] as $fid) { + $new_value = $submitted_value; + $new_value['fids'] = [$fid]; + if (!$first) { + unset($new_value['width'], $new_value['height']); + } + $new_values[] = $new_value; + $first = FALSE; + } + } + else { + $new_value = $submitted_value; + } + } + + // Re-index deltas after removing empty items. + $submitted_values = array_values($new_values); + + // Update form_state values. + NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values); + + // Update items. + $field_state = static::getWidgetState($parents, $field_name, $form_state); + $field_state['items'] = $submitted_values; + static::setWidgetState($parents, $field_name, $form_state, $field_state); + } + /** * Validate callback for alt and title field, if the user wants them required. * diff --git a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php index e69de29bb2..b909e8f12b 100644 --- a/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php +++ b/core/modules/image/tests/src/FunctionalJavascript/ImageFieldWidgetMultipleTest.php @@ -0,0 +1,75 @@ +getSession()->getDriver(); + + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $field_name = 'images'; + $storage_settings = ['cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED]; + $field_settings = ['alt_field_required' => 0]; + $this->createImageField($field_name, 'article', $storage_settings, $field_settings); + $this->drupalLogin($this->drupalCreateUser(['access content', 'create article content'])); + $this->drupalGet('node/add/article'); + $this->xpath('//input[@name="title[0][value]"]')[0]->setValue('Test'); + + $images = $this->getTestFiles('image'); + $images = array_slice($images, 0, 5); + + $paths = []; + foreach ($images as $image) { + $paths[] = $file_system->realpath($image->uri); + } + + $remote_paths = []; + foreach ($paths as $path) { + $remote_paths[] = $web_driver->uploadFileAndGetRemoteFilePath($path); + } + + $multiple_field = $this->xpath('//input[@multiple]')[0]; + $multiple_field->setValue(implode("\n", $remote_paths)); + $this->assertSession()->waitForElementVisible('css', '[data-drupal-selector="edit-images-4-preview"]'); + $this->getSession()->getPage()->findButton('Save')->click(); + + $node = Node::load(1); + foreach ($paths as $delta => $path) { + $node_image = $node->{$field_name}[$delta]; + $original_image = $image_factory->get($path); + $this->assertEquals($node_image->width, $original_image->getWidth(), "Correct width of image #$delta"); + $this->assertEquals($node_image->height, $original_image->getHeight(), "Correct height of image #$delta"); + } + } + +}