diff --git a/composer.lock b/composer.lock index 4e39b70777fc954e43e339a061e9f0262f32e27f..39148d71f3e880df7b4ddc385d0087dd48a91d9c 100644 --- a/composer.lock +++ b/composer.lock @@ -4548,17 +4548,17 @@ }, { "name": "drupal/media_entity_file_replace", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/media_entity_file_replace.git", - "reference": "8.x-1.1" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity_file_replace-8.x-1.1.zip", - "reference": "8.x-1.1", - "shasum": "4462d41e80de7880e556eb677cd1831a44470dac" + "url": "https://ftp.drupal.org/files/projects/media_entity_file_replace-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "e5e1aa2519c3e3f65e8a8291c324bc527f649147" }, "require": { "drupal/core": "^9 || ^10" @@ -4566,8 +4566,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.1", - "datestamp": "1665518899", + "version": "8.x-1.2", + "datestamp": "1701092779", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index f3f026ac506f5cb372d9c93290d49a3f5fa99fa0..2efff9b08b3b39b4a798034c39cbc4704348b024 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4766,18 +4766,18 @@ }, { "name": "drupal/media_entity_file_replace", - "version": "1.1.0", - "version_normalized": "1.1.0.0", + "version": "1.2.0", + "version_normalized": "1.2.0.0", "source": { "type": "git", "url": "https://git.drupalcode.org/project/media_entity_file_replace.git", - "reference": "8.x-1.1" + "reference": "8.x-1.2" }, "dist": { "type": "zip", - "url": "https://ftp.drupal.org/files/projects/media_entity_file_replace-8.x-1.1.zip", - "reference": "8.x-1.1", - "shasum": "4462d41e80de7880e556eb677cd1831a44470dac" + "url": "https://ftp.drupal.org/files/projects/media_entity_file_replace-8.x-1.2.zip", + "reference": "8.x-1.2", + "shasum": "e5e1aa2519c3e3f65e8a8291c324bc527f649147" }, "require": { "drupal/core": "^9 || ^10" @@ -4785,8 +4785,8 @@ "type": "drupal-module", "extra": { "drupal": { - "version": "8.x-1.1", - "datestamp": "1665518899", + "version": "8.x-1.2", + "datestamp": "1701092779", "security-coverage": { "status": "covered", "message": "Covered by Drupal's security advisory policy" @@ -4802,6 +4802,10 @@ { "name": "bkosborne", "homepage": "https://www.drupal.org/user/788032" + }, + { + "name": "joevagyok", + "homepage": "https://www.drupal.org/user/2876343" } ], "description": "Allows content editors to easily replace source files associated with any file-based media entity, preserving the original filename.", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index e3fcd7984fb6db8dfc67f89a8503e9d830da3fdb..a0b15fca972bc7b23ea08f1094280ea4dfc3f9c0 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'osu-asc-webservices/d8-upstream', 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => 'edbfaf1ddccdb6a39283b7a9eb6645dc02a2fe18', + 'reference' => '71d26703cd3da123c6e67b89b752d4e6f950a1c1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -818,9 +818,9 @@ 'dev_requirement' => false, ), 'drupal/media_entity_file_replace' => array( - 'pretty_version' => '1.1.0', - 'version' => '1.1.0.0', - 'reference' => '8.x-1.1', + 'pretty_version' => '1.2.0', + 'version' => '1.2.0.0', + 'reference' => '8.x-1.2', 'type' => 'drupal-module', 'install_path' => __DIR__ . '/../../web/modules/media_entity_file_replace', 'aliases' => array(), @@ -1402,7 +1402,7 @@ 'osu-asc-webservices/d8-upstream' => array( 'pretty_version' => 'dev-main', 'version' => 'dev-main', - 'reference' => 'edbfaf1ddccdb6a39283b7a9eb6645dc02a2fe18', + 'reference' => '71d26703cd3da123c6e67b89b752d4e6f950a1c1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), diff --git a/web/modules/media_entity_file_replace/media_entity_file_replace.info.yml b/web/modules/media_entity_file_replace/media_entity_file_replace.info.yml index 96c4b520cd06f121a95c94da977cffc5e2466415..591ee8a746c90d1cc7379d1e87ac9803bc2d2c25 100644 --- a/web/modules/media_entity_file_replace/media_entity_file_replace.info.yml +++ b/web/modules/media_entity_file_replace/media_entity_file_replace.info.yml @@ -6,7 +6,7 @@ package: Media dependencies: - drupal:media -# Information added by Drupal.org packaging script on 2022-10-11 -version: '8.x-1.1' +# Information added by Drupal.org packaging script on 2023-11-27 +version: '8.x-1.2' project: 'media_entity_file_replace' -datestamp: 1665518902 +datestamp: 1701092782 diff --git a/web/modules/media_entity_file_replace/media_entity_file_replace.module b/web/modules/media_entity_file_replace/media_entity_file_replace.module index ecee72f6c6c0e0590596ceb5282d5f654da010e4..097051e3d2fb348a9365c60808a176c9382929b9 100644 --- a/web/modules/media_entity_file_replace/media_entity_file_replace.module +++ b/web/modules/media_entity_file_replace/media_entity_file_replace.module @@ -5,6 +5,8 @@ * Media Entity File Replace module file. */ +declare(strict_types = 1); + use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; @@ -30,7 +32,7 @@ function media_entity_file_replace_help($route_name, RouteMatchInterface $route_ function media_entity_file_replace_entity_extra_field_info() { $extra = []; - // Create an pseudo-field on form displays to allow site builders to control + // Create a pseudo-field on form displays to allow site builders to control // if they want to enable our custom file replacement widget on media edit // forms. if (\Drupal::service('module_handler')->moduleExists('media')) { @@ -63,116 +65,183 @@ function media_entity_file_replace_entity_extra_field_info() { * media_entity_file_replace_entity_extra_field_info(). */ function media_entity_file_replace_form_media_form_alter(&$form, FormStateInterface $form_state, $form_id) { + /** @var \Drupal\media\MediaInterface $media */ $media = $form_state->getFormObject()->getEntity(); // Don't modify the form at all for new media that is being added, since there // is nothing for us to do. - if (!$media->isNew()) { - // Only run for media entity types that use a file based source field. - /** @var \Drupal\media\Entity\MediaType $mediaType */ - $mediaType = \Drupal::entityTypeManager()->getStorage('media_type')->load($media->bundle()); - if (!$mediaType->getSource() instanceof File) { - return; - } + if ($media->isNew()) { + return; + } + + // Only run for media entity types that use a file based source field. + /** @var \Drupal\media\Entity\MediaType $mediaType */ + $mediaType = \Drupal::entityTypeManager()->getStorage('media_type')->load($media->bundle()); + if (!$mediaType->getSource() instanceof File) { + return; + } + + $sourceFieldDefinition = $mediaType->getSource()->getSourceFieldDefinition($mediaType); + $sourceFieldName = $sourceFieldDefinition->getName(); - $sourceFieldDefinition = $mediaType->getSource()->getSourceFieldDefinition($mediaType); - $sourceFieldName = $sourceFieldDefinition->getName(); - // Make sure we have a file field item and that the file entity exists. - // It's possible the file field item still exists (the reference to it) - // but that the file entity was deleted. - /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $fileFieldItem */ - $fileFieldItem = $media->get($sourceFieldName)->first(); - if (!$fileFieldItem || !$fileFieldItem->entity) { + // Make sure we have a file field item and that the file entity exists. + // It's possible the file field item still exists (the reference to it) + // but that the file entity was deleted. + /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $fileFieldItem */ + $fileFieldItem = $media->get($sourceFieldName)->first(); + if (!$fileFieldItem || !$fileFieldItem->entity) { + return; + } + + // Content translation support for Media field. + if (!$media->isDefaultTranslation()) { + if ($media->isDefaultTranslationAffectedOnly() && !$sourceFieldDefinition->isTranslatable()) { + // Stop if the field is not translatable, and it is not visible on the + // translation form. return; } - $form['replace_file'] = [ - '#type' => 'fieldset', - '#title' => t('Replace file'), - ]; - - $uploadValidators = $fileFieldItem->getUploadValidators(); - $form['replace_file']['replacement_file'] = [ - '#title' => t('File'), - '#type' => 'file', - // Note that the 'file' element does not support automatic handling of - // upload_validators like 'file_managed' does, but we pass it here anyway - // so that we can manually use it in the submit handler. - '#upload_validators' => $uploadValidators, - // Pass source field name so we don't need to execute the logic again - // to figure it out in the submit handler. - '#source_field_name' => $sourceFieldName, - ]; - - // Build help text for the replacement file upload field that indicates - // what the upload restrictions are (which we get from the source field). - // This help text comes by default with the "managed_file" form element, - // but we are using the standard "file" form element. - $helpText = [ - '#theme' => 'file_upload_help', - '#upload_validators' => $uploadValidators, - '#cardinality' => 1, - ]; - $form['replace_file']['replacement_file']['#description'] = \Drupal::service('renderer')->renderPlain($helpText); - - // Inform the user that when replacing the original file, the new one - // must have the same extension. - $originalExtension = '.' . pathinfo($fileFieldItem->entity->getFilename(), PATHINFO_EXTENSION); - $form['replace_file']['keep_original_filename'] = [ - '#title' => t('Overwrite original file (@originalExtension)', ['@originalExtension' => $originalExtension]), - '#description' => t('When checked, the original filename is kept and its contents are replaced with the new file, which <strong>must have the same file extension: @originalExtension</strong>. If unchecked, the filename of the replacement file will be used with any allowed file type and the original file may be deleted if no previous revision references it (depending on your specific site configuration).', ['@originalExtension' => $originalExtension]), - '#type' => 'checkbox', - '#default_value' => TRUE, - ]; - - $form['#validate'][] = '_media_entity_file_replace_validate'; - - // We need a submit callback to handle our processing. We want it to run - // just before the normal MediaForm::save() callback is called, so that - // the various entity lifecycle hooks that are called there will have - // access to the changes we make. - $saveCallbackPosition = array_search('::save', $form['actions']['submit']['#submit']); - if ($saveCallbackPosition !== FALSE) { - array_splice($form['actions']['submit']['#submit'], $saveCallbackPosition, 0, '_media_entity_file_replace_submit'); - } - else { - // If for some reason we cannot find the normal save callback in the list, - // then just insert our callback at the end. - $form['actions']['submit']['#submit'][] = '_media_entity_file_replace_submit'; - } + if ($sourceFieldDefinition->isTranslatable()) { + // If the field is translatable, and it's visible on the translation form. + if ($media->isNewTranslation()) { + $contentTranslationSettings = $sourceFieldDefinition->getThirdPartySettings('content_translation'); + if (!empty($contentTranslationSettings['translation_sync']['file'])) { + // If the file column in the field is marked as translatable, + // we don't render the replacement widget because we risk to replace + // the file on the default language version. For example: image filed. + return; + } + } - // If the normal file/image widget is on the form, then we want to hide - // the action buttons that users would normally use to manage the file. - // This widget doesn't allow for true file replacement, so we don't want - // editors to use it. We do still want the portion of the widget that - // displays the name of the file to render, so we don't remove the entire - // widget outright. - // This must be done in a process callback, since the action buttons on - // the widget are themselves added in a process callback. - if (isset($form[$sourceFieldName]['widget'][0]) && $form[$sourceFieldName]['widget'][0]['#type'] === 'managed_file') { - $form[$sourceFieldName]['widget'][0]['#process'][] = '_media_entity_file_replace_disable_remove_button'; + // Get both untranslated and translated files. + $untranslatedMedia = $media->getUntranslated(); + $translatedFileId = $media->{$sourceFieldName}->target_id; + $untranslatedFileId = $untranslatedMedia->{$sourceFieldName}->target_id; + + if ($untranslatedFileId === $translatedFileId) { + // If the referenced file in the translation is the same as the default + // language, don't render the replacement, because we risk to override + // the default language version as well since it's the same file. + return; + } } } + + $form['replace_file'] = [ + '#type' => 'fieldset', + '#title' => t('Replace file'), + '#multilingual' => $sourceFieldDefinition->isTranslatable(), + ]; + + $uploadValidators = $media->get($sourceFieldName)->first()->getUploadValidators(); + $form['replace_file']['replacement_file'] = [ + '#title' => t('File'), + '#type' => 'file', + // Note that the 'file' element does not support automatic handling of + // upload_validators like 'file_managed' does, but we pass it here anyway + // so that we can manually use it in the submit handler. + '#upload_validators' => $uploadValidators, + // Pass source field name so we don't need to execute the logic again + // to figure it out in the submit handler. + '#source_field_name' => $sourceFieldName, + ]; + + // Build help text for the replacement file upload field that indicates + // what the upload restrictions are (which we get from the source field). + // This help text comes by default with the "managed_file" form element, + // but we are using the standard "file" form element. + $helpText = [ + '#theme' => 'file_upload_help', + '#upload_validators' => $uploadValidators, + '#cardinality' => 1, + ]; + $form['replace_file']['replacement_file']['#description'] = \Drupal::service('renderer')->renderPlain($helpText); + + // Inform the user that when replacing the original file, the new one + // must have the same extension. + $originalExtension = '.' . pathinfo($fileFieldItem->entity->getFilename(), PATHINFO_EXTENSION); + $form['replace_file']['keep_original_filename'] = [ + '#title' => t('Overwrite original file (@originalExtension)', ['@originalExtension' => $originalExtension]), + '#description' => t('When checked, the original filename is kept and its contents are replaced with the new file, which <strong>must have the same file extension: @originalExtension</strong>. If unchecked, the filename of the replacement file will be used with any allowed file type and the original file may be deleted if no previous revision references it (depending on your specific site configuration).', ['@originalExtension' => $originalExtension]), + '#type' => 'checkbox', + '#default_value' => TRUE, + ]; + + $form['#validate'][] = '_media_entity_file_replace_validate'; + + // We need a submit callback to handle our processing. We want it to run + // just before the normal MediaForm::save() callback is called, so that + // the various entity lifecycle hooks that are called there will have + // access to the changes we make. + $saveCallbackPosition = array_search('::save', $form['actions']['submit']['#submit']); + if ($saveCallbackPosition !== FALSE) { + array_splice($form['actions']['submit']['#submit'], $saveCallbackPosition, 0, '_media_entity_file_replace_submit'); + } + else { + // If for some reason we cannot find the normal save callback in the list, + // then just insert our callback at the end. + $form['actions']['submit']['#submit'][] = '_media_entity_file_replace_submit'; + } + + // If the normal file/image widget is on the form, then we want to hide + // the action buttons that users would normally use to manage the file. + // This widget doesn't allow for true file replacement, so we don't want + // editors to use it. We do still want the portion of the widget that + // displays the name of the file to render, so we don't remove the entire + // widget outright. + // This must be done in a process callback, since the action buttons on + // the widget are themselves added in a process callback. + if (isset($form[$sourceFieldName]['widget'][0]) && isset($form[$sourceFieldName]['widget'][0]['#type']) && $form[$sourceFieldName]['widget'][0]['#type'] === 'managed_file') { + $form[$sourceFieldName]['widget'][0]['#process'][] = '_media_entity_file_replace_disable_remove_button'; + } } /** * Custom process callback on file widget to disable remove/upload buttons. + * + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param array $complete_form + * The complete form array. + * + * @return array + * The element from the callback originates. */ -function _media_entity_file_replace_disable_remove_button(&$element, FormStateInterface $form_state, &$complete_form) { +function _media_entity_file_replace_disable_remove_button(array &$element, FormStateInterface $form_state, array $complete_form): array { // We only want to do this on media edit forms that are configured to use - // our "replace file" widget, so we check to make sure it's there and + // our "replace_file" widget, so we check to make sure it's there and // accessible before continuing. + if (!isset($complete_form['replace_file'])) { + return $element; + } + + // It can happen that the "#access" is not set, which means that the field + // is visible or when it's explicitly set we remove access to the original + // file upload buttons. if (!isset($complete_form['replace_file']['#access']) || $complete_form['replace_file']['#access'] === TRUE) { $element['remove_button']['#access'] = FALSE; $element['upload_button']['#access'] = FALSE; } + return $element; } /** * Custom validate handler for media entity edit form submissions. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $formState + * The form state. */ -function _media_entity_file_replace_validate($form, FormStateInterface $formState) { - // Nothing to do if the replace file widget was not enabled for this form. +function _media_entity_file_replace_validate(array $form, FormStateInterface $formState): void { + if (!isset($form['replace_file'])) { + // If the "replace_file" widget is not enabled in the form, we bail out. + return; + } + + // If no access is allowed to the widget we skip validation. if (isset($form['replace_file']['#access']) && !$form['replace_file']['#access']) { return; } @@ -186,7 +255,7 @@ function _media_entity_file_replace_validate($form, FormStateInterface $formStat // Determine where to place the replacement file that a user selected. // When overwriting the existing file, then the replacement file should be - // stored in temporary storage so we can then copy it over the existing one. + // stored in temporary storage, so we can then copy it over the existing one. // When not overwriting, we want to move it to the correct final destination // folder, which we determine by examining the settings of the source field // definition on the media entity. @@ -231,8 +300,13 @@ function _media_entity_file_replace_validate($form, FormStateInterface $formStat /** * Custom submit handler for media entity edit form submissions. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $formState + * The form state. */ -function _media_entity_file_replace_submit($form, FormStateInterface $formState) { +function _media_entity_file_replace_submit(array $form, FormStateInterface $formState): void { $replacementFile = $formState->get('replacement_file'); if (!$replacementFile) { return; @@ -254,7 +328,6 @@ function _media_entity_file_replace_submit($form, FormStateInterface $formState) // files. $destination = $fileSystem->dirname($originalFile->getFileUri()); $fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY); - if (!$fileSystem->copy($replacementFile->getFileUri(), $originalFile->getFileUri(), FileSystemInterface::EXISTS_REPLACE)) { \Drupal::messenger()->addError(t('Unable to overwrite original file with the replacement.')); return; diff --git a/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTest.php b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTest.php index b1c1fe682ad8ced1d1995e3aec84447e476910c2..0e102c32db7a39ebeddab24cb240a9a703b9ef94 100644 --- a/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTest.php +++ b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTest.php @@ -1,46 +1,20 @@ <?php -namespace Drupal\Tests\media_entity_file_replace\Functional; +declare(strict_types = 1); -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\media\Traits\MediaTypeCreationTrait; +namespace Drupal\Tests\media_entity_file_replace\Functional; /** - * Class MediaEntityFileReplaceTest. + * Tests the file replacement feature. * - * @group media + * @group media_entity_file_replace */ -class MediaEntityFileReplaceTest extends BrowserTestBase { - - use MediaTypeCreationTrait; - - protected $defaultTheme = 'stark'; - - /** - * Modules to install. - * - * @var array - */ - protected static $modules = [ - 'system', - 'field_ui', - 'media', - 'media_entity_file_replace', - ]; +class MediaEntityFileReplaceTest extends MediaEntityFileReplaceTestBase { /** - * Tests basic functionality of the module. + * Tests the basic functionality of the module. */ - public function testModule() { - $this->createMediaType('file', [ - 'id' => 'document', - 'label' => 'Document', - ]); - $this->createMediaType('oembed:video', [ - 'id' => 'remote_video', - 'label' => 'Remote Video', - ]); - + public function testFileReplacement(): void { $user = $this->drupalCreateUser([ 'access media overview', 'administer media form display', @@ -59,6 +33,7 @@ public function testModule() { // But not on media bundles that don't use a file source, like remote video. $this->drupalGet('/admin/structure/media/manage/remote_video/form-display'); $this->assertSession()->fieldNotExists("fields[replace_file][weight]"); + // While we're here, enable the name field so we can manually provide a name // for remote videos. This just makes tests easier. $page = $this->getSession()->getPage(); @@ -68,7 +43,6 @@ public function testModule() { // Create a video media entity and confirm we don't see the replacement // widget on the edit screen. $this->drupalGet('/media/add/remote_video'); - $page = $this->getSession()->getPage(); $this->assertSession()->pageTextNotContains('Replace file'); $this->assertSession()->fieldNotExists('files[replacement_file]'); $page->fillField('Name', 'DrupalCon Amsterdam Keynote'); @@ -82,31 +56,40 @@ public function testModule() { // Create a document entity and confirm it works as usual. // The file replacement widget should not appear on this form since we did // not enable the new replacement widget on the form display yet. - $uri = 'temporary://foo.txt'; - file_put_contents($uri, 'original'); $this->drupalGet('/media/add/document'); - $page = $this->getSession()->getPage(); + $file_1 = [ + 'uri' => 'temporary://file_1.txt', + 'data' => 'file 1 original', + ]; + file_put_contents($file_1['uri'], $file_1['data']); $this->assertSession()->pageTextNotContains('Replace file'); $page->fillField('Name', 'Foobar'); - $page->attachFileToField('File', $this->container->get('file_system')->realpath($uri)); + $page->attachFileToField('File', \Drupal::service('file_system')->realpath($file_1['uri'])); $this->assertSession()->fieldNotExists('files[replacement_file]'); $page->pressButton('Save'); $this->assertSession()->addressEquals('admin/content/media'); - unlink($uri); + unlink($file_1['uri']); + + // Reload the document from storage. + $originalDocument = $this->loadMediaEntityByName('Foobar'); + $file_entity_1 = $this->loadFileEntity($originalDocument->getSource()->getSourceFieldValue($originalDocument)); // Edit the document and confirm the remove button for the default file // widget is there, since our pseudo widget which normally removes it is not // yet active. - $originalDocument = $this->loadMediaEntityByName('Foobar'); $this->drupalGet("/media/{$originalDocument->id()}/edit"); $this->assertSession()->buttonExists('Remove'); // Now enable the file replacement widget for document media bundle. $this->drupalGet('/admin/structure/media/manage/document/form-display'); - $page = $this->getSession()->getPage(); $page->fillField('fields[replace_file][region]', 'content'); $page->pressButton('Save'); + // When creating new media, we should not see the replacement field. + $this->drupalGet("/media/add/document"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + // Edit the document again. The "remove" button on the default file // widget should be removed now. $this->drupalGet("/media/{$originalDocument->id()}/edit"); @@ -117,115 +100,63 @@ public function testModule() { $this->assertSession()->fieldExists('files[replacement_file]'); $this->assertSession()->fieldExists('keep_original_filename'); - // Upload a replacement file with new contents, overwriting the original - // file. - $originalFile = $this->loadFileEntity(($originalDocument->getSource()->getSourceFieldValue($originalDocument))); - $uri = 'temporary://foo.txt'; - file_put_contents($uri, 'new'); - $page = $this->getSession()->getPage(); - $page->attachFileToField('File', $this->container->get('file_system')->realpath($uri)); - $page->checkField('keep_original_filename'); - $page->pressButton('Save'); - unlink($uri); + // Upload a replacement file with new contents and different file name, + // overwriting the original file. + $this->uploadReplacementFile('temporary://file_2.txt', 'file 2', TRUE); // Reload document and confirm the filename and URI have not changed, but // the contents of the file have. $updatedDocument = $this->loadMediaEntityByName('Foobar'); - $updatedFile = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); - $this->assertEquals($updatedFile->id(), $originalFile->id()); - $this->assertEquals($updatedFile->getFileUri(), $originalFile->getFileUri()); - $this->assertEquals($updatedFile->getFilename(), $originalFile->getFilename()); - $this->assertNotEquals($updatedFile->getSize(), $originalFile->getSize()); - $this->assertEquals(file_get_contents($updatedFile->getFileUri()), 'new'); + $file_entity_2 = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals($file_entity_1->id(), $file_entity_2->id()); + $this->assertEquals($file_entity_1->getFileUri(), $file_entity_2->getFileUri()); + $this->assertEquals('file_1.txt', $file_entity_2->getFilename()); + $this->assertEquals('file 2', file_get_contents($file_entity_2->getFileUri())); + $this->assertFalse($file_entity_2->isTemporary()); + + // Assert the size of the file was updated. + $this->assertNotEquals($file_entity_1->getSize(), $file_entity_2->getSize()); // Now upload another replacement document, but this time don't overwrite - // the original. - $originalDocument = $updatedDocument; - $originalFile = $updatedFile; - $uri = 'temporary://foo-new.txt'; - file_put_contents($uri, 'foo-new'); + // the original file. $this->drupalGet("/media/{$originalDocument->id()}/edit"); - $page = $this->getSession()->getPage(); - $page->attachFileToField('File', $this->container->get('file_system')->realpath($uri)); - $page->uncheckField('keep_original_filename'); - $page->pressButton('Save'); - unlink($uri); + $this->uploadReplacementFile('temporary://file_3.txt', 'file 3', FALSE); - // Verify that the file associated with the document is different than the - // previous one. + // Verify that the file associated with the document has a different name + // and content since we didn't override the original. $updatedDocument = $this->loadMediaEntityByName('Foobar'); - $updatedFile = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); - $this->assertNotEquals($updatedFile->id(), $originalFile->id()); - $this->assertNotEquals($updatedFile->getFileUri(), $originalFile->getFileUri()); - $this->assertNotEquals($updatedFile->getFilename(), $originalFile->getFilename()); - $this->assertNotEquals($updatedFile->getSize(), $originalFile->getSize()); - $this->assertNotEquals(file_get_contents($updatedFile->getFileUri()), file_get_contents($originalFile->getFileUri())); - $this->assertEquals(file_get_contents($updatedFile->getFileUri()), 'foo-new'); - $this->assertFalse($updatedFile->isTemporary()); + $file_entity_3 = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals('file_3.txt', $file_entity_3->getFilename()); + $this->assertEquals('file 3', file_get_contents($file_entity_3->getFileUri())); + $this->assertFalse($file_entity_3->isTemporary()); // The old file entity should still exist, and should not be marked as // temporary since editing the document entity created a revision and the // old revision still references the old document. - $originalFile = $this->loadFileEntity($originalFile->id()); + $originalFile = $this->loadFileEntity($file_entity_1->id()); $this->assertFalse($originalFile->isTemporary()); // Verify that when uploading a replacement and overwriting the original, // the file extension is forced to be the same. - // Now upload another replacement document, but this time don't overwrite - // the original. $originalDocument = $this->loadMediaEntityByName('Foobar'); - $uri = 'temporary://foo.pdf'; - file_put_contents($uri, 'pdf contents'); $this->drupalGet("/media/{$originalDocument->id()}/edit"); - $page = $this->getSession()->getPage(); - $this->assertSession()->fieldExists('files[replacement_file]'); - $page->attachFileToField('File', $this->container->get('file_system')->realpath($uri)); - $page->checkField('keep_original_filename'); - $page->pressButton('Save'); + $this->uploadReplacementFile('temporary://file_4.pdf', 'file 4', TRUE); $this->assertSession()->pageTextContains('Only files with the following extensions are allowed: txt'); $this->assertSession()->addressEquals("/media/{$originalDocument->id()}/edit"); // It should be allowed if we opt NOT to overwrite the original though. - $originalDocument = $this->loadMediaEntityByName('Foobar'); - $page = $this->getSession()->getPage(); - $page->attachFileToField('File', $this->container->get('file_system')->realpath($uri)); - $page->uncheckField('keep_original_filename'); - $page->pressButton('Save'); + $this->uploadReplacementFile('temporary://file_4.pdf', 'file 4', FALSE); $this->assertSession()->pageTextNotContains('Only files with the following extensions are allowed: txt'); $this->assertSession()->addressEquals("/admin/content/media"); - $this->assertSession()->pageTextNotContains('foo.pdf'); - unlink($uri); // Simulate deleting the file and then revisit the media entity. Since // there is no longer a file associated to the media entity, there is - // nothing to replace and therefore the replace file widget should not show. + // nothing to replace and therefore the "replace_file" widget should + // not show. $originalDocument = $this->loadMediaEntityByName('Foobar'); $fileToDelete = $this->loadFileEntity($originalDocument->getSource()->getSourceFieldValue($originalDocument)); $fileToDelete->delete(); $this->drupalGet("/media/{$originalDocument->id()}/edit"); - $page = $this->getSession()->getPage(); $this->assertSession()->fieldNotExists('files[replacement_file]'); } - /** - * Load a single media entity by name, ignoring object cache. - */ - protected function loadMediaEntityByName($name) { - $mediaStorage = \Drupal::entityTypeManager()->getStorage('media'); - $mediaStorage->resetCache(); - $entities = $mediaStorage->loadByProperties(['name' => $name]); - $this->assertNotEmpty($entities, "No media entity with name $name was found."); - return array_pop($entities); - } - - /** - * Load a single file entity by ID, ignoring object cache. - */ - protected function loadFileEntity($id) { - $fileStorage = \Drupal::entityTypeManager()->getStorage('file'); - $fileStorage->resetCache(); - $file = $fileStorage->load($id); - $this->assertNotNull($file, "No file entity with id $id was found."); - return $file; - } - } diff --git a/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTestBase.php b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..b3e2bec6e898a395784342c97ebc40330b774a0d --- /dev/null +++ b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityFileReplaceTestBase.php @@ -0,0 +1,111 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\media_entity_file_replace\Functional; + +use Drupal\file\FileInterface; +use Drupal\media\MediaInterface; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; + +/** + * Base class for testing file replacement in functional tests. + */ +abstract class MediaEntityFileReplaceTestBase extends BrowserTestBase { + + use MediaTypeCreationTrait; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Modules to install. + * + * @var array + */ + protected static $modules = [ + 'system', + 'field_ui', + 'media', + 'media_entity_file_replace', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->createMediaType('file', [ + 'id' => 'document', + 'label' => 'Document', + ]); + $this->createMediaType('oembed:video', [ + 'id' => 'remote_video', + 'label' => 'Remote Video', + ]); + } + + /** + * Helper function to upload/replace file. + * + * @param string $uri + * The URI of the file to upload. + * @param string $data + * The content of the file. + * @param bool $override_original + * Whether we want to override the file or replace. + */ + protected function uploadReplacementFile(string $uri, string $data, bool $override_original): void { + file_put_contents($uri, $data); + $page = $this->getSession()->getPage(); + $page->attachFileToField('File', \Drupal::service('file_system')->realpath($uri)); + if ($override_original) { + $page->checkField('keep_original_filename'); + } + else { + $page->uncheckField('keep_original_filename'); + } + $page->pressButton('Save'); + unlink($uri); + } + + /** + * Load media entity by its name. + * + * @param string $name + * The media name. + * + * @return \Drupal\media\MediaInterface + * The loaded media from storage. + */ + protected function loadMediaEntityByName(string $name): MediaInterface { + $mediaStorage = \Drupal::entityTypeManager()->getStorage('media'); + $mediaStorage->resetCache(); + $entities = $mediaStorage->loadByProperties(['name' => $name]); + $this->assertNotEmpty($entities, "No media entity with name $name was found."); + return array_pop($entities); + } + + /** + * Load file entity by ID. + * + * @param int|string $id + * The file ID to load. + * + * @return \Drupal\file\FileInterface + * The loaded file from storage. + */ + protected function loadFileEntity(int|string $id): FileInterface { + $fileStorage = \Drupal::entityTypeManager()->getStorage('file'); + $fileStorage->resetCache(); + /** @var \Drupal\file\FileInterface $file */ + $file = $fileStorage->load($id); + $this->assertNotNull($file, "No file entity with id $id was found."); + return $file; + } + +} diff --git a/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityTranslationFileReplaceTest.php b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityTranslationFileReplaceTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d0e8a362c5ff4f7b2c0f9b190e73db96eab33f35 --- /dev/null +++ b/web/modules/media_entity_file_replace/tests/src/Functional/MediaEntityTranslationFileReplaceTest.php @@ -0,0 +1,464 @@ +<?php + +declare(strict_types = 1); + +namespace Drupal\Tests\media_entity_file_replace\Functional; + +use Drupal\field\Entity\FieldConfig; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\language\Entity\ContentLanguageSettings; +use Drupal\Tests\TestFileCreationTrait; + +/** + * Tests the file replacement feature with content translation. + * + * @group media_entity_file_replace + */ +class MediaEntityTranslationFileReplaceTest extends MediaEntityFileReplaceTestBase { + + use TestFileCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'content_translation', + 'language', + 'image', + ]; + + /** + * Tests the basic functionality of the module with translation in the scope. + */ + public function testFileReplacement(): void { + // Create a language and enable the translation for document media. + ConfigurableLanguage::createFromLangcode('hu')->save(); + \Drupal::service('content_translation.manager')->setEnabled('media', 'document', TRUE); + $field_config = FieldConfig::load('media.document.field_media_file'); + $field_config->setTranslatable(TRUE); + $field_config->save(); + + $user = $this->drupalCreateUser([ + 'access media overview', + 'administer media form display', + 'view media', + 'administer media', + 'access content', + 'translate any entity', + ]); + $this->drupalLogin($user); + + $page = $this->getSession()->getPage(); + + // Create a document entity and confirm it works as usual. + // The file replacement widget should not appear on this form since we did + // not enable the new replacement widget on the form display yet. + $this->drupalGet('/media/add/document'); + $file_1 = [ + 'uri' => 'temporary://file_1.txt', + 'data' => 'file 1 original', + ]; + file_put_contents($file_1['uri'], $file_1['data']); + $this->assertSession()->pageTextNotContains('Replace file'); + $page->fillField('Name', 'Foobar'); + $page->attachFileToField('File', \Drupal::service('file_system')->realpath($file_1['uri'])); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $page->pressButton('Save'); + $this->assertSession()->addressEquals('admin/content/media'); + unlink($file_1['uri']); + + // Save the original file for later assertions. + $originalDocument = $this->loadMediaEntityByName('Foobar'); + $file_entity_1 = $this->loadFileEntity($originalDocument->getSource()->getSourceFieldValue($originalDocument)); + + // Now enable the file replacement widget for document media bundle. + $this->drupalGet('/admin/structure/media/manage/document/form-display'); + $page->fillField('fields[replace_file][region]', 'content'); + $page->pressButton('Save'); + + // When creating new media, we should not see the replacement field. + $this->drupalGet("/media/add/document"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + + // Assert the file replacement field is not visible when adding a new + // translation. + $this->drupalGet("/hu/media/{$originalDocument->id()}/edit/translations/add/en/hu"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + + // Create a translation for the document media. + $values = $originalDocument->toArray(); + $values['name'] = 'Foobar HU'; + $originalDocument->addTranslation('hu', $values); + $originalDocument->save(); + + // And there should be additional fields for uploading replacement file and + // controlling behavior for overwriting it. + $this->drupalGet("/media/{$originalDocument->id()}/edit"); + $this->assertSession()->fieldExists('files[replacement_file]'); + $this->assertSession()->fieldExists('keep_original_filename'); + + // Upload a replacement file with new contents and different file name, + // overwriting the original file. + $this->uploadReplacementFile('temporary://file_2.txt', 'file 2', TRUE); + + // Reload document and confirm the filename and URI have not changed, but + // the contents of the file have. + $updatedDocument = $this->loadMediaEntityByName('Foobar'); + $file_entity_2 = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals($file_entity_1->id(), $file_entity_2->id()); + $this->assertEquals($file_entity_1->getFileUri(), $file_entity_2->getFileUri()); + $this->assertEquals('file_1.txt', $file_entity_2->getFilename()); + $this->assertEquals('file 2', file_get_contents($file_entity_2->getFileUri())); + $this->assertFalse($file_entity_2->isTemporary()); + + // Assert the size of the file was updated. + $this->assertNotEquals($file_entity_1->getSize(), $file_entity_2->getSize()); + + // Assert the translation file was overridden since it was referencing the + // same file as the default language one. + $translation = $updatedDocument->getTranslation('hu'); + $translationFile = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertEquals($file_entity_1->id(), $translationFile->id()); + $this->assertEquals($file_entity_1->getFileUri(), $translationFile->getFileUri()); + $this->assertEquals('file_1.txt', $translationFile->getFilename()); + $this->assertEquals('file 2', file_get_contents($translationFile->getFileUri())); + $this->assertFalse($translationFile->isTemporary()); + + // When we edit the translation, we should not see our replacement widget, + // otherwise we will be able to override the default language file from a + // translation. + $this->drupalGet("/hu/media/{$originalDocument->id()}/edit"); + $this->assertSession()->buttonExists('Remove'); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + + // Now upload another replacement document, but this time don't overwrite + // the original file. + $this->drupalGet("/media/{$originalDocument->id()}/edit"); + $this->uploadReplacementFile('temporary://file_3.txt', 'file 3', FALSE); + + // Verify that the file associated with the document has a different name + // and content since we didn't override the original. + $updatedDocument = $this->loadMediaEntityByName('Foobar'); + $file_entity_3 = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals('file_3.txt', $file_entity_3->getFilename()); + $this->assertEquals('file 3', file_get_contents($file_entity_3->getFileUri())); + $this->assertFalse($file_entity_3->isTemporary()); + + // Now assert that the translation file didn't change since we didn't + // override the referenced one but replaced with file_3. + $translation = $updatedDocument->getTranslation('hu'); + $translationFile = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertEquals($file_entity_1->id(), $translationFile->id()); + $this->assertEquals($file_entity_1->getFileUri(), $translationFile->getFileUri()); + $this->assertEquals('file_1.txt', $translationFile->getFilename()); + $this->assertEquals('file 2', file_get_contents($translationFile->getFileUri())); + $this->assertFalse($translationFile->isTemporary()); + + // Do a replacement in the translation with override. We are able to do that + // because the file_entity_1 is not referenced by the english one anymore. + $this->drupalGet("/hu/media/{$originalDocument->id()}/edit"); + $this->uploadReplacementFile('temporary://file_translation.txt', 'file translation', TRUE); + + // Assert the new document has the same name, id, uri, but different content + // as the original file. This means the file name will be kept. + $updatedDocument = $this->loadMediaEntityByName('Foobar'); + $translation = $updatedDocument->getTranslation('hu'); + $translationFile = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertEquals($file_entity_1->id(), $translationFile->id()); + $this->assertEquals($file_entity_1->getFileUri(), $translationFile->getFileUri()); + $this->assertEquals('file_1.txt', $translationFile->getFilename()); + $this->assertEquals('file translation', file_get_contents($translationFile->getFileUri())); + $this->assertFalse($translationFile->isTemporary()); + + // Assert the default language file didn't change. + $updatedFile = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals('file 3', file_get_contents($updatedFile->getFileUri())); + $this->assertEquals('file_3.txt', $updatedFile->getFilename()); + + // Do a replacement in the translation without override. + $this->drupalGet("/hu/media/{$originalDocument->id()}/edit"); + $this->uploadReplacementFile('temporary://file_4.txt', 'file 4', FALSE); + + // Assert the english translation didn't change. + $updatedDocument = $this->loadMediaEntityByName('Foobar'); + $file_entity_3 = $this->loadFileEntity($updatedDocument->getSource()->getSourceFieldValue($updatedDocument)); + $this->assertEquals('file_3.txt', $file_entity_3->getFilename()); + $this->assertEquals('file 3', file_get_contents($file_entity_3->getFileUri())); + $this->assertFalse($file_entity_3->isTemporary()); + + // Assert the translation file changed to new one. + $translation = $updatedDocument->getTranslation('hu'); + $translationFile = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertEquals('file_4.txt', $translationFile->getFilename()); + $this->assertEquals('file 4', file_get_contents($translationFile->getFileUri())); + $this->assertFalse($translationFile->isTemporary()); + + // The old file entity should still exist, and should not be marked as + // temporary since editing the document entity created a revision and the + // old revision still references the old document. + $originalFile = $this->loadFileEntity($file_entity_1->id()); + $this->assertFalse($originalFile->isTemporary()); + + // Verify that when uploading a replacement and overwriting the original, + // the file extension is forced to be the same. + $originalDocument = $this->loadMediaEntityByName('Foobar'); + $this->drupalGet("/media/{$originalDocument->id()}/edit"); + $this->uploadReplacementFile('temporary://file_5.pdf', 'file 5', TRUE); + $this->assertSession()->pageTextContains('Only files with the following extensions are allowed: txt'); + $this->assertSession()->addressEquals("/media/{$originalDocument->id()}/edit"); + // It should be allowed if we opt NOT to overwrite the original though. + $this->uploadReplacementFile('temporary://file_5.pdf', 'file 5', FALSE); + $this->assertSession()->pageTextNotContains('Only files with the following extensions are allowed: txt'); + $this->assertSession()->addressEquals("/admin/content/media"); + + // Simulate deleting the file and then revisit the media entity. Since + // there is no longer a file associated to the media entity, there is + // nothing to replace and therefore the "replace_file" widget should + // not show. + $originalDocument = $this->loadMediaEntityByName('Foobar'); + $fileToDelete = $this->loadFileEntity($originalDocument->getSource()->getSourceFieldValue($originalDocument)); + $fileToDelete->delete(); + $this->drupalGet("/media/{$originalDocument->id()}/edit"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + + // Do the same for the translation. + $translation = $originalDocument->getTranslation('hu'); + $fileToDelete = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $fileToDelete->delete(); + $this->drupalGet("/hu/media/{$originalDocument->id()}/edit"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + } + + /** + * Tests the image media type file replacement in the context of translations. + */ + public function testImageFileReplacement(): void { + $this->createMediaType('image', [ + 'id' => 'image', + 'label' => 'Image', + ]); + + // Create a language and enable the translation for image media. + ConfigurableLanguage::createFromLangcode('hu')->save(); + \Drupal::service('content_translation.manager')->setEnabled('media', 'image', TRUE); + $field_config = FieldConfig::load('media.image.field_media_image'); + $field_config->setTranslatable(TRUE); + $field_config->save(); + + $user = $this->drupalCreateUser([ + 'access media overview', + 'administer media form display', + 'view media', + 'administer media', + 'access content', + 'translate any entity', + ]); + $this->drupalLogin($user); + + $page = $this->getSession()->getPage(); + + // Now enable the file replacement widget for image media bundle. + $this->drupalGet('/admin/structure/media/manage/image/form-display'); + $page->fillField('fields[replace_file][region]', 'content'); + $page->pressButton('Save'); + + $this->drupalGet('/media/add/image'); + + // Assert the replacement field is not visible. + $this->assertSession()->pageTextNotContains('Replace file'); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + + // Upload an image. + $page->fillField('Name', 'Foobar'); + $page->attachFileToField('Image', \Drupal::root() . '/core/misc/druplicon.png'); + $page->pressButton('Save'); + $page->fillField('Alternative text', 'Foobar image'); + $page->pressButton('Save'); + $this->assertSession()->addressEquals('admin/content/media'); + + // Save the original file for later assertions. + $originalImage = $this->loadMediaEntityByName('Foobar'); + $file_entity_1 = $this->loadFileEntity($originalImage->getSource()->getSourceFieldValue($originalImage)); + + // Get a new test image. + $file_system = \Drupal::service('file_system'); + $test_files = $this->getTestFiles('image'); + foreach ($test_files as $test_file) { + if ($test_file->filename === 'image-test.png') { + $png_path = $file_system->realpath($test_file->uri); + } + if ($test_file->filename === 'image-test.jpg') { + $jpg_path = $file_system->realpath($test_file->uri); + } + } + + // Now make a replacement with content override. + $this->drupalGet("/media/{$originalImage->id()}/edit"); + $page->attachFileToField('File', $jpg_path); + $page->checkField('keep_original_filename'); + $page->pressButton('Save'); + + // We should get an error that the file extensions don't match. + $this->assertSession()->pageTextContains('Only files with the following extensions are allowed: png.'); + $this->assertSession()->pageTextContains('Unable to upload replacement file.'); + + // Now upload the png instead. + $page->attachFileToField('File', $png_path); + $page->pressButton('Save'); + $this->assertSession()->pageTextNotContains('Only files with the following extensions are allowed: png.'); + $this->assertSession()->pageTextNotContains('Unable to upload replacement file.'); + $this->assertSession()->addressEquals('admin/content/media'); + + // Reload the media and assert the image content was replaced but the name + // is maintained. + $updatedImage = $this->loadMediaEntityByName('Foobar'); + $file_entity_2 = $this->loadFileEntity($updatedImage->getSource()->getSourceFieldValue($updatedImage)); + $this->assertEquals($file_entity_1->id(), $file_entity_2->id()); + $this->assertEquals($file_entity_1->getFileUri(), $file_entity_2->getFileUri()); + $this->assertEquals('druplicon.png', $file_entity_2->getFilename()); + $this->assertFalse($file_entity_2->isTemporary()); + + // Assert the size of the file was updated. We can't compare the contents of + // the files because they are images, so we rely on the file size + // difference. + $this->assertNotEquals($file_entity_1->getSize(), $file_entity_2->getSize()); + + // Make a replacement without the override, so we replace the file not just + // the content. + $this->drupalGet("/media/{$originalImage->id()}/edit"); + $page->attachFileToField('File', $jpg_path); + $page->uncheckField('keep_original_filename'); + $page->pressButton('Save'); + $this->assertSession()->addressEquals('admin/content/media'); + + // Reload the media and assert the image is replaced. + $updatedImage = $this->loadMediaEntityByName('Foobar'); + $file_entity_3 = $this->loadFileEntity($updatedImage->getSource()->getSourceFieldValue($updatedImage)); + $this->assertNotEquals($file_entity_2->id(), $file_entity_3->id()); + $this->assertNotEquals($file_entity_2->getFileUri(), $file_entity_3->getFileUri()); + $this->assertEquals('image-test.jpg', $file_entity_3->getFilename()); + $this->assertFalse($file_entity_3->isTemporary()); + + $this->assertNotEquals($file_entity_2->getSize(), $file_entity_3->getSize()); + + // Add a new translation. + $this->drupalGet("/hu/media/{$updatedImage->id()}/edit/translations/add/en/hu"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + + // The remove button is in place, because we see the image from the default + // language. If we allowed override here, we would risk of changing the + // image file on the default language. + $this->assertSession()->buttonExists('Remove'); + + // Upload a translation file. + $page->pressButton('Remove'); + $page->fillField('Name', 'HU Foobar'); + $page->attachFileToField('Image', \Drupal::root() . '/core/misc/druplicon.png'); + $page->pressButton('Save'); + // The "Alternative text" field is required after uploading the image. + $page->fillField('Alternative text', 'HU Foobar image'); + $page->pressButton('Save'); + $this->assertSession()->addressEquals('hu/admin/content/media'); + + // Get the original translation file for assertions. + $updatedImage = $this->loadMediaEntityByName('Foobar'); + $translation = $updatedImage->getTranslation('hu'); + $translation_file_1 = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + // The name will increment to 0 because we used the same name before. + $this->assertEquals('druplicon_0.png', $translation_file_1->getFilename()); + + $this->drupalGet("/hu/media/{$updatedImage->id()}/edit"); + + // Now do an image override on the translation. + $page->attachFileToField('File', $jpg_path); + $page->checkField('keep_original_filename'); + $page->pressButton('Save'); + + // We should get an error that the file extensions don't match. + $this->assertSession()->pageTextContains('Only files with the following extensions are allowed: png.'); + $this->assertSession()->pageTextContains('Unable to upload replacement file.'); + + // Now upload the png instead. + $page->attachFileToField('File', $png_path); + $page->pressButton('Save'); + $this->assertSession()->pageTextNotContains('Only files with the following extensions are allowed: png.'); + $this->assertSession()->pageTextNotContains('Unable to upload replacement file.'); + $this->assertSession()->addressEquals('hu/admin/content/media'); + + // Assert the file content was overridden. + $updatedImage = $this->loadMediaEntityByName('Foobar'); + $translation = $updatedImage->getTranslation('hu'); + $translation_file_2 = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertEquals($translation_file_1->id(), $translation_file_2->id()); + $this->assertEquals($translation_file_1->getFileUri(), $translation_file_2->getFileUri()); + // File name incremented previously because we already used once this png. + $this->assertEquals('druplicon_0.png', $translation_file_2->getFilename()); + $this->assertFalse($translation_file_2->isTemporary()); + + // Assert the file size is different from the original. + $this->assertNotEquals($translation_file_1->getSize(), $translation_file_2->getSize()); + + // Now replace the file completely. + $this->drupalGet("/hu/media/{$originalImage->id()}/edit"); + $page->attachFileToField('File', $png_path); + $page->uncheckField('keep_original_filename'); + $page->pressButton('Save'); + $this->assertSession()->addressEquals('hu/admin/content/media'); + + // Reload the media and assert the image is replaced. + $updatedImage = $this->loadMediaEntityByName('Foobar'); + $translation = $updatedImage->getTranslation('hu'); + $translation_file_3 = $this->loadFileEntity($translation->getSource()->getSourceFieldValue($translation)); + $this->assertNotEquals($translation_file_2->id(), $translation_file_3->id()); + $this->assertNotEquals($translation_file_2->getFileUri(), $translation_file_3->getFileUri()); + $this->assertEquals('image-test.png', $translation_file_3->getFilename()); + $this->assertFalse($translation_file_3->isTemporary()); + + // Delete the translation. + $updatedImage->removeTranslation('hu'); + $updatedImage->save(); + + // Make the file column in the image field non-translatable. + $property_settings = [ + 'alt' => 'alt', + 'title' => 'title', + 'file' => 0, + ]; + $field_config->setThirdPartySetting('content_translation', 'translation_sync', $property_settings); + $field_config->save(); + + // Proceed with adding a new translation to the image media. + $this->drupalGet("/hu/media/{$updatedImage->id()}/edit/translations/add/en/hu"); + // Since the file in the image field is not translatable and visible on the + // form, we don't allow replacement because we would change the original. + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + + // Now make the image field itself non-translatable. + $field_config->setTranslatable(FALSE); + $field_config->save(); + + // Proceed with adding a new translation to the image media. + $this->drupalGet("/hu/media/{$updatedImage->id()}/edit/translations/add/en/hu"); + // Since the image field is not translatable and visible on the form, it + // acts just as the default language version where we allow replacement. + $this->assertSession()->fieldExists('files[replacement_file]'); + $this->assertSession()->fieldExists('keep_original_filename'); + + // Now make the non-translatable fields hidden on the translation form. + $contentLanguageSettings = ContentLanguageSettings::load('media.image'); + $setting = ['untranslatable_fields_hide' => 1]; + $contentLanguageSettings->setThirdPartySetting('content_translation', 'bundle_settings', $setting); + $contentLanguageSettings->save(); + + // Navigate to the translation form, and make sure our widget is not there. + $this->drupalGet("/hu/media/{$updatedImage->id()}/edit/translations/add/en/hu"); + $this->assertSession()->fieldNotExists('files[replacement_file]'); + $this->assertSession()->fieldNotExists('keep_original_filename'); + } + +}