From 2a753c1057911ff86f4600e215deb432fa96a8b0 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 21 Feb 2023 01:43:33 +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. git-svn-id: https://develop.svn.wordpress.org/trunk@55369 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/comment-template.php | 132 ++++-- src/wp-includes/comment.php | 25 +- tests/phpunit/tests/comment.php | 424 ++++++++++++++++++ .../comment/wpHandleCommentSubmission.php | 120 +++++ 4 files changed, 658 insertions(+), 43 deletions(-) diff --git a/src/wp-includes/comment-template.php b/src/wp-includes/comment-template.php index 512d8f2f1d..06b2ce2397 100644 --- a/src/wp-includes/comment-template.php +++ b/src/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/tests/phpunit/tests/comment.php b/tests/phpunit/tests/comment.php index 641b055ad4..a55ddd59e7 100644 --- a/tests/phpunit/tests/comment.php +++ b/tests/phpunit/tests/comment.php @@ -377,6 +377,430 @@ class Tests_Comment extends WP_UnitTestCase { $this->assertSame( array(), $found ); } + /** + * Tests that get_cancel_comment_reply_link() returns the expected value. + * + * @ticket 53962 + * + * @dataProvider data_get_cancel_comment_reply_link + * + * @covers ::get_cancel_comment_reply_link + * + * @param string $text Text to display for cancel reply link. + * If empty, defaults to 'Click here to cancel reply'. + * @param string|int $post The post the comment thread is being displayed for. + * Accepts 'POST_ID', 'POST', or an integer post ID. + * @param int|bool|null $replytocom A comment ID (int), whether to generate an approved (true) or unapproved (false) comment, + * or null not to create a comment. + * @param string $expected The expected reply link. + */ + public function test_get_cancel_comment_reply_link( $text, $post, $replytocom, $expected ) { + if ( 'POST_ID' === $post ) { + $post = self::$post_id; + } elseif ( 'POST' === $post ) { + $post = self::factory()->post->get_object_by_id( self::$post_id ); + } + + if ( null === $replytocom ) { + unset( $_GET['replytocom'] ); + } else { + $_GET['replytocom'] = $this->create_comment_with_approval_status( $replytocom ); + } + + $this->assertSame( $expected, get_cancel_comment_reply_link( $text, $post ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_get_cancel_comment_reply_link() { + return array( + 'text as empty string, a valid post ID and an approved comment' => array( + 'text' => '', + 'post' => 'POST_ID', + 'replytocom' => true, + 'expected' => 'Click here to cancel reply.', + ), + 'text as a custom string, a valid post ID and an approved comment' => array( + 'text' => 'Leave a reply!', + 'post' => 'POST_ID', + 'replytocom' => true, + 'expected' => 'Leave a reply!', + ), + 'text as empty string, a valid WP_Post object and an approved comment' => array( + 'text' => '', + 'post' => 'POST', + 'replytocom' => true, + 'expected' => 'Click here to cancel reply.', + ), + 'text as a custom string, a valid WP_Post object and an approved comment' => array( + 'text' => 'Leave a reply!', + 'post' => 'POST', + 'replytocom' => true, + 'expected' => 'Leave a reply!', + ), + 'text as empty string, an invalid post and an approved comment' => array( + 'text' => '', + 'post' => -99999, + 'replytocom' => true, + 'expected' => '', + ), + 'text as a custom string, a valid post, but no replytocom' => array( + 'text' => 'Leave a reply!', + 'post' => 'POST', + 'replytocom' => null, + 'expected' => '', + ), + ); + } + + /** + * Tests that comment_form_title() outputs the author of an approved comment. + * + * @ticket 53962 + * + * @covers ::comment_form_title + */ + public function test_should_output_the_author_of_an_approved_comment() { + // Must be set for `comment_form_title()`. + $_GET['replytocom'] = $this->create_comment_with_approval_status( true ); + + $comment = get_comment( $_GET['replytocom'] ); + comment_form_title( false, false, false, self::$post_id ); + + $this->assertInstanceOf( + 'WP_Comment', + $comment, + 'The comment is not an instance of WP_Comment.' + ); + + $this->assertObjectHasAttribute( + 'comment_author', + $comment, + 'The comment object does not have a "comment_author" property.' + ); + + $this->assertIsString( + $comment->comment_author, + 'The "comment_author" is not a string.' + ); + + $this->expectOutputString( + 'Leave a Reply to ' . $comment->comment_author, + 'The expected string was not output.' + ); + } + + /** + * Tests that get_comment_id_fields() allows replying to an approved comment. + * + * @ticket 53962 + * + * @dataProvider data_should_allow_reply_to_an_approved_comment + * + * @covers ::get_comment_id_fields + * + * @param string $comment_post The post of the comment. + * Accepts 'POST', 'NEW_POST', 'POST_ID' and 'NEW_POST_ID'. + */ + public function test_should_allow_reply_to_an_approved_comment( $comment_post ) { + // Must be set for `get_comment_id_fields()`. + $_GET['replytocom'] = $this->create_comment_with_approval_status( true ); + + if ( 'POST_ID' === $comment_post ) { + $comment_post = self::$post_id; + } elseif ( 'POST' === $comment_post ) { + $comment_post = self::factory()->post->get_object_by_id( self::$post_id ); + } + + $expected = "\n"; + $expected .= "\n"; + $actual = get_comment_id_fields( $comment_post ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_allow_reply_to_an_approved_comment() { + return array( + 'a post ID' => array( 'comment_post' => 'POST_ID' ), + 'a WP_Post object' => array( 'comment_post' => 'POST' ), + ); + } + + /** + * Tests that get_comment_id_fields() returns an empty string + * when the post cannot be retrieved. + * + * @ticket 53962 + * + * @dataProvider data_non_existent_posts + * + * @covers ::get_comment_id_fields + * + * @param bool $replytocom Whether to create an approved (true) or unapproved (false) comment. + * @param int $comment_post The post of the comment. + * + */ + public function test_should_return_empty_string( $replytocom, $comment_post ) { + if ( is_bool( $replytocom ) ) { + $replytocom = $this->create_comment_with_approval_status( $replytocom ); + } + + // Must be set for `get_comment_id_fields()`. + $_GET['replytocom'] = $replytocom; + + $actual = get_comment_id_fields( $comment_post ); + + $this->assertSame( '', $actual ); + } + + /** + * Tests that comment_form_title() does not output the author. + * + * @ticket 53962 + * + * @covers ::comment_form_title + * + * @dataProvider data_parent_comments + * @dataProvider data_non_existent_posts + * + * @param bool $replytocom Whether to create an approved (true) or unapproved (false) comment. + * @param string $comment_post The post of the comment. + * Accepts 'POST', 'NEW_POST', 'POST_ID' and 'NEW_POST_ID'. + */ + public function test_should_not_output_the_author( $replytocom, $comment_post ) { + if ( is_bool( $replytocom ) ) { + $replytocom = $this->create_comment_with_approval_status( $replytocom ); + } + + // Must be set for `comment_form_title()`. + $_GET['replytocom'] = $replytocom; + + if ( 'NEW_POST_ID' === $comment_post ) { + $comment_post = self::factory()->post->create(); + } elseif ( 'NEW_POST' === $comment_post ) { + $comment_post = self::factory()->post->create_and_get(); + } elseif ( 'POST_ID' === $comment_post ) { + $comment_post = self::$post_id; + } elseif ( 'POST' === $comment_post ) { + $comment_post = self::factory()->post->get_object_by_id( self::$post_id ); + } + + $comment_post_id = $comment_post instanceof WP_Post ? $comment_post->ID : $comment_post; + + get_comment( $_GET['replytocom'] ); + + comment_form_title( false, false, false, $comment_post_id ); + + $this->expectOutputString( 'Leave a Reply' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_non_existent_posts() { + return array( + 'an unapproved comment and a non-existent post ID' => array( + 'replytocom' => false, + 'comment_post' => -99999, + ), + 'an approved comment and a non-existent post ID' => array( + 'replytocom' => true, + 'comment_post' => -99999, + ), + ); + } + + /** + * Tests that get_comment_id_fields() does not allow replies when + * the comment does not have a parent post. + * + * @ticket 53962 + * + * @covers ::get_comment_id_fields + * + * @dataProvider data_parent_comments + * + * @param mixed $replytocom Whether to create an approved (true) or unapproved (false) comment, + * or an invalid comment ID. + * @param string $comment_post The post of the comment. + * Accepts 'POST', 'NEW_POST', 'POST_ID' and 'NEW_POST_ID'. + */ + public function test_should_not_allow_reply( $replytocom, $comment_post ) { + if ( is_bool( $replytocom ) ) { + $replytocom = $this->create_comment_with_approval_status( $replytocom ); + } + + // Must be set for `get_comment_id_fields()`. + $_GET['replytocom'] = $replytocom; + + if ( 'NEW_POST_ID' === $comment_post ) { + $comment_post = self::factory()->post->create(); + } elseif ( 'NEW_POST' === $comment_post ) { + $comment_post = self::factory()->post->create_and_get(); + } elseif ( 'POST_ID' === $comment_post ) { + $comment_post = self::$post_id; + } elseif ( 'POST' === $comment_post ) { + $comment_post = self::factory()->post->get_object_by_id( self::$post_id ); + } + + $comment_post_id = $comment_post instanceof WP_Post ? $comment_post->ID : $comment_post; + + $expected = "\n"; + $expected .= "\n"; + $actual = get_comment_id_fields( $comment_post ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_parent_comments() { + return array( + 'an unapproved parent comment (ID)' => array( + 'replytocom' => false, + 'comment_post' => 'POST_ID', + ), + 'an approved parent comment on another post (ID)' => array( + 'replytocom' => true, + 'comment_post' => 'NEW_POST_ID', + ), + 'an unapproved parent comment on another post (ID)' => array( + 'replytocom' => false, + 'comment_post' => 'NEW_POST_ID', + ), + 'a parent comment ID that cannot be cast to an integer' => array( + 'replytocom' => array( 'I cannot be cast to an integer.' ), + 'comment_post' => 'POST_ID', + ), + 'an unapproved parent comment (WP_Post)' => array( + 'replytocom' => false, + 'comment_post' => 'POST', + ), + 'an approved parent comment on another post (WP_Post)' => array( + 'replytocom' => true, + 'comment_post' => 'NEW_POST', + ), + 'an unapproved parent comment on another post (WP_Post)' => array( + 'replytocom' => false, + 'comment_post' => 'NEW_POST', + ), + 'a parent comment WP_Post that cannot be cast to an integer' => array( + 'replytocom' => array( 'I cannot be cast to an integer.' ), + 'comment_post' => 'POST', + ), + ); + } + + /** + * Helper function to create a comment with an approval status. + * + * @since 6.2.0 + * + * @param bool $approved Whether or not the comment is approved. + * @return int The comment ID. + */ + public function create_comment_with_approval_status( $approved ) { + return self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => ( $approved ) ? '1' : '0', + ) + ); + } + + /** + * Tests that _get_comment_reply_id() returns the expected value. + * + * @ticket 53962 + * + * @dataProvider data_get_comment_reply_id + * + * @covers ::_get_comment_reply_id + * + * @param int|bool|null $replytocom A comment ID (int), whether to generate an approved (true) or unapproved (false) comment, + * or null not to create a comment. + * @param string|int $post The post the comment thread is being displayed for. + * Accepts 'POST_ID', 'POST', or an integer post ID. + * @param int $expected The expected result. + */ + public function test_get_comment_reply_id( $replytocom, $post, $expected ) { + if ( false === $replytocom ) { + unset( $_GET['replytocom'] ); + } else { + $_GET['replytocom'] = $this->create_comment_with_approval_status( (bool) $replytocom ); + } + + if ( 'POST_ID' === $post ) { + $post = self::$post_id; + } elseif ( 'POST' === $post ) { + $post = self::factory()->post->get_object_by_id( self::$post_id ); + } + + if ( 'replytocom' === $expected ) { + $expected = $_GET['replytocom']; + } + + $this->assertSame( $expected, _get_comment_reply_id( $post ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_get_comment_reply_id() { + return array( + 'no comment ID set ($_GET["replytocom"])' => array( + 'replytocom' => false, + 'post' => 0, + 'expected' => 0, + ), + 'a non-numeric comment ID' => array( + 'replytocom' => 'three', + 'post' => 0, + 'expected' => 0, + ), + 'a non-existent comment ID' => array( + 'replytocom' => -999999, + 'post' => 0, + 'expected' => 0, + ), + 'an unapproved comment' => array( + 'replytocom' => false, + 'post' => 0, + 'expected' => 0, + ), + 'a post that does not match the parent' => array( + 'replytocom' => false, + 'post' => -999999, + 'expected' => 0, + ), + 'an approved comment and the correct post ID' => array( + 'replytocom' => true, + 'post' => 'POST_ID', + 'expected' => 'replytocom', + ), + 'an approved comment and the correct WP_Post object' => array( + 'replytocom' => true, + 'post' => 'POST', + 'expected' => 'replytocom', + ), + ); + } + /** * @ticket 14279 * diff --git a/tests/phpunit/tests/comment/wpHandleCommentSubmission.php b/tests/phpunit/tests/comment/wpHandleCommentSubmission.php index e4ec77af7a..ff2f3c8b7a 100644 --- a/tests/phpunit/tests/comment/wpHandleCommentSubmission.php +++ b/tests/phpunit/tests/comment/wpHandleCommentSubmission.php @@ -882,4 +882,124 @@ class Tests_Comment_wpHandleCommentSubmission extends WP_UnitTestCase { $this->assertNotWPError( $second_comment ); $this->assertEquals( self::$post->ID, $second_comment->comment_post_ID ); } + + /** + * Tests that wp_handle_comment_submission() only allows replying to + * an approved parent comment. + * + * @ticket 53962 + * + * @dataProvider data_should_only_allow_replying_to_an_approved_parent_comment + * + * @param int $approved Whether the parent comment is approved. + */ + public function test_should_only_allow_replying_to_an_approved_parent_comment( $approved ) { + wp_set_current_user( self::$editor_id ); + + $comment_parent = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post->ID, + 'comment_approved' => $approved, + ) + ); + + $comment = wp_handle_comment_submission( + array( + 'comment_post_ID' => self::$post->ID, + 'comment_author' => 'A comment author', + 'comment_author_email' => 'comment_author@example.org', + 'comment' => 'Howdy, comment!', + 'comment_parent' => $comment_parent, + ) + ); + + if ( $approved ) { + $this->assertInstanceOf( + 'WP_Comment', + $comment, + 'The comment was not submitted.' + ); + } else { + $this->assertWPError( $comment, 'The comment was submitted.' ); + $this->assertSame( + 'comment_reply_to_unapproved_comment', + $comment->get_error_code(), + 'The wrong error code was returned.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_only_allow_replying_to_an_approved_parent_comment() { + return array( + 'an approved parent comment' => array( 'approved' => 1 ), + 'an unapproved parent comment' => array( 'approved' => 0 ), + ); + } + + /** + * Tests that wp_handle_comment_submission() only allows replying to + * an existing parent comment. + * + * @ticket 53962 + * + * @dataProvider data_should_only_allow_replying_to_an_existing_parent_comment + * + * @param bool $exists Whether the parent comment exists. + */ + public function test_should_only_allow_replying_to_an_existing_parent_comment( $exists ) { + wp_set_current_user( self::$editor_id ); + + $parent_comment = -99999; + + if ( $exists ) { + $parent_comment = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post->ID, + 'comment_approved' => 1, + ) + ); + } + + $comment = wp_handle_comment_submission( + array( + 'comment_post_ID' => self::$post->ID, + 'comment_author' => 'A comment author', + 'comment_author_email' => 'comment_author@example.org', + 'comment' => 'Howdy, comment!', + 'comment_parent' => $parent_comment, + ) + ); + + if ( $exists ) { + $this->assertInstanceOf( + 'WP_Comment', + $comment, + 'The comment was not submitted.' + ); + } else { + $this->assertWPError( $comment, 'The comment was submitted.' ); + $this->assertSame( + 'comment_reply_to_unapproved_comment', + $comment->get_error_code(), + 'The wrong error code was returned.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_only_allow_replying_to_an_existing_parent_comment() { + return array( + 'an existing parent comment' => array( 'exists' => true ), + 'a non-existent parent comment' => array( 'exists' => false ), + ); + } }