Skip to content

Commit ee071f5

Browse files
authored
Merge branch 'trunk' into update/test-factory-usage
2 parents 8e8e7b0 + 1932704 commit ee071f5

File tree

4 files changed

+145
-8
lines changed

4 files changed

+145
-8
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Significance: patch
2+
Type: fixed
3+
4+
False mention email notifications for users in CC field without actual mention tags.

includes/class-mailer.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,8 @@ public static function direct_message( $activity, $user_ids ) {
327327
* @param int|int[] $user_ids The id(s) of the local blog-user(s).
328328
*/
329329
public static function mention( $activity, $user_ids ) {
330-
// Early return if activity has no cc recipients.
331-
if ( empty( $activity['cc'] ) ) {
330+
// Early return if activity has no mentions.
331+
if ( empty( $activity['object']['tag'] ) ) {
332332
return;
333333
}
334334

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

340-
// Normalize to array.
341-
$user_ids = (array) $user_ids;
342-
343-
// Build a map of user_id => actor_id and filter to only users in the "cc" field.
344340
$recipients = array();
345-
foreach ( $user_ids as $user_id ) {
341+
$mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) );
342+
$mentions = array_map( '\Activitypub\object_to_uri', $mentions );
343+
foreach ( (array) $user_ids as $user_id ) {
346344
$actor = Actors::get_by_id( $user_id );
347345
if ( \is_wp_error( $actor ) ) {
348346
continue;
349347
}
350348

351349
$actor_id = $actor->get_id();
352-
if ( \in_array( $actor_id, (array) $activity['cc'], true ) ) {
350+
if ( \in_array( $actor_id, $mentions, true ) ) {
353351
$recipients[ $user_id ] = $actor_id;
354352
}
355353
}

includes/functions.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,9 +809,12 @@ function object_to_uri( $data ) {
809809
case 'Video': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-video.
810810
$data = object_to_uri( $data['url'] );
811811
break;
812+
812813
case 'Link': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-link.
814+
case 'Mention': // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mention.
813815
$data = $data['href'];
814816
break;
817+
815818
default:
816819
$data = $data['id'];
817820
break;

tests/phpunit/tests/includes/class-test-mailer.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,13 @@ public function test_mention_with_array() {
814814
'object' => array(
815815
'id' => 'https://example.com/post/1',
816816
'content' => 'Test mention',
817+
'tag' => array(
818+
array(
819+
'type' => 'Mention',
820+
'href' => Actors::get_by_id( $user_id )->get_id(),
821+
'name' => '@test',
822+
),
823+
),
817824
),
818825
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
819826
);
@@ -935,6 +942,13 @@ public function test_mention_filters_recipients() {
935942
'object' => array(
936943
'id' => 'https://example.com/post/1',
937944
'content' => 'Test mention',
945+
'tag' => array(
946+
array(
947+
'type' => 'Mention',
948+
'href' => Actors::get_by_id( $user_id )->get_id(),
949+
'name' => '@test',
950+
),
951+
),
938952
),
939953
// Only user_id is in CC, not other_user_id.
940954
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
@@ -1115,4 +1129,122 @@ public function test_respect_existing_notification_settings() {
11151129
// Clean up.
11161130
\delete_option( 'activitypub_create_posts' );
11171131
}
1132+
1133+
/**
1134+
* Test that users in CC without actual mention tags do not receive mention notifications.
1135+
*
1136+
* This tests the bug fix where users added to CC (e.g., because they follow the actor)
1137+
* were incorrectly receiving mention notifications even when not actually mentioned.
1138+
*
1139+
* @covers ::mention
1140+
*/
1141+
public function test_mention_requires_tag_not_just_cc() {
1142+
$user_id = self::$user_id;
1143+
1144+
// Activity with user in CC but NOT mentioned in tags.
1145+
$activity = array(
1146+
'actor' => 'https://example.com/sports-account',
1147+
'object' => array(
1148+
'id' => 'https://example.com/sports-account/posts/123',
1149+
'type' => 'Note',
1150+
'content' => '<p>Join @user1 and @user2 on our stream...</p>',
1151+
'tag' => array(
1152+
// Other users mentioned, but NOT the local user.
1153+
array(
1154+
'type' => 'Mention',
1155+
'href' => 'https://example.com/user1',
1156+
'name' => '[email protected]',
1157+
),
1158+
array(
1159+
'type' => 'Mention',
1160+
'href' => 'https://example.com/user2',
1161+
'name' => '[email protected]',
1162+
),
1163+
),
1164+
),
1165+
// User is in CC (e.g., because they follow the actor).
1166+
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
1167+
);
1168+
1169+
// Mock remote metadata.
1170+
$metadata_filter = function () {
1171+
return array(
1172+
'name' => 'Sports Account',
1173+
'url' => 'https://example.com/sports-account',
1174+
);
1175+
};
1176+
\add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
1177+
1178+
$mock = new \MockAction();
1179+
\add_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
1180+
1181+
// Trigger mention notification.
1182+
Mailer::mention( $activity, $user_id );
1183+
1184+
// Should NOT send any email because user is not actually mentioned in tags.
1185+
$this->assertEquals( 0, $mock->get_call_count(), 'User in CC without mention tag should not receive notification' );
1186+
1187+
// Clean up.
1188+
\remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
1189+
\remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
1190+
}
1191+
1192+
/**
1193+
* Test that users with actual mention tags DO receive mention notifications.
1194+
*
1195+
* @covers ::mention
1196+
*/
1197+
public function test_mention_with_tag_sends_notification() {
1198+
$user_id = self::$user_id;
1199+
1200+
// Activity with user properly mentioned in both CC and tags.
1201+
$activity = array(
1202+
'actor' => 'https://example.com/author',
1203+
'object' => array(
1204+
'id' => 'https://example.com/post/1',
1205+
'type' => 'Note',
1206+
'content' => '<p>Hello @testuser, how are you?</p>',
1207+
'tag' => array(
1208+
array(
1209+
'type' => 'Mention',
1210+
'href' => Actors::get_by_id( $user_id )->get_id(),
1211+
'name' => '@testuser',
1212+
),
1213+
),
1214+
),
1215+
'cc' => array( Actors::get_by_id( $user_id )->get_id() ),
1216+
);
1217+
1218+
// Mock remote metadata.
1219+
$metadata_filter = function () {
1220+
return array(
1221+
'name' => 'Test Author',
1222+
'url' => 'https://example.com/author',
1223+
);
1224+
};
1225+
\add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
1226+
1227+
$mock = new \MockAction();
1228+
\add_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
1229+
1230+
// Capture email.
1231+
$mail_filter = function ( $args ) use ( $user_id ) {
1232+
$this->assertStringContainsString( 'Mention', $args['subject'] );
1233+
$this->assertStringContainsString( 'Test Author', $args['subject'] );
1234+
$this->assertEquals( \get_user_by( 'id', $user_id )->user_email, $args['to'] );
1235+
return $args;
1236+
};
1237+
\add_filter( 'wp_mail', $mail_filter );
1238+
1239+
// Trigger mention notification.
1240+
Mailer::mention( $activity, $user_id );
1241+
1242+
// Should send 1 email because user is properly mentioned.
1243+
$this->assertEquals( 1, $mock->get_call_count(), 'User properly mentioned in tags should receive notification' );
1244+
1245+
// Clean up.
1246+
\remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter );
1247+
\remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 );
1248+
\remove_filter( 'wp_mail', $mail_filter );
1249+
}
11181250
}

0 commit comments

Comments
 (0)