Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 88 additions & 14 deletions src/wp-includes/collaboration/class-wp-http-polling-sync-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 length of a single update data string.
*
* @since 7.0.0
* @var int
*/
const MAX_UPDATE_DATA_SIZE = MB_IN_BYTES;
Comment on lines +56 to +62
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

MAX_UPDATE_DATA_SIZE is documented as a size "in bytes", but it's currently enforced via the REST schema's maxLength (character count). If this is intended to limit the encoded string length, consider updating the docblock wording to avoid implying decoded byte size enforcement.

Copilot uses AI. Check for mistakes.

/**
* Sync update type: compaction.
*
Expand Down Expand Up @@ -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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@dd32 noted in slack that large content updates can be made via the code editor. Is there a way that can be handled, even if it's just to replace the content with the code edited version if it's too large.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The code editor targets post_content which is represented as a Y.Text data type in the Yjs document. Updates to this field are diffed and only the delta is transmitted as an update. It should not approach the MAX_UPDATE_DATA_SIZE except in extreme edge cases (e.g., instantly replacing a 1+ MB document with a completely different 1+ MB document).

You can follow the diffing logic here:
https://github.com/WordPress/gutenberg/blob/ccc474379718969f48c3ab0153b43d1e26b4ef51/packages/core-data/src/utils/crdt-blocks.ts#L645

),
'type' => array(
'type' => 'string',
Expand Down Expand Up @@ -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',
),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -278,24 +329,47 @@ 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 {
// Handle single post type entities with a defined object ID.
if ( 'postType' === $entity_kind && is_numeric( $object_id ) ) {
return current_user_can( 'edit_post', (int) $object_id );
if ( is_string( $object_id ) ) {
if ( ! ctype_digit( $object_id ) ) {
return false;
}
$object_id = (int) $object_id;
}

// Handle single taxonomy term entities with a defined object ID.
if ( 'taxonomy' === $entity_kind && is_numeric( $object_id ) ) {
$taxonomy = get_taxonomy( $entity_name );
return isset( $taxonomy->cap->assign_terms ) && current_user_can( $taxonomy->cap->assign_terms );
if ( null !== $object_id && $object_id <= 0 ) {
// Object ID must be numeric if provided.
return false;
}

// 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 );
// 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 );
}

// Handle single taxonomy term entities with a defined object ID.
if ( 'taxonomy' === $entity_kind ) {
$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;
}

return current_user_can( 'edit_term', $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,
Expand Down
Loading
Loading