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 d14aefb1f6308..96a1f6f83a42f 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 @@ -157,7 +157,7 @@ public function get_items_permissions_check( $request ) { foreach ( $protected_params as $param ) { if ( 'status' === $param ) { - if ( 'approve' !== $request[ $param ] ) { + if ( array( 'approve' ) !== $request[ $param ] ) { $forbidden_params[] = $param; } } elseif ( 'type' === $param ) { @@ -200,7 +200,7 @@ public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'edit_posts' ) ) { foreach ( $protected_params as $param ) { if ( 'status' === $param ) { - if ( 'approve' !== $request[ $param ] ) { + if ( array( 'approve' ) !== $request[ $param ] ) { $forbidden_params[] = $param; } } elseif ( 'type' === $param ) { @@ -1777,9 +1777,12 @@ public function get_collection_params() { $query_params['status'] = array( 'default' => 'approve', - 'description' => __( 'Limit result set to comments assigned a specific status. Requires authorization.' ), - 'sanitize_callback' => 'sanitize_key', - 'type' => 'string', + 'description' => __( 'Limit result set to comments assigned one or more statuses. Requires authorization.' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'sanitize_callback' => array( $this, 'sanitize_comment_statuses' ), 'validate_callback' => 'rest_validate_request_arg', ); @@ -2041,6 +2044,29 @@ protected function check_is_comment_content_allowed( $prepared_comment ) { return '' !== $check['comment_content']; } + /** + * Sanitizes a single comment status or a list of comment statuses. + * + * @since 7.1.0 + * + * @param string[]|string $statuses Comment status or array of comment statuses. + * @return string[] Sanitized array of comment statuses. + * @phpstan-return list + */ + public function sanitize_comment_statuses( $statuses ): array { + $statuses = array_unique( array_map( 'sanitize_key', wp_parse_list( $statuses ) ) ); + + // Only drop empty values. A literal '0' is a queryable status, as comments are held with `comment_approved` of '0'. + $statuses = array_filter( + $statuses, + static function ( $status ) { + return '' !== $status; + } + ); + + return array_values( $statuses ); + } + /** * Check if post type supports notes. * diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 8542bcd42af24..c2ae9ffcf7cf8 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -8,27 +8,27 @@ * @group restapi */ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase { - protected static $superadmin_id; - protected static $admin_id; - protected static $editor_id; - protected static $moderator_id; - protected static $contributor_id; - protected static $subscriber_id; - protected static $author_id; - protected static $user_ids = array(); - - protected static $post_id; - protected static $password_id; - protected static $private_id; - protected static $draft_id; - protected static $trash_id; - protected static $approved_id; - protected static $hold_id; - - protected static $comment_ids = array(); - protected static $total_comments = 30; - protected static $per_page = 50; - protected static $num_notes = 10; + protected static int $superadmin_id; + protected static int $admin_id; + protected static int $editor_id; + protected static int $moderator_id; + protected static int $contributor_id; + protected static int $subscriber_id; + protected static int $author_id; + protected static array $user_ids = array(); + + protected static int $post_id; + protected static int $password_id; + protected static int $private_id; + protected static int $draft_id; + protected static int $trash_id; + protected static int $approved_id; + protected static int $hold_id; + + protected static array $comment_ids = array(); + protected static int $total_comments = 30; + protected static int $per_page = 50; + protected static int $num_notes = 10; protected $endpoint; @@ -245,6 +245,134 @@ public function test_get_items() { $this->assertCount( self::$total_comments, $comments ); } + /** + * Test getting comments filtered by one or more statuses. + * + * @ticket 63982 + * + * @dataProvider data_get_items_by_status + * + * @param string|list $status Status value(s) to filter by. + */ + public function test_get_items_by_status( $status ) { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'status', $status ); + $request->set_param( 'per_page', self::$per_page ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $query = new WP_Comment_Query(); + $found = $query->query( + array( + 'status' => $status, + 'count' => true, + ) + ); + + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertCount( $found, $data ); + } + + /** + * Data provider. + * + * @return array }> + */ + public function data_get_items_by_status(): array { + return array( + 'a single status' => array( 'approve' ), + 'the hold status' => array( 'hold' ), + 'the all status' => array( 'all' ), + 'an array of statuses' => array( array( 'approve', 'hold' ) ), + 'a comma-separated list' => array( 'approve,hold' ), + ); + } + + /** + * Test that filtering by the default status does not require authorization. + * + * @ticket 63982 + */ + public function test_get_items_by_default_status_without_permission() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'status', 'approve' ); + $request->set_param( 'per_page', self::$per_page ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertCount( self::$total_comments, $data ); + } + + /** + * Test that filtering by multiple statuses requires authorization. + * + * @ticket 63982 + * + * @dataProvider data_get_items_by_multiple_statuses_without_permission + * + * @param string|list $status Status value(s) to filter by. + */ + public function test_get_items_by_multiple_statuses_without_permission( $status ) { + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'status', $status ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_forbidden_param', $response, 401 ); + } + + /** + * Data provider. + * + * @return array }> + */ + public function data_get_items_by_multiple_statuses_without_permission(): array { + return array( + 'an array of statuses' => array( array( 'approve', 'hold' ) ), + 'a comma-separated list' => array( 'approve,hold' ), + ); + } + + /** + * Test sanitization of the status parameter. + * + * @ticket 63982 + * + * @dataProvider data_sanitize_comment_statuses + * + * @param string|string[] $statuses Raw status value. + * @param list $expected Expected sanitized statuses. + */ + public function test_sanitize_comment_statuses( $statuses, array $expected ) { + $controller = new WP_REST_Comments_Controller(); + + $this->assertSame( $expected, $controller->sanitize_comment_statuses( $statuses ) ); + } + + /** + * Data provider. + * + * @return array }> + */ + public function data_sanitize_comment_statuses(): array { + return array( + 'a single status' => array( 'approve', array( 'approve' ) ), + 'a comma-separated list' => array( 'approve,hold', array( 'approve', 'hold' ) ), + 'an array of statuses' => array( array( 'approve', 'hold' ), array( 'approve', 'hold' ) ), + 'mixed case statuses' => array( 'APPROVE,Hold', array( 'approve', 'hold' ) ), + 'duplicate statuses' => array( array( 'approve', 'approve', 'hold' ), array( 'approve', 'hold' ) ), + 'invalid characters' => array( 'howdy admin', array( 'howdynbspadmin' ) ), + 'an empty string' => array( '', array() ), + 'a literal 0 status' => array( '0', array( '0' ) ), + '0 mixed with a status' => array( '0,approve', array( '0', 'approve' ) ), + ); + } + /** * @ticket 38692 */ diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 1b0eaac6cccd1..e4ae73cf0a706 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -10580,8 +10580,11 @@ mockedApiResponse.Schema = { }, "status": { "default": "approve", - "description": "Limit result set to comments assigned a specific status. Requires authorization.", - "type": "string", + "description": "Limit result set to comments assigned one or more statuses. Requires authorization.", + "type": "array", + "items": { + "type": "string" + }, "required": false }, "type": {