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
This commit is contained in:
Peter Wilson 2023-02-21 01:45:24 +00:00
parent 5bf74c0433
commit a77704f1a3
3 changed files with 115 additions and 44 deletions

View File

@ -1926,18 +1926,23 @@ function post_reply_link( $args = array(), $post = null ) {
* Retrieves HTML content for cancel comment reply link. * Retrieves HTML content for cancel comment reply link.
* *
* @since 2.7.0 * @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, * @param string $text Optional. Text to display for cancel reply link. If empty,
* defaults to 'Click here to cancel reply'. Default 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 * @return string
*/ */
function get_cancel_comment_reply_link( $text = '' ) { function get_cancel_comment_reply_link( $text = '', $post = null ) {
if ( empty( $text ) ) { if ( empty( $text ) ) {
$text = __( 'Click here to cancel reply.' ); $text = __( 'Click here to cancel reply.' );
} }
$style = isset( $_GET['replytocom'] ) ? '' : ' style="display:none;"'; $post = get_post( $post );
$link = esc_html( remove_query_arg( array( 'replytocom', 'unapproved', 'moderation-hash' ) ) ) . '#respond'; $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 = '<a rel="nofollow" id="cancel-comment-reply-link" href="' . $link . '"' . $style . '>' . $text . '</a>'; $formatted_link = '<a rel="nofollow" id="cancel-comment-reply-link" href="' . $link . '"' . $style . '>' . $text . '</a>';
@ -1969,16 +1974,20 @@ function cancel_comment_reply_link( $text = '' ) {
* Retrieves hidden input HTML for replying to comments. * Retrieves hidden input HTML for replying to comments.
* *
* @since 3.0.0 * @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. * @return string Hidden input HTML for replying to comments.
*/ */
function get_comment_id_fields( $post_id = 0 ) { function get_comment_id_fields( $post = null ) {
if ( empty( $post_id ) ) { $post = get_post( $post );
$post_id = get_the_ID(); 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 = "<input type='hidden' name='comment_post_ID' value='$post_id' id='comment_post_ID' />\n"; $result = "<input type='hidden' name='comment_post_ID' value='$post_id' id='comment_post_ID' />\n";
$result .= "<input type='hidden' name='comment_parent' id='comment_parent' value='$reply_to_id' />\n"; $result .= "<input type='hidden' name='comment_parent' id='comment_parent' value='$reply_to_id' />\n";
@ -2003,13 +2012,15 @@ function get_comment_id_fields( $post_id = 0 ) {
* This tag must be within the `<form>` section of the `comments.php` template. * This tag must be within the `<form>` section of the `comments.php` template.
* *
* @since 2.7.0 * @since 2.7.0
* @since 6.2.0 Renamed `$post_id` to `$post` and added WP_Post support.
* *
* @see get_comment_id_fields() * @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 ) { function comment_id_fields( $post = null ) {
echo get_comment_id_fields( $post_id ); 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. * comment. See https://core.trac.wordpress.org/changeset/36512.
* *
* @since 2.7.0 * @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 $no_reply_text Optional. Text to display when not replying to a comment. * @param string|false $reply_text Optional. Text to display when replying to a comment.
* Default false. * Default false. Accepts "%s" for the author of the comment
* @param string|false $reply_text Optional. Text to display when replying to a comment. * being replied to.
* Default false. Accepts "%s" for the author of the comment * @param bool $link_to_parent Optional. Boolean to control making the author's name a link
* being replied to. * to their comment. Default true.
* @param bool $link_to_parent Optional. Boolean to control making the author's name a link * @param int|WP_Post|null $post Optional. The post that the comment form is being displayed for.
* to their comment. Default true. * Defaults to the current global post.
*/ */
function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true ) { function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true, $post = null ) {
global $comment;
if ( false === $no_reply_text ) { if ( false === $no_reply_text ) {
$no_reply_text = __( 'Leave a Reply' ); $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_text = __( 'Leave a Reply to %s' );
} }
$reply_to_id = isset( $_GET['replytocom'] ) ? (int) $_GET['replytocom'] : 0; $post = get_post( $post );
if ( ! $post ) {
if ( 0 == $reply_to_id ) {
echo $no_reply_text; echo $no_reply_text;
} else { return;
// 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 = '<a href="#comment-' . get_comment_ID() . '">' . get_comment_author( $comment ) . '</a>';
} else {
$author = get_comment_author( $comment );
}
printf( $reply_text, $author );
} }
$reply_to_id = _get_comment_reply_id( $post->ID );
if ( 0 === $reply_to_id ) {
echo $no_reply_text;
return;
}
if ( $link_to_parent ) {
$author = '<a href="#comment-' . get_comment_ID() . '">' . get_comment_author( $reply_to_id ) . '</a>';
} 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 ) {
<?php <?php
echo $args['title_reply_before']; echo $args['title_reply_before'];
comment_form_title( $args['title_reply'], $args['title_reply_to'] ); comment_form_title( $args['title_reply'], $args['title_reply_to'], true, $post_id );
if ( get_option( 'thread_comments' ) ) { if ( get_option( 'thread_comments' ) ) {
echo $args['cancel_reply_before']; echo $args['cancel_reply_before'];

View File

@ -3475,7 +3475,28 @@ function wp_handle_comment_submission( $comment_data ) {
$comment_content = trim( $comment_data['comment'] ); $comment_content = trim( $comment_data['comment'] );
} }
if ( isset( $comment_data['comment_parent'] ) ) { if ( isset( $comment_data['comment_parent'] ) ) {
$comment_parent = absint( $comment_data['comment_parent'] ); $comment_parent = absint( $comment_data['comment_parent'] );
$comment_parent_object = get_comment( $comment_parent );
if (
0 !== $comment_parent &&
(
! $comment_parent_object instanceof WP_Comment ||
0 === (int) $comment_parent_object->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 ); $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' ); return new WP_Error( 'comment_on_password_protected' );
} else { } else {
/** /**
* Fires before a comment is posted. * Fires before a comment is posted.
* *
@ -3569,7 +3589,6 @@ function wp_handle_comment_submission( $comment_data ) {
* @param int $comment_post_id Post ID. * @param int $comment_post_id Post ID.
*/ */
do_action( 'pre_comment_on_post', $comment_post_id ); do_action( 'pre_comment_on_post', $comment_post_id );
} }
// If the user is logged in. // If the user is logged in.

View File

@ -16,7 +16,7 @@
* *
* @global string $wp_version * @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. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.