Make WordPress Core

Changeset 60908


Ignore:
Timestamp:
10/06/2025 11:50:00 PM (2 months ago)
Author:
ramonopoly
Message:

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 captiondescription, and titlepost and alt_text fields.

Props ramonopoly, mukesh27, isabel_brison, andrewserong.
Fixes #64035.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php

    r60893 r60908  
    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.
     
    584585        } else {
    585586            $modifiers = array();
     587
     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            }
    586601
    587602            if ( ! empty( $request['rotation'] ) ) {
     
    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.
     
    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         );
    721 
    722         // Copy post_content, post_excerpt, and post_title from the edited image's attachment post.
    723         $attachment_post = get_post( $attachment_id );
    724 
    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         }
    730 
     744        // Grab original attachment post so we can use it to set defaults.
     745        $original_attachment_post = get_post( $attachment_id );
     746
     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";
     751
     752        // Unset ID so wp_insert_attachment generates a new ID.
     753        unset( $new_attachment_post->ID );
     754
     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
     
    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 ) ) {
     
    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.' ),
     
    14921530                'required'    => true,
    14931531            ),
     1532            // The `modifiers` param takes precedence over the older format.
    14941533            'modifiers' => array(
    14951534                'description' => __( 'Array of image edits.' ),
     
    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' ),
     
    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}
  • trunk/tests/phpunit/tests/rest-api/rest-attachments-controller.php

    r60893 r60908  
    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}
  • trunk/tests/qunit/fixtures/wp-api-generated.js

    r60404 r60908  
    34283428                                ],
    34293429                                "oneOf": [
     3430                                    {
     3431                                        "title": "Flip",
     3432                                        "properties": {
     3433                                            "type": {
     3434                                                "description": "Flip type.",
     3435                                                "type": "string",
     3436                                                "enum": [
     3437                                                    "flip"
     3438                                                ]
     3439                                            },
     3440                                            "args": {
     3441                                                "description": "Flip arguments.",
     3442                                                "type": "object",
     3443                                                "required": [
     3444                                                    "flip"
     3445                                                ],
     3446                                                "properties": {
     3447                                                    "flip": {
     3448                                                        "description": "Flip direction.",
     3449                                                        "type": "object",
     3450                                                        "required": [
     3451                                                            "horizontal",
     3452                                                            "vertical"
     3453                                                        ],
     3454                                                        "properties": {
     3455                                                            "horizontal": {
     3456                                                                "description": "Whether to flip in the horizontal direction.",
     3457                                                                "type": "boolean"
     3458                                                            },
     3459                                                            "vertical": {
     3460                                                                "description": "Whether to flip in the vertical direction.",
     3461                                                                "type": "boolean"
     3462                                                            }
     3463                                                        }
     3464                                                    }
     3465                                                }
     3466                                            }
     3467                                        }
     3468                                    },
    34303469                                    {
    34313470                                        "title": "Rotation",
     
    35323571                            "minimum": 0,
    35333572                            "maximum": 100,
     3573                            "required": false
     3574                        },
     3575                        "caption": {
     3576                            "description": "The attachment caption.",
     3577                            "type": "object",
     3578                            "properties": {
     3579                                "raw": {
     3580                                    "description": "Caption for the attachment, as it exists in the database.",
     3581                                    "type": "string",
     3582                                    "context": [
     3583                                        "edit"
     3584                                    ]
     3585                                },
     3586                                "rendered": {
     3587                                    "description": "HTML caption for the attachment, transformed for display.",
     3588                                    "type": "string",
     3589                                    "context": [
     3590                                        "view",
     3591                                        "edit",
     3592                                        "embed"
     3593                                    ],
     3594                                    "readonly": true
     3595                                }
     3596                            },
     3597                            "required": false
     3598                        },
     3599                        "description": {
     3600                            "description": "The attachment description.",
     3601                            "type": "object",
     3602                            "properties": {
     3603                                "raw": {
     3604                                    "description": "Description for the attachment, as it exists in the database.",
     3605                                    "type": "string",
     3606                                    "context": [
     3607                                        "edit"
     3608                                    ]
     3609                                },
     3610                                "rendered": {
     3611                                    "description": "HTML description for the attachment, transformed for display.",
     3612                                    "type": "string",
     3613                                    "context": [
     3614                                        "view",
     3615                                        "edit"
     3616                                    ],
     3617                                    "readonly": true
     3618                                }
     3619                            },
     3620                            "required": false
     3621                        },
     3622                        "title": {
     3623                            "description": "The title for the post.",
     3624                            "type": "object",
     3625                            "properties": {
     3626                                "raw": {
     3627                                    "description": "Title for the post, as it exists in the database.",
     3628                                    "type": "string",
     3629                                    "context": [
     3630                                        "edit"
     3631                                    ]
     3632                                },
     3633                                "rendered": {
     3634                                    "description": "HTML title for the post, transformed for display.",
     3635                                    "type": "string",
     3636                                    "context": [
     3637                                        "view",
     3638                                        "edit",
     3639                                        "embed"
     3640                                    ],
     3641                                    "readonly": true
     3642                                }
     3643                            },
     3644                            "required": false
     3645                        },
     3646                        "post": {
     3647                            "description": "The ID for the associated post of the attachment.",
     3648                            "type": "integer",
     3649                            "required": false
     3650                        },
     3651                        "alt_text": {
     3652                            "description": "Alternative text to display when attachment is not displayed.",
     3653                            "type": "string",
    35343654                            "required": false
    35353655                        }
Note: See TracChangeset for help on using the changeset viewer.