Skip to content

Commit 117f23f

Browse files
committed
Attachments REST API endpoint: update attachments controller to support flip and to customize attachment fields
This commit enhances media editor capabilities pursuant to the Phase 3: Collaboration > Media Library. See https://make.wordpress.org/core/2023/07/07/media-library/ It adds the following functionality: - the ability to flip an image horizontally and vertically - the ability to send arguments to update the new image's `caption`, `description`, and `title`, `post` and `alt_text` fields. Props ramonopoly, mukesh27, isabel_brison, andrewserong. Fixes #64035. git-svn-id: https://develop.svn.wordpress.org/trunk@60908 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 3865859 commit 117f23f

File tree

3 files changed

+386
-17
lines changed

3 files changed

+386
-17
lines changed

src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ public function edit_media_item_permissions_check( $request ) {
543543
* Applies edits to a media item and creates a new attachment record.
544544
*
545545
* @since 5.5.0
546+
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
546547
*
547548
* @param WP_REST_Request $request Full details about the request.
548549
* @return WP_REST_Response|WP_Error Response object on success, WP_Error object on failure.
@@ -584,6 +585,20 @@ public function edit_media_item( $request ) {
584585
} else {
585586
$modifiers = array();
586587

588+
if ( isset( $request['flip']['horizontal'] ) || isset( $request['flip']['vertical'] ) ) {
589+
$flip_args = array(
590+
'vertical' => isset( $request['flip']['vertical'] ) ? (bool) $request['flip']['vertical'] : false,
591+
'horizontal' => isset( $request['flip']['horizontal'] ) ? (bool) $request['flip']['horizontal'] : false,
592+
);
593+
594+
$modifiers[] = array(
595+
'type' => 'flip',
596+
'args' => array(
597+
'flip' => $flip_args,
598+
),
599+
);
600+
}
601+
587602
if ( ! empty( $request['rotation'] ) ) {
588603
$modifiers[] = array(
589604
'type' => 'rotate',
@@ -637,6 +652,21 @@ public function edit_media_item( $request ) {
637652
foreach ( $modifiers as $modifier ) {
638653
$args = $modifier['args'];
639654
switch ( $modifier['type'] ) {
655+
case 'flip':
656+
/*
657+
* Flips the current image.
658+
* The vertical flip is the first argument (flip along horizontal axis), the horizontal flip is the second argument (flip along vertical axis).
659+
* See: WP_Image_Editor::flip()
660+
*/
661+
$result = $image_editor->flip( $args['flip']['vertical'], $args['flip']['horizontal'] );
662+
if ( is_wp_error( $result ) ) {
663+
return new WP_Error(
664+
'rest_image_flip_failed',
665+
__( 'Unable to flip this image.' ),
666+
array( 'status' => 500 )
667+
);
668+
}
669+
break;
640670
case 'rotate':
641671
// Rotation direction: clockwise vs. counterclockwise.
642672
$rotate = 0 - $args['angle'];
@@ -711,23 +741,30 @@ public function edit_media_item( $request ) {
711741
return $saved;
712742
}
713743

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

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

725-
if ( $attachment_post ) {
726-
$new_attachment_post['post_content'] = $attachment_post->post_content;
727-
$new_attachment_post['post_excerpt'] = $attachment_post->post_excerpt;
728-
$new_attachment_post['post_title'] = $attachment_post->post_title;
729-
}
752+
// Unset ID so wp_insert_attachment generates a new ID.
753+
unset( $new_attachment_post->ID );
730754

755+
// Set new attachment post title with fallbacks.
756+
$new_attachment_post->post_title = $new_attachment_post->post_title ?? $original_attachment_post->post_title ?? $image_name;
757+
758+
// Set new attachment post caption (post_excerpt).
759+
$new_attachment_post->post_excerpt = $new_attachment_post->post_excerpt ?? $original_attachment_post->post_excerpt ?? '';
760+
761+
// Set new attachment post description (post_content) with fallbacks.
762+
$new_attachment_post->post_content = $new_attachment_post->post_content ?? $original_attachment_post->post_content ?? '';
763+
764+
// Set post parent if set in request, else the default of `0` (no parent).
765+
$new_attachment_post->post_parent = $new_attachment_post->post_parent ?? 0;
766+
767+
// Insert the new attachment post.
731768
$new_attachment_id = wp_insert_attachment( wp_slash( $new_attachment_post ), $saved['path'], 0, true );
732769

733770
if ( is_wp_error( $new_attachment_id ) ) {
@@ -740,8 +777,8 @@ public function edit_media_item( $request ) {
740777
return $new_attachment_id;
741778
}
742779

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

746783
if ( ! empty( $image_alt ) ) {
747784
// update_post_meta() expects slashed.
@@ -1480,17 +1517,19 @@ protected function check_upload_size( $file ) {
14801517
* Gets the request args for the edit item route.
14811518
*
14821519
* @since 5.5.0
1520+
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
14831521
*
14841522
* @return array
14851523
*/
14861524
protected function get_edit_media_item_args() {
1487-
return array(
1525+
$args = array(
14881526
'src' => array(
14891527
'description' => __( 'URL to the edited image file.' ),
14901528
'type' => 'string',
14911529
'format' => 'uri',
14921530
'required' => true,
14931531
),
1532+
// The `modifiers` param takes precedence over the older format.
14941533
'modifiers' => array(
14951534
'description' => __( 'Array of image edits.' ),
14961535
'type' => 'array',
@@ -1503,6 +1542,43 @@ protected function get_edit_media_item_args() {
15031542
'args',
15041543
),
15051544
'oneOf' => array(
1545+
array(
1546+
'title' => __( 'Flip' ),
1547+
'properties' => array(
1548+
'type' => array(
1549+
'description' => __( 'Flip type.' ),
1550+
'type' => 'string',
1551+
'enum' => array( 'flip' ),
1552+
),
1553+
'args' => array(
1554+
'description' => __( 'Flip arguments.' ),
1555+
'type' => 'object',
1556+
'required' => array(
1557+
'flip',
1558+
),
1559+
'properties' => array(
1560+
'flip' => array(
1561+
'description' => __( 'Flip direction.' ),
1562+
'type' => 'object',
1563+
'required' => array(
1564+
'horizontal',
1565+
'vertical',
1566+
),
1567+
'properties' => array(
1568+
'horizontal' => array(
1569+
'description' => __( 'Whether to flip in the horizontal direction.' ),
1570+
'type' => 'boolean',
1571+
),
1572+
'vertical' => array(
1573+
'description' => __( 'Whether to flip in the vertical direction.' ),
1574+
'type' => 'boolean',
1575+
),
1576+
),
1577+
),
1578+
),
1579+
),
1580+
),
1581+
),
15061582
array(
15071583
'title' => __( 'Rotation' ),
15081584
'properties' => array(
@@ -1600,5 +1676,33 @@ protected function get_edit_media_item_args() {
16001676
'maximum' => 100,
16011677
),
16021678
);
1679+
1680+
/*
1681+
* Get the args based on the post schema. This calls `rest_get_endpoint_args_for_schema()`,
1682+
* which also takes care of sanitization and validation.
1683+
*/
1684+
$update_item_args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );
1685+
1686+
if ( isset( $update_item_args['caption'] ) ) {
1687+
$args['caption'] = $update_item_args['caption'];
1688+
}
1689+
1690+
if ( isset( $update_item_args['description'] ) ) {
1691+
$args['description'] = $update_item_args['description'];
1692+
}
1693+
1694+
if ( isset( $update_item_args['title'] ) ) {
1695+
$args['title'] = $update_item_args['title'];
1696+
}
1697+
1698+
if ( isset( $update_item_args['post'] ) ) {
1699+
$args['post'] = $update_item_args['post'];
1700+
}
1701+
1702+
if ( isset( $update_item_args['alt_text'] ) ) {
1703+
$args['alt_text'] = $update_item_args['alt_text'];
1704+
}
1705+
1706+
return $args;
16031707
}
16041708
}

tests/phpunit/tests/rest-api/rest-attachments-controller.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2682,4 +2682,149 @@ public function test_upload_svg_image() {
26822682

26832683
$this->assertTrue( $result );
26842684
}
2685+
2686+
/**
2687+
* Tests that the attachment fields caption, description, and title, post and alt_text are updated correctly.
2688+
* @ticket 64035
2689+
* @requires function imagejpeg
2690+
*/
2691+
public function test_edit_image_updates_attachment_fields() {
2692+
wp_set_current_user( self::$superadmin_id );
2693+
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );
2694+
2695+
// In order to test the edit endpoint editable fields, we need to create a new attachment.
2696+
$params = array(
2697+
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
2698+
'modifiers' => array(
2699+
array(
2700+
'type' => 'crop',
2701+
'args' => array(
2702+
'left' => 10,
2703+
'top' => 10,
2704+
'width' => 80,
2705+
'height' => 80,
2706+
),
2707+
),
2708+
),
2709+
'caption' => 'Test Caption',
2710+
'description' => 'Test Description',
2711+
'title' => 'Test Title',
2712+
'post' => 1,
2713+
'alt_text' => 'Test Alt Text',
2714+
);
2715+
2716+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
2717+
$request->set_body_params( $params );
2718+
$response = rest_do_request( $request );
2719+
2720+
// The edit endpoint creates a new attachment, so we expect a 201 status.
2721+
$this->assertEquals( 201, $response->get_status() );
2722+
2723+
$data = $response->get_data();
2724+
$new_attachment_id = $data['id'];
2725+
2726+
$updated_attachment = get_post( $new_attachment_id );
2727+
2728+
$this->assertSame( 'Test Title', $updated_attachment->post_title, 'Title of the updated attachment is not identical.' );
2729+
2730+
$this->assertSame( 'Test Caption', $updated_attachment->post_excerpt, 'Caption of the updated attachment is not identical.' );
2731+
2732+
$this->assertSame( 'Test Description', $updated_attachment->post_content, 'Description of the updated attachment is not identical.' );
2733+
2734+
$this->assertSame( 1, $updated_attachment->post_parent, 'Post parent of the updated attachment is not identical.' );
2735+
2736+
$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.' );
2737+
}
2738+
2739+
/**
2740+
* Tests that the image is flipped correctly vertically and horizontally.
2741+
*
2742+
* @ticket 64035
2743+
* @requires function imagejpeg
2744+
*/
2745+
public function test_edit_image_vertical_and_horizontal_flip() {
2746+
wp_set_current_user( self::$superadmin_id );
2747+
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );
2748+
2749+
$this->setup_mock_editor();
2750+
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();
2751+
2752+
$params = array(
2753+
'flip' => array(
2754+
'vertical' => true,
2755+
'horizontal' => true,
2756+
),
2757+
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
2758+
);
2759+
2760+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
2761+
$request->set_body_params( $params );
2762+
$response = rest_do_request( $request );
2763+
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );
2764+
2765+
$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
2766+
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
2767+
$this->assertSame( array( true, true ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical and horizontal flip of the image is not identical.' );
2768+
}
2769+
2770+
/**
2771+
* Tests that the image is flipped correctly vertically only.
2772+
*
2773+
* @ticket 64035
2774+
* @requires function imagejpeg
2775+
*/
2776+
public function test_edit_image_vertical_flip_with_horizontal_false() {
2777+
wp_set_current_user( self::$superadmin_id );
2778+
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );
2779+
2780+
$this->setup_mock_editor();
2781+
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();
2782+
2783+
$params = array(
2784+
'flip' => array(
2785+
'vertical' => true,
2786+
'horizontal' => false,
2787+
),
2788+
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
2789+
);
2790+
2791+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
2792+
$request->set_body_params( $params );
2793+
$response = rest_do_request( $request );
2794+
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );
2795+
2796+
$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
2797+
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
2798+
$this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' );
2799+
}
2800+
2801+
/**
2802+
* Tests that the image is flipped correctly with only vertical flip in arguments.
2803+
*
2804+
* @ticket 64035
2805+
* @requires function imagejpeg
2806+
*/
2807+
public function test_edit_image_vertical_flip_only() {
2808+
wp_set_current_user( self::$superadmin_id );
2809+
$attachment = self::factory()->attachment->create_upload_object( self::$test_file );
2810+
2811+
$this->setup_mock_editor();
2812+
WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error();
2813+
2814+
$params = array(
2815+
'flip' => array(
2816+
'vertical' => true,
2817+
),
2818+
'src' => wp_get_attachment_image_url( $attachment, 'full' ),
2819+
);
2820+
2821+
$request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" );
2822+
$request->set_body_params( $params );
2823+
$response = rest_do_request( $request );
2824+
$this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 );
2825+
2826+
$this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] );
2827+
// The controller converts the integer values to booleans: 0 !== (int) 1 = true.
2828+
$this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' );
2829+
}
26852830
}

0 commit comments

Comments
 (0)