From 877a4b04c56c46c564ea7b1a1020cc93733ffdd8 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Tue, 28 Jul 2026 14:13:35 -0600 Subject: [PATCH 1/7] Image editor: fatal error when attachment metadata has no `sizes` array https://core.trac.wordpress.org/ticket/65748 --- src/wp-admin/includes/image-edit.php | 12 ++ .../phpunit/tests/ajax/wpAjaxImageEditor.php | 128 ++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index a192ef0000c17..b399c1742821b 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -825,6 +825,14 @@ function wp_restore_image( $post_id ) { return $msg; } + if ( ! is_array( $meta ) ) { + $meta = array(); + } + + if ( ! isset( $meta['sizes'] ) || ! is_array( $meta['sizes'] ) ) { + $meta['sizes'] = array(); + } + $parts = pathinfo( $file ); $suffix = time() . rand( 100, 999 ); $default_sizes = get_intermediate_image_sizes(); @@ -983,6 +991,10 @@ function wp_save_image( $post_id ) { return $return; } + if ( ! isset( $meta['sizes'] ) || ! is_array( $meta['sizes'] ) ) { + $meta['sizes'] = array(); + } + if ( ! is_array( $backup_sizes ) ) { $backup_sizes = array(); } diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index 205f61636149c..82df94cd06c1b 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -194,4 +194,132 @@ public function test_filesize_restored_after_restoring_original_image() { $this->assertSameSetsWithIndex( $pre_file_sizes, $post_restore_file_sizes, 'Filesize should have restored after restoring the original image.' ); } + + /** + * Ensure editing an image does not fatal when the attachment metadata has no usable `sizes` data. + * + * Attachment metadata is not guaranteed to contain a `sizes` array. It can be missing when + * sub-size generation never ran or failed (for example `wp_create_image_subsizes()` returns an + * empty array when the file cannot be parsed), or when it is removed by a plugin filtering + * `wp_get_attachment_metadata`. `wp_save_image()` only validates that the metadata itself is an + * array, then passes `$meta['sizes']` straight to `array_merge()`. + * + * @ticket 65748 + * + * @dataProvider data_save_image_with_unusable_sizes_metadata + * + * @param array $meta Attachment metadata to store before editing, minus the file-specific keys. + * + * @covers ::wp_save_image + */ + public function test_save_image_with_unusable_sizes_metadata( $meta ) { + require_once ABSPATH . 'wp-admin/includes/image-edit.php'; + + $filename = DIR_TESTDATA . '/images/canola.jpg'; + $contents = file_get_contents( $filename ); + + $upload = wp_upload_bits( wp_basename( $filename ), null, $contents ); + $id = $this->_make_attachment( $upload ); + + $original_meta = wp_get_attachment_metadata( $id ); + + // Keep the real file/dimension data, only make `sizes` unusable. + $meta = array_merge( + array( + 'width' => $original_meta['width'], + 'height' => $original_meta['height'], + 'file' => $original_meta['file'], + 'filesize' => $original_meta['filesize'], + ), + $meta + ); + + wp_update_attachment_metadata( $id, $meta ); + + $_REQUEST['action'] = 'image-editor'; + $_REQUEST['context'] = 'edit-attachment'; + $_REQUEST['postid'] = $id; + $_REQUEST['target'] = 'all'; + $_REQUEST['do'] = 'save'; + $_REQUEST['history'] = '[{"c":{"x":5,"y":8,"w":289,"h":322}}]'; + + $ret = wp_save_image( $id ); + + $this->assertObjectNotHasProperty( 'error', $ret, 'Saving the image should not have returned an error.' ); + + $saved_meta = wp_get_attachment_metadata( $id ); + + $this->assertIsArray( $saved_meta, 'The saved attachment metadata should be an array.' ); + $this->assertIsArray( $saved_meta['sizes'], 'The saved attachment metadata should contain a `sizes` array.' ); + $this->assertArrayHasKey( 'thumbnail', $saved_meta['sizes'], 'The edited image should have regenerated the thumbnail size.' ); + } + + /** + * Ensure restoring an image does not fatal when the attachment metadata has no usable `sizes` data. + * + * `wp_restore_image()` writes each backed up size with `$meta['sizes'][ $default_size ] = $data` + * without ever checking that `$meta['sizes']` is an array. A scalar value raises + * "Cannot use a scalar value as an array", and `false` is deprecated as of PHP 8.1 and + * an error as of PHP 9. The same metadata that fatals `wp_save_image()` reaches this code. + * + * @ticket 65748 + * + * @dataProvider data_save_image_with_unusable_sizes_metadata + * + * @param array $meta Replacement `sizes` metadata to store before restoring. + * + * @covers ::wp_restore_image + */ + public function test_restore_image_with_unusable_sizes_metadata( $meta ) { + require_once ABSPATH . 'wp-admin/includes/image-edit.php'; + + $filename = DIR_TESTDATA . '/images/canola.jpg'; + $contents = file_get_contents( $filename ); + + $upload = wp_upload_bits( wp_basename( $filename ), null, $contents ); + $id = $this->_make_attachment( $upload ); + + $_REQUEST['action'] = 'image-editor'; + $_REQUEST['context'] = 'edit-attachment'; + $_REQUEST['postid'] = $id; + $_REQUEST['target'] = 'all'; + $_REQUEST['do'] = 'save'; + $_REQUEST['history'] = '[{"c":{"x":5,"y":8,"w":289,"h":322}}]'; + + // Edit the image first so that `_wp_attachment_backup_sizes` holds the original sizes. + wp_save_image( $id ); + + $this->assertNotEmpty( + get_post_meta( $id, '_wp_attachment_backup_sizes', true ), + 'The image edit should have stored backup sizes to restore from.' + ); + + // Keep the metadata written by the edit, only make `sizes` unusable. + $edited_meta = wp_get_attachment_metadata( $id ); + unset( $edited_meta['sizes'] ); + + wp_update_attachment_metadata( $id, array_merge( $edited_meta, $meta ) ); + + wp_restore_image( $id ); + + $restored_meta = wp_get_attachment_metadata( $id ); + + $this->assertIsArray( $restored_meta['sizes'], 'The restored attachment metadata should contain a `sizes` array.' ); + $this->assertArrayHasKey( 'thumbnail', $restored_meta['sizes'], 'The restored image should have the thumbnail size restored from the backup sizes.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_save_image_with_unusable_sizes_metadata() { + return array( + 'no sizes key' => array( array() ), + 'null sizes' => array( array( 'sizes' => null ) ), + 'empty string' => array( array( 'sizes' => '' ) ), + 'string sizes' => array( array( 'sizes' => 'not-an-array' ) ), + 'boolean sizes' => array( array( 'sizes' => false ) ), + ); + } } From f07fbdaab971942c336dfa81917b6384fdc53fdd Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:41:09 -0600 Subject: [PATCH 2/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/wp-admin/includes/image-edit.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/wp-admin/includes/image-edit.php b/src/wp-admin/includes/image-edit.php index b399c1742821b..dce312ed63145 100644 --- a/src/wp-admin/includes/image-edit.php +++ b/src/wp-admin/includes/image-edit.php @@ -826,7 +826,8 @@ function wp_restore_image( $post_id ) { } if ( ! is_array( $meta ) ) { - $meta = array(); + $msg->error = __( 'Cannot load image metadata.' ); + return $msg; } if ( ! isset( $meta['sizes'] ) || ! is_array( $meta['sizes'] ) ) { From 991857210e7a4f9157b35dc0600daa1da10a0b4a Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:41:28 -0600 Subject: [PATCH 3/7] Update tests/phpunit/tests/ajax/wpAjaxImageEditor.php Co-authored-by: Weston Ruter --- tests/phpunit/tests/ajax/wpAjaxImageEditor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index 82df94cd06c1b..f110a70549f60 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -311,9 +311,9 @@ public function test_restore_image_with_unusable_sizes_metadata( $meta ) { /** * Data provider. * - * @return array[] + * @return array */ - public function data_save_image_with_unusable_sizes_metadata() { + public function data_save_image_with_unusable_sizes_metadata(): array { return array( 'no sizes key' => array( array() ), 'null sizes' => array( array( 'sizes' => null ) ), From 1dec494413ae1c9b923ae7c55de7433caa816227 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:41:37 -0600 Subject: [PATCH 4/7] Update tests/phpunit/tests/ajax/wpAjaxImageEditor.php Co-authored-by: Weston Ruter --- tests/phpunit/tests/ajax/wpAjaxImageEditor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index f110a70549f60..9fa72ba8e4d17 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -270,7 +270,7 @@ public function test_save_image_with_unusable_sizes_metadata( $meta ) { * * @covers ::wp_restore_image */ - public function test_restore_image_with_unusable_sizes_metadata( $meta ) { + public function test_restore_image_with_unusable_sizes_metadata( array $meta ) { require_once ABSPATH . 'wp-admin/includes/image-edit.php'; $filename = DIR_TESTDATA . '/images/canola.jpg'; From feaef53fb5c4606b4b88548660571ff347643a32 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:41:47 -0600 Subject: [PATCH 5/7] Update tests/phpunit/tests/ajax/wpAjaxImageEditor.php Co-authored-by: Weston Ruter --- tests/phpunit/tests/ajax/wpAjaxImageEditor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index 9fa72ba8e4d17..ccdacf393995d 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -212,7 +212,7 @@ public function test_filesize_restored_after_restoring_original_image() { * * @covers ::wp_save_image */ - public function test_save_image_with_unusable_sizes_metadata( $meta ) { + public function test_save_image_with_unusable_sizes_metadata( array $meta ) { require_once ABSPATH . 'wp-admin/includes/image-edit.php'; $filename = DIR_TESTDATA . '/images/canola.jpg'; From d0e8c30d0069dfbaaa75e587875f042dc5a33478 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:42:02 -0600 Subject: [PATCH 6/7] Update tests/phpunit/tests/ajax/wpAjaxImageEditor.php Co-authored-by: Mukesh Panchal --- tests/phpunit/tests/ajax/wpAjaxImageEditor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index ccdacf393995d..c18ddaee8bafe 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -206,11 +206,11 @@ public function test_filesize_restored_after_restoring_original_image() { * * @ticket 65748 * + * @covers ::wp_save_image + * * @dataProvider data_save_image_with_unusable_sizes_metadata * * @param array $meta Attachment metadata to store before editing, minus the file-specific keys. - * - * @covers ::wp_save_image */ public function test_save_image_with_unusable_sizes_metadata( array $meta ) { require_once ABSPATH . 'wp-admin/includes/image-edit.php'; From e373c2aef178522294f74707a8bdefe74213ab20 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Wed, 29 Jul 2026 16:42:13 -0600 Subject: [PATCH 7/7] Update tests/phpunit/tests/ajax/wpAjaxImageEditor.php Co-authored-by: Mukesh Panchal --- tests/phpunit/tests/ajax/wpAjaxImageEditor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php index c18ddaee8bafe..c7af8a2f9c892 100644 --- a/tests/phpunit/tests/ajax/wpAjaxImageEditor.php +++ b/tests/phpunit/tests/ajax/wpAjaxImageEditor.php @@ -264,11 +264,11 @@ public function test_save_image_with_unusable_sizes_metadata( array $meta ) { * * @ticket 65748 * + * @covers ::wp_restore_image + * * @dataProvider data_save_image_with_unusable_sizes_metadata * * @param array $meta Replacement `sizes` metadata to store before restoring. - * - * @covers ::wp_restore_image */ public function test_restore_image_with_unusable_sizes_metadata( array $meta ) { require_once ABSPATH . 'wp-admin/includes/image-edit.php';