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: 17 additions & 0 deletions src/wp-includes/block-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,23 @@ function get_block_editor_settings( array $custom_settings, $block_editor_contex

$editor_settings['canUpdateBlockBindings'] = current_user_can( 'edit_block_binding', $block_editor_context );

/*
* Advertise which note actions are locked so the editor can hide affordances
* the REST API will refuse. This is a courtesy: enforcement lives in
* WP_REST_Comments_Controller.
*/
if ( ! empty( $block_editor_context->post ) ) {
$locked_note_actions = array();

foreach ( wp_get_note_lock_actions() as $note_action ) {
if ( wp_note_action_is_locked( $note_action, $block_editor_context->post ) ) {
$locked_note_actions[] = $note_action;
}
}

$editor_settings['lockedNoteActions'] = $locked_note_actions;
}

/**
* Filters the settings to pass to the block editor for all editor type.
*
Expand Down
96 changes: 96 additions & 0 deletions src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -4505,6 +4505,102 @@ function _wp_check_for_scheduled_update_comment_type() {
}
}

/**
* Retrieves the note actions a lock can apply to.
*
* Reopening a note is classified as `resolve`: it is the same state machine
* running in the opposite direction.
*
* @since 7.2.0
*
* @return string[] The action names.
*/
function wp_get_note_lock_actions() {
return array( 'create', 'reply', 'edit', 'resolve', 'delete' );
}

/**
* Determines whether a note action is locked for a post.
*
* A lock freezes note mutation while leaving notes readable. It is expressed
* per action, so a site can either freeze a post's notes entirely or preserve
* them selectively, for example by disallowing deletion while review continues.
*
* Two layers feed the result: the `_wp_notes_locked` post meta locks every
* action on that post, and the {@see 'note_action_is_locked'} filter then
* refines the computed value.
*
* @since 7.2.0
*
* @param string $action Note action. Accepts 'create', 'reply', 'edit', 'resolve', 'delete'.
* @param int|WP_Post $post Post ID or post object the note belongs to.
* @param WP_Comment|null $comment Optional. The note being mutated. Null when creating one. Default null.
* @return bool Whether the action is locked.
*/
function wp_note_action_is_locked( $action, $post, $comment = null ) {
$post = get_post( $post );

if ( ! $post instanceof WP_Post ) {
return false;
}

$locked = (bool) get_post_meta( $post->ID, '_wp_notes_locked', true );

/**
* Filters whether a note action is locked.
*
* Locking freezes note mutation but never affects reading notes. Returning
* true blocks the action for every user, including administrators, unless
* the filter itself carves out an exception.
*
* @since 7.2.0
*
* @param bool $locked Whether the action is locked. Defaults to the
* post's `_wp_notes_locked` meta value.
* @param string $action Note action. Accepts 'create', 'reply', 'edit', 'resolve', 'delete'.
* @param WP_Post $post The post the note belongs to.
* @param WP_Comment|null $comment The note being mutated, or null when creating one.
*/
return (bool) apply_filters( 'note_action_is_locked', $locked, $action, $post, $comment );
}

/**
* Registers the note lock meta on every post type that supports notes.
*
* Runs late on the `init` action so that post types registered at the default
* priority are already in place.
*
* @since 7.2.0
*/
function wp_register_note_lock_meta() {
foreach ( get_post_types_by_support( 'editor' ) as $post_type ) {
if ( ! _wp_post_type_supports_notes( $post_type ) ) {
continue;
}

register_post_meta(
$post_type,
'_wp_notes_locked',
array(
'type' => 'boolean',
'description' => __( 'Whether notes are locked for this post.' ),
'single' => true,
'default' => false,
'show_in_rest' => true,
'sanitize_callback' => 'rest_sanitize_boolean',
/*
* Locking is an editorial decision, so it takes more than being the
* post's author: an author locking reviewers out of their own review
* thread would defeat the point.
*/
'auth_callback' => static function ( $allowed, $meta_key, $post_id ) {
return current_user_can( 'edit_others_posts' ) && current_user_can( 'edit_post', $post_id );
},
)
);
}
}

/**
* Register initial note status meta.
*
Expand Down
3 changes: 3 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
add_action( 'deleted_comment_meta', 'wp_cache_set_comments_last_changed' );
add_action( 'init', 'wp_create_initial_comment_meta' );

// Note lock meta, registered late so that post types added on `init` are in place.
add_action( 'init', 'wp_register_note_lock_meta', 20 );

// Places to balance tags on input.
foreach ( array( 'content_save_pre', 'excerpt_save_pre', 'comment_save_pre', 'pre_comment_content' ) as $filter ) {
add_filter( $filter, 'convert_invalid_entities' );
Expand Down
23 changes: 23 additions & 0 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -2383,6 +2383,29 @@ function post_type_supports( $post_type, $feature ) {

return ( isset( $_wp_post_type_features[ $post_type ][ $feature ] ) );
}

/**
* Determines whether a post type supports notes.
*
* Notes support is declared through the arguments of the `editor` feature, as in
* `'supports' => array( 'editor' => array( 'notes' => true ) )`.
*
* @since 7.2.0
* @access private
*
* @param string $post_type Post type name.
* @return bool Whether the post type supports notes.
*/
function _wp_post_type_supports_notes( $post_type ) {
$supports = get_all_post_type_supports( $post_type );

if ( ! isset( $supports['editor'] ) || ! is_array( $supports['editor'] ) ) {
return false;
}

return array_any( $supports['editor'], static fn( $args ) => ! empty( $args['notes'] ) );
}

/**
* Retrieves a list of post type names that support a specific feature.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function get_items_permissions_check( $request ) {
);
}

if ( $post && $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
if ( $post && $is_note && ! _wp_post_type_supports_notes( $post->post_type ) ) {
if ( current_user_can( 'edit_post', $post->ID ) ) {
return new WP_Error(
'rest_comment_not_supported_post_type',
Expand Down Expand Up @@ -596,14 +596,22 @@ public function create_item_permissions_check( $request ) {
);
}

if ( $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
if ( $is_note && ! _wp_post_type_supports_notes( $post->post_type ) ) {
return new WP_Error(
'rest_comment_not_supported_post_type',
__( 'Sorry, this post type does not support notes.' ),
array( 'status' => 403 )
);
}

if ( $is_note ) {
$lock_check = $this->check_note_lock_permission( $request, $post );

if ( is_wp_error( $lock_check ) ) {
return $lock_check;
}
}

if ( 'draft' === $post->post_status && ! $is_note ) {
return new WP_Error(
'rest_comment_draft_post',
Expand Down Expand Up @@ -880,7 +888,7 @@ public function update_item_permissions_check( $request ) {
);
}

return true;
return $this->check_note_lock_for_comment( $request, $comment );
}

/**
Expand Down Expand Up @@ -1028,7 +1036,8 @@ public function delete_item_permissions_check( $request ) {
array( 'status' => rest_authorization_required_code() )
);
}
return true;

return $this->check_note_lock_for_comment( $request, $comment );
}

/**
Expand Down Expand Up @@ -2042,22 +2051,97 @@ protected function check_is_comment_content_allowed( $prepared_comment ) {
}

/**
* Check if post type supports notes.
* Determines which note actions a request performs.
*
* @param string $post_type Post type name.
* @return bool True if post type supports notes, false otherwise.
* @since 7.2.0
*
* @param WP_REST_Request $request Full details about the request.
* @param WP_Comment|null $comment The targeted note, or null when creating one.
* @return string[] The actions the request performs.
*/
private function check_post_type_supports_notes( $post_type ) {
$supports = get_all_post_type_supports( $post_type );
private function get_note_request_actions( $request, $comment ) {
if ( 'DELETE' === $request->get_method() ) {
return array( 'delete' );
}

if ( ! isset( $supports['editor'] ) ) {
return false;
// Creating a note: a resolution marker, a reply, or a new thread.
if ( ! $comment instanceof WP_Comment ) {
$meta = $request['meta'];

if ( is_array( $meta ) && isset( $meta['_wp_note_status'] ) ) {
return array( 'resolve' );
}

return empty( $request['parent'] ) ? array( 'create' ) : array( 'reply' );
}

if ( ! is_array( $supports['editor'] ) ) {
return false;
$actions = array();

if ( null !== $request['content'] ) {
$actions[] = 'edit';
}

/*
* A status change is the resolve/reopen toggle rather than an edit. Only a
* change counts: re-sending the status the note already has mutates nothing.
*/
if ( null !== $request['status'] &&
$this->prepare_status_response( $request['status'] ) !== $this->prepare_status_response( $comment->comment_approved )
) {
$actions[] = 'resolve';
}

// Any other field on the note (author, date, meta) counts as an edit.
return empty( $actions ) ? array( 'edit' ) : $actions;
}

/**
* Checks whether a lock forbids what a request does to an existing comment.
*
* Anything that is not a note, and any note whose post has gone missing, is
* left to the rest of the controller to deal with.
*
* @since 7.2.0
*
* @param WP_REST_Request $request Full details about the request.
* @param WP_Comment $comment The targeted comment.
* @return true|WP_Error True when nothing is locked, error object otherwise.
*/
private function check_note_lock_for_comment( $request, $comment ) {
if ( 'note' !== $comment->comment_type ) {
return true;
}

return array_any( $supports['editor'], fn( $item ) => ! empty( $item['notes'] ) );
$post = get_post( (int) $comment->comment_post_ID );

if ( ! $post instanceof WP_Post ) {
return true;
}

return $this->check_note_lock_permission( $request, $post, $comment );
}

/**
* Checks whether a lock forbids the note actions a request performs.
*
* @since 7.2.0
*
* @param WP_REST_Request $request Full details about the request.
* @param WP_Post $post The post the note belongs to.
* @param WP_Comment|null $comment Optional. The targeted note. Null when creating one. Default null.
* @return true|WP_Error True when nothing is locked, error object otherwise.
*/
private function check_note_lock_permission( $request, $post, $comment = null ) {
foreach ( $this->get_note_request_actions( $request, $comment ) as $action ) {
if ( wp_note_action_is_locked( $action, $post, $comment ) ) {
return new WP_Error(
'rest_notes_locked',
__( 'Notes are locked for this post.' ),
array( 'status' => 403 )
);
}
}

return true;
}
}
Loading
Loading