Conversation
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
f271a85 to
b6dff27
Compare
| * is indexed, so this only scans the rows for attachments created by editing an image, | ||
| * and it avoids searching the serialized attachment metadata for the ID. | ||
| */ | ||
| delete_metadata( 'post', 0, '_wp_attachment_original_id', $post_id, true ); |
There was a problem hiding this comment.
true is for $delete_all
We want this because, when the original is deleted we want to clear all descendants.
See: https://developer.wordpress.org/reference/functions/delete_metadata/
There was a problem hiding this comment.
A note on deletion paths:
wp_delete_post()delegates towp_delete_attachment()for attachments, so every Core route should reachdelete_attachmentand clear the record.- From what I've traced it has the same "reachability" as Core's
_thumbnail_idcleanup, which uses the identicaldelete_metadata() - doesn't fire on trash, which I think is the current pattern (the attachment still exists )
Plugins could still short circuit deletion and do it themselves via any filter, e.g., pre_delete_attachment. That means the clean up might not happen. This isn't Core's to fix, but we should mention this in the dev note for 7.2.
| * @return int ID of the attachment the chain started from, or `$attachment_id` when the | ||
| * attachment was not created by editing another one. | ||
| */ | ||
| function wp_get_original_attachment_id( $attachment_id ) { |
There was a problem hiding this comment.
wp_get_original_image_url() and wp_get_original_image_path() return the unscaled upload of the same attachment.
wp_get_original_attachment_id() returns the first image in an edit chain of different attachments.
On an edited image those near-identical names answer different questions. What about
wp_get_root_image_id()wp_get_source_attachment_id()
Also does this need a filter?
| ); | ||
|
|
||
| $schema['properties']['original_attachment'] = array( | ||
| 'description' => __( 'The ID of the attachment this image was created from by editing, or 0 if it was not created by editing another image.' ), |
There was a problem hiding this comment.
Images edited through /edit since 5.5 have parent_image in their metadata but no new meta, so they report 0. So "not created by editing another image" is false for them.
Editing an image via the `wp/v2/media/<id>/edit` REST endpoint saves the result as a new attachment and leaves the edited image untouched, so a site can build up a chain: an upload, a crop of it, a crop of that crop. `parent_image` records only the immediately preceding image, so finding the image a chain started from meant walking it one attachment at a time. Each attachment created by an edit now records the ID at the top of its chain in `_wp_attachment_original_id` postmeta, inheriting it from the image being edited. `wp_get_original_attachment_id()` reads it back in a single lookup, and returns the ID it was given for attachments that were uploaded rather than edited. The attachments REST controller exposes the result as an `original_attachment` field in the `edit` context only, giving editors what they need to offer a way back to the original without telling visitors which images were made from which. Deleting an attachment clears the record from any image edited from it, so nothing is left pointing at an ID that could later be reused. Records are written going forward only; images edited before this lands are not backfilled. Fixes #65987.
The field points at another attachment, and every other pointer to another entity in a media response is top level: `author`, `post`, `featured_media`. `media_details` holds the width, height, file, size and derived sizes of one image, and no references to anything else. Registering it properly also means it can be requested on its own with `_fields`, which was not possible while it was nested inside another object. Follow-up to the original commit on this branch. See #65987.
Adds tests for three cases the existing coverage left undefined. Trashing an original does not clear the record on images edited from it: `delete_attachment` only fires on permanent deletion, and keeping the record means untrashing restores the relationship intact. Editing an image whose original has been deleted starts a new chain from the image being edited, since there is no lineage left to inherit. Deleting an image from the middle of a chain leaves the images below it pointing at the start of the chain, because each one records where the chain started rather than the image directly above it. See #65987.
The field carried an `attachment_id` and `source_url` pair. Relationships in this API are bare IDs — `post`, `parent`, `featured_media` — so it is now just the ID of the original attachment. Clients that need the original's URL or dimensions get them from a `wp:original-attachment` link, which is embeddable in the same way as a featured image: `?_embed` hydrates the whole attachment record under `_embedded`. The link is added where the request is still in scope rather than in `prepare_links()`, which cannot see it, so the link stays in the `edit` context alongside the field. See #65987.
The field was left out entirely for an image that was not created by editing another one. `featured_media` reports `0` for "no featured image" rather than disappearing, so this now does the same, and clients get a field of one type that is always there in the `edit` context. The stored ID is no longer checked against the original's file before being sent. Deleting an attachment already clears the ID from everything edited from it, so the check only affected originals sitting in the trash, whose files still resolve. A client following an ID that has gone stale gets no record back, which it must handle in any case. See #65987.
The field comment said `original_attachment` is limited to the `edit` context so visitors cannot tell which images were made from which. `media_details` already exposes `parent_image` in the `view` and `embed` contexts, so that was not the reason. It is limited to `edit` because only editors need it. The schema description and the `@return` of `wp_get_original_attachment_id()` said a missing original meant the image was not created by editing another one. Images edited before this change have no record either, so both now say none is recorded. See #65987.
2c60221 to
65af124
Compare
`wp_get_original_attachment_id()` sat next to `wp_get_original_image_path()` and `wp_get_original_image_url()`, which describe the unscaled upload of the same attachment rather than the attachment a chain of edits started from. The names now say which one they mean: - `wp_get_original_attachment_id()` -> `wp_get_edit_root_attachment_id()` - `_wp_delete_original_attachment_id()` -> `_wp_delete_edit_root_attachment_id()` - `_wp_attachment_original_id` postmeta -> `_wp_attachment_edit_root_id` - `original_attachment` REST field -> `edit_root` - `wp:original-attachment` link relation -> `wp:edit-root` No change in behaviour. See #65987.
What? Why?
When Gutenberg crops an image, Core's /edit creates an entirely new attachment with no stable pointer back to the image the lineage started from.
parent_imagerecords only the immediate source; there is no root/original reference across crop-of-crop chains.That means we can't navigate a crop back to its original in a performant way, that is, without getting each post up the change where
parent_imageexists.This PR adds a top-level
edit_rootfield to the attachment REST response to track the lineage.Trac ticket: https://core.trac.wordpress.org/ticket/65987
Use of AI Tools
To create the backport of WordPress/gutenberg#81803 and its tests