Make WordPress Core

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: adamsilverstein's profile adamsilverstein Owned by: adamsilverstein's profile adamsilverstein
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

  1. Enable client-side media processing.
  2. Upload an opaque animated GIF in the block editor.
  3. 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.
  4. Inspect _wp_attachment_metadata and 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 touching original_image or sizes[].
  • 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 only wp_basename() of the recorded value, so the stored metadata can never reference another directory.
  • Add wp_delete_attachment_animated_gif_video( $post_id ) on delete_attachment — removes both companions via wp_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 — adds animated-video and animated-video-poster to the sideload image_size enum and two sideload_item() branches that record the basenames.
  • src/wp-includes/media.php — new wp_get_attachment_animated_gif_companion_path() helper and wp_delete_attachment_animated_gif_video() cleanup callback.
  • src/wp-includes/default-filters.php — registers the delete_attachment hook.
  • tests/phpunit/tests/media/wpDeleteAttachmentAnimatedGifVideo.php and tests/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 (2)

#1 @adamsilverstein
14 hours ago

  • Owner set to adamsilverstein
  • Status changed from new to reviewing

This ticket was mentioned in PR #12005 on WordPress/wordpress-develop by @adamsilverstein.


14 hours ago
#2

## Summary

  • Accept two new sideload sizes — animated-video (the converted MP4/WebM) and animated-video-poster (the static first-frame JPEG) — on the /wp/v2/media/<id>/sideload route. Each is written to its own metadata key (animated_video, animated_video_poster).
  • Add wp_delete_attachment_animated_gif_video() on delete_attachment so the sideloaded companions are removed when the GIF attachment is deleted (core's wp_delete_attachment_files() only tracks original_image).
  • Add a wp_get_attachment_animated_gif_companion_path() helper that rebuilds the companion path from the attachment's own directory + the recorded wp_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 onto trunk.

## Backport scope

### src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

  • register_routes() — adds animated-video and animated-video-poster to the sideload image_size enum.
  • sideload_item() — adds two new elseif branches that write the basename to $metadata['animated_video'] / $metadata['animated_video_poster'] without touching original_image or sizes[].

### src/wp-includes/media.php

  • New wp_get_attachment_animated_gif_companion_path( $attachment_id, $meta_key ) — rebuilds the absolute companion path from the attachment's own (trusted) directory plus only wp_basename() of the recorded value.
  • New wp_delete_attachment_animated_gif_video( $post_id )delete_attachment callback; removes both companions via wp_delete_file_from_directory( $path, $uploads['basedir'] ).

### src/wp-includes/default-filters.php

  • add_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).
  • [ ] Manual: with client-side media processing enabled, upload an opaque animated GIF; confirm the inserted block is the core/video GIF variation; confirm _wp_attachment_metadata has animated_video and animated_video_poster; confirm both files exist next to the GIF on disk.
  • [ ] Manual: upload a transparent animated GIF; confirm no companion metadata is written and the block is inserted as a normal image.
  • [ ] Manual: delete the GIF attachment; confirm both companions are removed from disk.

## Related

  • Upstream: WordPress/gutenberg#78410 (OPEN, Needs PHP backport). Keep this PR in draft until the GB PR merges.
  • Stacked on: #11323 — HEIC canvas fallback (introduces the companion-file pattern this PR mirrors).
  • Part of the 7.1 client-side media reintroduction: #11324.
Note: See TracTickets for help on using tickets.