diff --git a/plugins/webp-uploads/hooks.php b/plugins/webp-uploads/hooks.php index 7f5ef6958f..62d114639a 100644 --- a/plugins/webp-uploads/hooks.php +++ b/plugins/webp-uploads/hooks.php @@ -74,6 +74,30 @@ function webp_uploads_create_sources_property( array $metadata, int $attachment_ return $metadata; } + /* + * If the base image metadata is missing its dimensions, the core sub-size + * generation failed at upload time. This happens when wp_create_image_subsizes() + * bails early because wp_getimagesize() could not read the file -- typically an + * intermittent race where the upload is read before it has finished being written + * to disk. The result is metadata without `width`/`height`/`sizes`, which renders + * the attachment at 1x1 pixels both in the Media Library and on the front end. + * + * The file is readable by the time this filter runs, so attempt to recover the + * original dimensions from it. If they can be recovered, the full-size image will + * display correctly again (responsive sub-sizes still require regeneration). If the + * file still cannot be read, bail without decorating or persisting a partial record. + * + * See https://github.com/WordPress/performance/issues/2468. + */ + if ( ! isset( $metadata['width'], $metadata['height'] ) || (int) $metadata['width'] < 1 || (int) $metadata['height'] < 1 ) { + $image_size = wp_getimagesize( $file ); + if ( ! is_array( $image_size ) ) { + return $metadata; + } + $metadata['width'] = $image_size[0]; + $metadata['height'] = $image_size[1]; + } + // Make sure the top level `sources` key is a valid array. if ( ! isset( $metadata['sources'] ) || ! is_array( $metadata['sources'] ) ) { $metadata['sources'] = array(); diff --git a/plugins/webp-uploads/tests/test-load.php b/plugins/webp-uploads/tests/test-load.php index 0a90d88023..6ad883027a 100644 --- a/plugins/webp-uploads/tests/test-load.php +++ b/plugins/webp-uploads/tests/test-load.php @@ -123,6 +123,67 @@ public function test_it_should_create_the_original_mime_type_as_well_with_all_th } } + /** + * Recover the original image dimensions when the base metadata is incomplete. + * + * When core's wp_create_image_subsizes() cannot read the uploaded file (an + * intermittent race at upload time) it returns metadata without width/height/sizes, + * which makes the attachment render at 1x1 pixels. The filter should recover the + * dimensions from the now-readable file instead of decorating a 1x1 record. + * + * @link https://github.com/WordPress/performance/issues/2468 + * + * @covers ::webp_uploads_create_sources_property + */ + public function test_it_should_recover_dimensions_when_base_metadata_is_incomplete(): void { + $attachment_id = self::factory()->attachment->create_upload_object( TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg' ); + + // The metadata generated during upload holds the real dimensions. + $generated = wp_get_attachment_metadata( $attachment_id ); + $this->assertArrayHasKey( 'width', $generated ); + $this->assertArrayHasKey( 'height', $generated ); + $this->assertGreaterThan( 1, $generated['width'] ); + $this->assertGreaterThan( 1, $generated['height'] ); + + // Simulate the core failure: base metadata without dimensions or sizes. + $result = webp_uploads_create_sources_property( array(), $attachment_id ); + + // The dimensions should be recovered from the readable file, not left empty. + $this->assertArrayHasKey( 'width', $result ); + $this->assertArrayHasKey( 'height', $result ); + $this->assertSame( $generated['width'], $result['width'] ); + $this->assertSame( $generated['height'], $result['height'] ); + + // Sources are attached to the now-valid record rather than to a 1x1 placeholder. + $this->assertArrayHasKey( 'sources', $result ); + } + + /** + * Do not decorate or persist metadata when the file cannot be read. + * + * If the dimensions still cannot be recovered (the file is genuinely unreadable), + * the filter must bail without inventing dimensions or adding a sources property. + * + * @link https://github.com/WordPress/performance/issues/2468 + * + * @covers ::webp_uploads_create_sources_property + */ + public function test_it_should_not_decorate_metadata_when_file_is_missing(): void { + $attachment_id = self::factory()->attachment->create_upload_object( TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg' ); + + // Remove the underlying file so it can no longer be read. + $file = get_attached_file( $attachment_id ); + $this->assertIsString( $file ); + wp_delete_file( $file ); + + $result = webp_uploads_create_sources_property( array(), $attachment_id ); + + // No dimensions invented and no sources attached to the empty record. + $this->assertArrayNotHasKey( 'width', $result ); + $this->assertArrayNotHasKey( 'height', $result ); + $this->assertArrayNotHasKey( 'sources', $result ); + } + /** * Create JPEG and output format for JPEG images, if perflab_generate_webp_and_jpeg option set. *