From 96b105be7c60f740e69934bfcd5aff8a220d234b Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Tue, 9 Jun 2026 15:49:21 +0530 Subject: [PATCH 1/2] Add dynamic comment statuses --- src/wp-admin/edit-form-comment.php | 63 ++++- .../includes/class-wp-comments-list-table.php | 36 ++- src/wp-admin/includes/comment.php | 16 +- src/wp-includes/class-wp-comment-query.php | 4 +- src/wp-includes/class-wp-xmlrpc-server.php | 11 +- src/wp-includes/comment.php | 111 +++++++- .../class-wp-rest-comments-controller.php | 134 +++++++++- tests/phpunit/tests/comment/statuses.php | 246 ++++++++++++++++++ .../rest-api/rest-comments-controller.php | 153 +++++++++++ tests/phpunit/tests/xmlrpc/wp/editComment.php | 85 ++++++ 10 files changed, 815 insertions(+), 44 deletions(-) create mode 100644 tests/phpunit/tests/comment/statuses.php diff --git a/src/wp-admin/edit-form-comment.php b/src/wp-admin/edit-form-comment.php index e0bbc9f657a73..86f87f083aea7 100644 --- a/src/wp-admin/edit-form-comment.php +++ b/src/wp-admin/edit-form-comment.php @@ -112,17 +112,18 @@
comment_approved ) { - case '1': - _e( 'Approved' ); - break; - case '0': - _e( 'Pending' ); - break; - case 'spam': - _e( 'Spam' ); - break; -} +$comment_statuses = _wp_get_custom_comment_statuses(); +$status_labels = array_merge( + $comment_statuses, + array( + '0' => _x( 'Pending', 'comment status' ), + '1' => _x( 'Approved', 'comment status' ), + 'spam' => _x( 'Spam', 'comment status' ), + 'trash' => _x( 'Trash', 'comment status' ), + ) +); + +echo esc_html( $status_labels[ $comment->comment_approved ] ?? $comment->comment_approved ); ?> @@ -133,9 +134,43 @@ _e( 'Comment status' ); ?> -
-
- + _x( 'Approved', 'comment status' ), + '0' => _x( 'Pending', 'comment status' ), + 'spam' => _x( 'Spam', 'comment status' ), +); + +foreach ( _wp_get_custom_comment_statuses() as $status => $label ) { + $comment_status_radio[ $status ] = $label; +} + +/** + * Filters the editable comment statuses displayed on the edit comment screen. + * + * @since 7.1.0 + * + * @param string[] $comment_status_radio List of editable comment status labels keyed by status. + * @param WP_Comment $comment Current comment object. + */ +$comment_status_radio = apply_filters( 'editable_comment_statuses', $comment_status_radio, $comment ); + +$valid_comment_status_radio = array_merge( + array( + '1' => true, + '0' => true, + 'spam' => true, + ), + array_fill_keys( array_keys( _wp_get_custom_comment_statuses() ), true ) +); +$comment_status_radio = array_intersect_key( $comment_status_radio, $valid_comment_status_radio ); + +foreach ( $comment_status_radio as $status => $label ) : + ?> +
+
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 f0c909f0f5ce5..85ba558ec145b 100644 --- a/src/wp-admin/includes/class-wp-comments-list-table.php +++ b/src/wp-admin/includes/class-wp-comments-list-table.php @@ -97,9 +97,14 @@ public function prepare_items() { $mode = get_user_setting( 'posts_list_mode', 'list' ); } - $comment_status = $_REQUEST['comment_status'] ?? 'all'; + $comment_status = 'all'; + if ( isset( $_REQUEST['comment_status'] ) && is_scalar( $_REQUEST['comment_status'] ) ) { + $comment_status = sanitize_key( wp_unslash( $_REQUEST['comment_status'] ) ); + } + + $valid_statuses = array_merge( array( 'all', 'mine', 'moderated', 'approved', 'spam', 'trash' ), array_keys( _wp_get_custom_comment_statuses() ) ); - if ( ! in_array( $comment_status, array( 'all', 'mine', 'moderated', 'approved', 'spam', 'trash' ), true ) ) { + if ( ! in_array( $comment_status, $valid_statuses, true ) ) { $comment_status = 'all'; } @@ -301,6 +306,10 @@ protected function get_views() { unset( $statuses['trash'] ); } + foreach ( _wp_get_custom_comment_statuses() as $status => $label ) { + $statuses[ $status ] = $label; + } + $link = admin_url( 'edit-comments.php' ); if ( ! empty( $comment_type ) && 'all' !== $comment_type ) { @@ -324,7 +333,7 @@ protected function get_views() { } if ( ! isset( $num_comments->$status ) ) { - $num_comments->$status = 10; + $num_comments->$status = isset( _wp_get_custom_comment_statuses()[ $status ] ) ? 0 : 10; } $link = add_query_arg( 'comment_status', $status, $link ); @@ -339,16 +348,21 @@ protected function get_views() { $link = add_query_arg( 's', esc_attr( wp_unslash( $_REQUEST['s'] ) ), $link ); */ + $count = sprintf( + '%s', + ( 'moderated' === $status ) ? 'pending' : sanitize_html_class( $status ), + number_format_i18n( $num_comments->$status ) + ); + + if ( is_array( $label ) ) { + $label = sprintf( translate_nooped_plural( $label, $num_comments->$status ), $count ); + } else { + $label = sprintf( '%s (%s)', esc_html( $label ), $count ); + } + $status_links[ $status ] = array( 'url' => esc_url( $link ), - 'label' => sprintf( - translate_nooped_plural( $label, $num_comments->$status ), - sprintf( - '%s', - ( 'moderated' === $status ) ? 'pending' : $status, - number_format_i18n( $num_comments->$status ) - ) - ), + 'label' => $label, 'current' => $status === $comment_status, ); } diff --git a/src/wp-admin/includes/comment.php b/src/wp-admin/includes/comment.php index ae5ba9d223350..ffbd55b330cdf 100644 --- a/src/wp-admin/includes/comment.php +++ b/src/wp-admin/includes/comment.php @@ -64,8 +64,20 @@ function edit_comment() { if ( isset( $_POST['newcomment_author_url'] ) ) { $_POST['comment_author_url'] = $_POST['newcomment_author_url']; } - if ( isset( $_POST['comment_status'] ) ) { - $_POST['comment_approved'] = $_POST['comment_status']; + if ( isset( $_POST['comment_status'] ) && is_scalar( $_POST['comment_status'] ) ) { + $comment_status = sanitize_key( wp_unslash( $_POST['comment_status'] ) ); + $valid_comment_statuses = array_merge( + array( + '1' => true, + '0' => true, + 'spam' => true, + ), + array_fill_keys( array_keys( _wp_get_custom_comment_statuses() ), true ) + ); + + if ( isset( $valid_comment_statuses[ $comment_status ] ) ) { + $_POST['comment_approved'] = $comment_status; + } } if ( isset( $_POST['content'] ) ) { $_POST['comment_content'] = $_POST['content']; diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index cfabfd7e6b964..d96910895257e 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -571,7 +571,9 @@ protected function get_comment_ids() { case 'all': case '': - $status_clauses[] = "( comment_approved = '0' OR comment_approved = '1' )"; + $all_statuses = array_merge( array( '0', '1' ), array_keys( _wp_get_custom_comment_statuses() ) ); + $placeholders = implode( ', ', array_fill( 0, count( $all_statuses ), '%s' ) ); + $status_clauses[] = $wpdb->prepare( "comment_approved IN ($placeholders)", $all_statuses ); break; default: diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 8cbf6d977f5a2..9747373071e80 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -3837,8 +3837,15 @@ public function wp_editComment( $args ) { ); if ( isset( $content_struct['status'] ) ) { - $statuses = get_comment_statuses(); - $statuses = array_keys( $statuses ); + $statuses = array_merge( + array( + 'hold', + 'approve', + 'spam', + 'trash', + ), + array_keys( _wp_get_custom_comment_statuses() ) + ); if ( ! in_array( $content_struct['status'], $statuses, true ) ) { return new IXR_Error( 401, __( 'Invalid comment status.' ) ); diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 70d5c03b378f4..68e8de029aca4 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -291,7 +291,57 @@ function get_comment_statuses() { 'trash' => _x( 'Trash', 'comment status' ), ); - return $status; + /** + * Filters the list of supported comment statuses. + * + * The returned array is keyed by status and has translated status labels as values. + * Custom statuses are stored in the `comment_approved` database field using the + * array key as the raw status value. Custom status keys must be compatible with + * sanitize_key() and no longer than 20 characters. + * + * @since 7.1.0 + * + * @param string[] $status List of comment status labels keyed by status. + */ + return apply_filters( 'comment_statuses', $status ); +} + +/** + * Retrieves registered custom comment statuses. + * + * @since 7.1.0 + * @access private + * + * @return string[] List of valid custom comment status labels keyed by status. + * Invalid and reserved filtered statuses are excluded. + */ +function _wp_get_custom_comment_statuses() { + $reserved_statuses = array( + '0', + '1', + 'all', + 'any', + 'approve', + 'approved', + 'hold', + 'mine', + 'moderated', + 'post-trashed', + 'spam', + 'trash', + 'unapproved', + 'unspam', + 'untrash', + ); + $custom_statuses = array_diff_key( get_comment_statuses(), array_flip( $reserved_statuses ) ); + + foreach ( $custom_statuses as $status => $label ) { + if ( $status !== sanitize_key( $status ) || strlen( $status ) > 20 ) { + unset( $custom_statuses[ $status ] ); + } + } + + return $custom_statuses; } /** @@ -400,7 +450,8 @@ function get_lastcommentmodified( $timezone = 'server' ) { * @type int $trash The number of trashed comments. * @type int $post-trashed The number of comments for posts that are in the trash. * @type int $total_comments The total number of non-trashed comments, including spam. - * @type int $all The total number of pending or approved comments. + * @type int $all The total number of pending, approved, or custom status comments. + * @type int ...$custom_statuses The number of comments for each registered custom status. * } */ function get_comment_count( $post_id = 0 ) { @@ -436,7 +487,40 @@ function get_comment_count( $post_id = 0 ) { $comment_count[ $key ] = get_comments( array_merge( $args, array( 'status' => $value ) ) ); } - $comment_count['all'] = $comment_count['approved'] + $comment_count['awaiting_moderation']; + $custom_statuses = _wp_get_custom_comment_statuses(); + foreach ( $custom_statuses as $status => $label ) { + $comment_count[ $status ] = 0; + } + + if ( $custom_statuses ) { + global $wpdb; + + $custom_status_placeholders = implode( ', ', array_fill( 0, count( $custom_statuses ), '%s' ) ); + $custom_status_args = array_keys( $custom_statuses ); + $post_id_sql = ''; + + if ( $post_id > 0 ) { + $post_id_sql = ' AND comment_post_ID = %d'; + $custom_status_args[] = $post_id; + } + + $custom_status_counts = $wpdb->get_results( + $wpdb->prepare( + "SELECT comment_approved, COUNT(*) AS num_comments FROM $wpdb->comments WHERE comment_approved IN ($custom_status_placeholders)$post_id_sql GROUP BY comment_approved", + $custom_status_args + ), + OBJECT_K + ); + + foreach ( $custom_status_counts as $status => $row ) { + $comment_count[ $status ] = (int) $row->num_comments; + } + } + + $comment_count['all'] = $comment_count['approved'] + $comment_count['awaiting_moderation']; + foreach ( array_keys( $custom_statuses ) as $status ) { + $comment_count['all'] += $comment_count[ $status ]; + } $comment_count['total_comments'] = $comment_count['all'] + $comment_count['spam']; return array_map( 'intval', $comment_count ); @@ -1825,7 +1909,8 @@ function wp_unspam_comment( $comment_id ) { * @since 1.0.0 * * @param int|WP_Comment $comment_id Comment ID or WP_Comment object - * @return string|false Status might be 'trash', 'approved', 'unapproved', 'spam'. False on failure. + * @return string|false Status might be 'trash', 'approved', 'unapproved', 'spam', or a custom status. + * False on failure. */ function wp_get_comment_status( $comment_id ) { $comment = get_comment( $comment_id ); @@ -1845,6 +1930,8 @@ function wp_get_comment_status( $comment_id ) { return 'spam'; } elseif ( 'trash' === $approved ) { return 'trash'; + } elseif ( isset( _wp_get_custom_comment_statuses()[ $approved ] ) ) { + return $approved; } else { return false; } @@ -2529,7 +2616,8 @@ function wp_new_comment_via_rest_notify_postauthor( $comment ) { * @global wpdb $wpdb WordPress database abstraction object. * * @param int|WP_Comment $comment_id Comment ID or WP_Comment object. - * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', or 'trash'. + * @param string $comment_status New comment status, either 'hold', 'approve', 'spam', 'trash', + * or a custom status from get_comment_statuses(). * @param bool $wp_error Whether to return a WP_Error object if there is a failure. Default false. * @return bool|WP_Error True on success, false or WP_Error on failure. */ @@ -2553,7 +2641,18 @@ function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false $status = 'trash'; break; default: - return false; + if ( ! is_scalar( $comment_status ) ) { + return false; + } + + $comment_status = sanitize_key( (string) $comment_status ); + + if ( ! isset( _wp_get_custom_comment_statuses()[ $comment_status ] ) ) { + return false; + } + + $status = $comment_status; + break; } $comment_old = clone get_comment( $comment_id ); 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 f462928847c77..3037972122c0d 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 @@ -790,6 +790,14 @@ public function create_item( $request ) { return $prepared_comment; } + if ( isset( $request['status'] ) && ! $this->is_valid_comment_status_param( $request['status'] ) ) { + return new WP_Error( + 'rest_comment_invalid_status', + __( 'Invalid comment status.' ), + array( 'status' => 400 ) + ); + } + $comment_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_comment ) ) ); if ( ! $comment_id ) { @@ -801,7 +809,18 @@ public function create_item( $request ) { } if ( isset( $request['status'] ) ) { - $this->handle_status_param( $request['status'], $comment_id ); + $status_update = $this->handle_status_param( $request['status'], $comment_id ); + $comment = get_comment( $comment_id ); + + if ( ! $status_update && $this->normalize_status_param( $request['status'] ) !== $this->prepare_status_response( $comment->comment_approved ) ) { + wp_delete_comment( $comment_id, true ); + + return new WP_Error( + 'rest_comment_failed_create', + __( 'Creating comment failed.' ), + array( 'status' => 500 ) + ); + } } $comment = get_comment( $comment_id ); @@ -927,9 +946,18 @@ public function update_item( $request ) { if ( empty( $prepared_args ) && isset( $request['status'] ) ) { // Only the comment status is being changed. - $change = $this->handle_status_param( $request['status'], $id ); + if ( ! $this->is_valid_comment_status_param( $request['status'] ) ) { + return new WP_Error( + 'rest_comment_invalid_status', + __( 'Invalid comment status.' ), + array( 'status' => 400 ) + ); + } + + $change = $this->handle_status_param( $request['status'], $id ); + $comment = get_comment( $id ); - if ( ! $change ) { + if ( ! $change && $this->normalize_status_param( $request['status'] ) !== $this->prepare_status_response( $comment->comment_approved ) ) { return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment status failed.' ), @@ -948,6 +976,14 @@ public function update_item( $request ) { ); } + if ( isset( $request['status'] ) && ! $this->is_valid_comment_status_param( $request['status'] ) ) { + return new WP_Error( + 'rest_comment_invalid_status', + __( 'Invalid comment status.' ), + array( 'status' => 400 ) + ); + } + $prepared_args['comment_ID'] = $id; $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); @@ -961,19 +997,42 @@ public function update_item( $request ) { ); } + $old_comment_approved = null; + if ( isset( $request['status'] ) ) { + $comment = get_comment( $id ); + $old_comment_approved = $comment->comment_approved; + $status_update = $this->handle_status_param( $request['status'], $id ); + $comment = get_comment( $id ); + + if ( ! $status_update && $this->normalize_status_param( $request['status'] ) !== $this->prepare_status_response( $comment->comment_approved ) ) { + return new WP_Error( + 'rest_comment_failed_edit', + __( 'Updating comment status failed.' ), + array( 'status' => 500 ) + ); + } + } + $updated = wp_update_comment( wp_slash( (array) $prepared_args ), true ); if ( is_wp_error( $updated ) ) { + if ( null !== $old_comment_approved && ! wp_set_comment_status( $id, $old_comment_approved ) ) { + global $wpdb; + + $comment = get_comment( $id ); + if ( $comment ) { + $wpdb->update( $wpdb->comments, array( 'comment_approved' => $old_comment_approved ), array( 'comment_ID' => $id ) ); + clean_comment_cache( $id ); + wp_update_comment_count( $comment->comment_post_ID ); + } + } + return new WP_Error( 'rest_comment_failed_edit', __( 'Updating comment failed.' ), array( 'status' => 500 ) ); } - - if ( isset( $request['status'] ) ) { - $this->handle_status_param( $request['status'], $id ); - } } $comment = get_comment( $id ); @@ -1810,6 +1869,65 @@ public function get_collection_params() { return apply_filters( 'rest_comment_collection_params', $query_params ); } + /** + * Checks whether a comment status parameter can be handled. + * + * @since 7.1.0 + * + * @param string|int $new_status New comment status. + * @return bool Whether the status parameter is valid. + */ + protected function is_valid_comment_status_param( $new_status ) { + if ( ! is_scalar( $new_status ) ) { + return false; + } + + $new_status = sanitize_key( (string) $new_status ); + + switch ( $new_status ) { + case 'approved': + case 'approve': + case '1': + case 'hold': + case '0': + case 'spam': + case 'unspam': + case 'trash': + case 'untrash': + return true; + default: + return isset( _wp_get_custom_comment_statuses()[ $new_status ] ); + } + } + + /** + * Normalizes a comment status parameter for comparison with REST API responses. + * + * @since 7.1.0 + * + * @param string|int $new_status New comment status. + * @return string Normalized comment status. + */ + protected function normalize_status_param( $new_status ) { + if ( ! is_scalar( $new_status ) ) { + return ''; + } + + $new_status = sanitize_key( (string) $new_status ); + + switch ( $new_status ) { + case 'approved': + case 'approve': + case '1': + return 'approved'; + case 'hold': + case '0': + return 'hold'; + default: + return (string) $new_status; + } + } + /** * Sets the comment_status of a given comment object when creating or updating a comment. * @@ -1849,7 +1967,7 @@ protected function handle_status_param( $new_status, $comment_id ) { $changed = wp_untrash_comment( $comment_id ); break; default: - $changed = false; + $changed = wp_set_comment_status( $comment_id, $new_status ); break; } diff --git a/tests/phpunit/tests/comment/statuses.php b/tests/phpunit/tests/comment/statuses.php new file mode 100644 index 0000000000000..81438eba4c86d --- /dev/null +++ b/tests/phpunit/tests/comment/statuses.php @@ -0,0 +1,246 @@ +assertSame( 'Read', get_comment_statuses()['read'] ); + } + + /** + * @ticket 20977 + */ + public function test_wp_set_comment_status_supports_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_comment_statuses' ) ); + + $comment_id = self::factory()->comment->create(); + + $this->assertTrue( wp_set_comment_status( $comment_id, 'read' ) ); + $this->assertSame( 'read', get_comment( $comment_id )->comment_approved ); + $this->assertSame( 'read', wp_get_comment_status( $comment_id ) ); + } + + /** + * @ticket 20977 + */ + public function test_wp_set_comment_status_rejects_unregistered_custom_statuses() { + $comment_id = self::factory()->comment->create(); + + $this->assertFalse( wp_set_comment_status( $comment_id, 'read' ) ); + } + + /** + * @ticket 20977 + */ + public function test_wp_set_comment_status_rejects_invalid_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_invalid_comment_statuses' ) ); + + $comment_id = self::factory()->comment->create(); + + $this->assertFalse( wp_set_comment_status( $comment_id, 'needs review' ) ); + $this->assertFalse( wp_set_comment_status( $comment_id, 'more-than-20-chars-long' ) ); + $this->assertFalse( wp_set_comment_status( $comment_id, array( 'read' ) ) ); + } + + /** + * @ticket 20977 + */ + public function test_wp_set_comment_status_rejects_reserved_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_reserved_comment_statuses' ) ); + + $comment_id = self::factory()->comment->create(); + + $this->assertFalse( wp_set_comment_status( $comment_id, 'approved' ) ); + $this->assertFalse( wp_set_comment_status( $comment_id, 'moderated' ) ); + $this->assertFalse( wp_set_comment_status( $comment_id, 'all' ) ); + $this->assertFalse( wp_set_comment_status( $comment_id, 'unspam' ) ); + } + + /** + * @ticket 20977 + */ + public function test_wp_count_comments_includes_custom_status_counts() { + add_filter( 'comment_statuses', array( $this, 'filter_comment_statuses' ) ); + + self::factory()->comment->create( + array( + 'comment_approved' => 'read', + ) + ); + + $count = wp_count_comments(); + + $this->assertSame( 1, $count->read ); + $this->assertSame( 1, $count->total_comments ); + $this->assertSame( 1, $count->all ); + } + + /** + * @ticket 20977 + */ + /** + * @ticket 20977 + */ + public function test_get_comments_all_includes_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_comment_statuses' ) ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 'read', + ) + ); + + $comments = get_comments( + array( + 'status' => 'all', + 'fields' => 'ids', + ) + ); + + $this->assertContains( $comment_id, $comments ); + } + + public function test_wp_count_comments_ignores_invalid_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_invalid_comment_statuses' ) ); + + self::factory()->comment->create( + array( + 'comment_approved' => 'needs review', + ) + ); + + $count = wp_count_comments(); + + $this->assertFalse( property_exists( $count, 'needs review' ) ); + $this->assertFalse( property_exists( $count, 'more-than-20-chars-long' ) ); + } + + /** + * @ticket 20977 + */ + public function test_wp_count_comments_ignores_reserved_custom_statuses() { + add_filter( 'comment_statuses', array( $this, 'filter_reserved_comment_statuses' ) ); + + $count = wp_count_comments(); + + $this->assertFalse( property_exists( $count, 'unspam' ) ); + } + + /** + * @ticket 20977 + * + * @dataProvider data_invalid_edit_comment_statuses + * + * @param mixed $comment_status Invalid comment status. + */ + public function test_edit_comment_rejects_invalid_comment_status( $comment_status ) { + if ( ! function_exists( 'edit_comment' ) ) { + require_once ABSPATH . 'wp-admin/includes/comment.php'; + } + + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $user_id ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => '0', + ) + ); + + $_POST = add_magic_quotes( + array( + 'comment_ID' => $comment_id, + 'comment_status' => $comment_status, + 'newcomment_author' => 'Test Author', + 'newcomment_author_url' => '', + 'newcomment_author_email' => '', + 'content' => 'Test content', + ) + ); + + edit_comment(); + + $this->assertSame( '0', get_comment( $comment_id )->comment_approved ); + } + + /** + * Data provider for invalid comment statuses. + * + * @ticket 20977 + * + * @return array[] Invalid comment statuses. + */ + public function data_invalid_edit_comment_statuses() { + return array( + 'invalid string' => array( 'invalid-status' ), + 'array' => array( array( 'spam' ) ), + ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 8542bcd42af24..2ef67b549eabe 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -2582,6 +2582,41 @@ public function test_update_comment_status() { $this->assertEquals( 1, $updated->comment_approved ); } + /** + * @ticket 20977 + */ + public function test_update_comment_custom_status_no_change() { + wp_set_current_user( self::$admin_id ); + + $filter = static function ( $statuses ) { + $statuses['read'] = 'Read'; + + return $statuses; + }; + add_filter( 'comment_statuses', $filter ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 'read', + 'comment_post_ID' => self::$post_id, + ) + ); + + $params = array( + 'status' => 'read', + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + remove_filter( 'comment_statuses', $filter ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'read', get_comment( $comment_id )->comment_approved ); + } + public function test_update_comment_field_does_not_use_default_values() { wp_set_current_user( self::$admin_id ); @@ -2611,6 +2646,124 @@ public function test_update_comment_field_does_not_use_default_values() { $this->assertSame( 'some content', $updated->comment_content ); } + /** + * @ticket 20977 + */ + public function test_create_comment_with_invalid_status_returns_error() { + wp_set_current_user( self::$admin_id ); + + $params = array( + 'post' => self::$post_id, + 'author_name' => 'Homer J. Simpson', + 'author_email' => 'homer@example.org', + 'author_url' => 'http://compuglobalhypermeganet.com', + 'content' => 'Aw, he loves beer. Here, little fella.', + 'status' => 'invalid-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->assertErrorResponse( 'rest_comment_invalid_status', $response, 400 ); + } + + /** + * @ticket 20977 + */ + public function test_update_comment_with_invalid_status_only_returns_error() { + wp_set_current_user( self::$admin_id ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => '0', + 'comment_post_ID' => self::$post_id, + ) + ); + + $params = array( + 'status' => 'invalid-status', + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_invalid_status', $response, 400 ); + $this->assertSame( '0', get_comment( $comment_id )->comment_approved ); + } + + /** + * @ticket 20977 + */ + public function test_update_comment_status_is_rolled_back_when_comment_update_fails() { + wp_set_current_user( self::$admin_id ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => '0', + 'comment_post_ID' => self::$post_id, + 'comment_content' => 'Original content.', + ) + ); + + $filter = static function () { + return new WP_Error( 'comment_update_failed', 'Comment update failed.' ); + }; + add_filter( 'wp_update_comment_data', $filter ); + + $params = array( + 'content' => 'Updated content.', + 'status' => 'approve', + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + remove_filter( 'wp_update_comment_data', $filter ); + + $this->assertErrorResponse( 'rest_comment_failed_edit', $response, 500 ); + + $updated = get_comment( $comment_id ); + $this->assertSame( '0', $updated->comment_approved ); + $this->assertSame( 'Original content.', $updated->comment_content ); + } + + /** + * @ticket 20977 + */ + public function test_update_comment_with_invalid_status_returns_error() { + wp_set_current_user( self::$admin_id ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_approved' => '0', + 'comment_post_ID' => self::$post_id, + 'comment_content' => 'Original content.', + ) + ); + + $params = array( + 'content' => 'Updated content.', + 'status' => 'invalid-status', + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_invalid_status', $response, 400 ); + + $updated = get_comment( $comment_id ); + $this->assertSame( '0', $updated->comment_approved ); + $this->assertSame( 'Original content.', $updated->comment_content ); + } + public function test_update_comment_date_gmt() { wp_set_current_user( self::$admin_id ); diff --git a/tests/phpunit/tests/xmlrpc/wp/editComment.php b/tests/phpunit/tests/xmlrpc/wp/editComment.php index 0f998df96707a..0e7fdb80f606b 100644 --- a/tests/phpunit/tests/xmlrpc/wp/editComment.php +++ b/tests/phpunit/tests/xmlrpc/wp/editComment.php @@ -5,6 +5,41 @@ */ class Tests_XMLRPC_wp_editComment extends WP_XMLRPC_UnitTestCase { + public function tear_down() { + remove_filter( 'comment_statuses', array( $this, 'filter_comment_statuses' ) ); + remove_filter( 'comment_statuses', array( $this, 'filter_reserved_comment_statuses' ) ); + + parent::tear_down(); + } + + /** + * Adds a valid custom comment status. + * + * @ticket 20977 + * + * @param string[] $statuses Comment statuses. + * @return string[] Filtered comment statuses. + */ + public function filter_comment_statuses( $statuses ) { + $statuses['read'] = 'Read'; + + return $statuses; + } + + /** + * Adds a reserved custom comment status. + * + * @ticket 20977 + * + * @param string[] $statuses Comment statuses. + * @return string[] Filtered comment statuses. + */ + public function filter_reserved_comment_statuses( $statuses ) { + $statuses['approved'] = 'Custom Approved'; + + return $statuses; + } + public function test_author_can_edit_own_comment() { $author_id = $this->make_user_by_role( 'author' ); $post_id = self::factory()->post->create( @@ -93,4 +128,54 @@ public function test_trash_comment() { $this->assertSame( 'trash', get_comment( $comment_id )->comment_approved ); } + + /** + * @ticket 20977 + */ + public function test_custom_comment_status() { + add_filter( 'comment_statuses', array( $this, 'filter_comment_statuses' ) ); + + $this->make_user_by_role( 'administrator' ); + $comment_id = self::factory()->comment->create(); + + $result = $this->myxmlrpcserver->wp_editComment( + array( + 1, + 'administrator', + 'administrator', + $comment_id, + array( + 'status' => 'read', + ), + ) + ); + + $this->assertNotIXRError( $result ); + $this->assertSame( 'read', get_comment( $comment_id )->comment_approved ); + } + + /** + * @ticket 20977 + */ + public function test_reserved_custom_comment_status_is_rejected() { + add_filter( 'comment_statuses', array( $this, 'filter_reserved_comment_statuses' ) ); + + $this->make_user_by_role( 'administrator' ); + $comment_id = self::factory()->comment->create(); + + $result = $this->myxmlrpcserver->wp_editComment( + array( + 1, + 'administrator', + 'administrator', + $comment_id, + array( + 'status' => 'approved', + ), + ) + ); + + $this->assertIXRError( $result ); + $this->assertSame( 401, $result->code ); + } } From b51c8162c8186f104234caa7e68bdae9c1862d46 Mon Sep 17 00:00:00 2001 From: Sukhendu Sekhar Guria Date: Tue, 9 Jun 2026 15:57:13 +0530 Subject: [PATCH 2/2] Fix PHPCS errors --- src/wp-admin/edit-form-comment.php | 6 +++--- src/wp-includes/class-wp-comment-query.php | 2 +- src/wp-includes/comment.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wp-admin/edit-form-comment.php b/src/wp-admin/edit-form-comment.php index 86f87f083aea7..01aab9af08e3f 100644 --- a/src/wp-admin/edit-form-comment.php +++ b/src/wp-admin/edit-form-comment.php @@ -113,11 +113,11 @@ _x( 'Pending', 'comment status' ), - '1' => _x( 'Approved', 'comment status' ), + '0' => _x( 'Pending', 'comment status' ), + '1' => _x( 'Approved', 'comment status' ), 'spam' => _x( 'Spam', 'comment status' ), 'trash' => _x( 'Trash', 'comment status' ), ) diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index d96910895257e..cbf1fcabf7a25 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -572,7 +572,7 @@ protected function get_comment_ids() { case 'all': case '': $all_statuses = array_merge( array( '0', '1' ), array_keys( _wp_get_custom_comment_statuses() ) ); - $placeholders = implode( ', ', array_fill( 0, count( $all_statuses ), '%s' ) ); + $placeholders = implode( ', ', array_fill( 0, count( $all_statuses ), '%s' ) ); $status_clauses[] = $wpdb->prepare( "comment_approved IN ($placeholders)", $all_statuses ); break; diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 68e8de029aca4..de0b1201c80ea 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -336,7 +336,7 @@ function _wp_get_custom_comment_statuses() { $custom_statuses = array_diff_key( get_comment_statuses(), array_flip( $reserved_statuses ) ); foreach ( $custom_statuses as $status => $label ) { - if ( $status !== sanitize_key( $status ) || strlen( $status ) > 20 ) { + if ( sanitize_key( $status ) !== $status || 20 < strlen( $status ) ) { unset( $custom_statuses[ $status ] ); } }