Make WordPress Core


Ignore:
Timestamp:
08/04/2026 01:56:04 AM (2 months ago)
Author:
westonruter
Message:

Media: Normalize unusable sizes attachment metadata.

Attachment metadata is untyped, and the sizes key is not guaranteed to be present or to hold an array. Sub-size generation can leave it out entirely, and a plugin filtering wp_get_attachment_metadata can replace it with anything. wp_save_image() validated only that the metadata itself was an array before passing $meta['sizes'] to array_merge(), so an absent or scalar value raised a TypeError and the image editor returned an HTTP 500 mid-save. wp_restore_image() had the same gap at $meta['sizes'][ $default_size ] = $data, where a string 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.

wp_get_attachment_metadata() now returns false whenever the metadata is not an array, on the $unfiltered path as well as after the filter, matching the documented array|false return. A sizes key holding a non-array is replaced with an empty array, so every caller can rely on the key being an array whenever it is present. The key is not invented when it is absent: audio, video and document attachments legitimately store metadata without it, and callers such as wp-admin/post.php read the metadata unfiltered in order to modify it and write it back, so normalizing there would persist into the database.

The image editor entry points fill in the missing key themselves, and wp_prepare_attachment_for_js() now checks the dimensions of the full entry alongside its filename before reading them, removing the "Undefined array key" warnings raised for a sizes array that carries no usable full size. PHPUnit coverage is added for all three functions.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/12744.
Follow-up to r11965, r23873, r38949, r49084, r62978.

Props josephscott, westonruter, mukesh27, irozum, ugyensupport, nazmulasif.
See #65686, #64898.
Fixes #65748.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/ajax/wpAjaxImageEditor.php

    r59202 r63002  
    195195                $this->assertSameSetsWithIndex( $pre_file_sizes, $post_restore_file_sizes, 'Filesize should have restored after restoring the original image.' );
    196196        }
     197
     198        /**
     199         * Ensure editing an image does not fatal when the attachment metadata has no usable `sizes` data.
     200         *
     201         * Attachment metadata is not guaranteed to contain a `sizes` array. It can be missing when
     202         * sub-size generation never ran or failed (for example `wp_create_image_subsizes()` returns an
     203         * empty array when the file cannot be parsed), or when it is removed by a plugin filtering
     204         * `wp_get_attachment_metadata`. `wp_save_image()` only validates that the metadata itself is an
     205         * array, then passes `$meta['sizes']` straight to `array_merge()`.
     206         *
     207         * @ticket 65748
     208         *
     209         * @covers ::wp_save_image
     210         *
     211         * @dataProvider data_save_image_with_unusable_sizes_metadata
     212         *
     213         * @param array{ sizes?: mixed } $meta Attachment metadata to store before editing, minus the file-specific keys.
     214         */
     215        public function test_save_image_with_unusable_sizes_metadata( array $meta ) {
     216                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     217
     218                $filename = DIR_TESTDATA . '/images/canola.jpg';
     219                $contents = file_get_contents( $filename );
     220                $this->assertIsString( $contents );
     221
     222                $upload = wp_upload_bits( wp_basename( $filename ), null, $contents );
     223                $id     = $this->_make_attachment( $upload );
     224                $this->assertIsInt( $id );
     225
     226                $original_meta = wp_get_attachment_metadata( $id );
     227                $this->assertIsArray( $original_meta );
     228
     229                // Keep the real file/dimension data, only make `sizes` unusable.
     230                $meta = array_merge(
     231                        wp_array_slice_assoc( $original_meta, array( 'width', 'height', 'file', 'filesize' ) ),
     232                        $meta
     233                );
     234
     235                wp_update_attachment_metadata( $id, $meta );
     236
     237                $_REQUEST['action']  = 'image-editor';
     238                $_REQUEST['context'] = 'edit-attachment';
     239                $_REQUEST['postid']  = $id;
     240                $_REQUEST['target']  = 'all';
     241                $_REQUEST['do']      = 'save';
     242                $_REQUEST['history'] = '[{"c":{"x":5,"y":8,"w":289,"h":322}}]';
     243
     244                $ret = wp_save_image( $id );
     245
     246                $this->assertObjectNotHasProperty( 'error', $ret, 'Saving the image should not have returned an error.' );
     247
     248                $saved_meta = wp_get_attachment_metadata( $id );
     249
     250                $this->assertIsArray( $saved_meta, 'The saved attachment metadata should be an array.' );
     251                $this->assertArrayHasKey( 'sizes', $saved_meta );
     252                $this->assertIsArray( $saved_meta['sizes'], 'The saved attachment metadata should contain a `sizes` array.' );
     253                $this->assertArrayHasKey( 'thumbnail', $saved_meta['sizes'], 'The edited image should have regenerated the thumbnail size.' );
     254        }
     255
     256        /**
     257         * Ensure restoring an image does not fatal when the attachment metadata has no usable `sizes` data.
     258         *
     259         * `wp_restore_image()` writes each backed up size with `$meta['sizes'][ $default_size ] = $data`
     260         * without ever checking that `$meta['sizes']` is an array. A scalar value raises
     261         * "Cannot use a scalar value as an array", and `false` is deprecated as of PHP 8.1 and
     262         * an error as of PHP 9. The same metadata that fatals `wp_save_image()` reaches this code.
     263         *
     264         * @ticket 65748
     265         *
     266         * @covers ::wp_restore_image
     267         *
     268         * @dataProvider data_save_image_with_unusable_sizes_metadata
     269         *
     270         * @param array{ sizes?: mixed } $meta Replacement `sizes` metadata to store before restoring.
     271         */
     272        public function test_restore_image_with_unusable_sizes_metadata( array $meta ) {
     273                require_once ABSPATH . 'wp-admin/includes/image-edit.php';
     274
     275                $filename = DIR_TESTDATA . '/images/canola.jpg';
     276                $contents = file_get_contents( $filename );
     277                $this->assertIsString( $contents );
     278
     279                $upload = wp_upload_bits( wp_basename( $filename ), null, $contents );
     280                $id     = $this->_make_attachment( $upload );
     281                $this->assertIsInt( $id );
     282
     283                $_REQUEST['action']  = 'image-editor';
     284                $_REQUEST['context'] = 'edit-attachment';
     285                $_REQUEST['postid']  = $id;
     286                $_REQUEST['target']  = 'all';
     287                $_REQUEST['do']      = 'save';
     288                $_REQUEST['history'] = '[{"c":{"x":5,"y":8,"w":289,"h":322}}]';
     289
     290                // Edit the image first so that `_wp_attachment_backup_sizes` holds the original sizes.
     291                wp_save_image( $id );
     292
     293                $this->assertNotEmpty(
     294                        get_post_meta( $id, '_wp_attachment_backup_sizes', true ),
     295                        'The image edit should have stored backup sizes to restore from.'
     296                );
     297
     298                // Keep the metadata written by the edit, only make `sizes` unusable.
     299                $edited_meta = wp_get_attachment_metadata( $id );
     300                $this->assertIsArray( $edited_meta );
     301                unset( $edited_meta['sizes'] );
     302
     303                wp_update_attachment_metadata( $id, array_merge( $edited_meta, $meta ) );
     304
     305                wp_restore_image( $id );
     306
     307                $restored_meta = wp_get_attachment_metadata( $id );
     308                $this->assertIsArray( $restored_meta );
     309
     310                $this->assertArrayHasKey( 'sizes', $restored_meta );
     311                $this->assertIsArray( $restored_meta['sizes'], 'The restored attachment metadata should contain a `sizes` array.' );
     312                $this->assertArrayHasKey( 'thumbnail', $restored_meta['sizes'], 'The restored image should have the thumbnail size restored from the backup sizes.' );
     313        }
     314
     315        /**
     316         * Data provider.
     317         *
     318         * @return array<non-empty-string, array{ 0: array{ sizes?: mixed } }>
     319         */
     320        public function data_save_image_with_unusable_sizes_metadata(): array {
     321                return array(
     322                        'no sizes key'  => array( array() ),
     323                        'null sizes'    => array( array( 'sizes' => null ) ),
     324                        'empty string'  => array( array( 'sizes' => '' ) ),
     325                        'string sizes'  => array( array( 'sizes' => 'not-an-array' ) ),
     326                        'boolean sizes' => array( array( 'sizes' => false ) ),
     327                );
     328        }
    197329}
Note: See TracChangeset for help on using the changeset viewer.