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
4 changes: 2 additions & 2 deletions src/wp-admin/includes/class-wp-comments-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function prepare_items() {

$comment_type = '';

if ( ! empty( $_REQUEST['comment_type'] ) && 'note' !== $_REQUEST['comment_type'] ) {
if ( ! empty( $_REQUEST['comment_type'] ) && ! in_array( $_REQUEST['comment_type'], wp_get_internal_comment_types(), true ) ) {
$comment_type = $_REQUEST['comment_type'];
}

Expand Down Expand Up @@ -155,7 +155,7 @@ public function prepare_items() {
'number' => $number,
'post_id' => $post_id,
'type' => $comment_type,
'type__not_in' => array( 'note' ),
'type__not_in' => wp_get_internal_comment_types(),
'orderby' => $orderby,
'order' => $order,
'post_type' => $post_type,
Expand Down
11 changes: 10 additions & 1 deletion src/wp-admin/includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,16 @@ 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' AND comment_type != 'note' GROUP BY comment_post_ID", ARRAY_A );
$internal_comment_types = wp_get_internal_comment_types();
$type_placeholders = implode( ', ', array_fill( 0, count( $internal_comment_types ), '%s' ) );
$pending = $wpdb->get_results(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"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 NOT IN ( $type_placeholders ) GROUP BY comment_post_ID",
$internal_comment_types
),
ARRAY_A
);

if ( $single ) {
if ( empty( $pending ) ) {
Expand Down
32 changes: 31 additions & 1 deletion src/wp-includes/comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,29 @@ function get_comment_statuses() {
return $status;
}

/**
* Retrieves the list of internal comment types.
*
* Internal comment types are used by core features (such as block notes
* and emoji reactions) and are not user-authored discussion comments.
* They should typically be excluded from front-end and admin comment
* listings, counts, and similar contexts that target user discussion.
*
* @since 7.0.0
*
* @return string[] List of internal comment type slugs.
*/
function wp_get_internal_comment_types(): array {
/**
* Filters the list of internal comment types.
*
* @since 7.0.0
*
* @param string[] $types List of internal comment type slugs.
*/
return (array) apply_filters( 'wp_internal_comment_types', array( 'note', 'reaction' ) );
}

/**
* Gets the default comment status for a post type.
*
Expand Down Expand Up @@ -2876,7 +2899,14 @@ function wp_update_comment_count_now( $post_id ) {
$new = apply_filters( 'pre_wp_update_comment_count_now', null, $old, $post_id );

if ( is_null( $new ) ) {
$new = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type != 'note'", $post_id ) );
$internal_comment_types = wp_get_internal_comment_types();
$type_placeholders = implode( ', ', array_fill( 0, count( $internal_comment_types ), '%s' ) );
$new = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ( $type_placeholders )", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
array_merge( array( $post_id ), $internal_comment_types )
)
);
} else {
$new = (int) $new;
}
Expand Down
6 changes: 4 additions & 2 deletions src/wp-includes/link-template.php
Original file line number Diff line number Diff line change
Expand Up @@ -4349,10 +4349,12 @@ function is_avatar_comment_type( $comment_type ) {
* @since 3.0.0
*
* @since 6.9.0 The 'note' comment type was added.
* @since 7.0.0 The 'reaction' comment type was added.
*
* @param array $types An array of content types. Default contains 'comment' and 'note'.
* @param array $types An array of content types. Default contains 'comment' and the
* internal comment types returned by wp_get_internal_comment_types().
*/
$allowed_comment_types = apply_filters( 'get_avatar_comment_types', array( 'comment', 'note' ) );
$allowed_comment_types = apply_filters( 'get_avatar_comment_types', array_merge( array( 'comment' ), wp_get_internal_comment_types() ) );

return in_array( $comment_type, (array) $allowed_comment_types, true );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ 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_note = in_array( $request['type'], wp_get_internal_comment_types(), true );
$is_edit_context = 'edit' === $request['context'];
$protected_params = array( 'author', 'author_exclude', 'author_email', 'type', 'status' );
$forbidden_params = array();
Expand Down Expand Up @@ -437,8 +437,8 @@ public function get_item_permissions_check( $request ) {
return $comment;
}

// Re-map edit context capabilities when requesting `note` type.
$edit_cap = 'note' === $comment->comment_type ? array( 'edit_comment', $comment->comment_ID ) : array( 'moderate_comments' );
// Re-map edit context capabilities when requesting `note` or `reaction` type.
$edit_cap = in_array( $comment->comment_type, wp_get_internal_comment_types(), true ) ? 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',
Expand Down Expand Up @@ -497,7 +497,7 @@ 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'];
$is_note = ! empty( $request['type'] ) && in_array( $request['type'], wp_get_internal_comment_types(), true );

if ( ! is_user_logged_in() && $is_note ) {
return new WP_Error(
Expand Down Expand Up @@ -657,14 +657,65 @@ public function create_item( $request ) {
}

// Do not allow comments to be created with a non-core type.
if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array( 'comment', 'note' ), true ) ) {
if ( ! empty( $request['type'] ) && ! in_array( $request['type'], array_merge( array( 'comment' ), wp_get_internal_comment_types() ), true ) ) {
return new WP_Error(
'rest_invalid_comment_type',
__( 'Cannot create a comment with that type.' ),
array( 'status' => 400 )
);
}

// Validate reaction-specific constraints.
if ( ! empty( $request['type'] ) && 'reaction' === $request['type'] ) {
$valid_emojis = array( 'heart', 'celebration', 'smile', 'eyes', 'rocket' );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No 💩? 😜

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ideally the list will be filterable at some point. initially keeping the list short and simple to keep the UI simple.


// Reaction content must be a valid emoji slug.
if ( empty( $request['content'] ) || ! in_array( $request['content'], $valid_emojis, true ) ) {
return new WP_Error(
'rest_reaction_invalid_emoji',
__( 'Reaction content must be a valid emoji slug.' ),
array( 'status' => 400 )
);
}

// Reaction parent must exist and be a note.
if ( empty( $request['parent'] ) ) {
return new WP_Error(
'rest_reaction_parent_required',
__( 'Reactions must have a parent note.' ),
array( 'status' => 400 )
);
}

$parent_comment = get_comment( $request['parent'] );
if ( ! $parent_comment || 'note' !== $parent_comment->comment_type ) {
return new WP_Error(
'rest_reaction_invalid_parent',
__( 'Reactions can only be added to notes.' ),
array( 'status' => 400 )
);
}

// Enforce uniqueness: one emoji per user per note.
Comment on lines +690 to +699
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In the future, perhaps reactions could be enabled for non-note comments as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, or blocks could have reactions!

$existing = get_comments(
array(
'comment_type' => 'reaction',
'comment_parent' => $request['parent'],
'user_id' => get_current_user_id(),
'search' => $request['content'],
'count' => true,
)
);

if ( $existing > 0 ) {
return new WP_Error(
'rest_reaction_duplicate',
__( 'You have already added this reaction.' ),
array( 'status' => 409 )
);
}
}

$prepared_comment = $this->prepare_item_for_database( $request );
if ( is_wp_error( $prepared_comment ) ) {
return $prepared_comment;
Expand Down Expand Up @@ -743,9 +794,9 @@ public function create_item( $request ) {
);
}

// Don't check for duplicates or flooding for notes.
// Don't check for duplicates or flooding for notes or reactions.
$prepared_comment['comment_approved'] =
'note' === $prepared_comment['comment_type'] ?
in_array( $prepared_comment['comment_type'], wp_get_internal_comment_types(), true ) ?
'1' :
wp_allow_comment( $prepared_comment, true );

Expand Down Expand Up @@ -1305,7 +1356,7 @@ protected function prepare_links( $comment ) {
}

// Embedding children for notes requires `type` and `status` inheritance.
if ( isset( $links['children'] ) && 'note' === $comment->comment_type ) {
if ( isset( $links['children'] ) && in_array( $comment->comment_type, wp_get_internal_comment_types(), true ) ) {
$args = array(
'parent' => $comment->comment_ID,
'type' => $comment->comment_type,
Expand Down Expand Up @@ -1919,7 +1970,7 @@ protected function check_read_post_permission( $post, $request ) {
* @return bool Whether the comment can be read.
*/
protected function check_read_permission( $comment, $request ) {
if ( 'note' !== $comment->comment_type && ! empty( $comment->comment_post_ID ) ) {
if ( ! in_array( $comment->comment_type, wp_get_internal_comment_types(), true ) && ! empty( $comment->comment_post_ID ) ) {
$post = get_post( $comment->comment_post_ID );
if ( $post ) {
if ( $this->check_read_post_permission( $post, $request ) && 1 === (int) $comment->comment_approved ) {
Expand Down Expand Up @@ -2034,6 +2085,11 @@ protected function check_is_comment_content_allowed( $prepared_comment ) {
return true;
}

// Reactions always have content (the emoji slug), so allow them.
if ( isset( $check['comment_type'] ) && 'reaction' === $check['comment_type'] ) {
return true;
}

/*
* Do not allow a comment to be created with missing or empty
* comment_content. See wp_handle_comment_submission().
Expand Down
14 changes: 14 additions & 0 deletions tests/phpunit/tests/comment/wpUpdateCommentCountNow.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ public function test_only_approved_regular_comments_are_counted() {
'comment_approved' => 1,
)
);
self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'reaction',
'comment_approved' => 0,
)
);
self::factory()->comment->create(
array(
'comment_post_ID' => $post_id,
'comment_type' => 'reaction',
'comment_approved' => 1,
)
);

$this->assertTrue( wp_update_comment_count_now( $post_id ) );
$this->assertSame( '1', get_comments_number( $post_id ) );
Expand Down
Loading
Loading