Opened 14 hours ago
Last modified 14 hours ago
#65549 reviewing defect (bug)
Media: Sideload animated GIF → video companions and clean them up on delete
| Reported by: |
|
Owned by: |
|
|---|---|---|---|
| Milestone: | Awaiting Review | Priority: | normal |
| Severity: | normal | Version: | |
| Component: | Media | Keywords: | has-patch has-unit-tests needs-testing |
| Focuses: | Cc: |
Description
Summary
Opaque animated GIFs are large for the motion they encode. With client-side media processing enabled, the editor transcodes an animated GIF to a web-safe video (via WebCodecs) and sideloads both the converted video and a static first-frame poster as companion files of the GIF attachment.
This change adds the server-side support for that flow: it teaches the sideload REST route to accept the two companion sizes, and it cleans the companions up when the attachment is deleted.
Use case / steps to reproduce
- Enable client-side media processing.
- Upload an opaque animated GIF in the block editor.
- The editor converts it to a web-safe video and sideloads the video plus a first-frame poster against the same attachment; the inserted block is switched to the core/video block's "GIF" variation, which serializes a normal
<video autoplay loop muted playsinline>and renders natively on the front end with no render-time filtering. The author can restore the original GIF from the block toolbar. - Inspect
_wp_attachment_metadataand the uploads directory, then delete the attachment.
Expected: the attachment metadata records animated_video and animated_video_poster, both companion files sit next to the GIF on disk, and deleting the attachment removes them.
Actual (before this change): the sideload route rejects the animated-video / animated-video-poster sizes, and — were the companions stored — they would linger on disk after the attachment is deleted, because core's wp_delete_attachment_files() only tracks original_image.
Transparent animated GIFs are not converted (a <video> cannot reproduce GIF transparency), so they have no companion; the cleanup hook simply skips any companion key that isn't recorded.
Proposed change
- Accept two new sideload sizes on
/wp/v2/media/<id>/sideload:animated-video— the converted MP4/WebM, written to$metadata['animated_video'].animated-video-poster— the static first-frame JPEG, written to$metadata['animated_video_poster']. Both are written to their own metadata keys without touchingoriginal_imageorsizes[].
- Add
wp_get_attachment_animated_gif_companion_path( $attachment_id, $meta_key )— rebuilds the absolute companion path from the attachment's own (trusted) directory plus onlywp_basename()of the recorded value, so the stored metadata can never reference another directory. - Add
wp_delete_attachment_animated_gif_video( $post_id )ondelete_attachment— removes both companions viawp_delete_file_from_directory( $path, $uploads['basedir'] ), which enforces the realpath / uploads-basedir guard.
Patch
The patch is available as wordpress-develop PR #12005, a server-only backport of Gutenberg #78410. The block variation, store actions, WebCodecs worker, and editor UI ship through the normal Gutenberg → Core package sync.
Files touched:
src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php— addsanimated-videoandanimated-video-posterto the sideloadimage_sizeenum and twosideload_item()branches that record the basenames.src/wp-includes/media.php— newwp_get_attachment_animated_gif_companion_path()helper andwp_delete_attachment_animated_gif_video()cleanup callback.src/wp-includes/default-filters.php— registers thedelete_attachmenthook.tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.phpandtests/phpunit/tests/rest-api/rest-attachments-controller.php— coverage.tests/qunit/fixtures/wp-api-generated.js— regenerated for the new enum values.
Tests
wpDeleteAttachmentAnimatedGifVideo.php— path resolution, directory-traversal guard, non-string guard, both companions deleted, video-only deletion, and the no-companion no-op (transparent GIFs).rest-attachments-controller.php— the sideload route accepts the new sizes and writes the corresponding metadata keys.
## Summary
animated-video(the converted MP4/WebM) andanimated-video-poster(the static first-frame JPEG) — on the/wp/v2/media/<id>/sideloadroute. Each is written to its own metadata key (animated_video,animated_video_poster).wp_delete_attachment_animated_gif_video()ondelete_attachmentso the sideloaded companions are removed when the GIF attachment is deleted (core'swp_delete_attachment_files()only tracksoriginal_image).wp_get_attachment_animated_gif_companion_path()helper that rebuilds the companion path from the attachment's own directory + the recordedwp_basename(), so the metadata can never reference another directory.## Why
Opaque animated GIFs are large for the motion they encode. When client-side media processing is enabled, the editor transcodes the GIF to a video via WebCodecs and sideloads both the video and a poster as companion files of the same attachment. The GIF stays as the attachment; the inserted block is switched to the core/video block's "GIF" variation by the editor, which serializes a normal
<video autoplay loop muted playsinline>and so renders natively on the front end with no render-time filtering. The author can restore the original GIF from the block toolbar.Transparent GIFs are not converted (a
<video>cannot reproduce GIF transparency), so they have no companion. The cleanup hook handles this by simply skipping any companion key that isn't recorded.This PR is the server-only backport of WordPress/gutenberg#78410. The block variation, store actions, WebCodecs worker, and editor UI ship through the normal Gutenberg → Core package sync.
## Base
Stacked on #11323 (HEIC canvas fallback) — this PR follows the same companion-file pattern and reuses
wp_delete_file_from_directory()for the realpath/uploads-basedir guard. Merge #11323 first, then rebase this branch ontotrunk.## Backport scope
###
src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.phpregister_routes()— addsanimated-videoandanimated-video-posterto the sideloadimage_sizeenum.sideload_item()— adds two newelseifbranches that write the basename to$metadata['animated_video']/$metadata['animated_video_poster']without touchingoriginal_imageorsizes[].###
src/wp-includes/media.phpwp_get_attachment_animated_gif_companion_path( $attachment_id, $meta_key )— rebuilds the absolute companion path from the attachment's own (trusted) directory plus onlywp_basename()of the recorded value.wp_delete_attachment_animated_gif_video( $post_id )—delete_attachmentcallback; removes both companions viawp_delete_file_from_directory( $path, $uploads['basedir'] ).###
src/wp-includes/default-filters.phpadd_action( 'delete_attachment', 'wp_delete_attachment_animated_gif_video' );## Test plan
vendor/bin/phpunit tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php— 7 new tests pass (path resolution, traversal guard, non-string guard, both companions deleted, video-only deletion, no-companion noop).vendor/bin/phpunit tests/phpunit/tests/media/wpDeleteAttachmentHeicCompanionFile.php— HEIC tests still pass (no regression on the parallel hook)._wp_attachment_metadatahasanimated_videoandanimated_video_poster; confirm both files exist next to the GIF on disk.## Related
Needs PHP backport). Keep this PR in draft until the GB PR merges.