diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index 1fa960d58488c..277e2305bc5b0 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -326,10 +326,9 @@ public function get_items( $request ) { $query = new WP_Comment_Query(); $query_result = $query->query( $prepared_args ); + $comments = array(); if ( ! $is_head_request ) { - $comments = array(); - foreach ( $query_result as $comment ) { if ( ! $this->check_read_permission( $comment, $request ) ) { continue; @@ -1243,7 +1242,18 @@ public function prepare_item_for_response( $item, $request ) { $response = rest_ensure_response( $data ); if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { - $response->add_links( $this->prepare_links( $comment ) ); + $links = $this->prepare_links( $comment ); + $response->add_links( $links ); + + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions( $comment, $request ); + + $self = $links['self']['href']; + + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } } /** @@ -1346,6 +1356,32 @@ protected function prepare_links( $comment ) { return $links; } + /** + * Gets the link relations available for the comment and current user. + * + * @since 7.2.0 + * + * @param WP_Comment $comment Comment object. + * @param WP_REST_Request $request Request object. + * @return string[] List of link relations. + */ + protected function get_available_actions( $comment, $request ) { + if ( 'edit' !== $request['context'] ) { + return array(); + } + + $rels = array(); + + if ( 'trash' !== $comment->comment_approved && $this->check_edit_permission( $comment ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php */ + if ( apply_filters( 'rest_comment_trashable', ( EMPTY_TRASH_DAYS > 0 ), $comment ) ) { + $rels[] = 'https://api.w.org/action-trash'; + } + } + + return $rels; + } + /** * Prepends internal property prefix to query parameters to match our response fields. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php index 919714c5a96ef..25ff7be699759 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-menu-items-controller.php @@ -691,6 +691,23 @@ protected function prepare_links( $post ) { return $links; } + /** + * Gets the link relations available for the menu item and current user. + * + * Menu items can only be deleted permanently, so the `action-trash` link is skipped. + * + * @since 7.2.0 + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return array List of link relations. + */ + protected function get_available_actions( $post, $request ) { + $rels = parent::get_available_actions( $post, $request ); + + return array_values( array_diff( $rels, array( 'https://api.w.org/action-trash' ) ) ); + } + /** * Retrieves Link Description Objects that should be added to the Schema for the nav menu items collection. * @@ -700,6 +717,7 @@ protected function prepare_links( $post ) { */ protected function get_schema_links() { $links = parent::get_schema_links(); + $links = array_values( wp_list_filter( $links, array( 'rel' => 'https://api.w.org/action-trash' ), 'NOT' ) ); $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" ); $links[] = array( 'rel' => 'https://api.w.org/menu-item-object', diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 7310de28065e8..e276afe5a7d3d 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -455,9 +455,8 @@ static function ( $format ) { add_filter( 'post_password_required', array( $this, 'check_password_required' ), 10, 2 ); } + $posts = array(); if ( ! $is_head_request ) { - $posts = array(); - update_post_author_caches( $query_result ); update_post_parent_caches( $query_result ); @@ -2334,6 +2333,7 @@ protected function prepare_links( $post ) { * Gets the link relations available for the post and current user. * * @since 4.9.8 + * @since 7.2.0 Added the `action-trash` link relation. * * @param WP_Post $post Post object. * @param WP_REST_Request $request Request object. @@ -2369,6 +2369,19 @@ protected function get_available_actions( $post, $request ) { } } + if ( 'trash' !== $post->post_status && $this->check_delete_permission( $post ) ) { + $supports_trash = ( EMPTY_TRASH_DAYS > 0 ); + + if ( 'attachment' === $post->post_type ) { + $supports_trash = $supports_trash && MEDIA_TRASH; + } + + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ + if ( apply_filters( "rest_{$this->post_type}_trashable", $supports_trash, $post ) ) { + $rels[] = 'https://api.w.org/action-trash'; + } + } + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $tax ) { @@ -2840,6 +2853,7 @@ public function get_item_schema() { * Retrieves Link Description Objects that should be added to the Schema for the posts collection. * * @since 4.9.8 + * @since 7.2.0 Added the `action-trash` link relation. * * @return array */ @@ -2914,6 +2928,12 @@ protected function get_schema_links() { ); } + $links[] = array( + 'rel' => 'https://api.w.org/action-trash', + 'title' => __( 'The current user can move this post to the trash.' ), + 'href' => $href, + ); + $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); foreach ( $taxonomies as $tax ) { diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index aa6355d64b309..cdcf0fc99cae1 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -2635,6 +2635,27 @@ public function test_publish_action_link_does_not_exists() { $this->assertArrayNotHasKey( 'https://api.w.org/action-publish', $links ); } + /** + * @ticket 63670 + * + * @covers WP_REST_Posts_Controller::get_available_actions + */ + public function test_trash_action_link_does_not_exist_without_media_trash() { + $this->assertFalse( MEDIA_TRASH, 'The test expects the default MEDIA_TRASH value.' ); + + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->attachment->create( array( 'post_author' => self::$editor_id ) ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $links ); + } + protected function check_post_data( $attachment, $data, $context = 'view', $links = array() ) { parent::check_post_data( $attachment, $data, $context, $links ); diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 1ef7eab22ae6c..c9e6b56b87335 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -3494,15 +3494,19 @@ protected function check_comment_data( $data, $context, $links ) { $this->assertSame( mysql_to_rfc3339( $comment->comment_date_gmt ), $data['date_gmt'] ); $this->assertSame( get_comment_link( $comment ), $data['link'] ); $this->assertArrayHasKey( 'author_avatar_urls', $data ); - $this->assertSameSets( - array( - 'self', - 'collection', - 'up', - ), - array_keys( $links ) + + $expected_links = array( + 'self', + 'collection', + 'up', ); + if ( 'edit' === $context ) { + $expected_links[] = 'https://api.w.org/action-trash'; + } + + $this->assertSameSets( $expected_links, array_keys( $links ) ); + if ( $comment->comment_post_ID ) { $this->assertSame( rest_url( '/wp/v2/posts/' . $comment->comment_post_ID ), $links['up'][0]['href'] ); } @@ -4189,6 +4193,43 @@ public function test_get_note_with_children_link() { $this->assertStringContainsString( 'type=note', $children[0]['href'] ); } + /** + * @ticket 63670 + * + * @covers WP_REST_Comments_Controller::get_available_actions + */ + public function test_trash_action_link_does_not_exist_when_not_trashable() { + add_filter( 'rest_comment_trashable', '__return_false' ); + wp_set_current_user( self::$moderator_id ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $response->get_links() ); + } + + /** + * @ticket 63670 + * + * @covers WP_REST_Comments_Controller::get_available_actions + */ + public function test_trash_action_link_does_not_exist_for_trashed_comment() { + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 'trash', + 'comment_post_ID' => self::$post_id, + ) + ); + wp_set_current_user( self::$moderator_id ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $response->get_links() ); + } + /** * Test retrieving comments by type as authenticated user. * diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index 1dd9805c21692..c85e9af679be3 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -5229,6 +5229,22 @@ public function test_sticky_action_ldo_not_registered_for_non_posts() { $this->assertCount( 0, $publish, 'LDO found on schema.' ); } + /** + * @ticket 63670 + * + * @covers WP_REST_Posts_Controller::get_schema_links + */ + public function test_trash_action_ldo_registered() { + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $trash = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-trash' ) ); + + $this->assertCount( 1, $trash, 'LDO found on schema.' ); + } + public function test_author_action_ldo_registered_for_post_types_with_author_support() { $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ) ); $data = $response->get_data(); @@ -5373,6 +5389,62 @@ public function test_sticky_action_does_not_exist_for_non_post_posts() { $this->assertArrayNotHasKey( 'https://api.w.org/action-sticky', $links ); } + /** + * @ticket 63670 + * + * @covers WP_REST_Posts_Controller::get_available_actions + */ + public function test_trash_action_exists_for_author() { + wp_set_current_user( self::$author_id ); + + $post = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'https://api.w.org/action-trash', $links ); + } + + /** + * @ticket 63670 + * + * @covers WP_REST_Posts_Controller::get_available_actions + */ + public function test_trash_action_does_not_exist_when_not_trashable() { + add_filter( 'rest_post_trashable', '__return_false' ); + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $links ); + } + + /** + * @ticket 63670 + * + * @covers WP_REST_Posts_Controller::get_available_actions + */ + public function test_trash_action_does_not_exist_for_trashed_post() { + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( array( 'post_status' => 'trash' ) ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/posts/{$post}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $links ); + } + public function test_assign_author_action_exists_for_editor() { wp_set_current_user( self::$editor_id ); diff --git a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php index 514fa3835717b..85d5618ab964c 100644 --- a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php +++ b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php @@ -183,6 +183,38 @@ public function test_get_item() { $this->check_get_menu_item_response( $response, 'view' ); } + /** + * @ticket 63670 + * + * @covers ::get_available_actions + */ + public function test_get_item_has_no_trash_action_link() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'self', $links, 'The response should have a self link.' ); + $this->assertArrayNotHasKey( 'https://api.w.org/action-trash', $links, 'The response should not have a trash action link.' ); + } + + /** + * @ticket 63670 + * + * @covers ::get_schema_links + */ + public function test_trash_action_ldo_not_registered() { + $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' ) ); + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'links', $schema ); + $trash = wp_list_filter( $schema['links'], array( 'rel' => 'https://api.w.org/action-trash' ) ); + + $this->assertCount( 0, $trash, 'LDO found on schema.' ); + } + /** * @ticket 54304 * @covers ::get_items