Editor: Introduce the PHP-related code for Notes.

Bring the PHP part of the new Notes feature into core for the 6.9 release. See related Gutenberg Issue: https://github.com/WordPress/gutenberg/issues/71826. These changes do not impact any user facing functionality, they simply prepare core for the JavaScript functionality that will come over in a separate sync.

Overview of changes:
- Ensure Notes are not included in comment counts
- Enable the note type  (REST API)
- Adjust capabilities so edit_post cap implies ability to edit notes  
- Enable empty and duplicate notes for resolve/re-open actions  
- Add control over notes with post type supports check
- Register new note resolution status meta

Props: ristojovanovic, adamsilverstein, jeffpaul, wildworks, mamaduka, swissspidy, timothyblynjacobs, kadamwhite.
Fixes #64096.


Built from https://develop.svn.wordpress.org/trunk@60987


git-svn-id: http://core.svn.wordpress.org/trunk@60323 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Adam Silverstein
2025-10-20 19:22:33 +00:00
parent 8444ab7453
commit 956747c6e8
7 changed files with 147 additions and 15 deletions

View File

@@ -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' ),
);
/**

View File

@@ -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 ) ) {

View File

@@ -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' );

View File

@@ -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 );
}

View File

@@ -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.
*

View File

@@ -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;
}
}

View File

@@ -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.