Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,7 @@
add_action( 'customize_controls_enqueue_scripts', 'wp_plupload_default_settings' );
add_action( 'plugins_loaded', '_wp_add_additional_image_sizes', 0 );
add_filter( 'plupload_default_settings', 'wp_show_heic_upload_error' );
add_action( 'delete_attachment', '_wp_delete_edit_root_attachment_id' );

// Client-side media processing.
add_action( 'admin_init', 'wp_set_client_side_media_processing_flag' );
Expand Down
57 changes: 57 additions & 0 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -8826,6 +8826,63 @@ function wp_get_original_image_url( $attachment_id ) {
return apply_filters( 'wp_get_original_image_url', $original_image_url, $attachment_id );
}

/**
* Retrieves the edit root of an attachment: the attachment its chain of edits started from.
*
* Editing an image through the `wp/v2/media/<id>/edit` REST endpoint does not change the
* image that was edited. It saves the result as a brand new attachment, so a site can end
* up with a chain of attachments: an upload, a crop of it, a crop of that crop, and so on.
*
* Every attachment created that way stores the ID of the attachment at the top of its chain,
* so this function can find the edit root in one lookup no matter how long the chain is.
*
* Attachments that were uploaded rather than created by editing have no chain of their own,
* and this returns the ID that was passed in. To tell the two cases apart, compare the
* result against that ID.
*
* @since 7.2.0
*
* @param int $attachment_id Attachment ID.
* @return int ID of the attachment the chain of edits started from, or `$attachment_id`
* if none is recorded.
*/
function wp_get_edit_root_attachment_id( $attachment_id ) {
$edit_root_id = (int) get_post_meta( $attachment_id, '_wp_attachment_edit_root_id', true );

return $edit_root_id > 0 ? $edit_root_id : (int) $attachment_id;
}

/**
* Clears the recorded edit root ID from any attachment pointing at a deleted one.
*
* Without this, attachments created by editing the deleted image would keep pointing at an
* ID that no longer exists, and could later point at an unrelated attachment if WordPress
* reuses that ID.
*
* This only runs when an attachment is deleted for good. On sites where media goes to the
* trash first, attachments keep pointing at the trashed edit root until the trash is emptied.
*
* @since 7.2.0
*
* @access private
*
* @param int $post_id Attachment ID being deleted.
*/
function _wp_delete_edit_root_attachment_id( $post_id ) {
$post_id = (int) $post_id;

if ( $post_id <= 0 ) {
return;
}

/*
* Deletes the meta from every attachment recording this ID as its edit root. The meta key
* 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_edit_root_id', $post_id, true );
}

/**
* Filters callback which sets the status of an untrashed post to its previous status.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,18 @@ public function edit_media_item( $request ) {
'file' => _wp_relative_upload_path( $image_file ),
);

/*
* Record the attachment this chain of edits started from, so the edit root can be
* found in one lookup from any image later in the chain. The new attachment inherits
* the edit root recorded on the image being edited, or that image itself when it was
* uploaded rather than edited.
*/
update_post_meta(
$new_attachment_id,
'_wp_attachment_edit_root_id',
wp_get_edit_root_attachment_id( $attachment_id )
);

/**
* Filters the meta data for the new image created by editing an existing image.
*
Expand Down Expand Up @@ -1509,6 +1521,17 @@ public function prepare_item_for_response( $item, $request ) {
$data['post'] = ! empty( $post->post_parent ) ? (int) $post->post_parent : null;
}

/*
* ID of the attachment this image's chain of edits started from, or 0.
* Edit context only, since only editors need it.
* Not validated: deleting an attachment clears it from images edited from it.
*/
if ( in_array( 'edit_root', $fields, true ) && 'edit' === $request['context'] ) {
$edit_root_id = wp_get_edit_root_attachment_id( $post->ID );

$data['edit_root'] = $edit_root_id !== (int) $post->ID ? $edit_root_id : 0;
}

if ( in_array( 'source_url', $fields, true ) ) {
$data['source_url'] = wp_get_attachment_url( $post->ID );
}
Expand Down Expand Up @@ -1667,6 +1690,20 @@ public function prepare_item_for_response( $item, $request ) {
}
}

/*
* Let clients fetch the edit root in the same request with `_embed`,
* the way `featured_media` is paired with its own link. Added here rather than in
* `prepare_links()` because that method cannot see the request, and this belongs
* in the `edit` context only, alongside the field itself.
*/
if ( ! empty( $data['edit_root'] ) ) {
$response->add_link(
'https://api.w.org/edit-root',
rest_url( rest_get_route_for_post( $data['edit_root'] ) ),
array( 'embeddable' => true )
);
}

/**
* Filters an attachment returned from the REST API.
*
Expand Down Expand Up @@ -1805,6 +1842,13 @@ public function get_item_schema() {
'context' => array( 'view', 'edit' ),
);

$schema['properties']['edit_root'] = array(
'description' => __( 'The ID of the attachment this attachment\'s chain of edits started from, or 0 if none is recorded.' ),
'type' => 'integer',
'context' => array( 'edit' ),
'readonly' => true,
);

$schema['properties']['source_url'] = array(
'description' => __( 'URL to the original attachment file.' ),
'type' => 'string',
Expand Down
Loading
Loading