Make WordPress Core

Opened 2 months ago

Closed 2 months ago

Last modified 2 months ago

#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

  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 (9)

#1 @adamsilverstein
2 months ago

  • Owner set to adamsilverstein
  • Status newreviewing

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


2 months 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.

#3 @khokansardar
2 months ago

  • Keywords has-test-info added; needs-testing removed

Patch testing report

Patch / PR tested

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

  1. Checked out PR branch and reviewed the diff against trunk.
  2. vendor/bin/phpunit --filter wpDeleteAttachmentAnimatedGifVideo
  3. vendor/bin/phpunit --filter test_sideload
  4. vendor/bin/phpunit --filter WP_Test_REST_Attachments_Controller (full class, regression check)
  5. 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.

#8 @adamsilverstein
2 months ago

  • Resolutionfixed
  • Status reviewingclosed

In 62623:

Media: Sideload and clean up animated GIF video companion files

Add server-side support for storing a converted animated GIF's companion files. When client-side media processing is enabled, the editor transcodes an opaque animated GIF to a web-safe video and sideloads both the video and a static first-frame poster as companion files of the GIF attachment, which itself remains the attachment. This change enables those files to be sideloaded correctly.

See https://github.com/WordPress/gutenberg/pull/78410.

Props khokansardar, westonruter, andrewserong.
Fixes #65549.

#9 @sabernhardt
2 months ago

  • Milestone Awaiting Review7.1
Note: See TracTickets for help on using tickets.