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
16 changes: 16 additions & 0 deletions src/wp-includes/capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
}
break;
case 'edit_comment':
case 'delete_comment':
if ( ! isset( $args[0] ) ) {
/* translators: %s: Capability name. */
$message = __( 'When checking for the %s capability, you must always check it against a specific comment.' );
Expand All @@ -572,6 +573,21 @@ function map_meta_cap( $cap, $user_id, ...$args ) {
break;
}

/*
* Notes can only be edited or deleted by their author
* or by users who can moderate comments.
*
* @since 7.0.0
Comment on lines +579 to +580
Copy link
Member

Choose a reason for hiding this comment

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

Since this isn't a dockblock, a @since tag I don't think is warranted. I don't see other examples of this in the function.

Suggested change
*
* @since 7.0.0

*/
if ( 'note' === $comment->comment_type ) {
if ( (int) $user_id === (int) $comment->user_id ) {
$caps[] = 'edit_posts';
} else {
$caps[] = 'moderate_comments';
}
break;
}

$post = get_post( $comment->comment_post_ID );

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ public function get_item_permissions_check( $request ) {
}

// Re-map edit context capabilities when requesting `note` type.
$edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' );
$edit_cap = 'note' === $comment->comment_type ? array( 'edit_post', $comment->comment_post_ID ) : array( 'moderate_comments' );
if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) {
return new WP_Error(
'rest_forbidden_context',
Expand Down Expand Up @@ -1920,6 +1920,17 @@ protected function check_read_permission( $comment, $request ) {
}
}

/*
* Notes can be read by any user who can edit the parent post.
Copy link
Member

Choose a reason for hiding this comment

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

Minor point: I don't think “parent” is right to mention here, since it could be confused with comment_parent in that a comment can have a parent comment.

Suggested change
* Notes can be read by any user who can edit the parent post.
* Notes can be read by any user who can edit the associated post.

* This is separate from the edit_comment capability, which controls
* whether a user can modify or delete the note.
*
* @since 7.0.0
*/
if ( 'note' === $comment->comment_type && ! empty( $comment->comment_post_ID ) ) {
return current_user_can( 'edit_post', $comment->comment_post_ID );
}

if ( 0 === get_current_user_id() ) {
return false;
}
Expand Down
152 changes: 152 additions & 0 deletions tests/phpunit/tests/rest-api/rest-comments-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -4134,6 +4134,158 @@ public function test_get_note_with_children_link() {
$this->assertStringContainsString( 'type=note', $children[0]['href'] );
}

/**
* Tests that a contributor cannot update another user's note via the REST API.
*
* @ticket 64779
*/
public function test_contributor_cannot_update_others_note() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function test_contributor_cannot_update_others_note() {
public function test_contributor_cannot_update_others_note(): void {

$post_id = self::factory()->post->create(
array(
'post_author' => self::$contributor_id,
'post_status' => 'draft',
)
);
$note_id = self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'note',
'user_id' => self::$admin_id,
'comment_content' => 'Admin note',
)
);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
);
);
assert( is_int( $note_id ) );


wp_set_current_user( self::$contributor_id );

$request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . $note_id );
$request->set_param( 'content', 'Modified by contributor' );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_cannot_edit', $response, 403 );
}

/**
* Tests that a contributor cannot delete another user's note via the REST API.
*
* @ticket 64779
*/
public function test_contributor_cannot_delete_others_note() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function test_contributor_cannot_delete_others_note() {
public function test_contributor_cannot_delete_others_note(): void {

$post_id = self::factory()->post->create(
array(
'post_author' => self::$contributor_id,
'post_status' => 'draft',
)
);
$note_id = self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'note',
'user_id' => self::$admin_id,
'comment_content' => 'Admin note',
)
);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
);
);
assert( is_int( $note_id ) );


wp_set_current_user( self::$contributor_id );

$request = new WP_REST_Request( 'DELETE', '/wp/v2/comments/' . $note_id );
$request->set_param( 'force', true );
$response = rest_get_server()->dispatch( $request );

$this->assertErrorResponse( 'rest_cannot_delete', $response, 403 );
}

/**
* Tests that a note author can update their own note via the REST API.
*
* @ticket 64779
*/
public function test_note_author_can_update_own_note() {
$post_id = self::factory()->post->create(
array(
'post_author' => self::$contributor_id,
'post_status' => 'draft',
)
);
$note_id = self::factory()->comment->create(
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
$note_id = self::factory()->comment->create(
$note = self::factory()->comment->create_and_get(

array(
'comment_post_ID' => $post_id,
'comment_type' => 'note',
'user_id' => self::$contributor_id,
'comment_content' => 'Original content',
)
);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
);
);
assert( $note instanceof WP_Comment );


wp_set_current_user( self::$contributor_id );

$request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . $note_id );
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
$request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . $note_id );
$request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . $note->comment_ID);

$request->set_param( 'content', 'Updated content' );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
$this->assertSame( 'Updated content', get_comment( $note_id )->comment_content );
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
$this->assertSame( 'Updated content', get_comment( $note_id )->comment_content );
$this->assertSame( 'Updated content', $note->comment_content );

}

/**
* Tests that an editor can update another user's note via the REST API.
*
* @ticket 64779
*/
public function test_editor_can_update_others_note() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function test_editor_can_update_others_note() {
public function test_editor_can_update_others_note(): void {

$post_id = self::factory()->post->create(
array(
'post_author' => self::$contributor_id,
'post_status' => 'draft',
)
);
$note_id = self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'note',
'user_id' => self::$contributor_id,
'comment_content' => 'Contributor note',
)
);
Copy link
Member

Choose a reason for hiding this comment

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

Just helps with static analysis, since WP_Error can also be returned.

Suggested change
);
);
assert( is_int( $note_id ) );


wp_set_current_user( self::$editor_id );

$request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . $note_id );
$request->set_param( 'content', 'Edited by editor' );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
}

/**
* Tests that a contributor can still read notes on their own post.
*
* @ticket 64779
*/
public function test_contributor_can_read_others_notes_on_own_post() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public function test_contributor_can_read_others_notes_on_own_post() {
public function test_contributor_can_read_others_notes_on_own_post(): void {

$post_id = self::factory()->post->create(
array(
'post_author' => self::$contributor_id,
'post_status' => 'draft',
)
);
$note_id = self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'note',
'user_id' => self::$admin_id,
'comment_content' => 'Admin feedback',
)
);

wp_set_current_user( self::$contributor_id );

$request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $note_id );
$request->set_param( 'context', 'edit' );
$response = rest_get_server()->dispatch( $request );

$this->assertSame( 200, $response->get_status() );
$this->assertSame( $note_id, $response->get_data()['id'] );
}

/**
* Test retrieving comments by type as authenticated user.
*
Expand Down
Loading
Loading