diff --git a/src/wp-includes/class-wp-comment-query.php b/src/wp-includes/class-wp-comment-query.php index ac122a62c9..ccbf10c31b 100644 --- a/src/wp-includes/class-wp-comment-query.php +++ b/src/wp-includes/class-wp-comment-query.php @@ -59,6 +59,17 @@ class WP_Comment_Query { 'limits' => '', ); + /** + * SQL WHERE clause. + * + * Stored after the 'comments_clauses' filter is run on the compiled WHERE sub-clauses. + * + * @since 4.4.2 + * @access protected + * @var string + */ + protected $filtered_where_clause; + /** * Date query container * @@ -823,6 +834,8 @@ class WP_Comment_Query { $limits = isset( $clauses[ 'limits' ] ) ? $clauses[ 'limits' ] : ''; $groupby = isset( $clauses[ 'groupby' ] ) ? $clauses[ 'groupby' ] : ''; + $this->filtered_where_clause = $where; + if ( $where ) { $where = 'WHERE ' . $where; } @@ -874,12 +887,27 @@ class WP_Comment_Query { 0 => wp_list_pluck( $comments, 'comment_ID' ), ); - $where_clauses = $this->sql_clauses['where']; - unset( - $where_clauses['parent'], - $where_clauses['parent__in'], - $where_clauses['parent__not_in'] - ); + /* + * The WHERE clause for the descendant query is the same as for the top-level + * query, minus the `parent`, `parent__in`, and `parent__not_in` sub-clauses. + */ + $_where = $this->filtered_where_clause; + $exclude_keys = array( 'parent', 'parent__in', 'parent__not_in' ); + foreach ( $exclude_keys as $exclude_key ) { + if ( isset( $this->sql_clauses['where'][ $exclude_key ] ) ) { + $clause = $this->sql_clauses['where'][ $exclude_key ]; + + // Strip the clause as well as any adjacent ANDs. + $pattern = '|(?:AND)?\s*' . $clause . '\s*(?:AND)?|'; + $_where_parts = preg_split( $pattern, $_where ); + + // Remove empties. + $_where_parts = array_filter( array_map( 'trim', $_where_parts ) ); + + // Reassemble with an AND. + $_where = implode( ' AND ', $_where_parts ); + } + } // Fetch an entire level of the descendant tree at a time. $level = 0; @@ -889,7 +917,7 @@ class WP_Comment_Query { break; } - $where = 'WHERE ' . implode( ' AND ', $where_clauses ) . ' AND comment_parent IN (' . implode( ',', array_map( 'intval', $parent_ids ) ) . ')'; + $where = 'WHERE ' . $_where . ' AND comment_parent IN (' . implode( ',', array_map( 'intval', $parent_ids ) ) . ')'; $comment_ids = $wpdb->get_col( "{$this->sql_clauses['select']} {$this->sql_clauses['from']} {$where} {$this->sql_clauses['groupby']} ORDER BY comment_date_gmt ASC, comment_ID ASC" ); $level++; diff --git a/tests/phpunit/tests/comment/query.php b/tests/phpunit/tests/comment/query.php index c00ceee6a3..82c23bee41 100644 --- a/tests/phpunit/tests/comment/query.php +++ b/tests/phpunit/tests/comment/query.php @@ -2128,6 +2128,97 @@ class Tests_Comment_Query extends WP_UnitTestCase { $this->assertEqualSets( array(), array_values( wp_list_pluck( $q->comments[ $c1 ]->get_child( $c2 )->get_children( $args ), 'comment_ID' ) ) ); } + /** + * @ticket 35192 + */ + public function test_comment_clauses_prepend_callback_should_be_respected_when_filling_descendants() { + $top_level_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + ) ); + + $child1_of_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + 'comment_parent' => $top_level_0, + ) ); + + $child2_of_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + 'comment_parent' => $top_level_0, + ) ); + + $top_level_comments = self::factory()->comment->create_many( 3, array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + ) ); + + $this->to_exclude = array( $child2_of_0, $top_level_comments[1] ); + + add_filter( 'comments_clauses', array( $this, 'prepend_exclusions' ) ); + $q = new WP_Comment_Query( array( + 'post_id' => $this->post_id, + 'hierarchical' => 'flat', + ) ); + remove_filter( 'comments_clauses', array( $this, 'prepend_exclusions' ) ); + + unset( $this->to_exclude ); + + $this->assertEqualSets( array( $top_level_0, $child1_of_0, $top_level_comments[0], $top_level_comments[2] ), wp_list_pluck( $q->comments, 'comment_ID' ) ); + } + + public function prepend_exclusions( $clauses ) { + global $wpdb; + $clauses['where'] = $wpdb->prepare( 'comment_ID != %d AND comment_ID != %d AND ', $this->to_exclude[0], $this->to_exclude[1] ) . $clauses['where']; + return $clauses; + } + + /** + * @ticket 35192 + */ + public function test_comment_clauses_append_callback_should_be_respected_when_filling_descendants() { + $top_level_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + ) ); + + $child1_of_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + 'comment_parent' => $top_level_0, + ) ); + + $child2_of_0 = self::factory()->comment->create( array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + 'comment_parent' => $top_level_0, + ) ); + + $top_level_comments = self::factory()->comment->create_many( 3, array( + 'comment_post_ID' => $this->post_id, + 'comment_approved' => '1', + ) ); + + $this->to_exclude = array( $child2_of_0, $top_level_comments[1] ); + + add_filter( 'comments_clauses', array( $this, 'append_exclusions' ) ); + $q = new WP_Comment_Query( array( + 'post_id' => $this->post_id, + 'hierarchical' => 'flat', + ) ); + remove_filter( 'comments_clauses', array( $this, 'append_exclusions' ) ); + + unset( $this->to_exclude ); + + $this->assertEqualSets( array( $top_level_0, $child1_of_0, $top_level_comments[0], $top_level_comments[2] ), wp_list_pluck( $q->comments, 'comment_ID' ) ); + } + + public function append_exclusions( $clauses ) { + global $wpdb; + $clauses['where'] .= $wpdb->prepare( ' AND comment_ID != %d AND comment_ID != %d', $this->to_exclude[0], $this->to_exclude[1] ); + return $clauses; + } /** * @ticket 27571 */