#65549 closed defect (bug) (fixed)
Media: Sideload animated GIF → video companions and clean them up on delete
| Reported by: | adamsilverstein | Owned by: | adamsilverstein |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Media | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests has-test-info |
| Cc: | Focuses: |
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.
Change History (9)
This ticket was mentioned in PR #12005 on WordPress/wordpress-develop by @adamsilverstein.
2 months ago
#2
#3
@
2 months ago
- Keywords has-test-info added; needs-testing removed
Patch testing report
Patch / PR tested
- https://github.com/WordPress/wordpress-develop/pull/12005
- trunk @ cdf2433e06 with PR #12005 branch (backport/78410-animated-gif-video) checked out
Environment
WordPress: 7.1-alpha-62161-src
PHP: 8.2.18 (Docker)
MySQL: 8.0.36
OS: macOS 26.5.1
Browser: n/a — editor flow depends on the unmerged Gutenberg WebCodecs PR (#78410), so verification is the server-side PHPUnit suite
Local wordpress-develop @ http://localhost:8889
Steps
- Checked out PR branch and reviewed the diff against trunk.
- vendor/bin/phpunit --filter wpDeleteAttachmentAnimatedGifVideo
- vendor/bin/phpunit --filter test_sideload
- vendor/bin/phpunit --filter WP_Test_REST_Attachments_Controller (full class, regression check)
- Audited the REST test coverage actually added by the PR.
Results
- Delete-cleanup unit tests: pass — 7/7, 14 assertions (path resolution, directory-traversal guard, non-string guard, both companions deleted, video-only delete, no-companion no-op).
- Sideload route arg tests: pass — 6/6, 28 assertions.
- Full attachments controller suite: pass — 130/130 (2 skipped), no regressions.
- media.php helpers: pass — wp_get_attachment_animated_gif_companion_path() correctly trusts only wp_basename() of the recorded value; cleanup routes deletion through wp_delete_file_from_directory() with the uploads-basedir guard. Sound, minimal, well-guarded.
- Scope concern: pass with caveat — the diff against trunk also carries changes from the stacked base PR #11323: the HEIC create_item_permissions_check allow-through branch and the generate_sub_sizes sideload arg (+ its test + qunit fixture). These are not part of #65549.
- REST coverage gap: fail — the only REST test added (test_sideload_route_includes_generate_sub_sizes_arg) covers the out-of-scope generate_sub_sizes arg. There is no REST test exercising the animated-video / animated-video-poster sideload sizes writing the animated_video / animated_video_poster metadata keys, even though the ticket lists that as covered.
Conclusion
PR #12005's in-scope changes — the two new sizes on the sideload route, the animated_video/animated_video_poster metadata branches, the wp_get_attachment_animated_gif_companion_path() helper, and the wp_delete_attachment_animated_gif_video() delete_attachment cleanup — are correct, idiomatic, and well-guarded (basename-only trust + uploads-basedir realpath guard), with strong unit coverage for the cleanup path. However, the diff against trunk is not minimal: it still carries #11323's HEIC create_item_permissions_check branch and the generate_sub_sizes arg, and the only REST test added covers that out-of-scope arg rather than the new animated-video sizes. Recommend: rebase onto trunk after #11323 lands so the diff contains only this ticket's changes, and add a REST sideload test asserting that image_size=animated-video / animated-video-poster writes the corresponding metadata keys. The cleanup logic itself is commit-ready; the PR as a whole should not land before #11323 and the scope/coverage items above are resolved.
@adamsilverstein commented on PR #12005:
2 months ago
#4
Just left a couple of nit questions. Are you still wanting to land https://github.com/WordPress/wordpress-develop/pull/11323 first before this one?
Yes, exactly.
@adamsilverstein commented on PR #12005:
2 months ago
#5
Just left a couple of nit questions. Are you still wanting to land #11323 first before this one?
Yes, exactly.
rebasing on add/heic-canvas-fallback for now so the changes in this pr are easier to review.
@adamsilverstein commented on PR #12005:
2 months ago
#6
Just left a couple of nit questions. Are you still wanting to land #11323 first before this one?
Yes, exactly.
rebasing on add/heic-canvas-fallback for now so the changes in this pr are easier to review.
@adamsilverstein commented on PR #12005:
2 months ago
#7
@andrewserong this is cleaned up after the HEIC merge (https://github.com/WordPress/wordpress-develop/pull/11323) and has the updated underscore naming.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
## 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.