From 74d3090189f54a556ec0590a15242a7d66180c42 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Wed, 18 Mar 2026 16:23:15 -0600 Subject: [PATCH 01/10] Improve validation and permission checks for WP_HTTP_Polling_Sync_Server --- .../class-wp-http-polling-sync-server.php | 70 ++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index 88554a48c7d54..5812b40bb378c 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -37,6 +37,30 @@ class WP_HTTP_Polling_Sync_Server { */ const COMPACTION_THRESHOLD = 50; + /** + * Maximum total size (in bytes) of the request body. + * + * @since 7.0.0 + * @var int + */ + const MAX_BODY_SIZE = 16 * MB_IN_BYTES; + + /** + * Maximum number of rooms allowed per request. + * + * @since 7.0.0 + * @var int + */ + const MAX_ROOMS_PER_REQUEST = 50; + + /** + * Maximum size (in bytes) of a single update data string. + * + * @since 7.0.0 + * @var int + */ + const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES; + /** * Sync update type: compaction. * @@ -96,8 +120,9 @@ public function register_routes(): void { $typed_update_args = array( 'properties' => array( 'data' => array( - 'type' => 'string', - 'required' => true, + 'type' => 'string', + 'required' => true, + 'maxLength' => self::MAX_UPDATE_DATA_SIZE, ), 'type' => array( 'type' => 'string', @@ -149,12 +174,14 @@ public function register_routes(): void { 'methods' => array( WP_REST_Server::CREATABLE ), 'callback' => array( $this, 'handle_request' ), 'permission_callback' => array( $this, 'check_permissions' ), + 'validate_callback' => array( $this, 'validate_request' ), 'args' => array( 'rooms' => array( 'items' => array( 'properties' => $room_args, 'type' => 'object', ), + 'maxItems' => self::MAX_ROOMS_PER_REQUEST, 'required' => true, 'type' => 'array', ), @@ -223,6 +250,30 @@ public function check_permissions( WP_REST_Request $request ) { return true; } + /** + * Validates that the request body does not exceed the maximum allowed size. + * + * Runs as the route-level validate_callback, after per-arg schema + * validation has already passed. + * + * @since 7.0.0 + * + * @param WP_REST_Request $request The REST request. + * @return true|WP_Error True if valid, WP_Error if the body is too large. + */ + public function validate_request( WP_REST_Request $request ) { + $body = $request->get_body(); + if ( is_string( $body ) && strlen( $body ) > self::MAX_BODY_SIZE ) { + return new WP_Error( + 'rest_sync_body_too_large', + __( 'Request body is too large.' ), + array( 'status' => 413 ) + ); + } + + return true; + } + /** * Handles request: stores sync updates and awareness data, and returns * updates the client is missing. @@ -282,15 +333,28 @@ public function handle_request( WP_REST_Request $request ) { * @return bool True if user has permission, otherwise false. */ private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { + if ( ! is_null( $object_id ) && ! is_numeric( $object_id ) ) { + // Object ID must be numeric if provided. + return false; + } + // Handle single post type entities with a defined object ID. if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) { + if ( get_post_type( $object_id ) !== $entity_name ) { + // Post is not of the specified post type. + return false; + } return current_user_can( 'edit_post', (int) $object_id ); } // Handle single taxonomy term entities with a defined object ID. if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) { + if ( term_exists( (int) $object_id, $entity_name ) === false ) { + // Either term doesn't exist OR term is not in specified taxonomy. + return false; + } $taxonomy = get_taxonomy( $entity_name ); - return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms ); + return current_user_can( 'edit_term', (int) $object_id ); } // Handle single comment entities with a defined object ID. From 842e05648c00dce24fffcc99e72bb3be1cb458d4 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 20 Mar 2026 08:12:42 -0600 Subject: [PATCH 02/10] Add tests --- .../tests/rest-api/rest-sync-server.php | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index 7a04226ced8c9..e54735273d89a 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -293,6 +293,171 @@ public function test_sync_invalid_room_format_rejected() { $this->assertSame( 400, $response->get_status() ); } + /** + * Verifies that schema type validation rejects a non-string value for the + * update 'data' field, confirming that per-arg schema validation still runs + * with a route-level validate_callback registered. + */ + public function test_sync_rejects_non_string_update_data() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 12345, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema enum validation rejects an invalid update type, + * confirming that per-arg schema validation still runs with a route-level + * validate_callback registered. + */ + public function test_sync_rejects_invalid_update_type_enum() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => 'dGVzdA==', + 'type' => 'invalid_type', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that schema required-field validation rejects a room missing + * the 'client_id' field, confirming that per-arg schema validation still + * runs with a route-level validate_callback registered. + */ + public function test_sync_rejects_missing_required_room_field() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + // 'client_id' deliberately omitted. + 'room' => $this->get_post_room(), + 'updates' => array(), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxItems constraint rejects a request with more rooms + * than MAX_ROOMS_PER_REQUEST. + */ + public function test_sync_rejects_rooms_exceeding_max_items() { + wp_set_current_user( self::$editor_id ); + + $rooms = array(); + for ( $i = 0; $i < WP_HTTP_Polling_Sync_Server::MAX_ROOMS_PER_REQUEST + 1; $i++ ) { + $rooms[] = $this->build_room( 'root/site', $i + 1 ); + } + + $response = $this->dispatch_sync( $rooms ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the maxLength constraint rejects update data exceeding + * MAX_UPDATE_DATA_SIZE. + */ + public function test_sync_rejects_update_data_exceeding_max_length() { + wp_set_current_user( self::$editor_id ); + + $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + $request->set_body_params( + array( + 'rooms' => array( + array( + 'after' => 0, + 'awareness' => array( 'user' => 'test' ), + 'client_id' => 1, + 'room' => $this->get_post_room(), + 'updates' => array( + array( + 'data' => $oversized_data, + 'type' => 'update', + ), + ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Verifies that the route-level validate_callback rejects a request body + * exceeding MAX_BODY_SIZE. + */ + public function test_sync_rejects_oversized_request_body() { + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); + + // Set valid parsed params so per-arg schema validation passes first. + $request->set_body_params( + array( + 'rooms' => array( + $this->build_room( $this->get_post_room() ), + ), + ) + ); + + // Set an oversized raw body to trigger the route-level validate_callback. + $request->set_body( str_repeat( 'x', WP_HTTP_Polling_Sync_Server::MAX_BODY_SIZE + 1 ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_sync_body_too_large', $response, 413 ); + } + /* * Response format tests. */ From 52811526dca47db40ccc7d6de6cd5f710a02d5c0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Mar 2026 11:16:23 -0700 Subject: [PATCH 03/10] Add ticket references to tests and add void return type --- .../tests/rest-api/rest-sync-server.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index e54735273d89a..03a65a933979d 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -297,8 +297,10 @@ public function test_sync_invalid_room_format_rejected() { * Verifies that schema type validation rejects a non-string value for the * update 'data' field, confirming that per-arg schema validation still runs * with a route-level validate_callback registered. + * + * @ticket 64890 */ - public function test_sync_rejects_non_string_update_data() { + public function test_sync_rejects_non_string_update_data(): void { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); @@ -329,8 +331,10 @@ public function test_sync_rejects_non_string_update_data() { * Verifies that schema enum validation rejects an invalid update type, * confirming that per-arg schema validation still runs with a route-level * validate_callback registered. + * + * @ticket 64890 */ - public function test_sync_rejects_invalid_update_type_enum() { + public function test_sync_rejects_invalid_update_type_enum(): void { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); @@ -361,8 +365,10 @@ public function test_sync_rejects_invalid_update_type_enum() { * Verifies that schema required-field validation rejects a room missing * the 'client_id' field, confirming that per-arg schema validation still * runs with a route-level validate_callback registered. + * + * @ticket 64890 */ - public function test_sync_rejects_missing_required_room_field() { + public function test_sync_rejects_missing_required_room_field(): void { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); @@ -387,8 +393,10 @@ public function test_sync_rejects_missing_required_room_field() { /** * Verifies that the maxItems constraint rejects a request with more rooms * than MAX_ROOMS_PER_REQUEST. + * + * @ticket 64890 */ - public function test_sync_rejects_rooms_exceeding_max_items() { + public function test_sync_rejects_rooms_exceeding_max_items(): void { wp_set_current_user( self::$editor_id ); $rooms = array(); @@ -403,8 +411,10 @@ public function test_sync_rejects_rooms_exceeding_max_items() { /** * Verifies that the maxLength constraint rejects update data exceeding * MAX_UPDATE_DATA_SIZE. + * + * @ticket 64890 */ - public function test_sync_rejects_update_data_exceeding_max_length() { + public function test_sync_rejects_update_data_exceeding_max_length(): void { wp_set_current_user( self::$editor_id ); $oversized_data = str_repeat( 'a', WP_HTTP_Polling_Sync_Server::MAX_UPDATE_DATA_SIZE + 1 ); @@ -436,8 +446,10 @@ public function test_sync_rejects_update_data_exceeding_max_length() { /** * Verifies that the route-level validate_callback rejects a request body * exceeding MAX_BODY_SIZE. + * + * @ticket 64890 */ - public function test_sync_rejects_oversized_request_body() { + public function test_sync_rejects_oversized_request_body(): void { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp-sync/v1/updates' ); From 628a2ed0fe8ce8d79a59163ea9e282f449d21d92 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Mar 2026 11:28:16 -0700 Subject: [PATCH 04/10] Ensure object ID is an integer or not null --- .../class-wp-http-polling-sync-server.php | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index 5812b40bb378c..5b21652363742 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -329,37 +329,40 @@ public function handle_request( WP_REST_Request $request ) { * * @param string $entity_kind The entity kind, e.g. 'postType', 'taxonomy', 'root'. * @param string $entity_name The entity name, e.g. 'post', 'category', 'site'. - * @param string|null $object_id The object ID / entity key for single entities, null for collections. + * @param string|null $object_id The numeric object ID / entity key for single entities, null for collections. * @return bool True if user has permission, otherwise false. */ private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { - if ( ! is_null( $object_id ) && ! is_numeric( $object_id ) ) { + if ( is_string( $object_id ) ) { + $object_id = (int) $object_id; + } + if ( null !== $object_id && $object_id <= 0 ) { // Object ID must be numeric if provided. return false; } // Handle single post type entities with a defined object ID. - if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) { + if ( 'postType' === $entity_kind && is_int( $object_id ) ) { if ( get_post_type( $object_id ) !== $entity_name ) { // Post is not of the specified post type. return false; } - return current_user_can( 'edit_post', (int) $object_id ); + return current_user_can( 'edit_post', $object_id ); } // Handle single taxonomy term entities with a defined object ID. - if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) { - if ( term_exists( (int) $object_id, $entity_name ) === false ) { + if ( 'taxonomy' === $entity_kind && is_int( $object_id ) ) { + if ( term_exists( $object_id, $entity_name ) === false ) { // Either term doesn't exist OR term is not in specified taxonomy. return false; } $taxonomy = get_taxonomy( $entity_name ); - return current_user_can( 'edit_term', (int) $object_id ); + return current_user_can( 'edit_term', $object_id ); } // Handle single comment entities with a defined object ID. - if ( 'root' === $entity_kind && 'comment' === $entity_name && is_numeric( $object_id ) ) { - return current_user_can( 'edit_comment', (int) $object_id ); + if ( 'root' === $entity_kind && 'comment' === $entity_name && is_int( $object_id ) ) { + return current_user_can( 'edit_comment', $object_id ); } // All the remaining checks are for collections. If an object ID is provided, From 546d5f2e99ccde86361f191c428a0d2d8d4b6f37 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Mar 2026 11:33:20 -0700 Subject: [PATCH 05/10] Factor out common is_int() check --- .../class-wp-http-polling-sync-server.php | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index 5b21652363742..d8093116070a1 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -341,28 +341,31 @@ private function can_user_sync_entity_type( string $entity_kind, string $entity_ return false; } - // Handle single post type entities with a defined object ID. - if ( 'postType' === $entity_kind && is_int( $object_id ) ) { - if ( get_post_type( $object_id ) !== $entity_name ) { - // Post is not of the specified post type. - return false; + // Validate permissions for the provided object ID. + if ( is_int( $object_id ) ) { + // Handle single post type entities with a defined object ID. + if ( 'postType' === $entity_kind ) { + if ( get_post_type( $object_id ) !== $entity_name ) { + // Post is not of the specified post type. + return false; + } + return current_user_can( 'edit_post', $object_id ); } - return current_user_can( 'edit_post', $object_id ); - } - // Handle single taxonomy term entities with a defined object ID. - if ( 'taxonomy' === $entity_kind && is_int( $object_id ) ) { - if ( term_exists( $object_id, $entity_name ) === false ) { - // Either term doesn't exist OR term is not in specified taxonomy. - return false; + // Handle single taxonomy term entities with a defined object ID. + if ( 'taxonomy' === $entity_kind ) { + if ( term_exists( $object_id, $entity_name ) === false ) { + // Either term doesn't exist OR term is not in specified taxonomy. + return false; + } + $taxonomy = get_taxonomy( $entity_name ); + return current_user_can( 'edit_term', $object_id ); } - $taxonomy = get_taxonomy( $entity_name ); - return current_user_can( 'edit_term', $object_id ); - } - // Handle single comment entities with a defined object ID. - if ( 'root' === $entity_kind && 'comment' === $entity_name && is_int( $object_id ) ) { - return current_user_can( 'edit_comment', $object_id ); + // Handle single comment entities with a defined object ID. + if ( 'root' === $entity_kind && 'comment' === $entity_name ) { + return current_user_can( 'edit_comment', $object_id ); + } } // All the remaining checks are for collections. If an object ID is provided, From 9dd286b75688e27617ef702c954b2c4aab11b73f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 26 Mar 2026 15:27:34 -0700 Subject: [PATCH 06/10] Use ctype_digit() to ensure object ID is an integer string prior to casting --- .../collaboration/class-wp-http-polling-sync-server.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index d8093116070a1..d737d77f4ae1c 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -334,6 +334,9 @@ public function handle_request( WP_REST_Request $request ) { */ private function can_user_sync_entity_type( string $entity_kind, string $entity_name, ?string $object_id ): bool { if ( is_string( $object_id ) ) { + if ( ! ctype_digit( $object_id ) ) { + return false; + } $object_id = (int) $object_id; } if ( null !== $object_id && $object_id <= 0 ) { From dd187cb796e1378cfb67d58cb496ffc8a1bf57ad Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 27 Mar 2026 18:32:29 -0600 Subject: [PATCH 07/10] Update in response to feedback --- .../collaboration/class-wp-http-polling-sync-server.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php index d737d77f4ae1c..a90821ab78d3e 100644 --- a/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php +++ b/src/wp-includes/collaboration/class-wp-http-polling-sync-server.php @@ -54,7 +54,7 @@ class WP_HTTP_Polling_Sync_Server { const MAX_ROOMS_PER_REQUEST = 50; /** - * Maximum size (in bytes) of a single update data string. + * Maximum length of a single update data string. * * @since 7.0.0 * @var int @@ -357,11 +357,12 @@ private function can_user_sync_entity_type( string $entity_kind, string $entity_ // Handle single taxonomy term entities with a defined object ID. if ( 'taxonomy' === $entity_kind ) { - if ( term_exists( $object_id, $entity_name ) === false ) { + $term_exists = term_exists( $object_id, $entity_name ); + if ( ! is_array( $term_exists ) || ! isset( $term_exists['term_id'] ) ) { // Either term doesn't exist OR term is not in specified taxonomy. return false; } - $taxonomy = get_taxonomy( $entity_name ); + return current_user_can( 'edit_term', $object_id ); } From 3fd71b0be0714ce837b4790c285358fc7fb86009 Mon Sep 17 00:00:00 2001 From: chriszarate Date: Fri, 27 Mar 2026 18:37:52 -0600 Subject: [PATCH 08/10] Add permission check tests for taxonomy, comment, and edge cases Add targeted REST tests for permission checks in can_user_sync_entity_type() that previously lacked coverage: - Malformed object ID (non-numeric string like "1abc") rejected - Zero object ID rejected - Post type mismatch (e.g. postType/page for a post) rejected - Valid taxonomy term sync allowed - Non-existent taxonomy term rejected - Taxonomy term in wrong taxonomy rejected - Valid comment sync allowed - Non-existent comment rejected - Non-existent post type collection rejected --- .../tests/rest-api/rest-sync-server.php | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index 03a65a933979d..c71e62923ceca 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -12,11 +12,17 @@ class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase { protected static $editor_id; protected static $subscriber_id; protected static $post_id; + protected static $category_id; + protected static $tag_id; + protected static $comment_id; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); self::$post_id = $factory->post->create( array( 'post_author' => self::$editor_id ) ); + self::$category_id = $factory->category->create(); + self::$tag_id = $factory->tag->create(); + self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) ); // Enable option in setUpBeforeClass to ensure REST routes are registered. update_option( 'wp_collaboration_enabled', 1 ); @@ -27,6 +33,9 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); delete_option( 'wp_collaboration_enabled' ); wp_delete_post( self::$post_id, true ); + wp_delete_term( self::$category_id, 'category' ); + wp_delete_term( self::$tag_id, 'post_tag' ); + wp_delete_comment( self::$comment_id, true ); } public function set_up() { @@ -277,6 +286,107 @@ public function test_sync_permission_checked_per_room() { $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); } + /** + * @ticket 64890 + */ + public function test_sync_malformed_object_id_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:1abc' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_zero_object_id_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_post_type_mismatch_rejected() { + wp_set_current_user( self::$editor_id ); + + // The test post is of type 'post', not 'page'. + $response = $this->dispatch_sync( array( $this->build_room( 'postType/page:' . self::$post_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_allowed() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_taxonomy_term_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_taxonomy_term_wrong_taxonomy_rejected() { + wp_set_current_user( self::$editor_id ); + + // The tag term exists in 'post_tag', not 'category'. + $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$tag_id ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_comment_allowed() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) ); + + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_comment_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @ticket 64890 + */ + public function test_sync_nonexistent_post_type_collection_rejected() { + wp_set_current_user( self::$editor_id ); + + $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) ); + + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + /* * Validation tests. */ From 62d058f20cc8934b7ff675a8dc18201a4e5a5295 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 27 Mar 2026 20:35:47 -0700 Subject: [PATCH 09/10] Add types for protected member vars --- tests/phpunit/tests/rest-api/rest-sync-server.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index c71e62923ceca..f8a5e9a90735a 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -9,12 +9,12 @@ */ class WP_Test_REST_Sync_Server extends WP_Test_REST_Controller_Testcase { - protected static $editor_id; - protected static $subscriber_id; - protected static $post_id; - protected static $category_id; - protected static $tag_id; - protected static $comment_id; + protected static int $editor_id; + protected static int $subscriber_id; + protected static int $post_id; + protected static int $category_id; + protected static int $tag_id; + protected static int $comment_id; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); From 532aec1c2908dcd4e7d180ca3b7f9e8b56276d86 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 27 Mar 2026 20:42:28 -0700 Subject: [PATCH 10/10] Add void return types --- .../phpunit/tests/rest-api/rest-sync-server.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/rest-api/rest-sync-server.php b/tests/phpunit/tests/rest-api/rest-sync-server.php index f8a5e9a90735a..7ded16bd3b033 100644 --- a/tests/phpunit/tests/rest-api/rest-sync-server.php +++ b/tests/phpunit/tests/rest-api/rest-sync-server.php @@ -300,7 +300,7 @@ public function test_sync_malformed_object_id_rejected() { /** * @ticket 64890 */ - public function test_sync_zero_object_id_rejected() { + public function test_sync_zero_object_id_rejected(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'postType/post:0' ) ) ); @@ -311,7 +311,7 @@ public function test_sync_zero_object_id_rejected() { /** * @ticket 64890 */ - public function test_sync_post_type_mismatch_rejected() { + public function test_sync_post_type_mismatch_rejected(): void { wp_set_current_user( self::$editor_id ); // The test post is of type 'post', not 'page'. @@ -323,7 +323,7 @@ public function test_sync_post_type_mismatch_rejected() { /** * @ticket 64890 */ - public function test_sync_taxonomy_term_allowed() { + public function test_sync_taxonomy_term_allowed(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:' . self::$category_id ) ) ); @@ -334,7 +334,7 @@ public function test_sync_taxonomy_term_allowed() { /** * @ticket 64890 */ - public function test_sync_nonexistent_taxonomy_term_rejected() { + public function test_sync_nonexistent_taxonomy_term_rejected(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'taxonomy/category:999999' ) ) ); @@ -345,7 +345,7 @@ public function test_sync_nonexistent_taxonomy_term_rejected() { /** * @ticket 64890 */ - public function test_sync_taxonomy_term_wrong_taxonomy_rejected() { + public function test_sync_taxonomy_term_wrong_taxonomy_rejected(): void { wp_set_current_user( self::$editor_id ); // The tag term exists in 'post_tag', not 'category'. @@ -357,7 +357,7 @@ public function test_sync_taxonomy_term_wrong_taxonomy_rejected() { /** * @ticket 64890 */ - public function test_sync_comment_allowed() { + public function test_sync_comment_allowed(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:' . self::$comment_id ) ) ); @@ -368,7 +368,7 @@ public function test_sync_comment_allowed() { /** * @ticket 64890 */ - public function test_sync_nonexistent_comment_rejected() { + public function test_sync_nonexistent_comment_rejected(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'root/comment:999999' ) ) ); @@ -379,7 +379,7 @@ public function test_sync_nonexistent_comment_rejected() { /** * @ticket 64890 */ - public function test_sync_nonexistent_post_type_collection_rejected() { + public function test_sync_nonexistent_post_type_collection_rejected(): void { wp_set_current_user( self::$editor_id ); $response = $this->dispatch_sync( array( $this->build_room( 'postType/nonexistent_type' ) ) );