Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
82c76fa
Comments REST endpoint - support for multiple status values
adamsilverstein Sep 14, 2025
1fc983f
Add tests for getting comments by status, statuses or ‘all’
adamsilverstein Sep 14, 2025
4ee6fe8
spacing fixes for linter
adamsilverstein Sep 14, 2025
bdd5a6d
Use count
adamsilverstein Sep 14, 2025
b878d8b
Complete tests for status field
adamsilverstein Sep 15, 2025
f435d93
update wp-api-generated
adamsilverstein Sep 15, 2025
7b22c39
Update tests/phpunit/tests/rest-api/rest-comments-controller.php
adamsilverstein Sep 15, 2025
6225ae6
hello yoda my old friend
adamsilverstein Sep 15, 2025
f121b3e
API handles strings automatically when array type given
adamsilverstein Sep 15, 2025
5fba5f6
Handle multiple status sanitization
adamsilverstein Sep 15, 2025
1ee7a86
Add additional tests
adamsilverstein Sep 15, 2025
3bf3121
Update src/wp-includes/rest-api/endpoints/class-wp-rest-comments-cont…
adamsilverstein Sep 16, 2025
f210ce5
Update tests/phpunit/tests/rest-api/rest-comments-controller.php
adamsilverstein Sep 18, 2025
faca069
Merge trunk into add/comment-rest-api-types-support
adamsilverstein Jul 23, 2026
0426084
Fix comments status permission check for array values
adamsilverstein Jul 23, 2026
c4159ea
Consolidate and expand comment status query tests
adamsilverstein Jul 23, 2026
a2972fd
Update since tag to 7.1.0
adamsilverstein Jul 23, 2026
4b93f1a
Merge branch 'trunk' into add/comment-rest-api-types-support
adamsilverstein Jul 23, 2026
d064fb8
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develo…
westonruter Jul 23, 2026
5217fd2
Narrow types on sanitize_comment_statuses() method
westonruter Jul 23, 2026
cd4496f
Tighten sanitize_comment_statuses() to return list<non-empty-lowercas…
westonruter Jul 23, 2026
c1c7823
Add typing to data_sanitize_comment_statuses
westonruter Jul 23, 2026
f44de34
Add native type hints to test class properties
westonruter Jul 23, 2026
b9b22de
Address PHPStan issues in tests
westonruter Jul 23, 2026
9a34a07
Apply suggestions from code review
adamsilverstein Jul 23, 2026
f29b134
Cover comma-separated multi-status requests in the authorization test
adamsilverstein Jul 23, 2026
6dd63d2
Preserve a literal '0' status when sanitizing the status parameter
adamsilverstein Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 ] ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is there a back-compat concern with the status arg now becoming an array of strings? Could there be any plugins that are looking at this parsed request param which would fail now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. The raw query string is unchanged, but once sanitization runs (which happens before the permission callback), $request['status'] is always an array - so a plugin doing a string comparison against the parsed param, eg. 'approve' === $request['status'] inside a rest_comment_query filter callback, would now see array( 'approve' ) and fail. $prepared_args['status'] handed to WP_Comment_Query also becomes an array, though that part is transparent since the query class accepts both forms.

So yes, there is a narrow back compat surface for code inspecting the parsed request param directly. It matches the shape the posts controller status param has had since 4.7 ([39104], https://core.trac.wordpress.org/ticket/38420), so the pattern is well established. I'd rather call this out in the dev note than try to preserve the string shape for single values, which would make the parsed type unpredictable. What do you think?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try searching https://veloria.dev/ but may be hard to be sure without a ton of digging. I can also merge this in early 7.2 so there is plenty of time to get feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think it's ok.

$forbidden_params[] = $param;
}
} elseif ( 'type' === $param ) {
Expand Down Expand Up @@ -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 ) {
Expand Down Expand Up @@ -1777,9 +1777,12 @@ public function get_collection_params() {

$query_params['status'] = array(
'default' => 'approve',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamsilverstein Why this one is still string instead of array?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the endpoint will accept a string or array? Still for consistency you are right this should probably be an array.

'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',
Comment thread
adamsilverstein marked this conversation as resolved.
);

Expand Down Expand Up @@ -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<non-empty-lowercase-string>
*/
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.
*
Expand Down
170 changes: 149 additions & 21 deletions tests/phpunit/tests/rest-api/rest-comments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<string> $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<string, array{ 0: string|list<string> }>
*/
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<string> $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<string, array{ 0: string|list<string> }>
*/
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<non-empty-lowercase-string> $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<string, array{ 0: mixed, 1: list<non-empty-lowercase-string> }>
*/
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&nbsp;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
*/
Expand Down
7 changes: 5 additions & 2 deletions tests/qunit/fixtures/wp-api-generated.js
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Loading