diff --git a/wp-admin/includes/class-wp-comments-list-table.php b/wp-admin/includes/class-wp-comments-list-table.php index c4d323cf89..82cc921ee3 100644 --- a/wp-admin/includes/class-wp-comments-list-table.php +++ b/wp-admin/includes/class-wp-comments-list-table.php @@ -151,6 +151,7 @@ class WP_Comments_List_Table extends WP_List_Table { 'order' => $order, 'post_type' => $post_type, 'update_comment_post_cache' => true, + 'type__not_in' => array( 'note' ), ); /** diff --git a/wp-admin/includes/comment.php b/wp-admin/includes/comment.php index ad8b653161..d38ab9447c 100644 --- a/wp-admin/includes/comment.php +++ b/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/wp-includes/comment.php b/wp-includes/comment.php index aabe9f60db..8ae1c84325 100644 --- a/wp-includes/comment.php +++ b/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/wp-includes/link-template.php b/wp-includes/link-template.php index 5870a068a0..525a9efe53 100644 --- a/wp-includes/link-template.php +++ b/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/wp-includes/post.php b/wp-includes/post.php index a2d8fba9db..19cd927c5c 100644 --- a/wp-includes/post.php +++ b/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/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php index 0714a9ebfb..c519fce500 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php @@ -123,11 +123,21 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { * @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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { } } - 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { * @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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { } } - 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { ); } - 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { ); } - 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { ); } - // 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { 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 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), 'readonly' => true, + 'default' => 'comment', ), ), ); @@ -1925,10 +1973,42 @@ class WP_REST_Comments_Controller extends WP_REST_Controller { 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/wp-includes/version.php b/wp-includes/version.php index 46d9b19535..645f112c26 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.9-alpha-60986'; +$wp_version = '6.9-alpha-60987'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.