diff --git a/modules/images/webp-uploads/load.php b/modules/images/webp-uploads/load.php
index 3c8f915a90..e49da078fd 100644
--- a/modules/images/webp-uploads/load.php
+++ b/modules/images/webp-uploads/load.php
@@ -684,6 +684,151 @@ function webp_uploads_update_rest_attachment( WP_REST_Response $response, WP_Pos
}
add_filter( 'rest_prepare_attachment', 'webp_uploads_update_rest_attachment', 10, 3 );
+/**
+ * Adds sources to metadata for an attachment.
+ *
+ * @since n.e.x.t
+ *
+ * @param array $metadata Metadata of the attachment.
+ * @param array $valid_mime_transforms List of valid mime transforms for current image mime type.
+ * @param array $main_images Path of all main image files of all mime types.
+ * @param array $subsized_images Path of all subsized image file of all mime types.
+ * @return array Metadata with sources added.
+ */
+function webp_uploads_update_sources( $metadata, $valid_mime_transforms, $main_images, $subsized_images ) {
+ foreach ( $valid_mime_transforms as $targeted_mime ) {
+ // Make sure the path and file exists as those values are being accessed.
+ if ( ! isset( $main_images[ $targeted_mime ]['path'], $main_images[ $targeted_mime ]['file'] ) || ! file_exists( $main_images[ $targeted_mime ]['path'] ) ) {
+ continue;
+ }
+
+ $image_directory = pathinfo( $main_images[ $targeted_mime ]['path'], PATHINFO_DIRNAME );
+
+ // Add sources to original image metadata.
+ $metadata['sources'][ $targeted_mime ] = array(
+ 'file' => $main_images[ $targeted_mime ]['file'],
+ 'filesize' => filesize( $main_images[ $targeted_mime ]['path'] ),
+ );
+
+ foreach ( $metadata['sizes'] as $size_name => $size_details ) {
+ if ( empty( $subsized_images[ $targeted_mime ][ $size_name ]['file'] ) ) {
+ continue;
+ }
+
+ // Add sources to resized image metadata.
+ $subsize_path = path_join( $image_directory, $subsized_images[ $targeted_mime ][ $size_name ]['file'] );
+
+ if ( ! file_exists( $subsize_path ) ) {
+ continue;
+ }
+
+ $metadata['sizes'][ $size_name ]['sources'][ $targeted_mime ] = array(
+ 'file' => $subsized_images[ $targeted_mime ][ $size_name ]['file'],
+ 'filesize' => filesize( $subsize_path ),
+ );
+ }
+ }
+
+ return $metadata;
+}
+
+/**
+ * Creates additional image formats when original image is edited.
+ *
+ * @since n.e.x.t
+ *
+ * @param bool|null $override Value to return instead of saving. Default null.
+ * @param string $file_path Name of the file to be saved.
+ * @param WP_Image_Editor $editor The image editor instance.
+ * @param string $mime_type The mime type of the image.
+ * @param int $post_id Attachment post ID.
+ * @return bool|null Potentially modified $override value.
+ */
+function webp_uploads_update_image_onchange( $override, $file_path, $editor, $mime_type, $post_id ) {
+ if ( null !== $override ) {
+ return $override;
+ }
+
+ $transforms = webp_uploads_get_upload_image_mime_transforms();
+ if ( empty( $transforms[ $mime_type ] ) ) {
+ return $override;
+ }
+
+ $mime_transforms = $transforms[ $mime_type ];
+ // This variable allows to unhook the logic from within the closure without the need fo a function name.
+ $callback_executed = false;
+ add_filter(
+ 'wp_update_attachment_metadata',
+ function ( $metadata, $post_meta_id ) use ( $post_id, $file_path, $mime_type, $editor, $mime_transforms, &$callback_executed ) {
+ if ( $post_meta_id !== $post_id ) {
+ return $metadata;
+ }
+
+ // This callback was already executed for this post, nothing to do at this point.
+ if ( $callback_executed ) {
+ return $metadata;
+ }
+ $callback_executed = true;
+
+ // No sizes to be created.
+ if ( empty( $metadata['sizes'] ) ) {
+ return $metadata;
+ }
+
+ $old_metadata = wp_get_attachment_metadata( $post_id );
+ $resize_sizes = array();
+ foreach ( $old_metadata['sizes'] as $size_name => $size_details ) {
+ if ( isset( $metadata['sizes'][ $size_name ] ) && ! empty( $metadata['sizes'][ $size_name ] ) &&
+ $metadata['sizes'][ $size_name ]['file'] !== $old_metadata['sizes'][ $size_name ]['file'] ) {
+ $resize_sizes[ $size_name ] = $metadata['sizes'][ $size_name ];
+ }
+ }
+
+ $allowed_mimes = array_flip( wp_get_mime_types() );
+ $original_directory = pathinfo( $file_path, PATHINFO_DIRNAME );
+ $filename = pathinfo( $file_path, PATHINFO_FILENAME );
+ $main_images = array();
+ $subsized_images = array();
+ foreach ( $mime_transforms as $targeted_mime ) {
+ if ( $targeted_mime === $mime_type ) {
+ $main_images[ $targeted_mime ] = array(
+ 'path' => $file_path,
+ 'file' => pathinfo( $file_path, PATHINFO_BASENAME ),
+ );
+ $subsized_images[ $targeted_mime ] = $metadata['sizes'];
+ continue;
+ }
+
+ if ( ! isset( $allowed_mimes[ $targeted_mime ] ) || ! is_string( $allowed_mimes[ $targeted_mime ] ) ) {
+ continue;
+ }
+
+ if ( ! $editor::supports_mime_type( $targeted_mime ) ) {
+ continue;
+ }
+
+ $extension = explode( '|', $allowed_mimes[ $targeted_mime ] );
+ $destination = trailingslashit( $original_directory ) . "{$filename}.{$extension[0]}";
+ $result = $editor->save( $destination, $targeted_mime );
+
+ if ( is_wp_error( $result ) ) {
+ continue;
+ }
+
+ $main_images[ $targeted_mime ] = $result;
+ $subsized_images[ $targeted_mime ] = $editor->multi_resize( $resize_sizes );
+ }
+
+ return webp_uploads_update_sources( $metadata, $mime_transforms, $main_images, $subsized_images );
+ },
+ 10,
+ 2
+ );
+
+ return $override;
+}
+add_filter( 'wp_save_image_editor_file', 'webp_uploads_update_image_onchange', 10, 7 );
+
/**
* Inspect if the current call to `wp_update_attachment_metadata()` was done from within the context
* of an edit to an attachment either restore or other type of edit, in that case we perform operations
diff --git a/phpcs.xml.dist b/phpcs.xml.dist
index f542112bef..cfdc9b1708 100644
--- a/phpcs.xml.dist
+++ b/phpcs.xml.dist
@@ -19,6 +19,9 @@
./tests
+
+ module-i18n.php
+
tests/*
diff --git a/tests/modules/images/webp-uploads/webp-uploads-test.php b/tests/modules/images/webp-uploads/webp-uploads-test.php
index f1b9717df8..8a624ced51 100644
--- a/tests/modules/images/webp-uploads/webp-uploads-test.php
+++ b/tests/modules/images/webp-uploads/webp-uploads-test.php
@@ -873,15 +873,31 @@ public function it_should_restore_the_sources_array_from_the_backup_when_an_imag
wp_restore_image( $attachment_id );
+ $this->assertImageHasSource( $attachment_id, 'image/jpeg' );
+ $this->assertImageHasSource( $attachment_id, 'image/webp' );
+
$metadata = wp_get_attachment_metadata( $attachment_id );
- $this->assertArrayHasKey( 'sources', $metadata );
+
$this->assertSame( $backup_sources['full-orig'], $metadata['sources'] );
$this->assertSame( $backup_sources, get_post_meta( $attachment_id, '_wp_attachment_backup_sources', true ) );
$backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
- foreach ( $metadata['sizes'] as $size_name => $properties ) {
- $this->assertArrayHasKey( 'sources', $backup_sizes[ $size_name . '-orig' ] );
- $this->assertSame( $backup_sizes[ $size_name . '-orig' ]['sources'], $properties['sources'] );
+ foreach ( $backup_sizes as $size_name => $properties ) {
+ // We are only interested in the original filenames to be compared against the backup and restored values.
+ if ( false === strpos( $size_name, '-orig' ) ) {
+ $this->assertSizeNameIsHashed( '', $size_name, "{$size_name} is not a valid edited name" );
+ continue;
+ }
+
+ $size_name = str_replace( '-orig', '', $size_name );
+ // Full name is verified above.
+ if ( 'full' === $size_name ) {
+ continue;
+ }
+
+ $this->assertArrayHasKey( $size_name, $metadata['sizes'] );
+ $this->assertArrayHasKey( 'sources', $metadata['sizes'][ $size_name ] );
+ $this->assertSame( $properties['sources'], $metadata['sizes'][ $size_name ]['sources'] );
}
}
@@ -938,7 +954,17 @@ public function it_should_prevent_to_backup_the_full_size_image_if_only_the_thum
$this->assertArrayHasKey( 'sources', $backup_sizes['thumbnail-orig'] );
$metadata = wp_get_attachment_metadata( $attachment_id );
- $this->assertArrayHasKey( 'sources', $metadata );
+
+ $this->assertImageHasSource( $attachment_id, 'image/jpeg' );
+ $this->assertImageHasSource( $attachment_id, 'image/webp' );
+
+ $this->assertImageHasSizeSource( $attachment_id, 'thumbnail', 'image/jpeg' );
+ $this->assertImageHasSizeSource( $attachment_id, 'thumbnail', 'image/webp' );
+
+ foreach ( $metadata['sizes'] as $size_name => $properties ) {
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, 'image/jpeg' );
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, 'image/webp' );
+ }
}
/**
@@ -959,7 +985,12 @@ public function it_should_backup_the_image_when_all_images_except_the_thumbnail_
$this->assertArrayHasKey( 'full-orig', $backup_sources );
$this->assertSame( $metadata['sources'], $backup_sources['full-orig'] );
- $this->assertArrayNotHasKey( 'sources', wp_get_attachment_metadata( $attachment_id ), 'The sources attributes was not removed from the metadata.' );
+ $updated_metadata = wp_get_attachment_metadata( $attachment_id );
+
+ $this->assertArrayHasKey( 'sources', $updated_metadata );
+ $this->assertNotSame( $metadata['sources'], $updated_metadata['sources'] );
+ $this->assertImageHasSource( $attachment_id, 'image/jpeg' );
+ $this->assertImageHasSource( $attachment_id, 'image/webp' );
$backup_sizes = get_post_meta( $attachment_id, '_wp_attachment_backup_sizes', true );
$this->assertIsArray( $backup_sizes );
@@ -973,6 +1004,39 @@ public function it_should_backup_the_image_when_all_images_except_the_thumbnail_
}
}
+ /**
+ * Update source attributes when webp is edited.
+ *
+ * @test
+ */
+ public function it_should_validate_source_attribute_update_when_webp_edited() {
+ $attachment_id = $this->factory->attachment->create_upload_object( TESTS_PLUGIN_DIR . '/tests/testdata/modules/images/leafs.jpg' );
+
+ $editor = new WP_Image_Edit( $attachment_id );
+ $editor->crop( 1000, 200, 0, 0 )->save();
+ $this->assertTrue( $editor->success() );
+
+ $this->assertImageHasSource( $attachment_id, 'image/webp' );
+ $this->assertImageHasSource( $attachment_id, 'image/jpeg' );
+
+ $metadata = wp_get_attachment_metadata( $attachment_id );
+
+ $this->assertFileNameIsEdited( $metadata['sources']['image/webp']['file'] );
+ $this->assertFileNameIsEdited( $metadata['sources']['image/jpeg']['file'] );
+
+ $this->assertArrayHasKey( 'sources', $metadata );
+ $this->assertArrayHasKey( 'sizes', $metadata );
+
+ foreach ( $metadata['sizes'] as $size_name => $properties ) {
+ $this->assertArrayHasKey( 'sources', $properties );
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, 'image/webp' );
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, 'image/jpeg' );
+
+ $this->assertFileNameIsEdited( $properties['sources']['image/webp']['file'] );
+ $this->assertFileNameIsEdited( $properties['sources']['image/jpeg']['file'] );
+ }
+ }
+
/**
* Allow the upload of a WebP image if at least one editor supports the format
*
@@ -1058,7 +1122,7 @@ public function it_should_u_se_the_next_available_hash_for_the_full_size_image_o
remove_filter( 'wp_update_attachment_metadata', 'webp_uploads_update_attachment_metadata' );
$editor->rotate_right()->save();
- $this->assertRegExp( '/full-\d{13}/', webp_uploads_get_next_full_size_key_from_backup( $attachment_id ) );
+ $this->assertSizeNameIsHashed( 'full', webp_uploads_get_next_full_size_key_from_backup( $attachment_id ) );
}
/**
@@ -1106,7 +1170,7 @@ public function it_should_store_the_metadata_on_the_next_available_hash() {
$backup_sources_keys = array_keys( $backup_sources );
$this->assertSame( 'full-orig', reset( $backup_sources_keys ) );
- $this->assertRegExp( '/full-\d{13}/', end( $backup_sources_keys ) );
+ $this->assertSizeNameIsHashed( 'full', end( $backup_sources_keys ) );
$this->assertSame( $sources, end( $backup_sources ) );
}
diff --git a/tests/utils/TestCase/ImagesTestCase.php b/tests/utils/TestCase/ImagesTestCase.php
index 0deb152bc9..cc44dec263 100644
--- a/tests/utils/TestCase/ImagesTestCase.php
+++ b/tests/utils/TestCase/ImagesTestCase.php
@@ -13,6 +13,9 @@
* @method void assertImageHasSizeSource( $attachment_id, $size, $mime_type, $message ) Asserts that the image has the appropriate source for the subsize.
* @method void assertImageNotHasSource( $attachment_id, $mime_type, $message ) Asserts that the image doesn't have the appropriate source.
* @method void assertImageNotHasSizeSource( $attachment_id, $size, $mime_type, $message ) Asserts that the image doesn't have the appropriate source for the subsize.
+ * @method void assertFileNameIsEdited( string $filename, string $message = '' ) Asserts that the provided file name was edited by WordPress contains an e{WITH_13_DIGITS} on the filename.
+ * @method void assertFileNameIsNotEdited( string $filename, string $message = '' ) Asserts that the provided file name was edited by WordPress contains an e{WITH_13_DIGITS} on the filename.
+ * @method void assertSizeNameIsHashed( string $size_name, string $hashed_size_name, string $message = '' ) Asserts that the provided size name is an edited name that contains a hash with digits.
*/
abstract class ImagesTestCase extends WP_UnitTestCase {
@@ -76,4 +79,37 @@ public static function assertImageNotHasSizeSource( $attachment_id, $size, $mime
self::assertThat( $attachment_id, $constraint, $message );
}
+ /**
+ * Asserts that the provided file name was edited by WordPress contains an e{WITH_13_DIGITS} on the filename.
+ *
+ * @param string $filename The name of the filename to be asserted.
+ * @param string $message The Error message used to display when the assertion fails.
+ * @return void
+ */
+ public static function assertFileNameIsEdited( $filename, $message = '' ) {
+ self::assertRegExp( '/e\d{13}/', $filename, $message );
+ }
+
+ /**
+ * Asserts that the provided file name was edited by WordPress contains an e{WITH_13_DIGITS} on the filename.
+ *
+ * @param string $filename The name of the filename to be asserted.
+ * @param string $message The Error message used to display when the assertion fails.
+ * @return void
+ */
+ public static function assertFileNameIsNotEdited( $filename, $message = '' ) {
+ self::assertNotRegExp( '/e\d{13}/', $filename, $message );
+ }
+
+ /**
+ * Asserts that the provided size name is an edited name that contains a hash with digits.
+ *
+ * @param string $size_name The size name we are looking for.
+ * @param string $hashed_size_name The current size name we are comparing against.
+ * @param string $message The Error message used to display when the assertion fails.
+ * @return void
+ */
+ public static function assertSizeNameIsHashed( $size_name, $hashed_size_name, $message = '' ) {
+ self::assertRegExp( "/{$size_name}-\d{13}/", $hashed_size_name, $message );
+ }
}