| 1 |
<?php |
| 2 |
/** |
| 3 |
* Animated GIF → video: clean up the companion files of a converted GIF. |
| 4 |
* |
| 5 |
* When client-side media processing is enabled, an opaque animated GIF is |
| 6 |
* stored as a normal image attachment (it stays a single media library item). |
| 7 |
* The GIF is also transcoded to a video (MP4/WebM) and a static first-frame |
| 8 |
* poster, both sideloaded as *companion files* of that same attachment — like |
| 9 |
* the HEIC original — and recorded in the attachment metadata under the |
| 10 |
* `animated_video` and `animated_video_poster` keys. They are never separate |
| 11 |
* attachments. Transparent GIFs are not converted (a `<video>` cannot |
| 12 |
* reproduce GIF transparency), so they have no companion. |
| 13 |
* |
| 14 |
* The swap to a video is handled in the editor: an uploaded GIF whose companion |
| 15 |
* video is available is switched to the Video block's "GIF" variation, which |
| 16 |
* serializes a normal `<video autoplay loop muted playsinline>` and so renders |
| 17 |
* natively on the front end with no render-time filtering. The author can |
| 18 |
* restore the original GIF from the block toolbar. The only thing left for PHP |
| 19 |
* is removing the sideloaded companions when their attachment is deleted, which |
| 20 |
* core's wp_delete_attachment_files() does not know about. |
| 21 |
* |
| 22 |
* @package gutenberg |
| 23 |
*/ |
| 24 |
|
| 25 |
/** |
| 26 |
* Returns the absolute path to one of an attachment's animated-GIF companion |
| 27 |
* files (the converted video or its poster), if recorded. |
| 28 |
* |
| 29 |
* The path is rebuilt from the attachment's own (trusted) directory plus the |
| 30 |
* recorded basename, so the stored metadata cannot point anywhere else. |
| 31 |
* |
| 32 |
* @param int $attachment_id Attachment ID. |
| 33 |
* @param string $meta_key Metadata key holding the companion basename |
| 34 |
* ('animated_video' or 'animated_video_poster'). |
| 35 |
* @return string|null Absolute file path, or null when there is no companion. |
| 36 |
*/ |
| 37 |
function gutenberg_get_animated_gif_companion_path( int $attachment_id, string $meta_key ): ?string { |
| 38 |
$metadata = wp_get_attachment_metadata( $attachment_id, true ); |
| 39 |
|
| 40 |
if ( empty( $metadata[ $meta_key ] ) || ! is_string( $metadata[ $meta_key ] ) ) { |
| 41 |
return null; |
| 42 |
} |
| 43 |
|
| 44 |
// Only ever trust the basename of the recorded value; strip any path |
| 45 |
// components so the metadata can't reference another directory. |
| 46 |
$name = wp_basename( $metadata[ $meta_key ] ); |
| 47 |
|
| 48 |
if ( '' === $name ) { |
| 49 |
return null; |
| 50 |
} |
| 51 |
|
| 52 |
$attached_file = get_attached_file( $attachment_id, true ); |
| 53 |
|
| 54 |
if ( ! $attached_file ) { |
| 55 |
return null; |
| 56 |
} |
| 57 |
|
| 58 |
return path_join( dirname( $attached_file ), $name ); |
| 59 |
} |
| 60 |
|
| 61 |
/** |
| 62 |
* Deletes a sideloaded animated-GIF companion file from disk. |
| 63 |
* |
| 64 |
* Deletion is delegated to wp_delete_file_from_directory(), which confirms the |
| 65 |
* path resolves strictly inside the uploads directory before unlinking, so this |
| 66 |
* can only ever remove a sideloaded companion. Mirrors the HEIC companion |
| 67 |
* cleanup in lib/media/load.php. |
| 68 |
* |
| 69 |
* @param string|null $path Absolute path to the companion file, or null. |
| 70 |
*/ |
| 71 |
function gutenberg_delete_animated_gif_companion_file( ?string $path ): void { |
| 72 |
if ( ! $path || ! file_exists( $path ) ) { |
| 73 |
return; |
| 74 |
} |
| 75 |
|
| 76 |
$uploads = wp_get_upload_dir(); |
| 77 |
|
| 78 |
if ( empty( $uploads['basedir'] ) ) { |
| 79 |
return; |
| 80 |
} |
| 81 |
|
| 82 |
wp_delete_file_from_directory( $path, $uploads['basedir'] ); |
| 83 |
} |
| 84 |
|
| 85 |
/** |
| 86 |
* Deletes the companion video and poster when their GIF attachment is deleted. |
| 87 |
* |
| 88 |
* The companions are sideloaded next to the GIF and recorded in |
| 89 |
* $metadata['animated_video'] and $metadata['animated_video_poster']. WordPress |
| 90 |
* core's wp_delete_attachment_files() does not know about them, so without this |
| 91 |
* hook they would linger on disk after the attachment is deleted. |
| 92 |
* |
| 93 |
* @param int $post_id Attachment ID being deleted. |
| 94 |
*/ |
| 95 |
function gutenberg_delete_animated_gif_video( int $post_id ): void { |
| 96 |
gutenberg_delete_animated_gif_companion_file( |
| 97 |
gutenberg_get_animated_gif_companion_path( $post_id, 'animated_video' ) |
| 98 |
); |
| 99 |
gutenberg_delete_animated_gif_companion_file( |
| 100 |
gutenberg_get_animated_gif_companion_path( $post_id, 'animated_video_poster' ) |
| 101 |
); |
| 102 |
} |
| 103 |
|
| 104 |
add_action( 'delete_attachment', 'gutenberg_delete_animated_gif_video' ); |
| 105 |
|