diff --git a/src/wp-admin/custom-header.php b/src/wp-admin/custom-header.php index a06756a280..8b2973fc11 100644 --- a/src/wp-admin/custom-header.php +++ b/src/wp-admin/custom-header.php @@ -1164,7 +1164,8 @@ wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> 'post_title' => basename($cropped), 'post_mime_type' => $image_type, 'guid' => $url, - 'context' => 'custom-header' + 'context' => 'custom-header', + 'post_parent' => $parent_attachment_id, ); return $object; @@ -1180,8 +1181,12 @@ wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> * @return int Attachment ID. */ final public function insert_attachment( $object, $cropped ) { + $parent_id = isset( $object['post_parent'] ) ? $object['post_parent'] : null; + unset( $object['post_parent'] ); + $attachment_id = wp_insert_attachment( $object, $cropped ); $metadata = wp_generate_attachment_metadata( $attachment_id, $cropped ); + /** * Filters the header image attachment metadata. * @@ -1193,6 +1198,11 @@ wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> */ $metadata = apply_filters( 'wp_header_image_attachment_metadata', $metadata ); wp_update_attachment_metadata( $attachment_id, $metadata ); + + if ( $parent_id ) { + $meta = add_post_meta( $attachment_id, '_wp_attachment_parent', $parent_id, true ); + } + return $attachment_id; } @@ -1241,7 +1251,13 @@ wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> $object = $this->create_attachment_object( $cropped, $attachment_id ); - unset( $object['ID'] ); + $previous = $this->get_previous_crop( $object ); + + if ( $previous ) { + $object['ID'] = $previous; + } else { + unset( $object['ID'] ); + } $new_attachment_id = $this->insert_attachment( $object, $cropped ); @@ -1396,4 +1412,32 @@ wp_nonce_field( 'custom-header-options', '_wpnonce-custom-header-options' ); ?> return $header_images; } + + /** + * Get the ID of a previous crop from the same base image. + * + * @since 4.9.0 + * + * @param array $object A crop attachment object. + * @return int|false An attachment ID if one exists. False if none. + */ + public function get_previous_crop( $object ) { + $header_images = $this->get_uploaded_header_images(); + + // Bail early if there are no header images. + if ( empty( $header_images ) ) { + return false; + } + + $previous = false; + + foreach ( $header_images as $image ) { + if ( $image['attachment_parent'] === $object['post_parent'] ) { + $previous = $image['attachment_id']; + break; + } + } + + return $previous; + } } diff --git a/src/wp-includes/js/customize-models.js b/src/wp-includes/js/customize-models.js index 372cad6273..e2a5dfbcd3 100644 --- a/src/wp-includes/js/customize-models.js +++ b/src/wp-includes/js/customize-models.js @@ -164,6 +164,7 @@ this.on('control:setImage', this.setImage, this); this.on('control:removeImage', this.removeImage, this); + this.on('add', this.maybeRemoveOldCrop, this); this.on('add', this.maybeAddRandomChoice, this); _.each(this.data, function(elt, index) { @@ -187,6 +188,25 @@ } }, + maybeRemoveOldCrop: function( model ) { + var newID = model.get( 'header' ).attachment_id || false, + oldCrop; + + // Bail early if we don't have a new attachment ID. + if ( ! newID ) { + return; + } + + oldCrop = this.find( function( item ) { + return ( item.cid !== model.cid && item.get( 'header' ).attachment_id === newID ); + } ); + + // If we found an old crop, remove it from the collection. + if ( oldCrop ) { + this.remove( oldCrop ); + } + }, + maybeAddRandomChoice: function() { if (this.size() === 1) { this.addRandomChoice(); diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php index 6909d0fe2e..2ee66fbddf 100644 --- a/src/wp-includes/theme.php +++ b/src/wp-includes/theme.php @@ -1214,6 +1214,7 @@ function get_uploaded_header_images() { $header_images[$header_index]['url'] = $url; $header_images[$header_index]['thumbnail_url'] = $url; $header_images[$header_index]['alt_text'] = get_post_meta( $header->ID, '_wp_attachment_image_alt', true ); + $header_images[$header_index]['attachment_parent'] = (int) get_post_meta( $header->ID, '_wp_attachment_parent', true ); if ( isset( $header_data['width'] ) ) $header_images[$header_index]['width'] = $header_data['width']; diff --git a/tests/phpunit/tests/image/header.php b/tests/phpunit/tests/image/header.php index a76ee2cd6e..a6467c49a6 100644 --- a/tests/phpunit/tests/image/header.php +++ b/tests/phpunit/tests/image/header.php @@ -137,4 +137,37 @@ class Tests_Image_Header extends WP_UnitTestCase { $this->assertGreaterThan( 0, $cropped_id ); } + /** + * @ticket 21819 + */ + function test_check_get_previous_crop() { + $id = wp_insert_attachment( array( + 'post_status' => 'publish', + 'post_title' => 'foo.png', + 'post_type' => 'post', + 'guid' => 'http://localhost/foo.png' + ) ); + + // Create inital crop object. + $cropped_1 = 'foo-cropped-1.png'; + $object = $this->custom_image_header->create_attachment_object( $cropped_1, $id ); + + // Ensure no previous crop exists. + $previous = $this->custom_image_header->get_previous_crop( $object ); + $this->assertFalse( $previous ); + + // Create the inital crop attachment and set it as the header. + $cropped_1_id = $this->custom_image_header->insert_attachment( $object, $cropped_1 ); + $key = '_wp_attachment_custom_header_last_used_' . get_stylesheet(); + update_post_meta( $cropped_1_id, $key, time() ); + update_post_meta( $cropped_1_id, '_wp_attachment_is_custom_header', get_stylesheet() ); + + // Create second crop. + $cropped_2 = 'foo-cropped-2.png'; + $object = $this->custom_image_header->create_attachment_object( $cropped_2, $id ); + + // Test that a previous crop is found. + $previous = $this->custom_image_header->get_previous_crop( $object ); + $this->assertSame( $previous, $cropped_1_id ); + } }