Skip to content
Open
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
17 changes: 15 additions & 2 deletions lib/compat/wordpress-6.9/block-comments.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,22 @@ function gutenberg_register_block_comment_metadata() {
return $value;
},
'auth_callback' => function ( $allowed, $meta_key, $object_id ) {
// During comment creation the comment does not yet exist, so
// `object_id` is 0. Defer to the comment controller's own
// create permission — if the request can create the
// comment at all, it can set the suggestion meta on it.
if ( ! $object_id ) {
return current_user_can( 'edit_posts' );
}
$comment = get_comment( $object_id );
if ( $comment && 'note' === $comment->comment_type ) {
return current_user_can( 'edit_post', $comment->comment_post_ID );
}
return current_user_can( 'edit_comment', $object_id );
},
)
);

// Lifecycle status for a suggestion. `pending` on creation; moved to
// `applied` or `rejected` by the apply/reject actions.
register_meta(
'comment',
'_wp_suggestion_status',
Expand All @@ -141,6 +150,10 @@ function gutenberg_register_block_comment_metadata() {
),
),
'auth_callback' => function ( $allowed, $meta_key, $object_id ) {
$comment = get_comment( $object_id );
if ( $comment && 'note' === $comment->comment_type ) {
return current_user_can( 'edit_post', $comment->comment_post_ID );
}
return current_user_can( 'edit_comment', $object_id );
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,100 @@ public function get_item_permissions_check( $request ) {
return true;
}

/**
* Checks if a given request has access to update a comment.
*
* Extends core's check so that users who can `edit_post` on the parent
* post are also allowed to update note-type comments — but only for
* suggestion-lifecycle fields (status and `_wp_suggestion_status` meta).
* This unblocks the suggestion workflow where a post editor applies or
* rejects a suggestion authored by someone else, without granting them
* the ability to rewrite the note's content, reassign authorship, or
* otherwise modify another user's comment.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has access, WP_Error otherwise.
*/
public function update_item_permissions_check( $request ) {
$comment = $this->get_comment( $request['id'] );
if ( is_wp_error( $comment ) ) {
return $comment;
}

// For note comments, allow users who can edit the parent post to
// update suggestion-lifecycle fields only.
if (
'note' === $comment->comment_type &&
self::is_suggestion_lifecycle_update( $request )
) {
$post = get_post( $comment->comment_post_ID );
if ( $post && current_user_can( 'edit_post', $post->ID ) ) {
return true;
}
}

// Fall back to core's default check (moderate_comments or edit_comment).
return parent::update_item_permissions_check( $request );
}

/**
* Determines whether a note-update request touches only the fields used
* by the suggestion apply/reject lifecycle.
*
* Allowed fields:
* - `status` (limited to `approved` or `hold`)
* - `meta._wp_suggestion_status`
*
* Any other field present in the request body disqualifies the request
* from the `edit_post` shortcut, forcing it through core's edit_comment
* check instead.
*
* @param WP_REST_Request $request
* @return bool
*/
private static function is_suggestion_lifecycle_update( $request ) {
// Accept either a JSON body (the block editor client) or a form-
// encoded body (custom integrations / curl scripts). Either way the
// shortcut is gated by the same allowlist below — query/URL params
// are intentionally excluded so the body is the source of truth for
// what's being written.
$params = $request->get_json_params();
if ( ! is_array( $params ) ) {
$params = $request->get_body_params();
}
if ( ! is_array( $params ) || empty( $params ) ) {
return false;
}

$allowed_keys = array( 'id', 'status', 'meta' );
foreach ( array_keys( $params ) as $key ) {
if ( ! in_array( $key, $allowed_keys, true ) ) {
return false;
}
}

if (
isset( $params['status'] ) &&
! in_array( $params['status'], array( 'approved', 'hold' ), true )
) {
return false;
}

if ( isset( $params['meta'] ) ) {
if ( ! is_array( $params['meta'] ) ) {
return false;
}
$allowed_meta = array( '_wp_suggestion_status' );
foreach ( array_keys( $params['meta'] ) as $meta_key ) {
if ( ! in_array( $meta_key, $allowed_meta, true ) ) {
return false;
}
}
}

return true;
}

public function create_item_permissions_check( $request ) {
$is_note = ! empty( $request['type'] ) && 'note' === $request['type'];

Expand Down Expand Up @@ -296,6 +390,55 @@ public function create_item_permissions_check( $request ) {
return true;
}

/**
* Validates that an incoming request's `_wp_suggestion` meta is within the
* allowed byte budget. Truncating arbitrary JSON corrupts the payload, so
* we reject before any storage happens.
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if no payload or within bounds, WP_Error otherwise.
*/
protected static function validate_suggestion_payload_size( $request ) {
$meta = $request['meta'] ?? null;
if ( ! is_array( $meta ) || ! isset( $meta['_wp_suggestion'] ) ) {
return true;
}
$value = $meta['_wp_suggestion'];
if ( ! is_string( $value ) ) {
return true;
}
if ( strlen( $value ) > GUTENBERG_SUGGESTION_PAYLOAD_MAX_BYTES ) {
return new WP_Error(
'rest_suggestion_too_large',
sprintf(
/* translators: %d: maximum allowed byte length. */
__( 'Suggestion payload exceeds the %d-byte limit.', 'gutenberg' ),
GUTENBERG_SUGGESTION_PAYLOAD_MAX_BYTES
),
array( 'status' => 413 )
);
}
return true;
}

/**
* Updates a comment.
*
* Wraps core's update path with a pre-flight size check on the suggestion
* payload so oversized values are rejected with a clean 413 instead of
* silently dropped by the meta sanitize_callback.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_REST_Response|WP_Error
*/
public function update_item( $request ) {
$size_check = self::validate_suggestion_payload_size( $request );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}
return parent::update_item( $request );
}

/**
* Creates a comment.
*
Expand Down Expand Up @@ -325,6 +468,14 @@ public function create_item( $request ) {
);
}

// Reject oversized suggestion payloads with a 413 so the client knows.
// Without this, the meta sanitize_callback would silently reject the
// value and the suggestion would disappear server-side.
$size_check = self::validate_suggestion_payload_size( $request );
if ( is_wp_error( $size_check ) ) {
return $size_check;
}

$prepared_comment = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_comment ) ) {
return $prepared_comment;
Expand Down Expand Up @@ -655,5 +806,6 @@ protected function check_is_comment_content_allowed( $prepared_comment ) {
function () {
$controller = new Gutenberg_REST_Comment_Controller_6_9();
$controller->register_routes();
}
},
11
);
1 change: 1 addition & 0 deletions packages/editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Added an `editorIntent` preference (`edit`, `suggest`, `view`) with a matching `setEditorIntent` action and `getEditorIntent` selector. Surfaced as an Edit / Suggest / View menu in the editor options for post types that support notes. The `view` intent puts the block editor into a read-only preview. Keyboard shortcuts follow the Google Docs convention: Ctrl+Alt+Shift+Z (Edit), +X (Suggest), +C (View) on Windows / ⌘⌥⇧Z/X/C on macOS.
- Added a suggestion-overlay subsystem that powers the `suggest` intent. When active, an `editor.BlockEdit` filter diverts `setAttributes` into an in-memory overlay keyed by `clientId`; the block renders with the pending change merged on top of its real attributes, but the block-editor store stays at the baseline. A toolbar button on the selected block submits the overlay as a note comment with a `_wp_suggestion` meta payload (`schemaVersion`, `blockName`, `baseRevision`, `operations`) or discards it. Phase 2: capture only; Apply/Reject and diff rendering follow.
- REST: allow post editors to accept or reject suggestion notes on their posts without requiring the `moderate_comments` capability. Tightens the `comments` controller for `type=note` so non-author editors can only update the suggestion lifecycle (`status` and `_wp_suggestion_status`), and rejects suggestion payloads larger than 64 KB before they reach the database.

## 14.45.0 (2026-04-29)

Expand Down
Loading
Loading