From a77704f1a32f6778633a11e53baa4e47207dc619 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 21 Feb 2023 01:45:24 +0000 Subject: [PATCH] Comments: Prevent replying to unapproved comments. Introduces client and server side validation to ensure the `replytocom` query string parameter can not be exploited to reply to an unapproved comment or display the name of an unapproved commenter. This only affects commenting via the front end of the site. Comment replies via the dashboard continue their current behaviour of logging the reply and approving the parent comment. Introduces the `$post` parameter, defaulting to the current global post, to `get_cancel_comment_reply_link()` and `comment_form_title()`. Introduces `_get_comment_reply_id()` for determining the comment reply ID based on the `replytocom` query string parameter. Renames the parameter `$post_id` to `$post` in `get_comment_id_fields()` and `comment_id_fields()` to accept either a post ID or `WP_Post` object. Adds a new `WP_Error` return state to `wp_handle_comment_submission()` to prevent replies to unapproved comments. The error code is `comment_reply_to_unapproved_comment` with the message `Sorry, replies to unapproved comments are not allowed.`. Props costdev, jrf, hellofromtonya, fasuto, boniu91, milana_cap. Fixes #53962. Built from https://develop.svn.wordpress.org/trunk@55369 git-svn-id: http://core.svn.wordpress.org/trunk@54902 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/comment-template.php | 132 +++++++++++++++++++++---------- wp-includes/comment.php | 25 +++++- wp-includes/version.php | 2 +- 3 files changed, 115 insertions(+), 44 deletions(-) diff --git a/wp-includes/comment-template.php b/wp-includes/comment-template.php index 512d8f2f1d..06b2ce2397 100644 --- a/wp-includes/comment-template.php +++ b/wp-includes/comment-template.php @@ -1926,18 +1926,23 @@ function post_reply_link( $args = array(), $post = null ) { * Retrieves HTML content for cancel comment reply link. * * @since 2.7.0 + * @since 6.2.0 Added the `$post` parameter. * - * @param string $text Optional. Text to display for cancel reply link. If empty, - * defaults to 'Click here to cancel reply'. Default empty. + * @param string $text Optional. Text to display for cancel reply link. If empty, + * defaults to 'Click here to cancel reply'. Default empty. + * @param int|WP_Post|null $post Optional. The post the comment thread is being + * displayed for. Defaults to the current global post. * @return string */ -function get_cancel_comment_reply_link( $text = '' ) { +function get_cancel_comment_reply_link( $text = '', $post = null ) { if ( empty( $text ) ) { $text = __( 'Click here to cancel reply.' ); } - $style = isset( $_GET['replytocom'] ) ? '' : ' style="display:none;"'; - $link = esc_html( remove_query_arg( array( 'replytocom', 'unapproved', 'moderation-hash' ) ) ) . '#respond'; + $post = get_post( $post ); + $reply_to_id = $post ? _get_comment_reply_id( $post->ID ) : 0; + $style = 0 !== $reply_to_id ? '' : ' style="display:none;"'; + $link = esc_html( remove_query_arg( array( 'replytocom', 'unapproved', 'moderation-hash' ) ) ) . '#respond'; $formatted_link = '' . $text . ''; @@ -1969,16 +1974,20 @@ function cancel_comment_reply_link( $text = '' ) { * Retrieves hidden input HTML for replying to comments. * * @since 3.0.0 + * @since 6.2.0 Renamed `$post_id` to `$post` and added WP_Post support. * - * @param int $post_id Optional. Post ID. Defaults to the current post ID. + * @param int|WP_Post|null $post Optional. The post the comment is being displayed for. + * Defaults to the current global post. * @return string Hidden input HTML for replying to comments. */ -function get_comment_id_fields( $post_id = 0 ) { - if ( empty( $post_id ) ) { - $post_id = get_the_ID(); +function get_comment_id_fields( $post = null ) { + $post = get_post( $post ); + if ( ! $post ) { + return ''; } - $reply_to_id = isset( $_GET['replytocom'] ) ? (int) $_GET['replytocom'] : 0; + $post_id = $post->ID; + $reply_to_id = _get_comment_reply_id( $post_id ); $result = "\n"; $result .= "\n"; @@ -2003,13 +2012,15 @@ function get_comment_id_fields( $post_id = 0 ) { * This tag must be within the `
` section of the `comments.php` template. * * @since 2.7.0 + * @since 6.2.0 Renamed `$post_id` to `$post` and added WP_Post support. * * @see get_comment_id_fields() * - * @param int $post_id Optional. Post ID. Defaults to the current post ID. + * @param int|WP_Post|null $post Optional. The post the comment is being displayed for. + * Defaults to the current global post. */ -function comment_id_fields( $post_id = 0 ) { - echo get_comment_id_fields( $post_id ); +function comment_id_fields( $post = null ) { + echo get_comment_id_fields( $post ); } /** @@ -2021,20 +2032,19 @@ function comment_id_fields( $post_id = 0 ) { * comment. See https://core.trac.wordpress.org/changeset/36512. * * @since 2.7.0 + * @since 6.2.0 Added the `$post` parameter. * - * @global WP_Comment $comment Global comment object. - * - * @param string|false $no_reply_text Optional. Text to display when not replying to a comment. - * Default false. - * @param string|false $reply_text Optional. Text to display when replying to a comment. - * Default false. Accepts "%s" for the author of the comment - * being replied to. - * @param bool $link_to_parent Optional. Boolean to control making the author's name a link - * to their comment. Default true. + * @param string|false $no_reply_text Optional. Text to display when not replying to a comment. + * Default false. + * @param string|false $reply_text Optional. Text to display when replying to a comment. + * Default false. Accepts "%s" for the author of the comment + * being replied to. + * @param bool $link_to_parent Optional. Boolean to control making the author's name a link + * to their comment. Default true. + * @param int|WP_Post|null $post Optional. The post that the comment form is being displayed for. + * Defaults to the current global post. */ -function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true ) { - global $comment; - +function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true, $post = null ) { if ( false === $no_reply_text ) { $no_reply_text = __( 'Leave a Reply' ); } @@ -2044,22 +2054,64 @@ function comment_form_title( $no_reply_text = false, $reply_text = false, $link_ $reply_text = __( 'Leave a Reply to %s' ); } - $reply_to_id = isset( $_GET['replytocom'] ) ? (int) $_GET['replytocom'] : 0; - - if ( 0 == $reply_to_id ) { + $post = get_post( $post ); + if ( ! $post ) { echo $no_reply_text; - } else { - // Sets the global so that template tags can be used in the comment form. - $comment = get_comment( $reply_to_id ); - - if ( $link_to_parent ) { - $author = '' . get_comment_author( $comment ) . ''; - } else { - $author = get_comment_author( $comment ); - } - - printf( $reply_text, $author ); + return; } + + $reply_to_id = _get_comment_reply_id( $post->ID ); + + if ( 0 === $reply_to_id ) { + echo $no_reply_text; + return; + } + + if ( $link_to_parent ) { + $author = '' . get_comment_author( $reply_to_id ) . ''; + } else { + $author = get_comment_author( $reply_to_id ); + } + + printf( $reply_text, $author ); +} + +/** + * Gets the comment's reply to ID from the $_GET['replytocom']. + * + * @since 6.2.0 + * + * @access private + * + * @param int|WP_Post $post The post the comment is being displayed for. + * Defaults to the current global post. + * @return int Comment's reply to ID. + */ +function _get_comment_reply_id( $post = null ) { + $post = get_post( $post ); + + if ( ! $post || ! isset( $_GET['replytocom'] ) || ! is_numeric( $_GET['replytocom'] ) ) { + return 0; + } + + $reply_to_id = (int) $_GET['replytocom']; + + /* + * Validate the comment. + * Bail out if it does not exist, is not approved, or its + * `comment_post_ID` does not match the given post ID. + */ + $comment = get_comment( $reply_to_id ); + + if ( + ! $comment instanceof WP_Comment || + 0 === (int) $comment->comment_approved || + $post->ID !== (int) $comment->comment_post_ID + ) { + return 0; + } + + return $reply_to_id; } /** @@ -2570,7 +2622,7 @@ function comment_form( $args = array(), $post = null ) { comment_approved + ) + ) { + /** + * Fires when a comment reply is attempted to an unapproved comment. + * + * @since 6.2.0 + * + * @param int $comment_post_id Post ID. + * @param int $comment_parent Parent comment ID. + */ + do_action( 'comment_reply_to_unapproved_comment', $comment_post_id, $comment_parent ); + + return new WP_Error( 'comment_reply_to_unapproved_comment', __( 'Sorry, replies to unapproved comments are not allowed.' ), 403 ); + } } $post = get_post( $comment_post_id ); @@ -3560,7 +3581,6 @@ function wp_handle_comment_submission( $comment_data ) { return new WP_Error( 'comment_on_password_protected' ); } else { - /** * Fires before a comment is posted. * @@ -3569,7 +3589,6 @@ function wp_handle_comment_submission( $comment_data ) { * @param int $comment_post_id Post ID. */ do_action( 'pre_comment_on_post', $comment_post_id ); - } // If the user is logged in. diff --git a/wp-includes/version.php b/wp-includes/version.php index eae74933f4..da791970f4 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.2-beta2-55368'; +$wp_version = '6.2-beta2-55369'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.