From bf9f644f76d27f7bb1c71643491a7efaf266822e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 20 Mar 2026 09:59:22 -0700 Subject: [PATCH 01/11] Media: Allow HEIC/HEIF uploads when server lacks support Bypass the `wp_prevent_unsupported_mime_type_uploads` check for HEIC/HEIF images so they can be stored even when the server's image editor doesn't support them. The client-side canvas fallback handles processing using the browser's native HEVC decoder via createImageBitmap(). --- .../class-wp-rest-attachments-controller.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index cb714d5a5de71..50467f788ada4 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -258,6 +258,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 && From b261f30d1d30e43eef56ddd3b9896e6e632bc595 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 11:54:02 -0400 Subject: [PATCH 02/11] REST API: Add HEIC client-side support to the sideload route. Extends the /wp/v2/media//sideload route so the client-side media flow can upload a HEIC/HEIF companion original alongside the JPEG derivative: - Adds 'original-heic' to the allowed image_size enum. The companion filename is recorded under $metadata['original'] so it never collides with 'original_image', which the scaled-sideload flow owns. - Adds a 'generate_sub_sizes' boolean arg (default false) so callers that handle processing client-side can suppress server-side sub-size generation per request. - Adds 'image/heif' to the image_output_formats input list returned by the REST API root index. Backport of GB #76731. --- .../rest-api/class-wp-rest-server.php | 2 +- .../class-wp-rest-attachments-controller.php | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 704a990298826..192f76e2d4c5a 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -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' ); $output_formats = array(); foreach ( $input_formats as $mime_type ) { /** This filter is documented in wp-includes/media.php */ diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 3de551148d99d..d979872f96158 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -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'; // 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( + 'description' => __( 'Whether to generate image sub sizes from the sideloaded file.' ), + 'type' => 'boolean', + 'default' => false, + ), ), ), 'allow_batch' => $this->allow_batch, @@ -2101,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 ); } elseif ( 'scaled' === $image_size ) { // The current attached file is the original; record it as original_image. $current_file = get_attached_file( $attachment_id, true ); From c711280a0c2b811099c6ea57dcd848e4750eafa2 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 11:54:09 -0400 Subject: [PATCH 03/11] Media: Delete 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 stored in $metadata['original']. wp_delete_attachment_files() only tracks 'original_image', so without this hook the HEIC file would linger on disk after the attachment is removed. wp_delete_attachment_heic_companion_file() reads the meta key, guards against non-string values (e.g. arrays written by other flows), and deletes the file when present. Hooked into the delete_attachment action via default-filters.php. Backport of GB #76731, with the is_string() guard from GB #78128. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 39 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index cf895eb748dbe..de8e9fb3364d6 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -562,6 +562,7 @@ add_action( 'transition_post_status', '_wp_customize_publish_changeset', 10, 3 ); add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); +add_action( 'delete_attachment', 'wp_delete_attachment_heic_companion_file' ); add_action( 'transition_post_status', '_wp_keep_alive_customize_changeset_dependent_auto_drafts', 20, 3 ); // Block Theme Previews. diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index d318a275a9607..13fb4f9c6a7d9 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -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 ); + + 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'] ) ); + + if ( file_exists( $heic_path ) ) { + wp_delete_file_from_directory( $heic_path, $uploads['basedir'] ); + } +} + /** * Allows PHP's getimagesize() to be debuggable when necessary. * From b20bbcaf447c2478680897595abdd8bf23dfa0aa Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 11:57:14 -0400 Subject: [PATCH 04/11] Tests: Cover the HEIC client-side sideload and companion-delete flow. Adds REST API controller tests: - The sideload route exposes 'original-heic' in the image_size enum. - The sideload route exposes a 'generate_sub_sizes' boolean arg defaulting to false. - Sideloading an 'original-heic' image writes the filename to $metadata['original'] and leaves 'original_image' untouched. Adds wp_delete_attachment_heic_companion_file() unit tests: - The companion HEIC is removed when the attachment is deleted. - The hook is a no-op when $metadata['original'] is missing. - The hook bails when $metadata['original'] is not a string (regression coverage for the guard added in GB #78128). --- .../wpDeleteAttachmentHeicCompanionFile.php | 77 +++++++++++++++++++ .../rest-api/rest-attachments-controller.php | 74 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php diff --git a/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php b/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php new file mode 100644 index 0000000000000..d3e2dc0256321 --- /dev/null +++ b/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php @@ -0,0 +1,77 @@ +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.' ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 79e9d23cf9dd3..def3559b3d602 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3351,6 +3351,80 @@ public function test_sideload_route_includes_scaled_enum() { $this->assertContains( 'scaled', $args[ $param_name ]['enum'], 'image_size enum should include scaled.' ); } + /** + * Tests that the sideload endpoint includes 'original-heic' in the image_size enum. + * + * @ticket 64915 + */ + public function test_sideload_route_includes_original_heic_enum() { + $this->enable_client_side_media_processing(); + + $routes = rest_get_server()->get_routes(); + $endpoint = $routes['/wp/v2/media/(?P[\d]+)/sideload'][0]; + $args = $endpoint['args']; + + $this->assertArrayHasKey( 'image_size', $args, 'Route should have image_size arg.' ); + $this->assertContains( 'original-heic', $args['image_size']['enum'], 'image_size enum should include original-heic.' ); + } + + /** + * Tests that the sideload endpoint exposes the generate_sub_sizes arg. + * + * @ticket 64915 + */ + public function test_sideload_route_includes_generate_sub_sizes_arg() { + $this->enable_client_side_media_processing(); + + $routes = rest_get_server()->get_routes(); + $endpoint = $routes['/wp/v2/media/(?P[\d]+)/sideload'][0]; + $args = $endpoint['args']; + + $this->assertArrayHasKey( 'generate_sub_sizes', $args, 'Route should have generate_sub_sizes arg.' ); + $this->assertSame( 'boolean', $args['generate_sub_sizes']['type'], 'generate_sub_sizes should be a boolean.' ); + $this->assertFalse( $args['generate_sub_sizes']['default'], 'generate_sub_sizes should default to false on sideload.' ); + } + + /** + * Tests sideloading an 'original-heic' companion file alongside its JPEG + * derivative. The HEIC filename is recorded under $metadata['original'] + * so it does not collide with 'original_image', which the scaled-sideload + * flow owns. + * + * @ticket 64915 + * @requires function imagejpeg + */ + public function test_sideload_original_heic_writes_metadata_original() { + $this->enable_client_side_media_processing(); + + wp_set_current_user( self::$author_id ); + + // Create the JPEG attachment that the HEIC will be a companion to. + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment_id = $response->get_data()['id']; + + $this->assertSame( 201, $response->get_status() ); + + // Sideload the HEIC companion. Uses a JPEG body since the size enum, + // not the file format, is what we're exercising here. + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.heic' ); + $request->set_param( 'image_size', 'original-heic' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Sideloading original-heic should succeed.' ); + + $metadata = wp_get_attachment_metadata( $attachment_id ); + $this->assertArrayHasKey( 'original', $metadata, "Metadata should contain 'original' for the HEIC companion." ); + $this->assertMatchesRegularExpression( '/canola.*\.heic$/', $metadata['original'], "Metadata 'original' should reference the HEIC filename." ); + $this->assertArrayNotHasKey( 'original_image', $metadata, "Metadata 'original_image' should be untouched by the HEIC sideload." ); + } + /** * Tests the filter_wp_unique_filename method handles the -scaled suffix. * From eea07d222055212a5660255fb078aa6860778bee Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 12:39:17 -0400 Subject: [PATCH 05/11] Tests: Use HEIC fixture and convert_format=false for original-heic sideload. The test was sending JPEG bytes with a .heic filename, which wp_check_filetype_and_ext() corrected to canola-1.jpg before the metadata assertion ran. Switch to the real test-image.heic fixture, set Content-Type accordingly, and pass convert_format=false to disable the default HEIC -> JPEG output mapping so the .heic extension is preserved. --- .../tests/rest-api/rest-attachments-controller.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index def3559b3d602..acadc3556247a 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3408,13 +3408,15 @@ public function test_sideload_original_heic_writes_metadata_original() { $this->assertSame( 201, $response->get_status() ); - // Sideload the HEIC companion. Uses a JPEG body since the size enum, - // not the file format, is what we're exercising here. + // Sideload the HEIC companion using the real HEIC fixture. `convert_format` + // is disabled so the default HEIC -> JPEG output mapping does not rename + // the file or append an alt-extension suffix. $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" ); - $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Type', 'image/heic' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.heic' ); $request->set_param( 'image_size', 'original-heic' ); - $request->set_body( file_get_contents( self::$test_file ) ); + $request->set_param( 'convert_format', false ); + $request->set_body( file_get_contents( DIR_TESTDATA . '/images/test-image.heic' ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status(), 'Sideloading original-heic should succeed.' ); From d976d2e4a675d868621144acb429c0d06cdc1d2b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 13:28:42 -0400 Subject: [PATCH 06/11] Tests: Refresh wp-api-generated.js fixture for the sideload route. Add 'original-heic' to the image_size enum and the missing generate_sub_sizes arg so the schema fixture matches what the live REST index now reports. Without this the test-fixtures step fails the git diff --exit-code check. --- tests/qunit/fixtures/wp-api-generated.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index fa03d9751fe99..c3bf2bf452928 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3703,6 +3703,7 @@ mockedApiResponse.Schema = { "1536x1536", "2048x2048", "original", + "original-heic", "full", "scaled" ], @@ -3713,6 +3714,12 @@ mockedApiResponse.Schema = { "default": true, "description": "Whether to convert image formats.", "required": false + }, + "generate_sub_sizes": { + "description": "Whether to generate image sub sizes from the sideloaded file.", + "type": "boolean", + "default": false, + "required": false } } } From 615bc14e37c9f60455222de87e493810889c11ae Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 28 May 2026 14:00:23 -0400 Subject: [PATCH 07/11] Media: Add animated GIF to video sideload sizes and companion-file cleanup. When the client-side media flow converts an opaque animated GIF to a video, the converted MP4/WebM and a static first-frame JPEG poster are sideloaded alongside the GIF and recorded in the attachment metadata under 'animated_video' and 'animated_video_poster'. The GIF itself remains the attachment, and the editor switches the inserted block to the core/video block's 'GIF' variation by reading those keys. Accept the two new sideload sizes (`animated-video` and `animated-video-poster`) on the `/sideload` route by adding them to the `image_size` enum and writing each one to its own metadata key. Add `wp_delete_attachment_animated_gif_video()`, hooked on `delete_attachment`, plus a `wp_get_attachment_animated_gif_companion_path()` helper. Both apply the same path hardening already used for the HEIC companion: only the basename of the recorded metadata is trusted, the path is rebuilt from the attachment's own directory, and deletion goes through `wp_delete_file_from_directory()` against the uploads basedir so the cleanup can only ever remove a sibling file of the attachment. Backport of WordPress/gutenberg#78410. Builds on the HEIC companion infrastructure added in #11323. --- src/wp-includes/default-filters.php | 1 + src/wp-includes/media.php | 69 ++++++++ .../class-wp-rest-attachments-controller.php | 15 ++ .../wpDeleteAttachmentAnimatedGifVideo.php | 166 ++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index de8e9fb3364d6..c18bd913608cd 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -563,6 +563,7 @@ add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); add_action( 'delete_attachment', 'wp_delete_attachment_heic_companion_file' ); +add_action( 'delete_attachment', 'wp_delete_attachment_animated_gif_video' ); add_action( 'transition_post_status', '_wp_keep_alive_customize_changeset_dependent_auto_drafts', 20, 3 ); // Block Theme Previews. diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 13fb4f9c6a7d9..f8126dc7fc6b7 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5799,6 +5799,75 @@ function wp_delete_attachment_heic_companion_file( $post_id ) { } } +/** + * Returns the absolute path to one of an attachment's animated-GIF companion files + * (the converted video or its poster), if recorded. + * + * The path is rebuilt from the attachment's own (trusted) directory plus the + * recorded basename, so the stored metadata cannot point anywhere else. + * + * @since 7.1.0 + * + * @param int $attachment_id Attachment ID. + * @param string $meta_key Metadata key holding the companion basename + * ('animated_video' or 'animated_video_poster'). + * @return string|null Absolute file path, or null when there is no companion. + */ +function wp_get_attachment_animated_gif_companion_path( $attachment_id, $meta_key ) { + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + + if ( empty( $metadata[ $meta_key ] ) || ! is_string( $metadata[ $meta_key ] ) ) { + return null; + } + + // Only ever trust the basename of the recorded value; strip any path + // components so the metadata can't reference another directory. + $name = wp_basename( $metadata[ $meta_key ] ); + + if ( '' === $name ) { + return null; + } + + $attached_file = get_attached_file( $attachment_id, true ); + + if ( ! $attached_file ) { + return null; + } + + return path_join( dirname( $attached_file ), $name ); +} + +/** + * Deletes the sideloaded video and poster companions when their animated GIF + * attachment is deleted. + * + * When the client-side media flow converts an opaque animated GIF to a video, + * the converted MP4/WebM and a static first-frame JPEG poster are sideloaded + * alongside the GIF and recorded in $metadata['animated_video'] and + * $metadata['animated_video_poster']. WordPress core only tracks 'original_image' + * in wp_delete_attachment_files(), so without this hook the companions 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_animated_gif_video( $post_id ) { + $uploads = wp_get_upload_dir(); + + if ( empty( $uploads['basedir'] ) ) { + return; + } + + foreach ( array( 'animated_video', 'animated_video_poster' ) as $meta_key ) { + $path = wp_get_attachment_animated_gif_companion_path( $post_id, $meta_key ); + + if ( $path && file_exists( $path ) ) { + wp_delete_file_from_directory( $path, $uploads['basedir'] ); + } + } +} + /** * Allows PHP's getimagesize() to be debuggable when necessary. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index d979872f96158..dc7e57ff22ebe 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -72,6 +72,11 @@ public function register_routes() { // 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'; + // Animated GIF → video companions: the converted MP4/WebM and its + // first-frame poster. Stored under their own metadata keys and read + // by the editor to switch the block to the core/video GIF variation. + $valid_image_sizes[] = 'animated-video'; + $valid_image_sizes[] = 'animated-video-poster'; // Used for PDF thumbnails. $valid_image_sizes[] = 'full'; // Client-side big image threshold: sideload the scaled version. @@ -2117,6 +2122,16 @@ public function sideload_item( WP_REST_Request $request ) { // web-viewable JPEG derivative. Cleanup on attachment delete // is handled by wp_delete_attachment_heic_companion_file(). $metadata['original'] = wp_basename( $path ); + } elseif ( 'animated-video' === $image_size ) { + // Converted video companion of an animated GIF. The GIF stays + // the attachment; the editor reads this key to switch the block + // to the core/video block's "GIF" variation. Cleanup on attachment + // delete is handled by wp_delete_attachment_animated_gif_video(). + $metadata['animated_video'] = wp_basename( $path ); + } elseif ( 'animated-video-poster' === $image_size ) { + // Static first-frame poster for the converted video. Used as the + // video block's poster and deleted alongside the video. + $metadata['animated_video_poster'] = wp_basename( $path ); } elseif ( 'scaled' === $image_size ) { // The current attached file is the original; record it as original_image. $current_file = get_attached_file( $attachment_id, true ); diff --git a/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php b/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php new file mode 100644 index 0000000000000..f70f965bf3fab --- /dev/null +++ b/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php @@ -0,0 +1,166 @@ +remove_added_uploads(); + + parent::tear_down(); + } + + /** + * Creates a GIF attachment plus on-disk video + poster companions, and + * records the companion basenames under the attachment metadata as the + * sideload route does. + * + * @param bool $with_poster Whether to also create the poster companion. + * @return array{0:int,1:string,2:?string} [ attachment_id, video_path, poster_path ] + */ + private function create_gif_attachment_with_companions( $with_poster = true ) { + $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $dir = dirname( get_attached_file( $attachment_id, true ) ); + $video_name = 'animated-' . wp_generate_password( 6, false ) . '.mp4'; + $video_path = $dir . '/' . $video_name; + file_put_contents( $video_path, 'video' ); + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + $metadata = is_array( $metadata ) ? $metadata : array(); + $metadata['animated_video'] = $video_name; + + $poster_path = null; + if ( $with_poster ) { + $poster_name = 'poster-' . wp_generate_password( 6, false ) . '.jpg'; + $poster_path = $dir . '/' . $poster_name; + file_put_contents( $poster_path, 'poster' ); + $metadata['animated_video_poster'] = $poster_name; + } + + wp_update_attachment_metadata( $attachment_id, $metadata ); + + return array( $attachment_id, $video_path, $poster_path ); + } + + /** + * The companion path is rebuilt from the attachment's own directory plus + * the recorded basename. + * + * @ticket 64915 + */ + public function test_companion_path_resolves_inside_attachment_directory() { + list( $attachment_id, $video_path, $poster_path ) = $this->create_gif_attachment_with_companions(); + + $this->assertSame( + $video_path, + wp_get_attachment_animated_gif_companion_path( $attachment_id, 'animated_video' ) + ); + $this->assertSame( + $poster_path, + wp_get_attachment_animated_gif_companion_path( $attachment_id, 'animated_video_poster' ) + ); + } + + /** + * Only the basename of the recorded value is trusted, so a path traversal + * in the metadata cannot escape the attachment's directory. + * + * @ticket 64915 + */ + public function test_companion_path_ignores_directory_traversal() { + $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + $dir = dirname( get_attached_file( $attachment_id, true ) ); + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + $metadata['animated_video'] = '../../evil.mp4'; + wp_update_attachment_metadata( $attachment_id, $metadata ); + + $this->assertSame( + $dir . '/evil.mp4', + wp_get_attachment_animated_gif_companion_path( $attachment_id, 'animated_video' ) + ); + } + + /** + * @ticket 64915 + */ + public function test_companion_path_is_null_without_companion() { + $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $this->assertNull( + wp_get_attachment_animated_gif_companion_path( $attachment_id, 'animated_video' ) + ); + } + + /** + * @ticket 64915 + */ + public function test_companion_path_is_null_when_metadata_is_not_a_string() { + $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + $metadata['animated_video'] = array( 'file' => 'should-not-delete.mp4' ); + wp_update_attachment_metadata( $attachment_id, $metadata ); + + $this->assertNull( + wp_get_attachment_animated_gif_companion_path( $attachment_id, 'animated_video' ) + ); + } + + /** + * Both the converted video and the poster are removed when the + * attachment is deleted. + * + * @ticket 64915 + */ + public function test_deletes_companion_files_on_attachment_delete() { + list( $attachment_id, $video_path, $poster_path ) = $this->create_gif_attachment_with_companions(); + + $this->assertFileExists( $video_path, 'Test fixture video should be on disk.' ); + $this->assertFileExists( $poster_path, 'Test fixture poster should be on disk.' ); + + wp_delete_attachment( $attachment_id, true ); + + $this->assertFileDoesNotExist( $video_path, 'Converted video should be deleted alongside the attachment.' ); + $this->assertFileDoesNotExist( $poster_path, 'Poster should be deleted alongside the attachment.' ); + } + + /** + * Only the video companion is removed when only it is recorded + * (transparent GIFs have no poster). + * + * @ticket 64915 + */ + public function test_deletes_only_video_when_no_poster_recorded() { + list( $attachment_id, $video_path ) = $this->create_gif_attachment_with_companions( false ); + + $this->assertFileExists( $video_path, 'Test fixture video should be on disk.' ); + + wp_delete_attachment( $attachment_id, true ); + + $this->assertFileDoesNotExist( $video_path, 'Converted video should be deleted alongside the attachment.' ); + } + + /** + * @ticket 64915 + */ + public function test_noop_when_no_companion_metadata() { + $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); + + $metadata = wp_get_attachment_metadata( $attachment_id, true ); + $this->assertArrayNotHasKey( 'animated_video', $metadata ); + $this->assertArrayNotHasKey( 'animated_video_poster', $metadata ); + + // Should not raise even though the hook fires. + wp_delete_attachment( $attachment_id, true ); + + $this->assertNull( get_post( $attachment_id ) ); + } +} From c91074cefbbc4ab017c0da3808d586f68382bb97 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 18 Jun 2026 09:38:00 +0200 Subject: [PATCH 08/11] Tests: Regenerate REST API client fixture for animated-video sub-sizes. The sideload schema now exposes the animated-video and animated-video-poster sizes, so the generated QUnit fixture must be rebuilt to match. Keeps the "version-controlled files not modified" CI check green. --- tests/qunit/fixtures/wp-api-generated.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index c3bf2bf452928..23daae97c4378 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3704,6 +3704,8 @@ mockedApiResponse.Schema = { "2048x2048", "original", "original-heic", + "animated-video", + "animated-video-poster", "full", "scaled" ], From de54fdfb37071b31bde3b8bc5f4ed8bd0a3251e3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 27 Jun 2026 11:07:29 -0700 Subject: [PATCH 09/11] Media: Drop the duplicate HEIC companion handling from the animated-GIF backport The HEIC source-format companion (sideloaded as `original-heic`, recorded in attachment metadata, and cleaned up on `delete_attachment`) belongs to the HEIC upload backport, where it lands once under the `source_image` metadata key. This animated-GIF-to-video backport had bundled its own copy that wrote a bare `original` key and registered a second, conflicting `wp_delete_attachment_heic_companion_file()` definition. Remove that duplicate so this PR only carries the animated-video work: - Drop `wp_delete_attachment_heic_companion_file()` and its `delete_attachment` hook (defined by the HEIC backport instead). - Drop the `original-heic` image_size enum value and its sideload branch. - Remove the now-orphaned HEIC tests and the fixture enum entry. The animated-video / animated-video-poster companions and their dedicated `wp_delete_attachment_animated_gif_video()` cleanup are unchanged. --- src/wp-includes/default-filters.php | 1 - src/wp-includes/media.php | 39 ---------- .../class-wp-rest-attachments-controller.php | 11 --- .../wpDeleteAttachmentHeicCompanionFile.php | 77 ------------------- .../rest-api/rest-attachments-controller.php | 59 -------------- tests/qunit/fixtures/wp-api-generated.js | 1 - 6 files changed, 188 deletions(-) delete mode 100644 tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index c18bd913608cd..77ecbf9856376 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -562,7 +562,6 @@ add_action( 'transition_post_status', '_wp_customize_publish_changeset', 10, 3 ); add_action( 'admin_enqueue_scripts', '_wp_customize_loader_settings' ); add_action( 'delete_attachment', '_delete_attachment_theme_mod' ); -add_action( 'delete_attachment', 'wp_delete_attachment_heic_companion_file' ); add_action( 'delete_attachment', 'wp_delete_attachment_animated_gif_video' ); add_action( 'transition_post_status', '_wp_keep_alive_customize_changeset_dependent_auto_drafts', 20, 3 ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index f8126dc7fc6b7..16fa5bc49fdac 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5760,45 +5760,6 @@ 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 ); - - 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'] ) ); - - if ( file_exists( $heic_path ) ) { - wp_delete_file_from_directory( $heic_path, $uploads['basedir'] ); - } -} - /** * Returns the absolute path to one of an attachment's animated-GIF companion files * (the converted video or its poster), if recorded. diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index dc7e57ff22ebe..d6ad86c3fcf70 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -68,10 +68,6 @@ 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'; // Animated GIF → video companions: the converted MP4/WebM and its // first-frame poster. Stored under their own metadata keys and read // by the editor to switch the block to the core/video GIF variation. @@ -2115,13 +2111,6 @@ 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 ); } elseif ( 'animated-video' === $image_size ) { // Converted video companion of an animated GIF. The GIF stays // the attachment; the editor reads this key to switch the block diff --git a/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php b/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php deleted file mode 100644 index d3e2dc0256321..0000000000000 --- a/tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php +++ /dev/null @@ -1,77 +0,0 @@ -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.' ); - } -} diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index acadc3556247a..aa3f56af08cfa 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3351,22 +3351,6 @@ public function test_sideload_route_includes_scaled_enum() { $this->assertContains( 'scaled', $args[ $param_name ]['enum'], 'image_size enum should include scaled.' ); } - /** - * Tests that the sideload endpoint includes 'original-heic' in the image_size enum. - * - * @ticket 64915 - */ - public function test_sideload_route_includes_original_heic_enum() { - $this->enable_client_side_media_processing(); - - $routes = rest_get_server()->get_routes(); - $endpoint = $routes['/wp/v2/media/(?P[\d]+)/sideload'][0]; - $args = $endpoint['args']; - - $this->assertArrayHasKey( 'image_size', $args, 'Route should have image_size arg.' ); - $this->assertContains( 'original-heic', $args['image_size']['enum'], 'image_size enum should include original-heic.' ); - } - /** * Tests that the sideload endpoint exposes the generate_sub_sizes arg. * @@ -3384,49 +3368,6 @@ public function test_sideload_route_includes_generate_sub_sizes_arg() { $this->assertFalse( $args['generate_sub_sizes']['default'], 'generate_sub_sizes should default to false on sideload.' ); } - /** - * Tests sideloading an 'original-heic' companion file alongside its JPEG - * derivative. The HEIC filename is recorded under $metadata['original'] - * so it does not collide with 'original_image', which the scaled-sideload - * flow owns. - * - * @ticket 64915 - * @requires function imagejpeg - */ - public function test_sideload_original_heic_writes_metadata_original() { - $this->enable_client_side_media_processing(); - - wp_set_current_user( self::$author_id ); - - // Create the JPEG attachment that the HEIC will be a companion to. - $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); - $request->set_header( 'Content-Type', 'image/jpeg' ); - $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( self::$test_file ) ); - $response = rest_get_server()->dispatch( $request ); - $attachment_id = $response->get_data()['id']; - - $this->assertSame( 201, $response->get_status() ); - - // Sideload the HEIC companion using the real HEIC fixture. `convert_format` - // is disabled so the default HEIC -> JPEG output mapping does not rename - // the file or append an alt-extension suffix. - $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment_id}/sideload" ); - $request->set_header( 'Content-Type', 'image/heic' ); - $request->set_header( 'Content-Disposition', 'attachment; filename=canola.heic' ); - $request->set_param( 'image_size', 'original-heic' ); - $request->set_param( 'convert_format', false ); - $request->set_body( file_get_contents( DIR_TESTDATA . '/images/test-image.heic' ) ); - $response = rest_get_server()->dispatch( $request ); - - $this->assertSame( 200, $response->get_status(), 'Sideloading original-heic should succeed.' ); - - $metadata = wp_get_attachment_metadata( $attachment_id ); - $this->assertArrayHasKey( 'original', $metadata, "Metadata should contain 'original' for the HEIC companion." ); - $this->assertMatchesRegularExpression( '/canola.*\.heic$/', $metadata['original'], "Metadata 'original' should reference the HEIC filename." ); - $this->assertArrayNotHasKey( 'original_image', $metadata, "Metadata 'original_image' should be untouched by the HEIC sideload." ); - } - /** * Tests the filter_wp_unique_filename method handles the -scaled suffix. * diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 23daae97c4378..8fe3064651546 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -3703,7 +3703,6 @@ mockedApiResponse.Schema = { "1536x1536", "2048x2048", "original", - "original-heic", "animated-video", "animated-video-poster", "full", From 6e3df0f6a2219294501ede86f4f798a03c601ee5 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 27 Jun 2026 11:37:23 -0700 Subject: [PATCH 10/11] Media: Drop the stray HEIC/HEIF input-format change from the GIF backport. The animated-GIF backport was branched from the HEIC work, which had added image/heif to the sideload input formats. That belongs to the HEIC backport (#11323), not here, so revert the line to match trunk and keep this PR scoped to the animated-video companion handling. --- src/wp-includes/rest-api/class-wp-rest-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 192f76e2d4c5a..704a990298826 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -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', 'image/heif' ); + $input_formats = array( 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/heic' ); $output_formats = array(); foreach ( $input_formats as $mime_type ) { /** This filter is documented in wp-includes/media.php */ From cab6eaf79ed21c12e34fd2f9ee115e6eee1a9423 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 27 Jun 2026 16:25:27 -0700 Subject: [PATCH 11/11] Media: Reference Trac ticket 65549 in animated GIF video tests The animated GIF to video sideload and companion-cleanup tests were tagged with the broader client-side-media ticket they were branched from. Point them at the dedicated ticket 65549 opened for this change. --- .../media/wpDeleteAttachmentAnimatedGifVideo.php | 14 +++++++------- .../tests/rest-api/rest-attachments-controller.php | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php b/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php index f70f965bf3fab..0bb8091cb972d 100644 --- a/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php +++ b/tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php @@ -53,7 +53,7 @@ private function create_gif_attachment_with_companions( $with_poster = true ) { * The companion path is rebuilt from the attachment's own directory plus * the recorded basename. * - * @ticket 64915 + * @ticket 65549 */ public function test_companion_path_resolves_inside_attachment_directory() { list( $attachment_id, $video_path, $poster_path ) = $this->create_gif_attachment_with_companions(); @@ -72,7 +72,7 @@ public function test_companion_path_resolves_inside_attachment_directory() { * Only the basename of the recorded value is trusted, so a path traversal * in the metadata cannot escape the attachment's directory. * - * @ticket 64915 + * @ticket 65549 */ public function test_companion_path_ignores_directory_traversal() { $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); @@ -89,7 +89,7 @@ public function test_companion_path_ignores_directory_traversal() { } /** - * @ticket 64915 + * @ticket 65549 */ public function test_companion_path_is_null_without_companion() { $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); @@ -100,7 +100,7 @@ public function test_companion_path_is_null_without_companion() { } /** - * @ticket 64915 + * @ticket 65549 */ public function test_companion_path_is_null_when_metadata_is_not_a_string() { $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); @@ -118,7 +118,7 @@ public function test_companion_path_is_null_when_metadata_is_not_a_string() { * Both the converted video and the poster are removed when the * attachment is deleted. * - * @ticket 64915 + * @ticket 65549 */ public function test_deletes_companion_files_on_attachment_delete() { list( $attachment_id, $video_path, $poster_path ) = $this->create_gif_attachment_with_companions(); @@ -136,7 +136,7 @@ public function test_deletes_companion_files_on_attachment_delete() { * Only the video companion is removed when only it is recorded * (transparent GIFs have no poster). * - * @ticket 64915 + * @ticket 65549 */ public function test_deletes_only_video_when_no_poster_recorded() { list( $attachment_id, $video_path ) = $this->create_gif_attachment_with_companions( false ); @@ -149,7 +149,7 @@ public function test_deletes_only_video_when_no_poster_recorded() { } /** - * @ticket 64915 + * @ticket 65549 */ public function test_noop_when_no_companion_metadata() { $attachment_id = $this->factory->attachment->create_upload_object( DIR_TESTDATA . '/images/canola.jpg' ); diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index aa3f56af08cfa..20d5ea8c0bece 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -3354,7 +3354,7 @@ public function test_sideload_route_includes_scaled_enum() { /** * Tests that the sideload endpoint exposes the generate_sub_sizes arg. * - * @ticket 64915 + * @ticket 65549 */ public function test_sideload_route_includes_generate_sub_sizes_arg() { $this->enable_client_side_media_processing();