Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ public function edit_media_item_permissions_check( $request ) {
* Applies edits to a media item and creates a new attachment record.
*
* @since 5.5.0
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
Expand Down Expand Up @@ -584,6 +585,20 @@ public function edit_media_item( $request ) {
} else {
$modifiers = array();

if ( isset( $request['flip']['horizontal'] ) || isset( $request['flip']['vertical'] ) ) {
$flip_args = array(
'vertical' => isset( $request['flip']['vertical'] ) ? (bool) $request['flip']['vertical'] : false,
'horizontal' => isset( $request['flip']['horizontal'] ) ? (bool) $request['flip']['horizontal'] : false,
);

$modifiers[] = array(
'type' => 'flip',
'args' => array(
'flip' => $flip_args,
),
);
}

if ( ! empty( $request['rotation'] ) ) {
$modifiers[] = array(
'type' => 'rotate',
Expand Down Expand Up @@ -637,6 +652,21 @@ public function edit_media_item( $request ) {
foreach ( $modifiers as $modifier ) {
$args = $modifier['args'];
switch ( $modifier['type'] ) {
case 'flip':
/*
* Flips the current image.
* The vertical flip is the first argument (flip along horizontal axis), the horizontal flip is the second argument (flip along vertical axis).
* See: WP_Image_Editor::flip()
*/
$result = $image_editor->flip( $args['flip']['vertical'], $args['flip']['horizontal'] );
if ( is_wp_error( $result ) ) {
return new WP_Error(
'rest_image_flip_failed',
__( 'Unable to flip this image.' ),
array( 'status' => 500 )
);
}
break;
case 'rotate':
// Rotation direction: clockwise vs. counterclockwise.
$rotate = 0 - $args['angle'];
Expand Down Expand Up @@ -711,23 +741,30 @@ public function edit_media_item( $request ) {
return $saved;
}

// Create new attachment post.
$new_attachment_post = array(
'post_mime_type' => $saved['mime-type'],
'guid' => $uploads['url'] . "/$filename",
'post_title' => $image_name,
'post_content' => '',
);
// Grab original attachment post so we can use it to set defaults.
$original_attachment_post = get_post( $attachment_id );

// Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
$attachment_post = get_post( $attachment_id );
// Check request fields and assign default values.
$new_attachment_post = $this->prepare_item_for_database( $request );
$new_attachment_post->post_mime_type = $saved['mime-type'];
$new_attachment_post->guid = $uploads['url'] . "/$filename";

if ( $attachment_post ) {
$new_attachment_post['post_content'] = $attachment_post->post_content;
$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
$new_attachment_post['post_title'] = $attachment_post->post_title;
}
// Unset ID so wp_insert_attachment generates a new ID.
unset( $new_attachment_post->ID );

// Set new attachment post title with fallbacks.
$new_attachment_post->post_title = $new_attachment_post->post_title ?? $original_attachment_post->post_title ?? $image_name;

// Set new attachment post caption (post_excerpt).
$new_attachment_post->post_excerpt = $new_attachment_post->post_excerpt ?? $original_attachment_post->post_excerpt ?? '';

// Set new attachment post description (post_content) with fallbacks.
$new_attachment_post->post_content = $new_attachment_post->post_content ?? $original_attachment_post->post_content ?? '';

// Set post parent if set in request, else the default of `0` (no parent).
$new_attachment_post->post_parent = $new_attachment_post->post_parent ?? 0;

// Insert the new attachment post.
$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );

if ( is_wp_error( $new_attachment_id ) ) {
Expand All @@ -740,8 +777,8 @@ public function edit_media_item( $request ) {
return $new_attachment_id;
}

// Copy the image alt text from the edited image.
$image_alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
// First, try to use the alt text from the request. If not set, copy the image alt text from the original attachment.
$image_alt = isset( $request['alt_text'] ) ? sanitize_text_field( $request['alt_text'] ) : get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );

if ( ! empty( $image_alt ) ) {
// update_post_meta() expects slashed.
Expand Down Expand Up @@ -1480,17 +1517,19 @@ protected function check_upload_size( $file ) {
* Gets the request args for the edit item route.
*
* @since 5.5.0
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
*
* @return array
*/
protected function get_edit_media_item_args() {
return array(
$args = array(
'src' => array(
'description' => __( 'URL to the edited image file.' ),
'type' => 'string',
'format' => 'uri',
'required' => true,
),
// The `modifiers` param takes precedence over the older format.
'modifiers' => array(
'description' => __( 'Array of image edits.' ),
'type' => 'array',
Expand All @@ -1503,6 +1542,43 @@ protected function get_edit_media_item_args() {
'args',
),
'oneOf' => array(
array(
'title' => __( 'Flip' ),
'properties' => array(
'type' => array(
'description' => __( 'Flip type.' ),
'type' => 'string',
'enum' => array( 'flip' ),
),
'args' => array(
'description' => __( 'Flip arguments.' ),
'type' => 'object',
'required' => array(
'flip',
),
'properties' => array(
'flip' => array(
'description' => __( 'Flip direction.' ),
'type' => 'object',
'required' => array(
'horizontal',
'vertical',
),
'properties' => array(
'horizontal' => array(
'description' => __( 'Whether to flip in the horizontal direction.' ),
'type' => 'boolean',
),
'vertical' => array(
'description' => __( 'Whether to flip in the vertical direction.' ),
'type' => 'boolean',
),
),
),
),
),
),
),
array(
'title' => __( 'Rotation' ),
'properties' => array(
Expand Down Expand Up @@ -1600,5 +1676,33 @@ protected function get_edit_media_item_args() {
'maximum' => 100,
),
);

/*
* Get the args based on the post schema. This calls `rest_get_endpoint_args_for_schema()`,
* which also takes care of sanitization and validation.
*/
$update_item_args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );

if ( isset( $update_item_args['caption'] ) ) {
$args['caption'] = $update_item_args['caption'];
}

if ( isset( $update_item_args['description'] ) ) {
$args['description'] = $update_item_args['description'];
}

if ( isset( $update_item_args['title'] ) ) {
$args['title'] = $update_item_args['title'];
}

if ( isset( $update_item_args['post'] ) ) {
$args['post'] = $update_item_args['post'];
}

if ( isset( $update_item_args['alt_text'] ) ) {
$args['alt_text'] = $update_item_args['alt_text'];
}

return $args;
}
}
145 changes: 145 additions & 0 deletions tests/phpunit/tests/rest-api/rest-attachments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -2682,4 +2682,149 @@ public function test_upload_svg_image() {

$this->assertTrue( $result );
}

/**
* Tests that the attachment fields caption, description, and title, post and alt_text are updated correctly.
* @ticket 64035
* @requires function imagejpeg
*/
public function test_edit_image_updates_attachment_fields() {
wp_set_current_user( self::$superadmin_id );
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );

// In order to test the edit endpoint editable fields, we need to create a new attachment.
$params = array(
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
'modifiers' => array(
array(
'type' => 'crop',
'args' => array(
'left' => 10,
'top' => 10,
'width' => 80,
'height' => 80,
),
),
),
'caption' => 'Test Caption',
'description' => 'Test Description',
'title' => 'Test Title',
'post' => 1,
'alt_text' => 'Test Alt Text',
);

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
$request->set_body_params( $params );
$response = rest_do_request( $request );

// The edit endpoint creates a new attachment, so we expect a 201 status.
$this->assertEquals( 201, $response->get_status() );

$data = $response->get_data();
$new_attachment_id = $data['id'];

$updated_attachment = get_post( $new_attachment_id );

$this->assertSame( 'Test Title', $updated_attachment->post_title, 'Title of the updated attachment is not identical.' );

$this->assertSame( 'Test Caption', $updated_attachment->post_excerpt, 'Caption of the updated attachment is not identical.' );

$this->assertSame( 'Test Description', $updated_attachment->post_content, 'Description of the updated attachment is not identical.' );

$this->assertSame( 1, $updated_attachment->post_parent, 'Post parent of the updated attachment is not identical.' );

$this->assertSame( 'Test Alt Text', get_post_meta( $new_attachment_id, '_wp_attachment_image_alt', true ), 'Alt text of the updated attachment is not identical.' );
}

/**
* Tests that the image is flipped correctly vertically and horizontally.
*
* @ticket 64035
* @requires function imagejpeg
*/
public function test_edit_image_vertical_and_horizontal_flip() {
wp_set_current_user( self::$superadmin_id );
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );

$this->setup_mock_editor();
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();

$params = array(
'flip' => array(
'vertical' => true,
'horizontal' => true,
),
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
);

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
$request->set_body_params( $params );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );

$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
$this->assertSame( array( true, true ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical and horizontal flip of the image is not identical.' );
}

/**
* Tests that the image is flipped correctly vertically only.
*
* @ticket 64035
* @requires function imagejpeg
*/
public function test_edit_image_vertical_flip_with_horizontal_false() {
wp_set_current_user( self::$superadmin_id );
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );

$this->setup_mock_editor();
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();

$params = array(
'flip' => array(
'vertical' => true,
'horizontal' => false,
),
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
);

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
$request->set_body_params( $params );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );

$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
$this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' );
}

/**
* Tests that the image is flipped correctly with only vertical flip in arguments.
*
* @ticket 64035
* @requires function imagejpeg
*/
public function test_edit_image_vertical_flip_only() {
wp_set_current_user( self::$superadmin_id );
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );

$this->setup_mock_editor();
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();

$params = array(
'flip' => array(
'vertical' => true,
),
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
);

$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
$request->set_body_params( $params );
$response = rest_do_request( $request );
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );

$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
$this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' );
}
}
Loading
Loading