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 7fe79b57c1b9b..c96a3fe0ddbf5 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 @@ -123,21 +123,15 @@ public function register_routes() { * @return true|WP_Error True if the request has read access, error object otherwise. */ public function get_items_permissions_check( $request ) { - $is_note = 'note' === $request['type']; - $is_edit_context = 'edit' === $request['context']; + $is_note = 'note' === $request['type']; + $is_edit_context = 'edit' === $request['context']; + $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' ); + $forbidden_params = array(); if ( ! empty( $request['post'] ) ) { foreach ( (array) $request['post'] as $post_id ) { $post = get_post( $post_id ); - if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) { - return new WP_Error( - 'rest_comment_not_supported_post_type', - __( 'Sorry, this post type does not support notes.' ), - array( 'status' => 403 ) - ); - } - if ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) { return new WP_Error( 'rest_cannot_read_post', @@ -151,6 +145,36 @@ public function get_items_permissions_check( $request ) { array( 'status' => rest_authorization_required_code() ) ); } + + if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) { + if ( current_user_can( 'edit_post', $post->ID ) ) { + return new WP_Error( + 'rest_comment_not_supported_post_type', + __( 'Sorry, this post type does not support notes.' ), + array( 'status' => 403 ) + ); + } + + foreach ( $protected_params as $param ) { + if ( 'status' === $param ) { + if ( 'approve' !== $request[ $param ] ) { + $forbidden_params[] = $param; + } + } elseif ( 'type' === $param ) { + if ( 'comment' !== $request[ $param ] ) { + $forbidden_params[] = $param; + } + } elseif ( ! empty( $request[ $param ] ) ) { + $forbidden_params[] = $param; + } + } + return new WP_Error( + 'rest_forbidden_param', + /* translators: %s: List of forbidden parameters. */ + sprintf( __( 'Query parameter not permitted: %s' ), implode( ', ', $forbidden_params ) ), + array( 'status' => rest_authorization_required_code() ) + ); + } } } @@ -174,9 +198,6 @@ public function get_items_permissions_check( $request ) { } if ( ! current_user_can( 'edit_posts' ) ) { - $protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' ); - $forbidden_params = array(); - foreach ( $protected_params as $param ) { if ( 'status' === $param ) { if ( 'approve' !== $request[ $param ] ) { @@ -1890,7 +1911,7 @@ protected function check_read_post_permission( $post, $request ) { * @return bool Whether the comment can be read. */ protected function check_read_permission( $comment, $request ) { - if ( ! empty( $comment->comment_post_ID ) ) { + if ( 'note' !== $comment->comment_type && ! empty( $comment->comment_post_ID ) ) { $post = get_post( $comment->comment_post_ID ); if ( $post ) { if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) { diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index d6e66573596f4..26da64a7a211e 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -4133,4 +4133,118 @@ public function test_get_note_with_children_link() { $this->assertStringContainsString( 'status=all', $children[0]['href'] ); $this->assertStringContainsString( 'type=note', $children[0]['href'] ); } + + /** + * Test retrieving comments by type as authenticated user. + * + * @dataProvider data_comment_type_provider + * @ticket 44157 + * + * @param string $comment_type The comment type to test. + * @param int $count The number of comments to create. + */ + public function test_get_items_type_arg_authenticated( $comment_type, $count ) { + wp_set_current_user( self::$admin_id ); + + $args = array( + 'comment_approved' => 1, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$author_id, + 'comment_type' => $comment_type, + ); + + // Create comments of the specified type. + for ( $i = 0; $i < $count; $i++ ) { + self::factory()->comment->create( $args ); + } + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'type', $comment_type ); + $request->set_param( 'per_page', self::$per_page ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Comments endpoint is expected to return a 200 status' ); + + $comments = $response->get_data(); + $expected_count = 'comment' === $comment_type ? $count + self::$total_comments : $count; + $this->assertCount( $expected_count, $comments, "comment type '{$comment_type}' is expect to have {$expected_count} comments" ); + + // Next, test getting the individual comments. + foreach ( $comments as $comment ) { + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $comment['id'] ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Individual comment endpoint is expected to return a 200 status' ); + $data = $response->get_data(); + $this->assertSame( $comment_type, $data['type'], "Individual comment is expected to have type '{$comment_type}'" ); + } + } + + /** + * Test retrieving comments by type as unauthenticated user. + * + * @dataProvider data_comment_type_provider + * @ticket 44157 + * + * @param string $comment_type The comment type to test. + * @param int $count The number of comments to create. + */ + public function test_get_items_type_arg_unauthenticated( $comment_type, $count ) { + // First, create comments as admin. + wp_set_current_user( self::$admin_id ); + + $args = array( + 'comment_approved' => 1, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$author_id, + 'comment_type' => $comment_type, + ); + + $comments = array(); + + for ( $i = 0; $i < $count; $i++ ) { + $comments[] = self::factory()->comment->create( $args ); + } + + // Log out and test as unauthenticated user. + wp_logout(); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'type', $comment_type ); + $request->set_param( 'per_page', self::$per_page ); + + $response = rest_get_server()->dispatch( $request ); + + // Only comments can be retrieved from the /comments (multiple) endpoint when unauthenticated. + $expected_status = 'comment' === $comment_type ? 200 : 401; + $this->assertSame( $expected_status, $response->get_status(), 'Comments endpoint did not return the expected status' ); + if ( 'comment' !== $comment_type ) { + $this->assertErrorResponse( 'rest_forbidden_param', $response, 401, 'Comments endpoint did not return the expected error response for forbidden parameters' ); + } + + // Individual comments. + foreach ( $comments as $comment ) { + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $comment ) ); + $response = rest_get_server()->dispatch( $request ); + + // Individual comments using the /comments/ endpoint can be retrieved by + // unauthenticated users - except for the 'note' type which is restricted. + // See https://core.trac.wordpress.org/ticket/44157. + $this->assertSame( 'note' === $comment_type ? 401 : 200, $response->get_status(), 'Individual comment endpoint did not return the expected status' ); + } + } + + /** + * Data provider for comment type tests. + * + * @return array[] Data provider. + */ + public function data_comment_type_provider() { + return array( + 'comment type' => array( 'comment', 5 ), + 'annotation type' => array( 'annotation', 5 ), + 'discussion type' => array( 'discussion', 9 ), + 'note type' => array( 'note', 3 ), + ); + } }