Skip to content

Commit 45e2348

Browse files
authored
Merge branch 'trunk' into enhancement/wp-cron-health-check
2 parents 46b8a86 + 632b4bf commit 45e2348

File tree

5 files changed

+225
-7
lines changed

5 files changed

+225
-7
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+
Fix admin styling for quote comments to match likes and reposts
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+
Route public replies to all actors to ensure conversation continuity.

assets/css/activitypub-admin.css

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,11 @@ input.blog-user-identifier {
230230
font-family: dashicons;
231231
}
232232

233-
.repost .dashboard-comment-wrap,
234-
.like .dashboard-comment-wrap {
233+
.activitypub-comment .dashboard-comment-wrap {
235234
padding-inline-start: 63px;
236235
}
237236

238-
.repost .dashboard-comment-wrap .comment-author,
239-
.like .dashboard-comment-wrap .comment-author {
237+
.activitypub-comment .dashboard-comment-wrap .comment-author {
240238
margin-block: 0;
241239
}
242240

includes/rest/class-inbox-controller.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,13 @@ public function get_item_schema() {
368368
* @return array An array of user IDs who are the recipients of the activity.
369369
*/
370370
private function get_local_recipients( $activity ) {
371-
if ( is_activity_public( $activity ) ) { // Public activity, deliver to followers of the actor.
372-
return Following::get_follower_ids( $activity['actor'] );
371+
$user_ids = array();
372+
373+
if ( is_activity_public( $activity ) ) {
374+
$user_ids = Following::get_follower_ids( $activity['actor'] );
373375
}
374376

375377
$recipients = extract_recipients_from_activity( $activity );
376-
$user_ids = array();
377378

378379
foreach ( $recipients as $recipient ) {
379380

tests/phpunit/tests/includes/rest/class-test-inbox-controller.php

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,4 +935,215 @@ public function test_regular_inbox_action_with_shared_inbox_context() {
935935
\remove_action( 'activitypub_inbox', $inbox_action );
936936
\delete_option( 'activitypub_actor_mode' );
937937
}
938+
939+
/**
940+
* Test get_local_recipients combines followers and explicit recipients for public activities.
941+
*
942+
* @covers ::get_local_recipients
943+
*/
944+
public function test_get_local_recipients_public_activity_with_explicit_recipients() {
945+
// Enable actor mode to allow user actors.
946+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
947+
948+
// Create test users (authors have activitypub capability by default).
949+
$follower_user_id = self::factory()->user->create( array( 'role' => 'author' ) );
950+
$mentioned_user_id = self::factory()->user->create( array( 'role' => 'author' ) );
951+
952+
// Get actor IDs.
953+
$mentioned_actor = Actors::get_by_id( $mentioned_user_id );
954+
$mentioned_actor_id = $mentioned_actor->get_id();
955+
956+
// Create a remote actor and make follower_user follow them.
957+
$remote_actor_url = 'https://example.com/actor/combined-test';
958+
959+
// Mock the remote actor fetch.
960+
$remote_object_filter = function ( $pre, $url ) use ( $remote_actor_url ) {
961+
if ( $url === $remote_actor_url ) {
962+
return array(
963+
'@context' => 'https://www.w3.org/ns/activitystreams',
964+
'id' => $remote_actor_url,
965+
'type' => 'Person',
966+
'preferredUsername' => 'testactor',
967+
'name' => 'Test Actor',
968+
'inbox' => 'https://example.com/actor/combined-test/inbox',
969+
);
970+
}
971+
return $pre;
972+
};
973+
\add_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter, 10, 2 );
974+
975+
$remote_actor = \Activitypub\Collection\Remote_Actors::fetch_by_uri( $remote_actor_url );
976+
977+
// Make follower_user follow the remote actor.
978+
\add_post_meta( $remote_actor->ID, '_activitypub_followed_by', $follower_user_id );
979+
980+
// Public activity that explicitly addresses mentioned_user (who is NOT a follower).
981+
$activity = array(
982+
'type' => 'Create',
983+
'actor' => $remote_actor_url,
984+
'to' => array(
985+
'https://www.w3.org/ns/activitystreams#Public',
986+
$mentioned_actor_id,
987+
),
988+
);
989+
990+
// Use reflection to test the private method.
991+
$reflection = new \ReflectionClass( $this->inbox_controller );
992+
$method = $reflection->getMethod( 'get_local_recipients' );
993+
if ( \PHP_VERSION_ID < 80100 ) {
994+
$method->setAccessible( true );
995+
}
996+
997+
$result = $method->invoke( $this->inbox_controller, $activity );
998+
999+
// Should return BOTH the follower AND the explicitly mentioned user.
1000+
$this->assertNotEmpty( $result, 'Should return recipients for public activity with explicit addressing' );
1001+
$this->assertContains( $follower_user_id, $result, 'Should contain follower' );
1002+
$this->assertContains( $mentioned_user_id, $result, 'Should contain explicitly mentioned user' );
1003+
1004+
// Verify no duplicates if someone is both a follower and explicitly mentioned.
1005+
$this->assertEquals( count( $result ), count( array_unique( $result ) ), 'Should not contain duplicate user IDs' );
1006+
1007+
// Clean up.
1008+
\delete_option( 'activitypub_actor_mode' );
1009+
\remove_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter );
1010+
}
1011+
1012+
/**
1013+
* Test get_local_recipients deduplicates when user is both follower and explicit recipient.
1014+
*
1015+
* @covers ::get_local_recipients
1016+
*/
1017+
public function test_get_local_recipients_deduplicates_follower_and_explicit() {
1018+
// Enable actor mode to allow user actors.
1019+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
1020+
1021+
// Create test user (author has activitypub capability by default).
1022+
$user_id = self::factory()->user->create( array( 'role' => 'author' ) );
1023+
1024+
// Get actor ID.
1025+
$user_actor = Actors::get_by_id( $user_id );
1026+
$user_actor_id = $user_actor->get_id();
1027+
1028+
// Create a remote actor and make user follow them.
1029+
$remote_actor_url = 'https://example.com/actor/dedup-test';
1030+
1031+
// Mock the remote actor fetch.
1032+
$remote_object_filter = function ( $pre, $url ) use ( $remote_actor_url ) {
1033+
if ( $url === $remote_actor_url ) {
1034+
return array(
1035+
'@context' => 'https://www.w3.org/ns/activitystreams',
1036+
'id' => $remote_actor_url,
1037+
'type' => 'Person',
1038+
'preferredUsername' => 'testactor',
1039+
'name' => 'Test Actor',
1040+
'inbox' => 'https://example.com/actor/dedup-test/inbox',
1041+
);
1042+
}
1043+
return $pre;
1044+
};
1045+
\add_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter, 10, 2 );
1046+
1047+
$remote_actor = \Activitypub\Collection\Remote_Actors::fetch_by_uri( $remote_actor_url );
1048+
1049+
// Make user follow the remote actor.
1050+
\add_post_meta( $remote_actor->ID, '_activitypub_followed_by', $user_id );
1051+
1052+
// Public activity that ALSO explicitly addresses the same user.
1053+
$activity = array(
1054+
'type' => 'Create',
1055+
'actor' => $remote_actor_url,
1056+
'to' => array(
1057+
'https://www.w3.org/ns/activitystreams#Public',
1058+
$user_actor_id, // User is both follower AND explicitly addressed.
1059+
),
1060+
);
1061+
1062+
// Use reflection to test the private method.
1063+
$reflection = new \ReflectionClass( $this->inbox_controller );
1064+
$method = $reflection->getMethod( 'get_local_recipients' );
1065+
if ( \PHP_VERSION_ID < 80100 ) {
1066+
$method->setAccessible( true );
1067+
}
1068+
1069+
$result = $method->invoke( $this->inbox_controller, $activity );
1070+
1071+
// Should return the user only once (no duplicates).
1072+
$this->assertContains( $user_id, $result, 'Should contain user' );
1073+
1074+
// Count occurrences of user_id in result.
1075+
$occurrences = count( array_keys( $result, $user_id, true ) );
1076+
$this->assertEquals( 1, $occurrences, 'User should appear exactly once in result' );
1077+
1078+
// Clean up.
1079+
\delete_option( 'activitypub_actor_mode' );
1080+
\remove_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter );
1081+
}
1082+
1083+
/**
1084+
* Test get_local_recipients for non-public activity with followers.
1085+
*
1086+
* @covers ::get_local_recipients
1087+
*/
1088+
public function test_get_local_recipients_non_public_activity_ignores_followers() {
1089+
// Enable actor mode to allow user actors.
1090+
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
1091+
1092+
// Create test users.
1093+
$follower_user_id = self::factory()->user->create( array( 'role' => 'author' ) );
1094+
$mentioned_user_id = self::factory()->user->create( array( 'role' => 'author' ) );
1095+
1096+
// Get actor ID for mentioned user.
1097+
$mentioned_actor = Actors::get_by_id( $mentioned_user_id );
1098+
$mentioned_actor_id = $mentioned_actor->get_id();
1099+
1100+
// Create a remote actor and make follower_user follow them.
1101+
$remote_actor_url = 'https://example.com/actor/non-public-test';
1102+
1103+
// Mock the remote actor fetch.
1104+
$remote_object_filter = function ( $pre, $url ) use ( $remote_actor_url ) {
1105+
if ( $url === $remote_actor_url ) {
1106+
return array(
1107+
'@context' => 'https://www.w3.org/ns/activitystreams',
1108+
'id' => $remote_actor_url,
1109+
'type' => 'Person',
1110+
'preferredUsername' => 'testactor',
1111+
'name' => 'Test Actor',
1112+
'inbox' => 'https://example.com/actor/non-public-test/inbox',
1113+
);
1114+
}
1115+
return $pre;
1116+
};
1117+
\add_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter, 10, 2 );
1118+
1119+
$remote_actor = \Activitypub\Collection\Remote_Actors::fetch_by_uri( $remote_actor_url );
1120+
1121+
// Make follower_user follow the remote actor.
1122+
\add_post_meta( $remote_actor->ID, '_activitypub_followed_by', $follower_user_id );
1123+
1124+
// Non-public activity (direct message) that explicitly addresses only mentioned_user.
1125+
$activity = array(
1126+
'type' => 'Create',
1127+
'actor' => $remote_actor_url,
1128+
'to' => array( $mentioned_actor_id ), // Only mentioned user, NOT public.
1129+
);
1130+
1131+
// Use reflection to test the private method.
1132+
$reflection = new \ReflectionClass( $this->inbox_controller );
1133+
$method = $reflection->getMethod( 'get_local_recipients' );
1134+
if ( \PHP_VERSION_ID < 80100 ) {
1135+
$method->setAccessible( true );
1136+
}
1137+
1138+
$result = $method->invoke( $this->inbox_controller, $activity );
1139+
1140+
// Should return ONLY the mentioned user, NOT the follower.
1141+
$this->assertNotEmpty( $result, 'Should return explicitly addressed recipient' );
1142+
$this->assertContains( $mentioned_user_id, $result, 'Should contain mentioned user' );
1143+
$this->assertNotContains( $follower_user_id, $result, 'Should NOT contain follower for non-public activity' );
1144+
1145+
// Clean up.
1146+
\delete_option( 'activitypub_actor_mode' );
1147+
\remove_filter( 'activitypub_pre_http_get_remote_object', $remote_object_filter );
1148+
}
9381149
}

0 commit comments

Comments
 (0)