diff --git a/src/wp-admin/includes/class-wp-comments-list-table.php b/src/wp-admin/includes/class-wp-comments-list-table.php index c4d323cf892b0..82cc921ee3bc0 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -151,6 +151,7 @@ public function prepare_items() { 'order' => $order, 'post_type' => $post_type, 'update_comment_post_cache' => true, + 'type__not_in' => array( 'note' ), ); /** diff --git a/src/wp-admin/includes/comment.php b/src/wp-admin/includes/comment.php index ad8b653161ba3..d38ab9447cf95 100644 --- a/src/wp-admin/includes/comment.php +++ b/src/wp-admin/includes/comment.php @@ -157,7 +157,7 @@ function get_pending_comments_num( $post_id ) { $post_id_array = array_map( 'intval', $post_id_array ); $post_id_in = "'" . implode( "', '", $post_id_array ) . "'"; - $pending = $wpdb->get_results( "SELECT comment_post_ID, COUNT(comment_ID) as num_comments FROM $wpdb->comments WHERE comment_post_ID IN ( $post_id_in ) AND comment_approved = '0' GROUP BY comment_post_ID", ARRAY_A ); + $pending = $wpdb->get_results( "SELECT comment_post_ID, COUNT(comment_ID) as num_comments FROM $wpdb->comments WHERE comment_post_ID IN ( $post_id_in ) AND comment_approved = '0' AND comment_type != 'note' GROUP BY comment_post_ID", ARRAY_A ); if ( $single ) { if ( empty( $pending ) ) { diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index aabe9f60dbb4a..8ae1c843252d7 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -417,6 +417,7 @@ function get_comment_count( $post_id = 0 ) { 'count' => true, 'update_comment_meta_cache' => false, 'orderby' => 'none', + 'type__not_in' => array( 'note' ), ); if ( $post_id > 0 ) { $args['post_id'] = $post_id; @@ -714,6 +715,11 @@ function wp_allow_comment( $commentdata, $wp_error = false ) { $dupe_id = $wpdb->get_var( $dupe ); + // Allow duplicate notes for resolution purposes. + if ( isset( $commentdata['comment_type'] ) && 'note' === $commentdata['comment_type'] ) { + $dupe_id = false; + } + /** * Filters the ID, if any, of the duplicate comment found when creating a new comment. * @@ -4103,3 +4109,27 @@ function _wp_check_for_scheduled_update_comment_type() { wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'wp_update_comment_type_batch' ); } } + +/** + * Register initial note status meta. + * + * @since 6.9.0 + */ +function wp_create_initial_comment_meta() { + register_meta( + 'comment', + '_wp_note_status', + array( + 'type' => 'string', + 'description' => __( 'Note resolution status' ), + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'enum' => array( 'resolved', 'reopen' ), + ), + ), + ) + ); +} +add_action( 'init', 'wp_create_initial_comment_meta' ); diff --git a/src/wp-includes/link-template.php b/src/wp-includes/link-template.php index 5870a068a0a89..525a9efe53eb8 100644 --- a/src/wp-includes/link-template.php +++ b/src/wp-includes/link-template.php @@ -4343,9 +4343,11 @@ function is_avatar_comment_type( $comment_type ) { * * @since 3.0.0 * - * @param array $types An array of content types. Default only contains 'comment'. + * @since 6.9.0 The 'note' comment type was added. + * + * @param array $types An array of content types. Default contains 'comment' and 'note'. */ - $allowed_comment_types = apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); + $allowed_comment_types = apply_filters( 'get_avatar_comment_types', array( 'comment', 'note' ) ); return in_array( $comment_type, (array) $allowed_comment_types, true ); } diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index a2d8fba9db341..19cd927c5c8dc 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -37,7 +37,18 @@ function create_initial_post_types() { 'rewrite' => false, 'query_var' => false, 'delete_with_user' => true, - 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ), + 'supports' => array( + 'title', + 'editor' => array( 'notes' => true ), + 'author', + 'thumbnail', + 'excerpt', + 'trackbacks', + 'custom-fields', + 'comments', + 'revisions', + 'post-formats', + ), 'show_in_rest' => true, 'rest_base' => 'posts', 'rest_controller_class' => 'WP_REST_Posts_Controller', @@ -62,7 +73,16 @@ function create_initial_post_types() { 'rewrite' => false, 'query_var' => false, 'delete_with_user' => true, - 'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'page-attributes', 'custom-fields', 'comments', 'revisions' ), + 'supports' => array( + 'title', + 'editor' => array( 'notes' => true ), + 'author', + 'thumbnail', + 'page-attributes', + 'custom-fields', + 'comments', + 'revisions', + ), 'show_in_rest' => true, 'rest_base' => 'pages', 'rest_controller_class' => 'WP_REST_Posts_Controller', @@ -2329,7 +2349,6 @@ function post_type_supports( $post_type, $feature ) { return ( isset( $_wp_post_type_features[ $post_type ][ $feature ] ) ); } - /** * Retrieves a list of post type names that support a specific feature. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index 0714a9ebfb108..c519fce500a2e 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -123,11 +123,21 @@ public function register_routes() { * @return true|WP_Error True if the request has read access, error object otherwise. */ public function get_items_permissions_check( $request ) { + $is_note = 'note' === $request['type']; + $is_edit_context = 'edit' === $request['context']; if ( ! empty( $request['post'] ) ) { foreach ( (array) $request['post'] as $post_id ) { $post = get_post( $post_id ); + if ( $post && $is_note && ! $this->check_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 ( ! empty( $post_id ) && $post && ! $this->check_read_post_permission( $post, $request ) ) { return new WP_Error( 'rest_cannot_read_post', @@ -144,7 +154,18 @@ public function get_items_permissions_check( $request ) { } } - if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + // Re-map edit context capabilities when requesting `note` for a post. + if ( $is_edit_context && $is_note && ! empty( $request['post'] ) ) { + foreach ( (array) $request['post'] as $post_id ) { + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return new WP_Error( + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to edit comments.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + } + } elseif ( $is_edit_context && ! current_user_can( 'moderate_comments' ) ) { return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), @@ -394,7 +415,9 @@ public function get_item_permissions_check( $request ) { return $comment; } - if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + // Re-map edit context capabilities when requesting `note` type. + $edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' ); + if ( ! empty( $request['context'] ) && 'edit' === $request['context'] && ! current_user_can( ...$edit_cap ) ) { return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit comments.' ), @@ -452,6 +475,16 @@ public function get_item( $request ) { * @return true|WP_Error True if the request has access to create items, error object otherwise. */ public function create_item_permissions_check( $request ) { + $is_note = ! empty( $request['type'] ) && 'note' === $request['type']; + + if ( ! is_user_logged_in() && $is_note ) { + return new WP_Error( + 'rest_comment_login_required', + __( 'Sorry, you must be logged in to comment.' ), + array( 'status' => 401 ) + ); + } + if ( ! is_user_logged_in() ) { if ( get_option( 'comment_registration' ) ) { return new WP_Error( @@ -505,7 +538,8 @@ public function create_item_permissions_check( $request ) { } } - if ( isset( $request['status'] ) && ! current_user_can( 'moderate_comments' ) ) { + $edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' ); + if ( isset( $request['status'] ) && ! current_user_can( ...$edit_cap ) ) { return new WP_Error( 'rest_comment_invalid_status', /* translators: %s: Request parameter. */ @@ -532,7 +566,15 @@ public function create_item_permissions_check( $request ) { ); } - if ( 'draft' === $post->post_status ) { + if ( $is_note && ! $this->check_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 ( 'draft' === $post->post_status && ! $is_note ) { return new WP_Error( 'rest_comment_draft_post', __( 'Sorry, you are not allowed to create a comment on this post.' ), @@ -556,7 +598,7 @@ public function create_item_permissions_check( $request ) { ); } - if ( ! comments_open( $post->ID ) ) { + if ( ! comments_open( $post->ID ) && ! $is_note ) { return new WP_Error( 'rest_comment_closed', __( 'Sorry, comments are closed for this item.' ), @@ -584,8 +626,8 @@ public function create_item( $request ) { ); } - // Do not allow comments to be created with a non-default type. - if ( ! empty( $request['type'] ) && 'comment' !== $request['type'] ) { + // Do not allow comments to be created with a non-core type. + if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note' ), true ) ) { return new WP_Error( 'rest_invalid_comment_type', __( 'Cannot create a comment with that type.' ), @@ -598,12 +640,17 @@ public function create_item( $request ) { return $prepared_comment; } - $prepared_comment['comment_type'] = 'comment'; + $prepared_comment['comment_type'] = $request['type']; if ( ! isset( $prepared_comment['comment_content'] ) ) { $prepared_comment['comment_content'] = ''; } + // Include note metadata into check_is_comment_content_allowed. + if ( isset( $request['meta']['_wp_note_status'] ) ) { + $prepared_comment['meta']['_wp_note_status'] = $request['meta']['_wp_note_status']; + } + if ( ! $this->check_is_comment_content_allowed( $prepared_comment ) ) { return new WP_Error( 'rest_comment_content_invalid', @@ -1519,6 +1566,7 @@ public function get_item_schema() { 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, + 'default' => 'comment', ), ), ); @@ -1925,10 +1973,42 @@ protected function check_is_comment_content_allowed( $prepared_comment ) { return true; } + // Allow empty notes only when resolution metadata is valid. + if ( + isset( $check['comment_type'] ) && + 'note' === $check['comment_type'] && + isset( $check['meta']['_wp_note_status'] ) && + in_array( $check['meta']['_wp_note_status'], array( 'resolved', 'reopen' ), true ) + ) { + return true; + } + /* * Do not allow a comment to be created with missing or empty * comment_content. See wp_handle_comment_submission(). */ return '' !== $check['comment_content']; } + + /** + * Check if post type supports notes. + * + * @param string $post_type Post type name. + * @return bool True if post type supports notes, false otherwise. + */ + private function check_post_type_supports_notes( $post_type ) { + $supports = get_all_post_type_supports( $post_type ); + if ( ! isset( $supports['editor'] ) ) { + return false; + } + if ( ! is_array( $supports['editor'] ) ) { + return false; + } + foreach ( $supports['editor'] as $item ) { + if ( ! empty( $item['notes'] ) ) { + return true; + } + } + return false; + } } diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 6dafd4b407c48..e0868bbeca362 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -12,8 +12,10 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase protected static $admin_id; protected static $editor_id; protected static $moderator_id; + protected static $contributor_id; protected static $subscriber_id; protected static $author_id; + protected static $user_ids = array(); protected static $post_id; protected static $password_id; @@ -26,6 +28,7 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase protected static $comment_ids = array(); protected static $total_comments = 30; protected static $per_page = 50; + protected static $num_notes = 10; protected $endpoint; @@ -39,33 +42,38 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); - self::$superadmin_id = $factory->user->create( + self::$superadmin_id = $factory->user->create( array( 'role' => 'administrator', 'user_login' => 'superadmin', ) ); - self::$admin_id = $factory->user->create( + self::$admin_id = $factory->user->create( array( 'role' => 'administrator', ) ); - self::$editor_id = $factory->user->create( + self::$editor_id = $factory->user->create( array( 'role' => 'editor', ) ); - self::$moderator_id = $factory->user->create( + self::$moderator_id = $factory->user->create( array( 'role' => 'comment_moderator', ) ); - self::$subscriber_id = $factory->user->create( + self::$contributor_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber', ) ); - self::$author_id = $factory->user->create( + self::$author_id = $factory->user->create( array( 'role' => 'author', 'display_name' => 'Sea Captain', @@ -113,6 +121,16 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); + self::$user_ids = array( + 'superadmin' => self::$superadmin_id, + 'administrator' => self::$admin_id, + 'editor' => self::$editor_id, + 'moderator' => self::$moderator_id, + 'contributor' => self::$contributor_id, + 'subscriber' => self::$subscriber_id, + 'author' => self::$author_id, + ); + // Set up comments for pagination tests. for ( $i = 0; $i < self::$total_comments - 1; $i++ ) { self::$comment_ids[] = $factory->comment->create( @@ -131,6 +149,7 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); self::delete_user( self::$editor_id ); self::delete_user( self::$moderator_id ); + self::delete_user( self::$contributor_id ); self::delete_user( self::$subscriber_id ); self::delete_user( self::$author_id ); @@ -3621,4 +3640,433 @@ public static function data_head_request_with_specified_fields_returns_success_r 'get_items request' => array( '/wp/v2/comments' ), ); } + + /** + * Create a test post with note. + * + * @param int $user_id Post author's user ID. + * @return int Post ID. + */ + protected function create_test_post_with_note( $role ) { + $user_id = self::$user_ids[ $role ]; + $post_id = self::factory()->post->create( + array( + 'post_title' => 'Test Post for Notes', + 'post_content' => 'This is a test post to check note permissions.', + 'post_status' => 'contributor' === $role ? 'draft' : 'publish', + 'post_author' => $user_id, + ) + ); + + for ( $i = 0; $i < self::$num_notes; $i++ ) { + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'note', + 'comment_approved' => 0 === $i % 2 ? 1 : 0, + ) + ); + } + + return $post_id; + } + + /** + * @ticket 64096 + */ + public function test_cannot_read_note_without_post_type_support() { + register_post_type( + 'no-notes', + array( + 'label' => 'No Notes', + 'supports' => array( 'title', 'editor', 'author', 'comments' ), + 'show_in_rest' => true, + 'public' => true, + ) + ); + + create_initial_rest_routes(); + wp_set_current_user( self::$admin_id ); + + $post_id = self::factory()->post->create( array( 'post_type' => 'no-notes' ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_not_supported_post_type', $response, 403 ); + + _unregister_post_type( 'no-notes' ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_require_login() { + wp_set_current_user( 0 ); + + $post_id = self::factory()->post->create(); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_comment_login_required', $response, 401 ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_note_without_post_type_support() { + register_post_type( + 'no-note', + array( + 'label' => 'No Notes', + 'supports' => array( 'title', 'editor', 'author', 'comments' ), + 'show_in_rest' => true, + 'public' => true, + ) + ); + + wp_set_current_user( self::$admin_id ); + $post_id = self::factory()->post->create( array( 'post_type' => 'no-note' ) ); + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Call me Ishmael.', + 'author' => self::$admin_id, + 'type' => 'note', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_not_supported_post_type', $response, 403 ); + + _unregister_post_type( 'no-note' ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_draft_post() { + wp_set_current_user( self::$editor_id ); + $draft_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + ) + ); + $params = array( + 'post' => $draft_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Call me Ishmael.', + 'author' => self::$editor_id, + 'type' => 'note', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + $this->assertSame( 'Call me Ishmael.', $new_comment->comment_content ); + $this->assertSame( 'note', $new_comment->comment_type ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_status() { + wp_set_current_user( self::$author_id ); + $post_id = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$author_id, + 'type' => 'note', + 'status' => 'hold', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + + $this->assertSame( '0', $new_comment->comment_approved ); + $this->assertSame( 'note', $new_comment->comment_type ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_with_non_valid_comment_type() { + wp_set_current_user( self::$admin_id ); + $post_id = $this->factory->post->create(); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$admin_id, + 'type' => 'review', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_comment_type', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_create_assigns_default_type() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$editor_id, + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + + $this->assertSame( 'comment', $new_comment->comment_type ); + } + + /** + * @dataProvider data_note_status_provider + * @ticket 64096 + */ + public function test_create_empty_note_with_resolution_meta( $status ) { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + 'meta' => array( + '_wp_note_status' => $status, + ), + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_empty_note_without_resolution_meta() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_empty_note_with_invalid_resolution_meta() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + 'meta' => array( + '_wp_note_status' => 'invalid', + ), + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_create_duplicate_note() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + + for ( $i = 0; $i < 2; $i++ ) { + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => 'Doplicated comment', + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + } + } + + /** + * @dataProvider data_note_get_items_permissions_data_provider + * @ticket 64096 + */ + public function test_note_get_items_permissions_edit_context( $role, $post_author_role, $can_read ) { + wp_set_current_user( self::$user_ids[ $role ] ); + $post_id = $this->create_test_post_with_note( $post_author_role ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'status', 'all' ); + $request->set_param( 'per_page', 100 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + if ( $can_read ) { + $comments = $response->get_data(); + $this->assertEquals( self::$num_notes, count( $comments ) ); + } else { + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + wp_delete_post( $post_id, true ); + } + + /** + * @ticket 64096 + */ + public function test_note_get_items_permissions_mixed_post_authors() { + $author_post_id = $this->create_test_post_with_note( 'author' ); + $editor_post_id = $this->create_test_post_with_note( 'editor' ); + + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', array( $author_post_id, $editor_post_id ) ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'status', 'all' ); + $request->set_param( 'per_page', 100 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + + wp_delete_post( $author_post_id, true ); + wp_delete_post( $editor_post_id, true ); + } + + /** + * @dataProvider data_note_get_items_permissions_data_provider + * @ticket 64096 + */ + public function test_note_get_item_permissions_edit_context( $role, $post_author_role, $can_read ) { + wp_set_current_user( self::$user_ids[ $role ] ); + + $post_id = self::factory()->post->create( + array( + 'post_title' => 'Test Post for Block Comments', + 'post_content' => 'This is a test post to check block comment permissions.', + 'post_status' => 'contributor' === $post_author_role ? 'draft' : 'publish', + 'post_author' => self::$user_ids[ $post_author_role ], + ) + ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'note', + // Test with unapproved comment, which is more restrictive. + 'comment_approved' => 0, + 'user_id' => self::$user_ids[ $post_author_role ], + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + if ( $can_read ) { + $comment = $response->get_data(); + $this->assertEquals( $comment_id, $comment['id'] ); + } else { + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + wp_delete_post( $post_id, true ); + } + + public function data_note_get_items_permissions_data_provider() { + return array( + 'Administrator can see note on other posts' => array( 'administrator', 'author', true ), + 'Editor can see note on other posts' => array( 'editor', 'contributor', true ), + 'Author cannot see note on other posts' => array( 'author', 'editor', false ), + 'Contributor cannot see note on other posts' => array( 'contributor', 'author', false ), + 'Subscriber cannot see note' => array( 'subscriber', 'author', false ), + 'Author can see note on own post' => array( 'author', 'author', true ), + 'Contributor can see note on own post' => array( 'contributor', 'contributor', true ), + ); + } + + public function data_note_status_provider() { + return array( + 'resolved' => array( 'resolved' ), + 'reopen' => array( 'reopen' ), + ); + } }