<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[55369] trunk: Comments: Prevent replying to unapproved comments.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/55369">55369</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/55369","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>peterwilsoncc</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2023-02-21 01:43:33 +0000 (Tue, 21 Feb 2023)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>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 <a href="https://core.trac.wordpress.org/ticket/53962">#53962</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludescommenttemplatephp">trunk/src/wp-includes/comment-template.php</a></li>
<li><a href="#trunksrcwpincludescommentphp">trunk/src/wp-includes/comment.php</a></li>
<li><a href="#trunktestsphpunittestscommentwpHandleCommentSubmissionphp">trunk/tests/phpunit/tests/comment/wpHandleCommentSubmission.php</a></li>
<li><a href="#trunktestsphpunittestscommentphp">trunk/tests/phpunit/tests/comment.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludescommenttemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/comment-template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/comment-template.php        2023-02-20 21:11:57 UTC (rev 55368)
+++ trunk/src/wp-includes/comment-template.php  2023-02-21 01:43:33 UTC (rev 55369)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1926,18 +1926,23 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Retrieves HTML content for cancel comment reply link.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.2.0 Added the `$post` parameter.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param string $text Optional. Text to display for cancel reply link. If empty,
- *                     defaults to 'Click here to cancel reply'. Default empty.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @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.
</ins><span class="cx" style="display: block; padding: 0 10px">  * @return string
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function get_cancel_comment_reply_link( $text = '' ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function get_cancel_comment_reply_link( $text = '', $post = null ) {
</ins><span class="cx" style="display: block; padding: 0 10px">         if ( empty( $text ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $text = __( 'Click here to cancel reply.' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $style = isset( $_GET['replytocom'] ) ? '' : ' style="display:none;"';
-       $link  = esc_html( remove_query_arg( array( 'replytocom', 'unapproved', 'moderation-hash' ) ) ) . '#respond';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $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';
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $formatted_link = '<a rel="nofollow" id="cancel-comment-reply-link" href="' . $link . '"' . $style . '>' . $text . '</a>';
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1969,16 +1974,20 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * Retrieves hidden input HTML for replying to comments.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 3.0.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.2.0 Renamed `$post_id` to `$post` and added WP_Post support.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param int $post_id Optional. Post ID. Defaults to the current post ID.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param int|WP_Post|null $post Optional. The post the comment is being displayed for.
+ *                               Defaults to the current global post.
</ins><span class="cx" style="display: block; padding: 0 10px">  * @return string Hidden input HTML for replying to comments.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function get_comment_id_fields( $post_id = 0 ) {
-       if ( empty( $post_id ) ) {
-               $post_id = get_the_ID();
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function get_comment_id_fields( $post = null ) {
+       $post = get_post( $post );
+       if ( ! $post ) {
+               return '';
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $reply_to_id = isset( $_GET['replytocom'] ) ? (int) $_GET['replytocom'] : 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $post_id     = $post->ID;
+       $reply_to_id = _get_comment_reply_id( $post_id );
</ins><span class="cx" style="display: block; padding: 0 10px">         $result      = "<input type='hidden' name='comment_post_ID' value='$post_id' id='comment_post_ID' />\n";
</span><span class="cx" style="display: block; padding: 0 10px">        $result     .= "<input type='hidden' name='comment_parent' id='comment_parent' value='$reply_to_id' />\n";
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2003,13 +2012,15 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * This tag must be within the `<form>` section of the `comments.php` template.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.2.0 Renamed `$post_id` to `$post` and added WP_Post support.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @see get_comment_id_fields()
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @param int $post_id Optional. Post ID. Defaults to the current post ID.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @param int|WP_Post|null $post Optional. The post the comment is being displayed for.
+ *                               Defaults to the current global post.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function comment_id_fields( $post_id = 0 ) {
-       echo get_comment_id_fields( $post_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function comment_id_fields( $post = null ) {
+       echo get_comment_id_fields( $post );
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2021,20 +2032,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">  *           comment. See https://core.trac.wordpress.org/changeset/36512.
</span><span class="cx" style="display: block; padding: 0 10px">  *
</span><span class="cx" style="display: block; padding: 0 10px">  * @since 2.7.0
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @since 6.2.0 Added the `$post` parameter.
</ins><span class="cx" style="display: block; padding: 0 10px">  *
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">- * @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.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ * @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.
</ins><span class="cx" style="display: block; padding: 0 10px">  */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true ) {
-       global $comment;
-
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+function comment_form_title( $no_reply_text = false, $reply_text = false, $link_to_parent = true, $post = null ) {
</ins><span class="cx" style="display: block; padding: 0 10px">         if ( false === $no_reply_text ) {
</span><span class="cx" style="display: block; padding: 0 10px">                $no_reply_text = __( 'Leave a Reply' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2044,22 +2054,64 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $reply_text = __( 'Leave a Reply to %s' );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $reply_to_id = isset( $_GET['replytocom'] ) ? (int) $_GET['replytocom'] : 0;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $post = get_post( $post );
+       if ( ! $post ) {
+               echo $no_reply_text;
+               return;
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        if ( 0 == $reply_to_id ) {
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $reply_to_id = _get_comment_reply_id( $post->ID );
+
+       if ( 0 === $reply_to_id ) {
</ins><span class="cx" style="display: block; padding: 0 10px">                 echo $no_reply_text;
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                return;
+       }
+
+       if ( $link_to_parent ) {
+               $author = '<a href="#comment-' . get_comment_ID() . '">' . get_comment_author( $reply_to_id ) . '</a>';
</ins><span class="cx" style="display: block; padding: 0 10px">         } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Sets the global so that template tags can be used in the comment form.
-               $comment = get_comment( $reply_to_id );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $author = get_comment_author( $reply_to_id );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $link_to_parent ) {
-                       $author = '<a href="#comment-' . get_comment_ID() . '">' . get_comment_author( $comment ) . '</a>';
-               } else {
-                       $author = get_comment_author( $comment );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ printf( $reply_text, $author );
+}
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                printf( $reply_text, $author );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+/**
+ * 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;
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       $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;
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2570,7 +2622,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                <?php
</span><span class="cx" style="display: block; padding: 0 10px">                echo $args['title_reply_before'];
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                comment_form_title( $args['title_reply'], $args['title_reply_to'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         comment_form_title( $args['title_reply'], $args['title_reply_to'], true, $post_id );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( get_option( 'thread_comments' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                        echo $args['cancel_reply_before'];
</span></span></pre></div>
<a id="trunksrcwpincludescommentphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/comment.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/comment.php 2023-02-20 21:11:57 UTC (rev 55368)
+++ trunk/src/wp-includes/comment.php   2023-02-21 01:43:33 UTC (rev 55369)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3475,7 +3475,28 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $comment_content = trim( $comment_data['comment'] );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px">        if ( isset( $comment_data['comment_parent'] ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                $comment_parent = absint( $comment_data['comment_parent'] );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         $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 );
+               }
</ins><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        $post = get_post( $comment_post_id );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3560,7 +3581,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return new WP_Error( 'comment_on_password_protected' );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        } else {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px">                 /**
</span><span class="cx" style="display: block; padding: 0 10px">                 * Fires before a comment is posted.
</span><span class="cx" style="display: block; padding: 0 10px">                 *
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3569,7 +3589,6 @@
</span><span class="cx" style="display: block; padding: 0 10px">                 * @param int $comment_post_id Post ID.
</span><span class="cx" style="display: block; padding: 0 10px">                 */
</span><span class="cx" style="display: block; padding: 0 10px">                do_action( 'pre_comment_on_post', $comment_post_id );
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-
</del><span class="cx" style="display: block; padding: 0 10px">         }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        // If the user is logged in.
</span></span></pre></div>
<a id="trunktestsphpunittestscommentwpHandleCommentSubmissionphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/comment/wpHandleCommentSubmission.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/comment/wpHandleCommentSubmission.php   2023-02-20 21:11:57 UTC (rev 55368)
+++ trunk/tests/phpunit/tests/comment/wpHandleCommentSubmission.php     2023-02-21 01:43:33 UTC (rev 55369)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -882,4 +882,124 @@
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertNotWPError( $second_comment );
</span><span class="cx" style="display: block; padding: 0 10px">                $this->assertEquals( self::$post->ID, $second_comment->comment_post_ID );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * 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 ),
+               );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre></div>
<a id="trunktestsphpunittestscommentphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/comment.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/comment.php     2023-02-20 21:11:57 UTC (rev 55368)
+++ trunk/tests/phpunit/tests/comment.php       2023-02-21 01:43:33 UTC (rev 55369)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -378,6 +378,430 @@
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * 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'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond">Click here to cancel reply.</a>',
+                       ),
+                       'text as a custom string, a valid post ID and an approved comment' => array(
+                               'text'       => 'Leave a reply!',
+                               'post'       => 'POST_ID',
+                               'replytocom' => true,
+                               'expected'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond">Leave a reply!</a>',
+                       ),
+                       'text as empty string, a valid WP_Post object and an approved comment' => array(
+                               'text'       => '',
+                               'post'       => 'POST',
+                               'replytocom' => true,
+                               'expected'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond">Click here to cancel reply.</a>',
+                       ),
+                       'text as a custom string, a valid WP_Post object and an approved comment' => array(
+                               'text'       => 'Leave a reply!',
+                               'post'       => 'POST',
+                               'replytocom' => true,
+                               'expected'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond">Leave a reply!</a>',
+                       ),
+                       'text as empty string, an invalid post and an approved comment'    => array(
+                               'text'       => '',
+                               'post'       => -99999,
+                               'replytocom' => true,
+                               'expected'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond" style="display:none;">Click here to cancel reply.</a>',
+                       ),
+                       'text as a custom string, a valid post, but no replytocom' => array(
+                               'text'       => 'Leave a reply!',
+                               'post'       => 'POST',
+                               'replytocom' => null,
+                               'expected'   => '<a rel="nofollow" id="cancel-comment-reply-link" href="#respond" style="display:none;">Leave a reply!</a>',
+                       ),
+               );
+       }
+
+       /**
+        * 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  = "<input type='hidden' name='comment_post_ID' value='" . self::$post_id . "' id='comment_post_ID' />\n";
+               $expected .= "<input type='hidden' name='comment_parent' id='comment_parent' value='" . $_GET['replytocom'] . "' />\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  = "<input type='hidden' name='comment_post_ID' value='" . $comment_post_id . "' id='comment_post_ID' />\n";
+               $expected .= "<input type='hidden' name='comment_parent' id='comment_parent' value='0' />\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',
+                       ),
+               );
+       }
+
+       /**
</ins><span class="cx" style="display: block; padding: 0 10px">          * @ticket 14279
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @covers ::wp_new_comment
</span></span></pre>
</div>
</div>

</body>
</html>