Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/wp-admin/includes/image-edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,15 @@ function wp_restore_image( $post_id ) {
return $msg;
}

if ( ! is_array( $meta ) ) {
$msg->error = __( 'Cannot load image metadata.' );
return $msg;
}

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();
Expand Down Expand Up @@ -983,6 +992,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();
}
Expand Down
128 changes: 128 additions & 0 deletions tests/phpunit/tests/ajax/wpAjaxImageEditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
* @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.
*/
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';
$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
*
* @covers ::wp_restore_image
*
* @dataProvider data_save_image_with_unusable_sizes_metadata
*
* @param array $meta Replacement `sizes` metadata to store before restoring.
*/
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';
$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<non-empty-string, array{ 0: array{ sizes?: mixed } }>
*/
public function data_save_image_with_unusable_sizes_metadata(): array {
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 ) ),
);
}
}
Loading