Skip to content
Merged
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/2532-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

False mention email notifications for users in CC field without actual mention tags.
14 changes: 6 additions & 8 deletions includes/class-mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,8 @@ public static function direct_message( $activity, $user_ids ) {
* @param int|int[] $user_ids The id(s) of the local blog-user(s).
*/
public static function mention( $activity, $user_ids ) {
// Early return if activity has no cc recipients.
if ( empty( $activity['cc'] ) ) {
// Early return if activity has no mentions.
if ( empty( $activity['object']['tag'] ) ) {
return;
}

Expand All @@ -337,19 +337,17 @@ public static function mention( $activity, $user_ids ) {
return;
}

// Normalize to array.
$user_ids = (array) $user_ids;

// Build a map of user_id => actor_id and filter to only users in the "cc" field.
$recipients = array();
foreach ( $user_ids as $user_id ) {
$mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
$mentions = array_map( '\Activitypub\object_to_uri', $mentions );
Comment on lines +341 to +342
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
$mentions = array_map( '\Activitypub\object_to_uri', $mentions );
$mentions = \wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
$mentions = \array_map( '\Activitypub\object_to_uri', $mentions );

foreach ( (array) $user_ids as $user_id ) {
$actor = Actors::get_by_id( $user_id );
if ( \is_wp_error( $actor ) ) {
continue;
}

$actor_id = $actor->get_id();
if ( \in_array( $actor_id, (array) $activity['cc'], true ) ) {
if ( \in_array( $actor_id, $mentions, true ) ) {
$recipients[ $user_id ] = $actor_id;
}
}
Expand Down
3 changes: 3 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -809,9 +809,12 @@ function object_to_uri( $data ) {
case 'Video': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video.
$data = object_to_uri( $data['url'] );
break;

case 'Link': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link.
case 'Mention': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mention.
$data = $data['href'];
break;

default:
$data = $data['id'];
break;
Expand Down
132 changes: 132 additions & 0 deletions tests/phpunit/tests/includes/class-test-mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,13 @@ public function test_mention_with_array() {
'object' => array(
'id' => 'https://example.com/post/1',
'content' => 'Test mention',
'tag' => array(
array(
'type' => 'Mention',
'href' => Actors::get_by_id( $user_id )->get_id(),
'name' => '@test',
),
),
),
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
);
Expand Down Expand Up @@ -935,6 +942,13 @@ public function test_mention_filters_recipients() {
'object' => array(
'id' => 'https://example.com/post/1',
'content' => 'Test mention',
'tag' => array(
array(
'type' => 'Mention',
'href' => Actors::get_by_id( $user_id )->get_id(),
'name' => '@test',
),
),
),
// Only user_id is in CC, not other_user_id.
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
Expand Down Expand Up @@ -1115,4 +1129,122 @@ public function test_respect_existing_notification_settings() {
// Clean up.
\delete_option( 'activitypub_create_posts' );
}

/**
* Test that users in CC without actual mention tags do not receive mention notifications.
*
* This tests the bug fix where users added to CC (e.g., because they follow the actor)
* were incorrectly receiving mention notifications even when not actually mentioned.
*
* @covers ::mention
*/
public function test_mention_requires_tag_not_just_cc() {
$user_id = self::$user_id;

// Activity with user in CC but NOT mentioned in tags.
$activity = array(
'actor' => 'https://example.com/sports-account',
'object' => array(
'id' => 'https://example.com/sports-account/posts/123',
'type' => 'Note',
'content' => '<p>Join @user1 and @user2 on our stream...</p>',
'tag' => array(
// Other users mentioned, but NOT the local user.
array(
'type' => 'Mention',
'href' => 'https://example.com/user1',
'name' => '[email protected]',
),
array(
'type' => 'Mention',
'href' => 'https://example.com/user2',
'name' => '[email protected]',
),
),
),
// User is in CC (e.g., because they follow the actor).
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
);

// Mock remote metadata.
$metadata_filter = function () {
return array(
'name' => 'Sports Account',
'url' => 'https://example.com/sports-account',
);
};
\add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );

$mock = new \MockAction();
\add_filter( 'wp_mail', array( $mock, 'filter' ), 1 );

// Trigger mention notification.
Mailer::mention( $activity, $user_id );

// Should NOT send any email because user is not actually mentioned in tags.
$this->assertEquals( 0, $mock->get_call_count(), 'User in CC without mention tag should not receive notification' );

// Clean up.
\remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
\remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
}

/**
* Test that users with actual mention tags DO receive mention notifications.
*
* @covers ::mention
*/
public function test_mention_with_tag_sends_notification() {
$user_id = self::$user_id;

// Activity with user properly mentioned in both CC and tags.
$activity = array(
'actor' => 'https://example.com/author',
'object' => array(
'id' => 'https://example.com/post/1',
'type' => 'Note',
'content' => '<p>Hello @testuser, how are you?</p>',
'tag' => array(
array(
'type' => 'Mention',
'href' => Actors::get_by_id( $user_id )->get_id(),
'name' => '@testuser',
),
),
),
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
);

// Mock remote metadata.
$metadata_filter = function () {
return array(
'name' => 'Test Author',
'url' => 'https://example.com/author',
);
};
\add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );

$mock = new \MockAction();
\add_filter( 'wp_mail', array( $mock, 'filter' ), 1 );

// Capture email.
$mail_filter = function ( $args ) use ( $user_id ) {
$this->assertStringContainsString( 'Mention', $args['subject'] );
$this->assertStringContainsString( 'Test Author', $args['subject'] );
$this->assertEquals( \get_user_by( 'id', $user_id )->user_email, $args['to'] );
return $args;
};
\add_filter( 'wp_mail', $mail_filter );

// Trigger mention notification.
Mailer::mention( $activity, $user_id );

// Should send 1 email because user is properly mentioned.
$this->assertEquals( 1, $mock->get_call_count(), 'User properly mentioned in tags should receive notification' );

// Clean up.
\remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
\remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
\remove_filter( 'wp_mail', $mail_filter );
}
}