From 7eb78aeb3a6c3ffb5680e90bee5febd17f1110a5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 12 Nov 2025 12:26:33 +0100 Subject: [PATCH 1/6] Normalize trailing slashes in Like activity URLs Ensures object URLs in Like activities are normalized by removing trailing slashes before processing, improving compatibility with platforms like Pixelfed. Adds tests to verify correct handling and storage of Like activities with trailing slashes in object URLs. --- includes/collection/class-interactions.php | 3 +- .../includes/collection/class-test-inbox.php | 56 +++++++++++++++++++ .../includes/handler/class-test-like.php | 44 +++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php index efa640ceb..7c5cb6982 100644 --- a/includes/collection/class-interactions.php +++ b/includes/collection/class-interactions.php @@ -39,7 +39,7 @@ public static function add_comment( $activity ) { } $in_reply_to = object_to_uri( $activity['object']['inReplyTo'] ); - $in_reply_to = \esc_url_raw( $in_reply_to ); + $in_reply_to = \untrailingslashit( \esc_url_raw( $in_reply_to ) ); $comment_post_id = \url_to_postid( $in_reply_to ); $parent_comment_id = url_to_commentid( $in_reply_to ); @@ -93,6 +93,7 @@ public static function update_comment( $activity ) { */ public static function add_reaction( $activity ) { $url = object_to_uri( $activity['object'] ); + $url = \untrailingslashit( $url ); $comment_post_id = \url_to_postid( $url ); $parent_comment_id = url_to_commentid( $url ); diff --git a/tests/phpunit/tests/includes/collection/class-test-inbox.php b/tests/phpunit/tests/includes/collection/class-test-inbox.php index a135f3342..3e7c0c471 100644 --- a/tests/phpunit/tests/includes/collection/class-test-inbox.php +++ b/tests/phpunit/tests/includes/collection/class-test-inbox.php @@ -723,4 +723,60 @@ public function test_deduplicate_non_existent() { $result = Inbox::deduplicate( 'https://remote.example.com/activities/non-existent' ); $this->assertFalse( $result ); } + + /** + * Test adding Like activity with trailing slash in object URL. + * + * This test verifies that Like activities from Pixelfed and other platforms + * that include trailing slashes in object URLs are stored correctly in the inbox. + * + * @covers ::add + */ + public function test_add_like_activity_with_trailing_slash() { + // Create a post to be liked. + $post_id = self::factory()->post->create( + array( + 'post_title' => 'Test Post for Like', + 'post_content' => 'Test content', + 'post_status' => 'publish', + ) + ); + $post_permalink = \get_permalink( $post_id ); + + // Create a Like activity with trailing slash in object URL (as Pixelfed sends). + $activity = new Activity(); + $activity->set_id( 'https://pixelfed.social/users/pfefferle#likes/30434186' ); + $activity->set_type( 'Like' ); + $activity->set_actor( 'https://pixelfed.social/users/pfefferle' ); + $activity->set_object( $post_permalink . '/' ); // Add trailing slash. + + $user_id = 1; + + // Add activity to inbox. + $inbox_id = Inbox::add( $activity, $user_id ); + + $this->assertIsInt( $inbox_id ); + $this->assertGreaterThan( 0, $inbox_id ); + + // Verify the post was created. + $post = \get_post( $inbox_id ); + $this->assertInstanceOf( 'WP_Post', $post ); + $this->assertEquals( Inbox::POST_TYPE, $post->post_type ); + + // Test _activitypub_object_id meta - should preserve the trailing slash as-is. + $object_id_meta = \get_post_meta( $inbox_id, '_activitypub_object_id', true ); + $this->assertEquals( $post_permalink . '/', $object_id_meta ); + + // Test _activitypub_activity_type meta. + $activity_type_meta = \get_post_meta( $inbox_id, '_activitypub_activity_type', true ); + $this->assertEquals( 'Like', $activity_type_meta ); + + // Test _activitypub_user_id meta. + $user_id_meta = \get_post_meta( $inbox_id, '_activitypub_user_id', true ); + $this->assertEquals( $user_id, $user_id_meta ); + + // Test _activitypub_activity_remote_actor meta. + $remote_actor_meta = \get_post_meta( $inbox_id, '_activitypub_activity_remote_actor', true ); + $this->assertEquals( 'https://pixelfed.social/users/pfefferle', $remote_actor_meta ); + } } diff --git a/tests/phpunit/tests/includes/handler/class-test-like.php b/tests/phpunit/tests/includes/handler/class-test-like.php index c01cf1d5a..8159b8e44 100644 --- a/tests/phpunit/tests/includes/handler/class-test-like.php +++ b/tests/phpunit/tests/includes/handler/class-test-like.php @@ -185,6 +185,50 @@ public function handle_like_provider() { ); } + /** + * Test Like activity with trailing slash in object URL. + * + * This test verifies that Like activities from Pixelfed and other platforms + * that include trailing slashes in object URLs are processed correctly. + * + * @covers ::handle_like + * @covers \Activitypub\Collection\Interactions::add_reaction + */ + public function test_handle_like_with_trailing_slash() { + // Create activity with trailing slash in object URL (as Pixelfed sends). + $activity = array( + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://pixelfed.social/users/pfefferle#likes/30434186', + 'type' => 'Like', + 'actor' => $this->user_url, + 'object' => $this->post_permalink . '/', // Add trailing slash. + ); + + // Get comment count before. + $comments_before = \get_comments( + array( + 'type' => 'like', + 'post_id' => $this->post_id, + ) + ); + $count_before = count( $comments_before ); + + // Process the like. + Like::handle_like( $activity, $this->user_id ); + + // Check that comment was created despite trailing slash. + $comments_after = \get_comments( + array( + 'type' => 'like', + 'post_id' => $this->post_id, + ) + ); + $count_after = count( $comments_after ); + + $this->assertEquals( $count_before + 1, $count_after, 'Like with trailing slash should create comment' ); + $this->assertInstanceOf( 'WP_Comment', $comments_after[0], 'Should create WP_Comment object' ); + } + /** * Test duplicate like handling. * From 93056a732bed13485eac571f50d2670c4c14edfd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 12 Nov 2025 12:33:44 +0100 Subject: [PATCH 2/6] Treat activities with no recipients as public Added a check in is_activity_public to return true when the recipients list is empty, ensuring activities without specified recipients are considered public. --- includes/functions.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index f236c15c1..92339ac84 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -607,6 +607,10 @@ function is_activity_public( $data ) { $recipients = extract_recipients_from_activity( $data ); + if ( empty( $recipients ) ) { + return true; + } + return ! empty( array_intersect( $recipients, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ); } From 39eb30d8a06729666f42e1de3117f0aa2e62b5e9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 12 Nov 2025 12:42:16 +0100 Subject: [PATCH 3/6] Treat activities with no recipients as public Updated logic to treat activities without recipients as public, delivering them to all local actors. Adjusted related tests to reflect this behavior and removed unnecessary untrailingslashit calls for URL normalization. --- includes/collection/class-interactions.php | 3 +-- tests/phpunit/tests/includes/class-test-functions.php | 2 +- tests/phpunit/tests/includes/collection/class-test-inbox.php | 3 ++- .../tests/includes/rest/class-test-inbox-controller.php | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php index 7c5cb6982..efa640ceb 100644 --- a/includes/collection/class-interactions.php +++ b/includes/collection/class-interactions.php @@ -39,7 +39,7 @@ public static function add_comment( $activity ) { } $in_reply_to = object_to_uri( $activity['object']['inReplyTo'] ); - $in_reply_to = \untrailingslashit( \esc_url_raw( $in_reply_to ) ); + $in_reply_to = \esc_url_raw( $in_reply_to ); $comment_post_id = \url_to_postid( $in_reply_to ); $parent_comment_id = url_to_commentid( $in_reply_to ); @@ -93,7 +93,6 @@ public static function update_comment( $activity ) { */ public static function add_reaction( $activity ) { $url = object_to_uri( $activity['object'] ); - $url = \untrailingslashit( $url ); $comment_post_id = \url_to_postid( $url ); $parent_comment_id = url_to_commentid( $url ); diff --git a/tests/phpunit/tests/includes/class-test-functions.php b/tests/phpunit/tests/includes/class-test-functions.php index 7fd00663a..1e2f9093a 100644 --- a/tests/phpunit/tests/includes/class-test-functions.php +++ b/tests/phpunit/tests/includes/class-test-functions.php @@ -1003,7 +1003,7 @@ public function public_activity_provider() { 'monkey' => 'https://www.w3.org/ns/activitystreams#Public', ), ), - false, + true, ), array( array( diff --git a/tests/phpunit/tests/includes/collection/class-test-inbox.php b/tests/phpunit/tests/includes/collection/class-test-inbox.php index 3e7c0c471..045dac8e6 100644 --- a/tests/phpunit/tests/includes/collection/class-test-inbox.php +++ b/tests/phpunit/tests/includes/collection/class-test-inbox.php @@ -76,8 +76,9 @@ public function test_add_activity_with_post_meta() { $this->assertEquals( 'https://remote.example.com/users/testuser', $remote_actor_meta ); // Test activitypub_content_visibility meta. + // Activities with no recipients are treated as public. $visibility_meta = \get_post_meta( $inbox_id, 'activitypub_content_visibility', true ); - $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, $visibility_meta ); + $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $visibility_meta ); } /** diff --git a/tests/phpunit/tests/includes/rest/class-test-inbox-controller.php b/tests/phpunit/tests/includes/rest/class-test-inbox-controller.php index e16bb23a4..a90be7f7f 100644 --- a/tests/phpunit/tests/includes/rest/class-test-inbox-controller.php +++ b/tests/phpunit/tests/includes/rest/class-test-inbox-controller.php @@ -475,7 +475,9 @@ public function test_get_local_recipients_no_recipients() { $method->setAccessible( true ); $result = $method->invoke( $this->inbox_controller, $activity ); - $this->assertEmpty( $result, 'Should return empty array when no recipients' ); + // Activities with no recipients are treated as public and delivered to all local actors. + $this->assertNotEmpty( $result, 'Should return all local actors when no recipients (treated as public)' ); + $this->assertEquals( Actors::get_all_ids(), $result, 'Should match all local actor IDs' ); } /** From b29b994837a6878e9ce5981468bae934b6b395e3 Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Wed, 12 Nov 2025 13:55:12 +0200 Subject: [PATCH 4/6] Add changelog --- .github/changelog/2448-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2448-from-description diff --git a/.github/changelog/2448-from-description b/.github/changelog/2448-from-description new file mode 100644 index 000000000..f5320084d --- /dev/null +++ b/.github/changelog/2448-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Fixed compatibility with Pixelfed and similar platforms by treating activities without recipients as public, ensuring boosts and reposts work correctly. From 9493ef71762d8b4f3dd0a60480f7ac1e101c2962 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 12 Nov 2025 15:20:30 +0100 Subject: [PATCH 5/6] Update tests/phpunit/tests/includes/collection/class-test-inbox.php Co-authored-by: Konstantin Obenland --- tests/phpunit/tests/includes/collection/class-test-inbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/tests/includes/collection/class-test-inbox.php b/tests/phpunit/tests/includes/collection/class-test-inbox.php index 045dac8e6..02aef2b40 100644 --- a/tests/phpunit/tests/includes/collection/class-test-inbox.php +++ b/tests/phpunit/tests/includes/collection/class-test-inbox.php @@ -75,7 +75,6 @@ public function test_add_activity_with_post_meta() { $remote_actor_meta = \get_post_meta( $inbox_id, '_activitypub_activity_remote_actor', true ); $this->assertEquals( 'https://remote.example.com/users/testuser', $remote_actor_meta ); - // Test activitypub_content_visibility meta. // Activities with no recipients are treated as public. $visibility_meta = \get_post_meta( $inbox_id, 'activitypub_content_visibility', true ); $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $visibility_meta ); From 40713a8aa9fa83dc031b06fa6f0afc3fbdd43662 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 12 Nov 2025 15:25:12 +0100 Subject: [PATCH 6/6] Treat activities with no recipients as public Updated get_activity_visibility to return public visibility for activities without recipients. Adjusted related unit test to reflect this behavior change, ensuring empty activities are now considered public. --- includes/functions.php | 6 ++++++ tests/phpunit/tests/includes/class-test-functions.php | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 92339ac84..34db3b0f0 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -588,6 +588,12 @@ function get_activity_visibility( $activity ) { return ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC; } + // Activities with no recipients are treated as public. + $recipients = extract_recipients_from_activity( $activity ); + if ( empty( $recipients ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; } diff --git a/tests/phpunit/tests/includes/class-test-functions.php b/tests/phpunit/tests/includes/class-test-functions.php index 1e2f9093a..072a0999c 100644 --- a/tests/phpunit/tests/includes/class-test-functions.php +++ b/tests/phpunit/tests/includes/class-test-functions.php @@ -1334,13 +1334,13 @@ public function visibility_data_provider() { 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, 'description' => 'Public visibility via as:Public identifier', ), - // Empty activity. + // Empty activity - no recipients means public. array( 'activity' => array( 'type' => 'Create', ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Empty activity defaults to private', + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'description' => 'Empty activity (no recipients) is treated as public', ), ); }