-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Media: Enable HEIC/HEIF uploads when server lacks image editor support #11323
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from 8 commits
bf9f644
71a9e8b
630266f
b261f30
c711280
b20bbca
eea07d2
d976d2e
d01bab6
f728cfb
d6f69d6
beb9ffe
9b6b768
7f976bb
df33a07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5760,6 +5760,45 @@ function wp_show_heic_upload_error( $plupload_settings ) { | |
| return $plupload_settings; | ||
| } | ||
|
|
||
| /** | ||
| * Deletes the HEIC companion file when its attachment is deleted. | ||
| * | ||
| * When the client-side media flow sideloads a HEIC original alongside a | ||
| * JPEG derivative, the HEIC filename is recorded in $metadata['original']. | ||
| * WordPress only tracks 'original_image' in wp_delete_attachment_files(), | ||
| * so without this hook the HEIC file would linger on disk after the | ||
| * attachment is deleted. | ||
| * | ||
| * @since 7.1.0 | ||
| * | ||
| * @param int $post_id Attachment ID being deleted. | ||
| */ | ||
| function wp_delete_attachment_heic_companion_file( $post_id ) { | ||
| $metadata = wp_get_attachment_metadata( $post_id, true ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. DB read on every
Bulk-deleting 10 000 non-HEIC attachments now triggers 10 000 extra Altitude note (separate concern): the whole companion-file mechanism is a HEIC-specific bandaid bolted next to — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, the change outlined above will address this
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Key renamed in 7f976bb. Leaving the per-delete metadata read / move into |
||
|
|
||
| if ( empty( $metadata['original'] ) || ! is_string( $metadata['original'] ) ) { | ||
| return; | ||
| } | ||
|
|
||
| $attached_file = get_attached_file( $post_id, true ); | ||
|
|
||
| if ( ! $attached_file ) { | ||
| return; | ||
| } | ||
|
|
||
| $uploads = wp_get_upload_dir(); | ||
|
|
||
| if ( empty( $uploads['basedir'] ) ) { | ||
| return; | ||
| } | ||
|
|
||
| $heic_path = path_join( dirname( $attached_file ), wp_basename( (string) $metadata['original'] ) ); | ||
|
adamsilverstein marked this conversation as resolved.
Outdated
|
||
|
|
||
| if ( file_exists( $heic_path ) ) { | ||
| wp_delete_file_from_directory( $heic_path, $uploads['basedir'] ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Allows PHP's getimagesize() to be debuggable when necessary. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1380,7 +1380,7 @@ public function get_index( $request ) { | |
| $available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 ); | ||
|
|
||
| // Image output formats. | ||
| $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' ); | ||
| $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic', 'image/heif' ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The permissions bypass added in this PR uses Result: a — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. makes sense, would be good to make sure we have test coverage for these image types.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7f976bb — added the |
||
| $output_formats = array(); | ||
| foreach ( $input_formats as $mime_type ) { | ||
| /** This filter is documented in wp-includes/media.php */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,10 @@ public function register_routes() { | |
| $valid_image_sizes = array_keys( wp_get_registered_image_subsizes() ); | ||
| // Special case to set 'original_image' in attachment metadata. | ||
| $valid_image_sizes[] = 'original'; | ||
| // HEIC/HEIF companion original preserved alongside the JPEG derivative. | ||
| // Stored under its own meta key so it never collides with 'original' | ||
| // (which the scaled-sideload flow also writes to). | ||
| $valid_image_sizes[] = 'original-heic'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Magic string Rename one site, miss the other, and the enum + handler disagree — sideloads of — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will update
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7f976bb — extracted |
||
| // Used for PDF thumbnails. | ||
| $valid_image_sizes[] = 'full'; | ||
| // Client-side big image threshold: sideload the scaled version. | ||
|
|
@@ -82,21 +86,26 @@ public function register_routes() { | |
| 'callback' => array( $this, 'sideload_item' ), | ||
| 'permission_callback' => array( $this, 'sideload_item_permissions_check' ), | ||
| 'args' => array( | ||
| 'id' => array( | ||
| 'id' => array( | ||
| 'description' => __( 'Unique identifier for the attachment.' ), | ||
| 'type' => 'integer', | ||
| ), | ||
| 'image_size' => array( | ||
| 'image_size' => array( | ||
| 'description' => __( 'Image size.' ), | ||
| 'type' => 'string', | ||
| 'enum' => $valid_image_sizes, | ||
| 'required' => true, | ||
| ), | ||
| 'convert_format' => array( | ||
| 'convert_format' => array( | ||
| 'type' => 'boolean', | ||
| 'default' => true, | ||
| 'description' => __( 'Whether to convert image formats.' ), | ||
| ), | ||
| 'generate_sub_sizes' => array( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This arg is added to the sideload route schema, but Failure mode: a client POSTs to — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| 'description' => __( 'Whether to generate image sub sizes from the sideloaded file.' ), | ||
| 'type' => 'boolean', | ||
| 'default' => false, | ||
| ), | ||
| ), | ||
| ), | ||
| 'allow_batch' => $this->allow_batch, | ||
|
|
@@ -258,6 +267,17 @@ public function create_item_permissions_check( $request ) { | |
| $prevent_unsupported_uploads = false; | ||
| } | ||
|
|
||
| // Always allow HEIC/HEIF uploads through even if the server's image | ||
| // editor doesn't support them. The client-side canvas fallback will | ||
| // handle processing using the browser's native HEVC decoder. | ||
| if ( | ||
| $prevent_unsupported_uploads && | ||
| ! empty( $files['file']['type'] ) && | ||
| wp_is_heic_image_mime_type( $files['file']['type'] ) | ||
| ) { | ||
| $prevent_unsupported_uploads = false; | ||
| } | ||
|
|
||
| // If the upload is an image, check if the server can handle the mime type. | ||
| if ( | ||
| $prevent_unsupported_uploads && | ||
|
|
@@ -2090,6 +2110,13 @@ public function sideload_item( WP_REST_Request $request ) { | |
|
|
||
| if ( 'original' === $image_size ) { | ||
| $metadata['original_image'] = wp_basename( $path ); | ||
| } elseif ( 'original-heic' === $image_size ) { | ||
| // HEIC companion original: stored under its own meta key so | ||
| // the scaled-sideload flow (which writes 'original_image') | ||
| // cannot clobber it. 'original_image' keeps pointing at the | ||
| // web-viewable JPEG derivative. Cleanup on attachment delete | ||
| // is handled by wp_delete_attachment_heic_companion_file(). | ||
| $metadata['original'] = wp_basename( $path ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generic metadata key + comment contradicts the code. The companion HEIC filename is stored under The comment at lines 2113–2118 (and its twin at line 72) even claims "Stored under its own meta key so it never collides with 'original'" — but the implementation writes to — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, right - i just commented on this above. this was changed during work on the PR and I agree it became too generic. I'll fix this with a less generic name, not specific to heic though so we can use it with other formats as needed.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7f976bb — renamed the key to |
||
| } elseif ( 'scaled' === $image_size ) { | ||
| // The current attached file is the original; record it as original_image. | ||
| $current_file = get_attached_file( $attachment_id, true ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Tests for the `wp_delete_attachment_heic_companion_file()` function. | ||
| * | ||
| * @group media | ||
| * @covers ::wp_delete_attachment_heic_companion_file | ||
| */ | ||
| class Tests_Media_wpDeleteAttachmentHeicCompanionFile extends WP_UnitTestCase { | ||
|
|
||
| public function tear_down() { | ||
| $this->remove_added_uploads(); | ||
|
|
||
| parent::tear_down(); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64915 | ||
| */ | ||
| public function test_deletes_heic_file_recorded_in_metadata_original() { | ||
| $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); | ||
|
|
||
| $attached_file = get_attached_file( $attachment_id, true ); | ||
| $dir = dirname( $attached_file ); | ||
| $heic_name = 'companion-' . wp_generate_password( 6, false ) . '.heic'; | ||
| $heic_path = $dir . '/' . $heic_name; | ||
|
|
||
| // Create a dummy companion file on disk. | ||
| file_put_contents( $heic_path, 'test' ); | ||
| $this->assertFileExists( $heic_path, 'Test fixture should be on disk.' ); | ||
|
|
||
| // Record the companion under metadata['original'] as the sideload route does. | ||
| $metadata = wp_get_attachment_metadata( $attachment_id, true ); | ||
| $metadata['original'] = $heic_name; | ||
| wp_update_attachment_metadata( $attachment_id, $metadata ); | ||
|
|
||
| wp_delete_attachment( $attachment_id, true ); | ||
|
|
||
| $this->assertFileDoesNotExist( $heic_path, 'Companion HEIC file should be deleted alongside the attachment.' ); | ||
| } | ||
|
|
||
| /** | ||
| * @ticket 64915 | ||
| */ | ||
| public function test_noop_when_metadata_original_is_missing() { | ||
| $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); | ||
|
|
||
| // Sanity: no 'original' key on freshly-created metadata. | ||
| $metadata = wp_get_attachment_metadata( $attachment_id, true ); | ||
| $this->assertArrayNotHasKey( 'original', $metadata ); | ||
|
|
||
| // Should not raise even though the hook fires. | ||
| wp_delete_attachment( $attachment_id, true ); | ||
|
|
||
| $this->assertNull( get_post( $attachment_id ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Guards against $metadata['original'] holding a non-string value (e.g. | ||
| * the array form some flows write). Regression coverage for GB #78128. | ||
| * | ||
| * @ticket 64915 | ||
| */ | ||
| public function test_noop_when_metadata_original_is_not_a_string() { | ||
| $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); | ||
| $attached_file = get_attached_file( $attachment_id, true ); | ||
|
|
||
| $metadata = wp_get_attachment_metadata( $attachment_id, true ); | ||
| $metadata['original'] = array( 'file' => 'should-not-delete.heic' ); | ||
| wp_update_attachment_metadata( $attachment_id, $metadata ); | ||
|
|
||
| // Should not raise (no path_join() / file_exists() on an array). | ||
| wp_delete_attachment_heic_companion_file( $attachment_id ); | ||
|
|
||
| $this->assertFileExists( $attached_file, 'Attached file should still be on disk; the hook must bail on non-string original.' ); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This assertion can't fail. The test sets To actually cover the regression you're guarding against (someone simplifies the early return and trips a — 🤖 Claude Code
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7f976bb — the guard test now writes a real |
||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.