Skip to content

Commit 797627e

Browse files
committed
Enhancement: Add flip capability and editable fields for newly-created attachments in the REST API.
This update introduces the ability to flip images both vertically and horizontally through the REST API. Additionally, it allows for the editing of attachment fields such as title, caption, description, and alt text when creating new attachments. Fixes #64035.
1 parent fe2b33f commit 797627e

File tree

2 files changed

+261
-42
lines changed

2 files changed

+261
-42
lines changed

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

Lines changed: 147 additions & 42 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.
@@ -563,7 +564,7 @@ public function edit_media_item( $request ) {
563564
) {
564565
return new WP_Error(
565566
'rest_unknown_attachment',
566-
__( 'Unable to get meta information for file.' ),
567+
__( 'Unable to get meta information for file.', 'gutenberg' ),
567568
array( 'status' => 404 )
568569
);
569570
}
@@ -573,7 +574,7 @@ public function edit_media_item( $request ) {
573574
if ( ! in_array( $mime_type, $supported_types, true ) ) {
574575
return new WP_Error(
575576
'rest_cannot_edit_file_type',
576-
__( 'This type of file cannot be edited.' ),
577+
__( 'This type of file cannot be edited.', 'gutenberg' ),
577578
array( 'status' => 400 )
578579
);
579580
}
@@ -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' => $request['flip']['vertical'] ?? 0,
591+
'horizontal' => $request['flip']['horizontal'] ?? 0,
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',
@@ -608,7 +623,7 @@ public function edit_media_item( $request ) {
608623
if ( 0 === count( $modifiers ) ) {
609624
return new WP_Error(
610625
'rest_image_not_edited',
611-
__( 'The image was not edited. Edit the image before applying the changes.' ),
626+
__( 'The image was not edited. Edit the image before applying the changes.', 'gutenberg' ),
612627
array( 'status' => 400 )
613628
);
614629
}
@@ -629,14 +644,29 @@ public function edit_media_item( $request ) {
629644
if ( is_wp_error( $image_editor ) ) {
630645
return new WP_Error(
631646
'rest_unknown_image_file_type',
632-
__( 'Unable to edit this image.' ),
647+
__( 'Unable to edit this image.', 'gutenberg' ),
633648
array( 'status' => 500 )
634649
);
635650
}
636651

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( 0 !== (int) $args['flip']['vertical'], 0 !== (int) $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.', 'gutenberg' ),
666+
array( 'status' => 500 )
667+
);
668+
}
669+
break;
640670
case 'rotate':
641671
// Rotation direction: clockwise vs. counterclockwise.
642672
$rotate = 0 - $args['angle'];
@@ -647,7 +677,7 @@ public function edit_media_item( $request ) {
647677
if ( is_wp_error( $result ) ) {
648678
return new WP_Error(
649679
'rest_image_rotation_failed',
650-
__( 'Unable to rotate this image.' ),
680+
__( 'Unable to rotate this image.', 'gutenberg' ),
651681
array( 'status' => 500 )
652682
);
653683
}
@@ -669,7 +699,7 @@ public function edit_media_item( $request ) {
669699
if ( is_wp_error( $result ) ) {
670700
return new WP_Error(
671701
'rest_image_crop_failed',
672-
__( 'Unable to crop this image.' ),
702+
__( 'Unable to crop this image.', 'gutenberg' ),
673703
array( 'status' => 500 )
674704
);
675705
}
@@ -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.
@@ -790,6 +827,7 @@ public function edit_media_item( $request ) {
790827
* @param int $new_attachment_id Attachment post ID for the new image.
791828
* @param int $attachment_id Attachment post ID for the edited (parent) image.
792829
*/
830+
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
793831
$new_image_meta = apply_filters( 'wp_edited_image_metadata', $new_image_meta, $new_attachment_id, $attachment_id );
794832

795833
wp_update_attachment_metadata( $new_attachment_id, $new_image_meta );
@@ -1480,62 +1518,101 @@ protected function check_upload_size( $file ) {
14801518
* Gets the request args for the edit item route.
14811519
*
14821520
* @since 5.5.0
1521+
* @since 6.9.0 Adds flips capability and editable fields for the newly-created attachment post.
14831522
*
14841523
* @return array
14851524
*/
14861525
protected function get_edit_media_item_args() {
1487-
return array(
1526+
$args = array(
14881527
'src' => array(
1489-
'description' => __( 'URL to the edited image file.' ),
1528+
'description' => __( 'URL to the edited image file.', 'gutenberg' ),
14901529
'type' => 'string',
14911530
'format' => 'uri',
14921531
'required' => true,
14931532
),
1533+
// The `modifiers` param takes precedence over the older format.
14941534
'modifiers' => array(
1495-
'description' => __( 'Array of image edits.' ),
1535+
'description' => __( 'Array of image edits.', 'gutenberg' ),
14961536
'type' => 'array',
14971537
'minItems' => 1,
14981538
'items' => array(
1499-
'description' => __( 'Image edit.' ),
1539+
'description' => __( 'Image edit.', 'gutenberg' ),
15001540
'type' => 'object',
15011541
'required' => array(
15021542
'type',
15031543
'args',
15041544
),
15051545
'oneOf' => array(
15061546
array(
1507-
'title' => __( 'Rotation' ),
1547+
'title' => __( 'Flip', 'gutenberg' ),
1548+
'properties' => array(
1549+
'type' => array(
1550+
'description' => __( 'Flip type.', 'gutenberg' ),
1551+
'type' => 'string',
1552+
'enum' => array( 'flip' ),
1553+
),
1554+
'args' => array(
1555+
'description' => __( 'Flip arguments.', 'gutenberg' ),
1556+
'type' => 'object',
1557+
'required' => array(
1558+
'flip',
1559+
),
1560+
'properties' => array(
1561+
'flip' => array(
1562+
'description' => __( 'Flip direction. [ horizontal, vertical ] 0 for no flip, 1 for flip.', 'gutenberg' ),
1563+
'type' => 'object',
1564+
'required' => array(
1565+
'horizontal',
1566+
'vertical',
1567+
),
1568+
'properties' => array(
1569+
'horizontal' => array(
1570+
'description' => __( 'Horizontal flip direction. 0 for no flip, 1 for flip.', 'gutenberg' ),
1571+
'type' => 'number',
1572+
),
1573+
'vertical' => array(
1574+
'description' => __( 'Vertical flip direction. 0 for no flip, 1 for flip.', 'gutenberg' ),
1575+
'type' => 'number',
1576+
),
1577+
),
1578+
),
1579+
),
1580+
),
1581+
),
1582+
),
1583+
array(
1584+
'title' => __( 'Rotation', 'gutenberg' ),
15081585
'properties' => array(
15091586
'type' => array(
1510-
'description' => __( 'Rotation type.' ),
1587+
'description' => __( 'Rotation type.', 'gutenberg' ),
15111588
'type' => 'string',
15121589
'enum' => array( 'rotate' ),
15131590
),
15141591
'args' => array(
1515-
'description' => __( 'Rotation arguments.' ),
1592+
'description' => __( 'Rotation arguments.', 'gutenberg' ),
15161593
'type' => 'object',
15171594
'required' => array(
15181595
'angle',
15191596
),
15201597
'properties' => array(
15211598
'angle' => array(
1522-
'description' => __( 'Angle to rotate clockwise in degrees.' ),
1599+
'description' => __( 'Angle to rotate clockwise in degrees.', 'gutenberg' ),
15231600
'type' => 'number',
15241601
),
15251602
),
15261603
),
15271604
),
15281605
),
15291606
array(
1530-
'title' => __( 'Crop' ),
1607+
'title' => __( 'Crop', 'gutenberg' ),
15311608
'properties' => array(
15321609
'type' => array(
1533-
'description' => __( 'Crop type.' ),
1610+
'description' => __( 'Crop type.', 'gutenberg' ),
15341611
'type' => 'string',
15351612
'enum' => array( 'crop' ),
15361613
),
15371614
'args' => array(
1538-
'description' => __( 'Crop arguments.' ),
1615+
'description' => __( 'Crop arguments.', 'gutenberg' ),
15391616
'type' => 'object',
15401617
'required' => array(
15411618
'left',
@@ -1545,19 +1622,19 @@ protected function get_edit_media_item_args() {
15451622
),
15461623
'properties' => array(
15471624
'left' => array(
1548-
'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.' ),
1625+
'description' => __( 'Horizontal position from the left to begin the crop as a percentage of the image width.', 'gutenberg' ),
15491626
'type' => 'number',
15501627
),
15511628
'top' => array(
1552-
'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.' ),
1629+
'description' => __( 'Vertical position from the top to begin the crop as a percentage of the image height.', 'gutenberg' ),
15531630
'type' => 'number',
15541631
),
15551632
'width' => array(
1556-
'description' => __( 'Width of the crop as a percentage of the image width.' ),
1633+
'description' => __( 'Width of the crop as a percentage of the image width.', 'gutenberg' ),
15571634
'type' => 'number',
15581635
),
15591636
'height' => array(
1560-
'description' => __( 'Height of the crop as a percentage of the image height.' ),
1637+
'description' => __( 'Height of the crop as a percentage of the image height.', 'gutenberg' ),
15611638
'type' => 'number',
15621639
),
15631640
),
@@ -1568,37 +1645,65 @@ protected function get_edit_media_item_args() {
15681645
),
15691646
),
15701647
'rotation' => array(
1571-
'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.' ),
1648+
'description' => __( 'The amount to rotate the image clockwise in degrees. DEPRECATED: Use `modifiers` instead.', 'gutenberg' ),
15721649
'type' => 'integer',
15731650
'minimum' => 0,
15741651
'exclusiveMinimum' => true,
15751652
'maximum' => 360,
15761653
'exclusiveMaximum' => true,
15771654
),
15781655
'x' => array(
1579-
'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
1656+
'description' => __( 'As a percentage of the image, the x position to start the crop from. DEPRECATED: Use `modifiers` instead.', 'gutenberg' ),
15801657
'type' => 'number',
15811658
'minimum' => 0,
15821659
'maximum' => 100,
15831660
),
15841661
'y' => array(
1585-
'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.' ),
1662+
'description' => __( 'As a percentage of the image, the y position to start the crop from. DEPRECATED: Use `modifiers` instead.', 'gutenberg' ),
15861663
'type' => 'number',
15871664
'minimum' => 0,
15881665
'maximum' => 100,
15891666
),
15901667
'width' => array(
1591-
'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
1668+
'description' => __( 'As a percentage of the image, the width to crop the image to. DEPRECATED: Use `modifiers` instead.', 'gutenberg' ),
15921669
'type' => 'number',
15931670
'minimum' => 0,
15941671
'maximum' => 100,
15951672
),
15961673
'height' => array(
1597-
'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.' ),
1674+
'description' => __( 'As a percentage of the image, the height to crop the image to. DEPRECATED: Use `modifiers` instead.', 'gutenberg' ),
15981675
'type' => 'number',
15991676
'minimum' => 0,
16001677
'maximum' => 100,
16011678
),
16021679
);
1680+
1681+
/*
1682+
* Get the args based on the post schema. This calls `rest_get_endpoint_args_for_schema()`,
1683+
* which also takes care of sanitization and validation.
1684+
*/
1685+
$update_item_args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );
1686+
1687+
if ( isset( $update_item_args['caption'] ) ) {
1688+
$args['caption'] = $update_item_args['caption'];
1689+
}
1690+
1691+
if ( isset( $update_item_args['description'] ) ) {
1692+
$args['description'] = $update_item_args['description'];
1693+
}
1694+
1695+
if ( isset( $update_item_args['title'] ) ) {
1696+
$args['title'] = $update_item_args['title'];
1697+
}
1698+
1699+
if ( isset( $update_item_args['post'] ) ) {
1700+
$args['post'] = $update_item_args['post'];
1701+
}
1702+
1703+
if ( isset( $update_item_args['alt_text'] ) ) {
1704+
$args['alt_text'] = $update_item_args['alt_text'];
1705+
}
1706+
1707+
return $args;
16031708
}
16041709
}

0 commit comments

Comments
 (0)