Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/fix-comment-query-type-coexistence
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Stop ActivityPub Likes, Reposts, and Quotes from leaking into the front-end comment list on sites that also use other comment-filtering plugins.
33 changes: 24 additions & 9 deletions includes/class-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -784,18 +784,33 @@ public static function comment_query( $query ) {
return;
}

// Do not exclude likes and reposts if the query is for specific types.
if ( ! empty( $query->query_vars['type__in'] ) || ! empty( $query->query_vars['type'] ) ) {
return;
}
$ap_types = self::get_comment_type_slugs();

// Do not exclude likes and reposts if the query is already excluding other comment types.
if ( ! empty( $query->query_vars['type__not_in'] ) ) {
return;
/*
* If the caller is explicitly asking for one of the ActivityPub
* comment types (likes, reposts, …) — or for `'all'`, which WP
* treats as a sentinel meaning "include everything, even types we
* would normally exclude" — respect that. Otherwise we still merge
* our slugs into `type__not_in`, so the AP exclusion composes with
* whatever other plugins are filtering on. The previous version
* bailed out as soon as any of `type__in`, `type` or `type__not_in`
* was set — which let AP comments leak through on themes that use
* plugins like GatherPress (which sets `type__in` for its own RSVP
* filtering).
*/
foreach ( array( 'type__in', 'type' ) as $key ) {
if ( empty( $query->query_vars[ $key ] ) ) {
continue;
}

$requested = (array) $query->query_vars[ $key ];
if ( \in_array( 'all', $requested, true ) || \array_intersect( $requested, $ap_types ) ) {
return;
}
}

// Exclude likes and reposts by the ActivityPub plugin.
$query->query_vars['type__not_in'] = self::get_comment_type_slugs();
$existing = (array) ( $query->query_vars['type__not_in'] ?? array() );
$query->query_vars['type__not_in'] = \array_values( \array_unique( \array_merge( $existing, $ap_types ) ) );
}

/**
Expand Down
104 changes: 104 additions & 0 deletions tests/phpunit/tests/includes/class-test-comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,110 @@ public function test_post_comments_filtered_by_type__not_in() {
$this->assertNotContains( (string) $comment_id, $comment_ids, 'AP post comment should be hidden even when querying specific post' );
}

/**
* Test that AP exclusion composes with a caller's existing `type__in`.
*
* Regression for #3306: GatherPress sets `type__in` for its own RSVP
* filtering, which previously caused `comment_query` to bail out, and
* AP comment types leaked into the front-end comment list. The new
* behavior merges AP slugs into `type__not_in` whenever `type__in`
* does not request an AP type.
*
* @covers ::comment_query
*/
public function test_comment_query_merges_ap_exclusion_with_existing_type_in() {
$post_id = self::factory()->post->create();
$this->go_to( \get_permalink( $post_id ) );

$this->assertTrue( \is_singular(), 'Sanity: test setup must reach the singular branch.' );

$query = new \WP_Comment_Query();
$query->query_vars = array( 'type__in' => array( 'gatherpress_rsvp' ) );

\Activitypub\Comment::comment_query( $query );

$this->assertSame( array( 'gatherpress_rsvp' ), $query->query_vars['type__in'] );
$this->assertArrayHasKey( 'type__not_in', $query->query_vars );
$this->assertContains( 'like', $query->query_vars['type__not_in'] );
$this->assertContains( 'repost', $query->query_vars['type__not_in'] );
}

/**
* Test that AP exclusion composes with a caller's existing `type__not_in`.
*
* @covers ::comment_query
*/
public function test_comment_query_merges_ap_exclusion_with_existing_type_not_in() {
$post_id = self::factory()->post->create();
$this->go_to( \get_permalink( $post_id ) );

$query = new \WP_Comment_Query();
$query->query_vars = array( 'type__not_in' => array( 'pingback' ) );

\Activitypub\Comment::comment_query( $query );

$this->assertContains( 'pingback', $query->query_vars['type__not_in'] );
$this->assertContains( 'like', $query->query_vars['type__not_in'] );
$this->assertContains( 'repost', $query->query_vars['type__not_in'] );
}

/**
* Test that an explicit request for an AP comment type is respected.
*
* @covers ::comment_query
*/
public function test_comment_query_respects_explicit_ap_type_request() {
$post_id = self::factory()->post->create();
$this->go_to( \get_permalink( $post_id ) );

$query = new \WP_Comment_Query();
$query->query_vars = array( 'type__in' => array( 'like' ) );

\Activitypub\Comment::comment_query( $query );

$this->assertTrue(
empty( $query->query_vars['type__not_in'] ),
'Caller is explicitly asking for AP likes; we must not add likes to type__not_in.'
);
}

/**
* Test that the WordPress `'all'` sentinel disables AP exclusion.
*
* WP_Comment_Query treats `type => 'all'` and `type__in => array('all')`
* as "include everything, even types we'd normally hide" (e.g. the
* built-in `note` exclusion is also disabled in that case). Our filter
* has to honor the same sentinel, otherwise a caller asking for the
* full set still can't see ActivityPub Likes / Reposts / Quotes.
*
* @covers ::comment_query
*/
public function test_comment_query_respects_all_sentinel() {
$post_id = self::factory()->post->create();
$this->go_to( \get_permalink( $post_id ) );

$query = new \WP_Comment_Query();
$query->query_vars = array( 'type' => 'all' );

\Activitypub\Comment::comment_query( $query );

$this->assertTrue(
empty( $query->query_vars['type__not_in'] ),
"Caller passed `type => 'all'`; AP slugs must not be appended to type__not_in."
);

// Same again via `type__in`.
$query = new \WP_Comment_Query();
$query->query_vars = array( 'type__in' => array( 'all' ) );

\Activitypub\Comment::comment_query( $query );

$this->assertTrue(
empty( $query->query_vars['type__not_in'] ),
"Caller passed `type__in => array('all')`; AP slugs must not be appended to type__not_in."
);
}

/**
* Test auto-approving comments on ap_post when option is enabled.
*
Expand Down