From 523defe52a9fd302cc37e9f20445b6d8b745bf22 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 18 Sep 2025 12:02:44 +0200 Subject: [PATCH 01/30] Refactor recipient extraction and add unit tests Refactored recipient extraction logic by introducing extract_recipients_from_activity_property() for improved modularity and clarity. Updated extract_recipients_from_activity() to use the new helper. Added comprehensive unit tests for both functions to ensure correct handling of various recipient formats and uniqueness. --- includes/functions.php | 53 +++--- tests/includes/class-test-functions.php | 209 ++++++++++++++++++++++++ 2 files changed, 239 insertions(+), 23 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 8cfb06fd6..b0d698678 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -525,35 +525,42 @@ function extract_recipients_from_activity( $data ) { $recipient_items = array(); foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { - if ( array_key_exists( $i, $data ) ) { - if ( is_array( $data[ $i ] ) ) { - $recipient = $data[ $i ]; - } else { - $recipient = array( $data[ $i ] ); - } - $recipient_items = array_merge( $recipient_items, $recipient ); - } + $recipient_items = array_merge( $recipient_items, extract_recipients_from_activity_property( $data, $i ) ); + } - if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) { - if ( is_array( $data['object'][ $i ] ) ) { - $recipient = $data['object'][ $i ]; - } else { - $recipient = array( $data['object'][ $i ] ); - } - $recipient_items = array_merge( $recipient_items, $recipient ); + return array_unique( $recipient_items ); +} + +/** + * Extract recipient URLs from a specific attribute of an Activity object. + * + * @param array $data The Activity object as array. + * @param string $attribute The attribute to extract recipients from (e.g., 'to', 'cc'). + * + * @return array The list of user URLs. + */ +function extract_recipients_from_activity_property( $data, $attribute ) { + $recipient_items = array(); + + // Extract from main data and object data. + $sources = array( $data ); + if ( is_array( $data['object'] ?? null ) ) { + $sources[] = $data['object']; + } + + foreach ( $sources as $source ) { + if ( array_key_exists( $attribute, $source ) ) { + $recipients = (array) $source[ $attribute ]; + $recipient_items = array_merge( $recipient_items, $recipients ); } } + // Flatten and extract IDs. $recipients = array(); - - // Flatten array. foreach ( $recipient_items as $recipient ) { - if ( is_array( $recipient ) ) { - // Check if recipient is an object. - if ( array_key_exists( 'id', $recipient ) ) { - $recipients[] = $recipient['id']; - } - } else { + if ( is_array( $recipient ) && isset( $recipient['id'] ) ) { + $recipients[] = $recipient['id']; + } elseif ( ! is_array( $recipient ) ) { $recipients[] = $recipient; } } diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index 8ef835afa..5444b5748 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -11,6 +11,8 @@ use Activitypub\Collection\Outbox; use function Activitypub\add_to_outbox; +use function Activitypub\extract_recipients_from_activity; +use function Activitypub\extract_recipients_from_activity_property; /** * Test class for Functions. @@ -1008,4 +1010,211 @@ public function public_activity_provider() { ), ); } + + /** + * Data provider for testing extract_recipients_from_activity_property. + * + * @return array Test data sets. + */ + public function data_provider_extract_recipients() { + return array( + 'simple_string_recipient' => array( + 'data' => array( + 'to' => 'https://example.com/users/alice', + ), + 'attribute' => 'to', + 'expected' => array( 'https://example.com/users/alice' ), + ), + 'array_of_recipients' => array( + 'data' => array( + 'to' => array( + 'https://example.com/users/alice', + 'https://example.com/users/bob', + ), + ), + 'attribute' => 'to', + 'expected' => array( + 'https://example.com/users/alice', + 'https://example.com/users/bob', + ), + ), + 'object_recipients_with_id' => array( + 'data' => array( + 'cc' => array( + array( 'id' => 'https://example.com/users/charlie' ), + array( 'id' => 'https://example.com/users/diana' ), + ), + ), + 'attribute' => 'cc', + 'expected' => array( + 'https://example.com/users/charlie', + 'https://example.com/users/diana', + ), + ), + 'mixed_recipients' => array( + 'data' => array( + 'bcc' => array( + 'https://example.com/users/eve', + array( 'id' => 'https://example.com/users/frank' ), + ), + ), + 'attribute' => 'bcc', + 'expected' => array( + 'https://example.com/users/eve', + 'https://example.com/users/frank', + ), + ), + 'recipients_in_object' => array( + 'data' => array( + 'object' => array( + 'to' => 'https://example.com/users/grace', + ), + ), + 'attribute' => 'to', + 'expected' => array( 'https://example.com/users/grace' ), + ), + 'recipients_in_both_main_and_object' => array( + 'data' => array( + 'to' => 'https://example.com/users/henry', + 'object' => array( + 'to' => 'https://example.com/users/iris', + ), + ), + 'attribute' => 'to', + 'expected' => array( + 'https://example.com/users/henry', + 'https://example.com/users/iris', + ), + ), + 'duplicate_recipients' => array( + 'data' => array( + 'to' => array( + 'https://example.com/users/jack', + 'https://example.com/users/jack', // Duplicate. + ), + ), + 'attribute' => 'to', + 'expected' => array( 'https://example.com/users/jack' ), // Should be unique. + ), + 'no_recipients' => array( + 'data' => array( + 'cc' => array(), + ), + 'attribute' => 'to', // Different attribute. + 'expected' => array(), + ), + 'empty_data' => array( + 'data' => array(), + 'attribute' => 'to', + 'expected' => array(), + ), + 'object_without_id' => array( + 'data' => array( + 'to' => array( + array( + 'type' => 'Person', + 'name' => 'Kate', + ), // No 'id' key. + ), + ), + 'attribute' => 'to', + 'expected' => array(), // Should be ignored. + ), + 'public_recipients' => array( + 'data' => array( + 'to' => array( + 'https://www.w3.org/ns/activitystreams#Public', + 'https://example.com/users/liam', + ), + ), + 'attribute' => 'to', + 'expected' => array( + 'https://www.w3.org/ns/activitystreams#Public', + 'https://example.com/users/liam', + ), + ), + 'audience_attribute' => array( + 'data' => array( + 'audience' => 'https://example.com/groups/followers', + ), + 'attribute' => 'audience', + 'expected' => array( 'https://example.com/groups/followers' ), + ), + ); + } + + /** + * Test extract_recipients_from_activity_property function. + * + * @dataProvider data_provider_extract_recipients + * + * @param array $data The activity data. + * @param string $attribute The attribute to extract. + * @param array $expected The expected recipients. + */ + public function test_extract_recipients_from_activity_property( $data, $attribute, $expected ) { + $actual = extract_recipients_from_activity_property( $data, $attribute ); + + // Sort both arrays to ensure order doesn't matter in comparison. + sort( $expected ); + sort( $actual ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Test extract_recipients_from_activity_attribute function. + * + * @dataProvider data_provider_extract_recipients + * + * @param array $data The activity data. + * @param string $attribute The attribute to extract. + * @param array $expected The expected recipients. + */ + public function test_extract_recipients_from_activity( $data, $attribute, $expected ) { + $actual = extract_recipients_from_activity( $data ); + + // Sort both arrays to ensure order doesn't matter in comparison. + sort( $expected ); + sort( $actual ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Test that the function returns unique recipients. + */ + public function test_unique_recipients() { + $data = array( + 'to' => array( + 'https://example.com/users/alice', + 'https://example.com/users/alice', // Duplicate. + ), + 'object' => array( + 'to' => 'https://example.com/users/alice', // Another duplicate. + ), + ); + $actual = extract_recipients_from_activity_property( $data, 'to' ); + + $this->assertSame( array( 'https://example.com/users/alice' ), $actual ); + $this->assertCount( 1, $actual, 'Should return unique recipients only.' ); + } + + /** + * Test that the function returns unique recipients from extract_recipients_from_activity. + */ + public function test_unique_recipients_from_activity() { + $data = array( + 'to' => array( + 'https://example.com/users/alice', + 'https://example.com/users/alice', // Duplicate. + ), + 'object' => array( + 'to' => 'https://example.com/users/alice', // Another duplicate. + ), + ); + $actual = extract_recipients_from_activity( $data ); + $this->assertSame( array( 'https://example.com/users/alice' ), $actual ); + $this->assertCount( 1, $actual, 'Should return unique recipients only.' ); + } } From 51fe6730fd7c5eb627cb0bf7e8ee771ee374475a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 18 Sep 2025 18:27:12 +0200 Subject: [PATCH 02/30] Rename parameter from attribute to property in function Renamed the parameter and related documentation in extract_recipients_from_activity_property from 'attribute' to 'property' for clarity and consistency. --- includes/functions.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index b0d698678..7363e0a3c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -532,14 +532,14 @@ function extract_recipients_from_activity( $data ) { } /** - * Extract recipient URLs from a specific attribute of an Activity object. + * Extract recipient URLs from a specific property of an Activity object. * * @param array $data The Activity object as array. - * @param string $attribute The attribute to extract recipients from (e.g., 'to', 'cc'). + * @param string $property The property to extract recipients from (e.g., 'to', 'cc'). * * @return array The list of user URLs. */ -function extract_recipients_from_activity_property( $data, $attribute ) { +function extract_recipients_from_activity_property( $data, $property ) { $recipient_items = array(); // Extract from main data and object data. @@ -549,8 +549,8 @@ function extract_recipients_from_activity_property( $data, $attribute ) { } foreach ( $sources as $source ) { - if ( array_key_exists( $attribute, $source ) ) { - $recipients = (array) $source[ $attribute ]; + if ( array_key_exists( $property, $source ) ) { + $recipients = (array) $source[ $property ]; $recipient_items = array_merge( $recipient_items, $recipients ); } } From d25fd5f6e3c8eb03539a27e63113c2d5cf3c3bdf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 18 Sep 2025 19:28:47 +0200 Subject: [PATCH 03/30] Refactor recipient extraction and object_to_uri handling Simplifies extract_recipients_from_activity_property by streamlining recipient extraction and flattening logic. Updates object_to_uri to return false if 'href' or 'id' are not set, improving robustness when handling malformed or incomplete ActivityPub objects. --- includes/functions.php | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index ffcd1b702..d00465327 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -540,30 +540,15 @@ function extract_recipients_from_activity( $data ) { * @return array The list of user URLs. */ function extract_recipients_from_activity_property( $data, $property ) { - $recipient_items = array(); - - // Extract from main data and object data. - $sources = array( $data ); - if ( is_array( $data['object'] ?? null ) ) { - $sources[] = $data['object']; - } + $recipients = array(); - foreach ( $sources as $source ) { - if ( array_key_exists( $property, $source ) ) { - $recipients = (array) $source[ $property ]; - $recipient_items = array_merge( $recipient_items, $recipients ); - } + if ( ! empty( $data[ $property ] ) ) { + $recipients = $data[ $property ]; + } elseif ( ! empty( $data['object'][ $property ] ) ) { + $recipients = $data['object'][ $property ]; } - // Flatten and extract IDs. - $recipients = array(); - foreach ( $recipient_items as $recipient ) { - if ( is_array( $recipient ) && isset( $recipient['id'] ) ) { - $recipients[] = $recipient['id']; - } elseif ( ! is_array( $recipient ) ) { - $recipients[] = $recipient; - } - } + $recipients = \array_map( '\Activitypub\object_to_uri', (array) $recipients ); return array_unique( $recipients ); } @@ -703,7 +688,7 @@ function url_to_commentid( $url ) { * * @param array|string $data The ActivityPub object. * - * @return string The URI of the ActivityPub object + * @return string|false The URI of the ActivityPub object or false if not found. */ function object_to_uri( $data ) { // Check whether it is already simple. @@ -740,10 +725,10 @@ function object_to_uri( $data ) { $data = object_to_uri( $data['url'] ); break; case 'Link': - $data = $data['href']; + $data = $data['href'] ?? false; break; default: - $data = $data['id']; + $data = $data['id'] ?? false; break; } From 70ce80f44395baaf8c22a463e9378f1357cd0ba9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 18 Sep 2025 20:04:41 +0200 Subject: [PATCH 04/30] Filter empty values from recipients array Updated extract_recipients_from_activity_property to remove empty values before returning unique recipients. Adjusted related test to reflect the change in behavior. --- includes/functions.php | 2 +- tests/includes/class-test-functions.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index d00465327..119f55b8b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -550,7 +550,7 @@ function extract_recipients_from_activity_property( $data, $property ) { $recipients = \array_map( '\Activitypub\object_to_uri', (array) $recipients ); - return array_unique( $recipients ); + return \array_unique( \array_filter( $recipients ) ); } /** diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index 70ae34fa6..f9366d6d8 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -1107,7 +1107,6 @@ public function data_provider_extract_recipients() { 'attribute' => 'to', 'expected' => array( 'https://example.com/users/henry', - 'https://example.com/users/iris', ), ), 'duplicate_recipients' => array( From 79429da0b16116cb95bc9541360e9a86e4422b7b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 18 Sep 2025 22:59:24 +0200 Subject: [PATCH 05/30] Improve object_to_uri fallback and update related tests Updated object_to_uri to use 'url' as a fallback if 'id' is not present. Adjusted related unit tests to reflect this logic and removed unnecessary error handling in moderation tests. --- includes/functions.php | 2 +- tests/includes/class-test-functions.php | 2 +- tests/includes/class-test-moderation.php | 18 ------------------ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 119f55b8b..94ac982df 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -728,7 +728,7 @@ function object_to_uri( $data ) { $data = $data['href'] ?? false; break; default: - $data = $data['id'] ?? false; + $data = $data['id'] ?? $data['url'] ?? false; break; } diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index f9366d6d8..9e31090d5 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -956,7 +956,7 @@ public function public_activity_provider() { ), ), ), - true, + false, ), array( array( diff --git a/tests/includes/class-test-moderation.php b/tests/includes/class-test-moderation.php index 537e4f350..f3b099269 100644 --- a/tests/includes/class-test-moderation.php +++ b/tests/includes/class-test-moderation.php @@ -446,7 +446,6 @@ public function test_hierarchical_blocking() { * Test edge cases with malformed activity data. * * @covers ::activity_is_blocked - * @throws \Exception Thrown when an error occurs. */ public function test_activity_blocking_edge_cases() { // Test with empty activity. @@ -506,24 +505,7 @@ public function test_activity_blocking_edge_cases() { ), ) ); - - // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler - \set_error_handler( - static function ( $errno, $errstr ) { - throw new \Exception( \esc_html( $errstr ), \esc_html( $errno ) ); - }, - E_NOTICE | E_WARNING - ); - - // PHP 7.2 uses "Undefined index", PHP 8+ uses "Undefined array key". - if ( version_compare( PHP_VERSION, '8.0.0', '>=' ) ) { - $this->expectExceptionMessage( 'Undefined array key "id"' ); - } else { - $this->expectExceptionMessage( 'Undefined index: id' ); - } $this->assertFalse( Moderation::activity_is_blocked( $activity ) ); - - \restore_error_handler(); } /** From 35dbe06592047912c7082d586d5b999d45bd2378 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 10:25:50 +0200 Subject: [PATCH 06/30] Refactor recipient extraction and add Audience trait Refactored recipient extraction functions to standardize argument order. Introduced an Audience trait to encapsulate recipient and visibility determination logic, and updated controllers to use this trait. Adjusted related tests to match the new function signatures. --- includes/functions.php | 6 +- .../rest/class-actors-inbox-controller.php | 1 + includes/rest/class-inbox-controller.php | 31 ++------ includes/rest/trait-audience.php | 74 +++++++++++++++++++ tests/includes/class-test-functions.php | 4 +- 5 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 includes/rest/trait-audience.php diff --git a/includes/functions.php b/includes/functions.php index 94ac982df..5085ee364 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -525,7 +525,7 @@ function extract_recipients_from_activity( $data ) { $recipient_items = array(); foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { - $recipient_items = array_merge( $recipient_items, extract_recipients_from_activity_property( $data, $i ) ); + $recipient_items = array_merge( $recipient_items, extract_recipients_from_activity_property( $i, $data ) ); } return array_unique( $recipient_items ); @@ -534,12 +534,12 @@ function extract_recipients_from_activity( $data ) { /** * Extract recipient URLs from a specific property of an Activity object. * - * @param array $data The Activity object as array. * @param string $property The property to extract recipients from (e.g., 'to', 'cc'). + * @param array $data The Activity object as array. * * @return array The list of user URLs. */ -function extract_recipients_from_activity_property( $data, $property ) { +function extract_recipients_from_activity_property( $property, $data ) { $recipients = array(); if ( ! empty( $data[ $property ] ) ) { diff --git a/includes/rest/class-actors-inbox-controller.php b/includes/rest/class-actors-inbox-controller.php index 86ed35cd3..80e9ce52f 100644 --- a/includes/rest/class-actors-inbox-controller.php +++ b/includes/rest/class-actors-inbox-controller.php @@ -23,6 +23,7 @@ */ class Actors_Inbox_Controller extends Actors_Controller { use Collection; + use Audience; /** * Register routes. diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 3846d5d4c..f1ef48488 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -8,13 +8,8 @@ namespace Activitypub\Rest; use Activitypub\Activity\Activity; -use Activitypub\Collection\Actors; use Activitypub\Moderation; -use function Activitypub\extract_recipients_from_activity; -use function Activitypub\is_same_domain; -use function Activitypub\user_can_activitypub; - /** * Inbox_Controller class. * @@ -23,6 +18,8 @@ * @see https://www.w3.org/TR/activitypub/#inbox */ class Inbox_Controller extends \WP_REST_Controller { + use Audience; + /** * The namespace of this controller's route. * @@ -149,25 +146,11 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { - $recipients = extract_recipients_from_activity( $data ); + $recipients = $this->determine_recipients( $data ); foreach ( $recipients as $recipient ) { - if ( ! is_same_domain( $recipient ) ) { - continue; - } - - $user_id = Actors::get_id_by_various( $recipient ); - - if ( \is_wp_error( $user_id ) ) { - continue; - } - - if ( ! user_can_activitypub( $user_id ) ) { - continue; - } - // Check user-specific blocks for this recipient. - if ( Moderation::activity_is_blocked_for_user( $activity, $user_id ) ) { + if ( Moderation::activity_is_blocked_for_user( $activity, $recipient ) ) { /** * ActivityPub inbox disallowed activity for specific user. * @@ -176,7 +159,7 @@ public function create_item( $request ) { * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity ); + \do_action( 'activitypub_rest_inbox_disallowed', $data, $recipient, $type, $activity ); continue; } @@ -188,7 +171,7 @@ public function create_item( $request ) { * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity ); + \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity ); /** * ActivityPub inbox action for specific activity types. @@ -197,7 +180,7 @@ public function create_item( $request ) { * @param int $user_id The user ID. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity ); + \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); } } diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php new file mode 100644 index 000000000..ecd5aed1f --- /dev/null +++ b/includes/rest/trait-audience.php @@ -0,0 +1,74 @@ + 'https://example.com/users/alice', // Another duplicate. ), ); - $actual = extract_recipients_from_activity_property( $data, 'to' ); + $actual = extract_recipients_from_activity_property( 'to', $data ); $this->assertSame( array( 'https://example.com/users/alice' ), $actual ); $this->assertCount( 1, $actual, 'Should return unique recipients only.' ); From 615bd19ea6d598a7afb6233987b439c3b60a6c31 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 10:38:10 +0200 Subject: [PATCH 07/30] Add visibility parameter to ActivityPub inbox actions Introduces a visibility parameter to the 'activitypub_inbox' and 'activitypub_inbox_{type}' actions, allowing handlers to determine the visibility of incoming activities. Refactors recipient determination to use 'determine_local_recipients' and updates the visibility logic for more accurate classification based on activity recipients. --- .../rest/class-actors-inbox-controller.php | 22 +++++++------ includes/rest/class-inbox-controller.php | 23 +++++++------ includes/rest/trait-audience.php | 32 +++++++++++-------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/includes/rest/class-actors-inbox-controller.php b/includes/rest/class-actors-inbox-controller.php index 80e9ce52f..764650c55 100644 --- a/includes/rest/class-actors-inbox-controller.php +++ b/includes/rest/class-actors-inbox-controller.php @@ -178,24 +178,28 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity ); } else { + $visibility = $this->determine_visibility( $data ); + /** * ActivityPub inbox action. * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. + * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity ); + \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity, $visibility ); /** * ActivityPub inbox action for specific activity types. * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. + * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity ); + \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity, $visibility ); } $response = \rest_ensure_response( diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index f1ef48488..1314e3046 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -146,7 +146,8 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { - $recipients = $this->determine_recipients( $data ); + $recipients = $this->determine_local_recipients( $data ); + $visibility = $this->determine_visibility( $data ); foreach ( $recipients as $recipient ) { // Check user-specific blocks for this recipient. @@ -166,21 +167,23 @@ public function create_item( $request ) { /** * ActivityPub inbox action. * - * @param array $data The data array. - * @param int $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. + * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity ); + \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity, $visibility ); /** * ActivityPub inbox action for specific activity types. * - * @param array $data The data array. - * @param int $user_id The user ID. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. + * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); + \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity, $visibility ); } } diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php index ecd5aed1f..cfdb27793 100644 --- a/includes/rest/trait-audience.php +++ b/includes/rest/trait-audience.php @@ -27,7 +27,7 @@ trait Audience { * * @return array An array of user IDs who are the recipients of the activity. */ - public function determine_recipients( $activity ) { + public function determine_local_recipients( $activity ) { $recipients = extract_recipients_from_activity( $activity ); $user_ids = array(); @@ -53,22 +53,26 @@ public function determine_recipients( $activity ) { return $user_ids; } + /** + * Determine the visibility of the activity based on its recipients. + * + * @param array $activity The activity data. + * + * @return string The visibility level: 'public', 'private', or 'direct'. + */ public function determine_visibility( $activity ) { - $recipients = extract_recipients_from_activity_property( 'to', $activity ); - $visibility = 'private'; - - foreach ( $recipients as $recipient ) { - if ( is_same_domain( $recipient ) ) { - $visibility = 'direct'; - break; - } + // Check 'to' field for public visibility. + $to = extract_recipients_from_activity_property( 'to', $activity ); + if ( ! empty( array_intersect( $to, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } - if ( \in_array( $recipient, array( 'https://www.w3.org/ns/activitystreams#Public', 'as:Public' ), true ) ) { - $visibility = 'public'; - break; - } + // Check 'cc' field for quiet public visibility. + $cc = extract_recipients_from_activity_property( 'cc', $activity ); + if ( ! empty( array_intersect( $cc, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC; } - return $visibility; + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; } } From 93cf6779e57c11217ea905b4f9a86a5322b1d708 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 10:46:56 +0200 Subject: [PATCH 08/30] Refactor handle_create to use visibility parameter Updated the handle_create method in Create handler to accept a visibility parameter instead of checking public status via is_activity_public. Adjusted related tests to pass the appropriate visibility constant, improving clarity and control over activity visibility handling. --- includes/handler/class-create.php | 21 +++++--------------- tests/includes/handler/class-test-create.php | 4 ++-- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/includes/handler/class-create.php b/includes/handler/class-create.php index 8886a857c..fba791ba5 100644 --- a/includes/handler/class-create.php +++ b/includes/handler/class-create.php @@ -9,7 +9,6 @@ use Activitypub\Collection\Interactions; -use function Activitypub\is_activity_public; use function Activitypub\is_activity_reply; use function Activitypub\is_self_ping; use function Activitypub\object_id_to_comment; @@ -22,19 +21,8 @@ class Create { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( - 'activitypub_inbox_create', - array( self::class, 'handle_create' ), - 10, - 3 - ); - - \add_filter( - 'activitypub_validate_object', - array( self::class, 'validate_object' ), - 10, - 3 - ); + \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 4 ); + \add_filter( 'activitypub_validate_object', array( self::class, 'validate_object' ), 10, 3 ); } /** @@ -43,11 +31,12 @@ public static function init() { * @param array $activity The activity-object. * @param int $user_id The id of the local blog-user. * @param \Activitypub\Activity\Activity $activity_object Optional. The activity object. Default null. + * @param string $visibility The visibility of the activity. */ - public static function handle_create( $activity, $user_id, $activity_object = null ) { + public static function handle_create( $activity, $user_id, $activity_object = null, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ) { // Check if Activity is public or not. if ( - ! is_activity_public( $activity ) || + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility || ! is_activity_reply( $activity ) ) { return; diff --git a/tests/includes/handler/class-test-create.php b/tests/includes/handler/class-test-create.php index b3ebd650b..b29abd793 100644 --- a/tests/includes/handler/class-test-create.php +++ b/tests/includes/handler/class-test-create.php @@ -118,7 +118,7 @@ public function create_test_object( $id = 'https://example.com/123' ) { public function test_handle_create_non_public_rejected() { $object = $this->create_test_object(); $object['cc'] = array(); - $converted = Create::handle_create( $object, $this->user_id ); + $converted = Create::handle_create( $object, $this->user_id, null, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); $this->assertNull( $converted ); } @@ -129,7 +129,7 @@ public function test_handle_create_non_public_rejected() { */ public function test_handle_create_public_accepted() { $object = $this->create_test_object(); - Create::handle_create( $object, $this->user_id ); + Create::handle_create( $object, $this->user_id, null, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); $args = array( 'type' => 'comment', From 5e823d1e796fac0094205e9bf452d56ed26f0426 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 10:50:47 +0200 Subject: [PATCH 09/30] Set default private visibility for certain activity types Activities of types 'Follow', 'Accept', 'Reject', 'Undo', and 'Delete' now default to private visibility. This ensures these sensitive actions are not exposed publicly unless explicitly specified. --- includes/rest/trait-audience.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php index cfdb27793..37d85b752 100644 --- a/includes/rest/trait-audience.php +++ b/includes/rest/trait-audience.php @@ -61,6 +61,11 @@ public function determine_local_recipients( $activity ) { * @return string The visibility level: 'public', 'private', or 'direct'. */ public function determine_visibility( $activity ) { + // Set default visibility for specific activity types. + if ( in_array( $activity['type'], array( 'Follow', 'Accept', 'Reject', 'Undo', 'Delete' ), true ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; + } + // Check 'to' field for public visibility. $to = extract_recipients_from_activity_property( 'to', $activity ); if ( ! empty( array_intersect( $to, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { From 17fb6f9f06a8f87366547e1ef78fde0a443d72cc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 10:51:46 +0200 Subject: [PATCH 10/30] Reorder activity types in visibility check The array of activity types in the determine_visibility method was reordered for consistency. No functional changes were made. --- includes/rest/trait-audience.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php index 37d85b752..ecb9bda68 100644 --- a/includes/rest/trait-audience.php +++ b/includes/rest/trait-audience.php @@ -62,7 +62,7 @@ public function determine_local_recipients( $activity ) { */ public function determine_visibility( $activity ) { // Set default visibility for specific activity types. - if ( in_array( $activity['type'], array( 'Follow', 'Accept', 'Reject', 'Undo', 'Delete' ), true ) ) { + if ( in_array( $activity['type'], array( 'Accept', 'Delete', 'Follow', 'Reject', 'Undo' ), true ) ) { return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; } From 2e621070920cbadfbb4ac77d1c843cbf36d5ce0a Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Fri, 19 Sep 2025 11:19:46 +0200 Subject: [PATCH 11/30] Add changelog --- .github/changelog/2210-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2210-from-description diff --git a/.github/changelog/2210-from-description b/.github/changelog/2210-from-description new file mode 100644 index 000000000..c58d319b0 --- /dev/null +++ b/.github/changelog/2210-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Improved recipient handling for clarity and added better inbox support. From be0b67fbd1647f79b65d0376998d0466a88099e3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 11:40:00 +0200 Subject: [PATCH 12/30] Add unit tests for Audience trait in REST API Introduces a new test class covering the Audience trait's methods, including visibility determination and local recipient extraction. Tests various scenarios for public, private, and malformed activities to ensure correct behavior. --- .../rest/class-test-trait-audience.php | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 tests/includes/rest/class-test-trait-audience.php diff --git a/tests/includes/rest/class-test-trait-audience.php b/tests/includes/rest/class-test-trait-audience.php new file mode 100644 index 000000000..2ab5a4bd9 --- /dev/null +++ b/tests/includes/rest/class-test-trait-audience.php @@ -0,0 +1,271 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$actor_url = get_rest_url( null, sprintf( '/activitypub/1.0/actors/%d', self::$user_id ) ); + + // Grant the activitypub capability to the user. + $user = new \WP_User( self::$user_id ); + $user->add_cap( 'activitypub' ); + } + + /** + * Set up the test. + */ + public function set_up() { + parent::set_up(); + + // Create a test class that uses the Audience trait. + $this->audience_test_class = new class() { + use Audience; + }; + } + + /** + * Data provider for visibility determination tests. + * + * @return array + */ + public function visibility_data_provider() { + return array( + // Public visibility - 'to' contains public identifier. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'description' => 'Public visibility via to field', + ), + // Quiet public visibility - 'cc' contains public identifier. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://example.com/user/123' ), + 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + 'description' => 'Quiet public visibility via cc field', + ), + // Private visibility - no public identifiers. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://example.com/user/123' ), + 'cc' => array( 'https://example.com/user/456' ), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Private visibility', + ), + // Special activity types always private - Accept. + array( + 'activity' => array( + 'type' => 'Accept', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Accept activity always private', + ), + // Special activity types always private - Delete. + array( + 'activity' => array( + 'type' => 'Delete', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Delete activity always private', + ), + // Special activity types always private - Follow. + array( + 'activity' => array( + 'type' => 'Follow', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Follow activity always private', + ), + // Alternative public identifier - as:Public. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'as:Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'description' => 'Public visibility via as:Public identifier', + ), + // Empty activity. + array( + 'activity' => array( + 'type' => 'Create', + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Empty activity defaults to private', + ), + ); + } + + /** + * Test determine_visibility method. + * + * @dataProvider visibility_data_provider + * @covers ::determine_visibility + * + * @param array $activity The activity data. + * @param string $expected Expected visibility level. + * @param string $description Test description. + */ + public function test_determine_visibility( $activity, $expected, $description ) { + $result = $this->audience_test_class->determine_visibility( $activity ); + $this->assertSame( $expected, $result, $description ); + } + + /** + * Test determine_local_recipients method with no recipients. + * + * @covers ::determine_local_recipients + */ + public function test_determine_local_recipients_no_recipients() { + $activity = array( + 'type' => 'Create', + ); + + $result = $this->audience_test_class->determine_local_recipients( $activity ); + $this->assertEmpty( $result, 'Should return empty array when no recipients' ); + } + + /** + * Test determine_local_recipients with external recipients only. + * + * @covers ::determine_local_recipients + */ + public function test_determine_local_recipients_external_only() { + $activity = array( + 'type' => 'Create', + 'to' => array( 'https://external.example.com/user/123' ), + 'cc' => array( 'https://another.example.com/user/456' ), + ); + + $result = $this->audience_test_class->determine_local_recipients( $activity ); + $this->assertEmpty( $result, 'Should return empty array for external recipients only' ); + } + + /** + * Test determine_local_recipients with actual local actor. + * + * @covers ::determine_local_recipients + */ + public function test_determine_local_recipients_with_local_actor() { + // Get the actual actor ID for the user. + $actor = Actors::get_by_id( self::$user_id ); + $actor_id = $actor->get_id(); + + $activity = array( + 'type' => 'Create', + 'to' => array( $actor_id ), + 'cc' => array( 'https://external.example.com/user/123' ), + ); + + $result = $this->audience_test_class->determine_local_recipients( $activity ); + $this->assertContains( self::$user_id, $result, 'Should contain local user ID' ); + $this->assertCount( 1, $result, 'Should contain exactly one recipient' ); + } + + /** + * Test determine_visibility with string recipients instead of arrays. + * + * @covers ::determine_visibility + */ + public function test_determine_visibility_with_string_recipients() { + $activity = array( + 'type' => 'Create', + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + 'cc' => 'https://example.com/user/123', + ); + + $result = $this->audience_test_class->determine_visibility( $activity ); + $this->assertSame( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should handle string recipients' ); + } + + /** + * Test determine_local_recipients handles malformed actor URLs. + * + * @covers ::determine_local_recipients + */ + public function test_determine_local_recipients_with_malformed_urls() { + $activity = array( + 'type' => 'Create', + 'to' => array( + 'not-a-valid-url', + get_home_url() . '/invalid-actor-path', + ), + 'cc' => array(), + ); + + $result = $this->audience_test_class->determine_local_recipients( $activity ); + $this->assertEmpty( $result, 'Should handle malformed URLs gracefully' ); + } + + /** + * Test determine_visibility with minimal activity data. + * + * @covers ::determine_visibility + */ + public function test_determine_visibility_with_minimal_activity() { + $activity = array( + 'type' => 'Create', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ); + + $result = $this->audience_test_class->determine_visibility( $activity ); + $this->assertSame( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should work with minimal activity data' ); + } +} From 23b92eab08f1ad172f8555b07d9963f32523f447 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 Sep 2025 16:45:57 +0200 Subject: [PATCH 13/30] Use global namespace for array functions Prefixed array_merge and array_unique with backslashes in extract_recipients_from_activity to ensure use of PHP's global functions and avoid potential namespace issues. --- includes/functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 5085ee364..e8b7900b2 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -525,10 +525,10 @@ function extract_recipients_from_activity( $data ) { $recipient_items = array(); foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { - $recipient_items = array_merge( $recipient_items, extract_recipients_from_activity_property( $i, $data ) ); + $recipient_items = \array_merge( $recipient_items, extract_recipients_from_activity_property( $i, $data ) ); } - return array_unique( $recipient_items ); + return \array_unique( $recipient_items ); } /** From 4ed4a4aa43b542c0ec287922f6bc64c30cd993e1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 20 Sep 2025 00:53:12 +0200 Subject: [PATCH 14/30] Update object_to_uri to return empty string on failure Changed object_to_uri to return an empty string instead of false when a URI is not found. Updated the function's docblock and logic to reflect this behavior for consistency and clarity. --- includes/functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index e8b7900b2..db324a4f4 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -688,7 +688,7 @@ function url_to_commentid( $url ) { * * @param array|string $data The ActivityPub object. * - * @return string|false The URI of the ActivityPub object or false if not found. + * @return string The URI of the ActivityPub object or empty string if not found. */ function object_to_uri( $data ) { // Check whether it is already simple. @@ -725,10 +725,10 @@ function object_to_uri( $data ) { $data = object_to_uri( $data['url'] ); break; case 'Link': - $data = $data['href'] ?? false; + $data = $data['href'] ?? ''; break; default: - $data = $data['id'] ?? $data['url'] ?? false; + $data = $data['id'] ?? $data['url'] ?? ''; break; } From 8a731d848aadbf8b6c165bd41b34cf9ae39d5de4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 20 Sep 2025 01:12:34 +0200 Subject: [PATCH 15/30] Move recipient extraction logic to Inbox_Controller Refactored recipient extraction from the Audience trait into a new private get_recipients method in Inbox_Controller. Updated all usages and tests to reflect this change, and removed related tests and code from the Audience trait and its test class. This improves encapsulation and testability of recipient handling in the REST inbox controller. --- includes/rest/class-inbox-controller.php | 40 ++++++- includes/rest/trait-audience.php | 38 ------ .../rest/class-test-inbox-controller.php | 108 ++++++++++++++++++ .../rest/class-test-trait-audience.php | 87 -------------- 4 files changed, 147 insertions(+), 126 deletions(-) diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 1314e3046..d235defe4 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -8,8 +8,13 @@ namespace Activitypub\Rest; use Activitypub\Activity\Activity; +use Activitypub\Collection\Actors; use Activitypub\Moderation; +use function Activitypub\extract_recipients_from_activity; +use function Activitypub\is_same_domain; +use function Activitypub\user_can_activitypub; + /** * Inbox_Controller class. * @@ -146,7 +151,7 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { - $recipients = $this->determine_local_recipients( $data ); + $recipients = $this->get_recipients( $data ); $visibility = $this->determine_visibility( $data ); foreach ( $recipients as $recipient ) { @@ -274,4 +279,37 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $this->schema ); } + + /** + * Extract recipients from the given Activity. + * + * @param array $activity The activity data. + * + * @return array An array of user IDs who are the recipients of the activity. + */ + private function get_recipients( $activity ) { + $recipients = extract_recipients_from_activity( $activity ); + $user_ids = array(); + + foreach ( $recipients as $recipient ) { + + if ( ! is_same_domain( $recipient ) ) { + continue; + } + + $user_id = Actors::get_id_by_resource( $recipient ); + + if ( \is_wp_error( $user_id ) ) { + continue; + } + + if ( ! user_can_activitypub( $user_id ) ) { + continue; + } + + $user_ids[] = $user_id; + } + + return $user_ids; + } } diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php index ecb9bda68..372c1b819 100644 --- a/includes/rest/trait-audience.php +++ b/includes/rest/trait-audience.php @@ -7,12 +7,7 @@ namespace Activitypub\Rest; -use Activitypub\Collection\Actors; - -use function Activitypub\extract_recipients_from_activity; use function Activitypub\extract_recipients_from_activity_property; -use function Activitypub\is_same_domain; -use function Activitypub\user_can_activitypub; /** * Audience Trait. @@ -20,39 +15,6 @@ * Provides methods for handling ActivityPub audience and recipient extraction. */ trait Audience { - /** - * Extract recipients from the given Activity. - * - * @param array $activity The activity data. - * - * @return array An array of user IDs who are the recipients of the activity. - */ - public function determine_local_recipients( $activity ) { - $recipients = extract_recipients_from_activity( $activity ); - $user_ids = array(); - - foreach ( $recipients as $recipient ) { - - if ( ! is_same_domain( $recipient ) ) { - continue; - } - - $user_id = Actors::get_id_by_resource( $recipient ); - - if ( \is_wp_error( $user_id ) ) { - continue; - } - - if ( ! user_can_activitypub( $user_id ) ) { - continue; - } - - $user_ids[] = $user_id; - } - - return $user_ids; - } - /** * Determine the visibility of the activity based on its recipients. * diff --git a/tests/includes/rest/class-test-inbox-controller.php b/tests/includes/rest/class-test-inbox-controller.php index b1d98d607..3470cc870 100644 --- a/tests/includes/rest/class-test-inbox-controller.php +++ b/tests/includes/rest/class-test-inbox-controller.php @@ -7,6 +7,8 @@ namespace Activitypub\Tests\Rest; +use Activitypub\Collection\Actors; + /** * Test class for Activitypub Rest Inbox. * @@ -21,6 +23,13 @@ class Test_Inbox_Controller extends \Activitypub\Tests\Test_REST_Controller_Test */ protected static $user_id; + /** + * Inbox Controller instance for testing. + * + * @var \Activitypub\Rest\Inbox_Controller + */ + private $inbox_controller; + /** * Create fake data before tests run. */ @@ -28,6 +37,15 @@ public static function set_up_before_class() { self::$user_id = self::factory()->user->create( array( 'role' => 'author' ) ); } + /** + * Set up the test. + */ + public function set_up() { + parent::set_up(); + + $this->inbox_controller = new \Activitypub\Rest\Inbox_Controller(); + } + /** * Delete fake data after tests run. */ @@ -434,4 +452,94 @@ public function test_create_item_with_invalid_request() { \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); } + + /** + * Test get_recipients method with no recipients. + * + * @covers ::get_recipients + */ + public function test_get_recipients_no_recipients() { + $activity = array( + 'type' => 'Create', + ); + + // Use reflection to test the private method. + $reflection = new \ReflectionClass( $this->inbox_controller ); + $method = $reflection->getMethod( 'get_recipients' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->inbox_controller, $activity ); + $this->assertEmpty( $result, 'Should return empty array when no recipients' ); + } + + /** + * Test get_recipients with external recipients only. + * + * @covers ::get_recipients + */ + public function test_get_recipients_external_only() { + $activity = array( + 'type' => 'Create', + 'to' => array( 'https://external.example.com/user/123' ), + 'cc' => array( 'https://another.example.com/user/456' ), + ); + + // Use reflection to test the private method. + $reflection = new \ReflectionClass( $this->inbox_controller ); + $method = $reflection->getMethod( 'get_recipients' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->inbox_controller, $activity ); + $this->assertEmpty( $result, 'Should return empty array for external recipients only' ); + } + + /** + * Test get_recipients with actual local actor. + * + * @covers ::get_recipients + */ + public function test_get_recipients_with_local_actor() { + // Get the actual actor ID for the user. + $actor = Actors::get_by_id( self::$user_id ); + $actor_id = $actor->get_id(); + + $activity = array( + 'type' => 'Create', + 'to' => array( $actor_id ), + 'cc' => array( 'https://external.example.com/user/123' ), + ); + + // Use reflection to test the private method. + $reflection = new \ReflectionClass( $this->inbox_controller ); + $method = $reflection->getMethod( 'get_recipients' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->inbox_controller, $activity ); + $this->assertContains( self::$user_id, $result, 'Should contain local user ID' ); + $this->assertCount( 1, $result, 'Should contain exactly one recipient' ); + } + + /** + * Test get_recipients handles malformed actor URLs. + * + * @covers ::get_recipients + */ + public function test_get_recipients_with_malformed_urls() { + $activity = array( + 'type' => 'Create', + 'to' => array( + 'not-a-valid-url', + get_home_url() . '/invalid-actor-path', + ), + 'cc' => array(), + ); + + // Use reflection to test the private method. + $reflection = new \ReflectionClass( $this->inbox_controller ); + $method = $reflection->getMethod( 'get_recipients' ); + $method->setAccessible( true ); + + $result = $method->invoke( $this->inbox_controller, $activity ); + $this->assertEmpty( $result, 'Should handle malformed URLs gracefully' ); + } } diff --git a/tests/includes/rest/class-test-trait-audience.php b/tests/includes/rest/class-test-trait-audience.php index 2ab5a4bd9..97934bc85 100644 --- a/tests/includes/rest/class-test-trait-audience.php +++ b/tests/includes/rest/class-test-trait-audience.php @@ -7,7 +7,6 @@ namespace Activitypub\Tests\Rest; -use Activitypub\Collection\Actors; use Activitypub\Rest\Audience; /** @@ -167,92 +166,6 @@ public function test_determine_visibility( $activity, $expected, $description ) $this->assertSame( $expected, $result, $description ); } - /** - * Test determine_local_recipients method with no recipients. - * - * @covers ::determine_local_recipients - */ - public function test_determine_local_recipients_no_recipients() { - $activity = array( - 'type' => 'Create', - ); - - $result = $this->audience_test_class->determine_local_recipients( $activity ); - $this->assertEmpty( $result, 'Should return empty array when no recipients' ); - } - - /** - * Test determine_local_recipients with external recipients only. - * - * @covers ::determine_local_recipients - */ - public function test_determine_local_recipients_external_only() { - $activity = array( - 'type' => 'Create', - 'to' => array( 'https://external.example.com/user/123' ), - 'cc' => array( 'https://another.example.com/user/456' ), - ); - - $result = $this->audience_test_class->determine_local_recipients( $activity ); - $this->assertEmpty( $result, 'Should return empty array for external recipients only' ); - } - - /** - * Test determine_local_recipients with actual local actor. - * - * @covers ::determine_local_recipients - */ - public function test_determine_local_recipients_with_local_actor() { - // Get the actual actor ID for the user. - $actor = Actors::get_by_id( self::$user_id ); - $actor_id = $actor->get_id(); - - $activity = array( - 'type' => 'Create', - 'to' => array( $actor_id ), - 'cc' => array( 'https://external.example.com/user/123' ), - ); - - $result = $this->audience_test_class->determine_local_recipients( $activity ); - $this->assertContains( self::$user_id, $result, 'Should contain local user ID' ); - $this->assertCount( 1, $result, 'Should contain exactly one recipient' ); - } - - /** - * Test determine_visibility with string recipients instead of arrays. - * - * @covers ::determine_visibility - */ - public function test_determine_visibility_with_string_recipients() { - $activity = array( - 'type' => 'Create', - 'to' => 'https://www.w3.org/ns/activitystreams#Public', - 'cc' => 'https://example.com/user/123', - ); - - $result = $this->audience_test_class->determine_visibility( $activity ); - $this->assertSame( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should handle string recipients' ); - } - - /** - * Test determine_local_recipients handles malformed actor URLs. - * - * @covers ::determine_local_recipients - */ - public function test_determine_local_recipients_with_malformed_urls() { - $activity = array( - 'type' => 'Create', - 'to' => array( - 'not-a-valid-url', - get_home_url() . '/invalid-actor-path', - ), - 'cc' => array(), - ); - - $result = $this->audience_test_class->determine_local_recipients( $activity ); - $this->assertEmpty( $result, 'Should handle malformed URLs gracefully' ); - } - /** * Test determine_visibility with minimal activity data. * From eb12652940c19943e4060df18ed90cc094a2db3a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 14:57:19 +0200 Subject: [PATCH 16/30] Refactor activity visibility logic and remove Audience trait Moved the activity visibility determination logic from the Audience trait to a new get_activity_visibility() function in functions.php. Updated controllers to use the new function and removed the now-unnecessary trait and its tests. Added comprehensive tests for get_activity_visibility in the functions test suite. --- includes/functions.php | 25 +++ .../rest/class-actors-inbox-controller.php | 4 +- includes/rest/class-inbox-controller.php | 5 +- includes/rest/trait-audience.php | 45 ----- tests/includes/class-test-functions.php | 120 ++++++++++++ .../rest/class-test-trait-audience.php | 184 ------------------ 6 files changed, 149 insertions(+), 234 deletions(-) delete mode 100644 includes/rest/trait-audience.php delete mode 100644 tests/includes/rest/class-test-trait-audience.php diff --git a/includes/functions.php b/includes/functions.php index db324a4f4..d7e5fb060 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -553,6 +553,31 @@ function extract_recipients_from_activity_property( $property, $data ) { return \array_unique( \array_filter( $recipients ) ); } +/** + * Determine the visibility of the activity based on its recipients. + * + * @param array $activity The activity data. + * + * @return string The visibility level: 'public', 'private', or 'direct'. + */ +function get_activity_visibility( $activity ) { + // Set default visibility for specific activity types. + if ( in_array( $activity['type'], array( 'Accept', 'Delete', 'Follow', 'Reject', 'Undo' ), true ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; + } + // Check 'to' field for public visibility. + $to = extract_recipients_from_activity_property( 'to', $activity ); + if ( ! empty( array_intersect( $to, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } + // Check 'cc' field for quiet public visibility. + $cc = extract_recipients_from_activity_property( 'cc', $activity ); + if ( ! empty( array_intersect( $cc, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC; + } + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; +} + /** * Check if passed Activity is Public. * diff --git a/includes/rest/class-actors-inbox-controller.php b/includes/rest/class-actors-inbox-controller.php index 764650c55..c4f494af0 100644 --- a/includes/rest/class-actors-inbox-controller.php +++ b/includes/rest/class-actors-inbox-controller.php @@ -10,6 +10,7 @@ use Activitypub\Activity\Activity; use Activitypub\Moderation; +use function Activitypub\get_activity_visibility; use function Activitypub\get_context; use function Activitypub\get_masked_wp_version; use function Activitypub\get_rest_url_by_path; @@ -23,7 +24,6 @@ */ class Actors_Inbox_Controller extends Actors_Controller { use Collection; - use Audience; /** * Register routes. @@ -178,7 +178,7 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity ); } else { - $visibility = $this->determine_visibility( $data ); + $visibility = get_activity_visibility( $data ); /** * ActivityPub inbox action. diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index d235defe4..75c79e88d 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -12,6 +12,7 @@ use Activitypub\Moderation; use function Activitypub\extract_recipients_from_activity; +use function Activitypub\get_activity_visibility; use function Activitypub\is_same_domain; use function Activitypub\user_can_activitypub; @@ -23,8 +24,6 @@ * @see https://www.w3.org/TR/activitypub/#inbox */ class Inbox_Controller extends \WP_REST_Controller { - use Audience; - /** * The namespace of this controller's route. * @@ -152,7 +151,7 @@ public function create_item( $request ) { do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { $recipients = $this->get_recipients( $data ); - $visibility = $this->determine_visibility( $data ); + $visibility = get_activity_visibility( $data ); foreach ( $recipients as $recipient ) { // Check user-specific blocks for this recipient. diff --git a/includes/rest/trait-audience.php b/includes/rest/trait-audience.php deleted file mode 100644 index 372c1b819..000000000 --- a/includes/rest/trait-audience.php +++ /dev/null @@ -1,45 +0,0 @@ -assertSame( array( 'https://example.com/users/alice' ), $actual ); $this->assertCount( 1, $actual, 'Should return unique recipients only.' ); } + + /** + * Data provider for visibility determination tests. + * + * @return array + */ + public function visibility_data_provider() { + return array( + // Public visibility - 'to' contains public identifier. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'description' => 'Public visibility via to field', + ), + // Quiet public visibility - 'cc' contains public identifier. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://example.com/user/123' ), + 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + 'description' => 'Quiet public visibility via cc field', + ), + // Private visibility - no public identifiers. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'https://example.com/user/123' ), + 'cc' => array( 'https://example.com/user/456' ), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Private visibility', + ), + // Special activity types always private - Accept. + array( + 'activity' => array( + 'type' => 'Accept', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Accept activity always private', + ), + // Special activity types always private - Delete. + array( + 'activity' => array( + 'type' => 'Delete', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Delete activity always private', + ), + // Special activity types always private - Follow. + array( + 'activity' => array( + 'type' => 'Follow', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Follow activity always private', + ), + // Alternative public identifier - as:Public. + array( + 'activity' => array( + 'type' => 'Create', + 'to' => array( 'as:Public' ), + 'cc' => array(), + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'description' => 'Public visibility via as:Public identifier', + ), + // Empty activity. + array( + 'activity' => array( + 'type' => 'Create', + ), + 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'description' => 'Empty activity defaults to private', + ), + ); + } + + /** + * Test get_activity_visibility function. + * + * @dataProvider visibility_data_provider + * @covers ::get_activity_visibility + * + * @param array $activity The activity data. + * @param string $expected Expected visibility level. + * @param string $description Test description. + */ + public function test_get_activity_visibility( $activity, $expected, $description ) { + $result = \Activitypub\get_activity_visibility( $activity ); + $this->assertSame( $expected, $result, $description ); + } + + /** + * Test get_activity_visibility with minimal activity data. + * + * @covers ::get_activity_visibility + */ + public function test_get_activity_visibility_with_minimal_activity() { + $activity = array( + 'type' => 'Create', + 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'cc' => array(), + ); + + $result = \Activitypub\get_activity_visibility( $activity ); + $this->assertSame( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should work with minimal activity data' ); + } } diff --git a/tests/includes/rest/class-test-trait-audience.php b/tests/includes/rest/class-test-trait-audience.php deleted file mode 100644 index 97934bc85..000000000 --- a/tests/includes/rest/class-test-trait-audience.php +++ /dev/null @@ -1,184 +0,0 @@ -user->create( array( 'role' => 'administrator' ) ); - self::$actor_url = get_rest_url( null, sprintf( '/activitypub/1.0/actors/%d', self::$user_id ) ); - - // Grant the activitypub capability to the user. - $user = new \WP_User( self::$user_id ); - $user->add_cap( 'activitypub' ); - } - - /** - * Set up the test. - */ - public function set_up() { - parent::set_up(); - - // Create a test class that uses the Audience trait. - $this->audience_test_class = new class() { - use Audience; - }; - } - - /** - * Data provider for visibility determination tests. - * - * @return array - */ - public function visibility_data_provider() { - return array( - // Public visibility - 'to' contains public identifier. - array( - 'activity' => array( - 'type' => 'Create', - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array(), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, - 'description' => 'Public visibility via to field', - ), - // Quiet public visibility - 'cc' contains public identifier. - array( - 'activity' => array( - 'type' => 'Create', - 'to' => array( 'https://example.com/user/123' ), - 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, - 'description' => 'Quiet public visibility via cc field', - ), - // Private visibility - no public identifiers. - array( - 'activity' => array( - 'type' => 'Create', - 'to' => array( 'https://example.com/user/123' ), - 'cc' => array( 'https://example.com/user/456' ), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Private visibility', - ), - // Special activity types always private - Accept. - array( - 'activity' => array( - 'type' => 'Accept', - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array(), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Accept activity always private', - ), - // Special activity types always private - Delete. - array( - 'activity' => array( - 'type' => 'Delete', - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array(), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Delete activity always private', - ), - // Special activity types always private - Follow. - array( - 'activity' => array( - 'type' => 'Follow', - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array(), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Follow activity always private', - ), - // Alternative public identifier - as:Public. - array( - 'activity' => array( - 'type' => 'Create', - 'to' => array( 'as:Public' ), - 'cc' => array(), - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, - 'description' => 'Public visibility via as:Public identifier', - ), - // Empty activity. - array( - 'activity' => array( - 'type' => 'Create', - ), - 'expected' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, - 'description' => 'Empty activity defaults to private', - ), - ); - } - - /** - * Test determine_visibility method. - * - * @dataProvider visibility_data_provider - * @covers ::determine_visibility - * - * @param array $activity The activity data. - * @param string $expected Expected visibility level. - * @param string $description Test description. - */ - public function test_determine_visibility( $activity, $expected, $description ) { - $result = $this->audience_test_class->determine_visibility( $activity ); - $this->assertSame( $expected, $result, $description ); - } - - /** - * Test determine_visibility with minimal activity data. - * - * @covers ::determine_visibility - */ - public function test_determine_visibility_with_minimal_activity() { - $activity = array( - 'type' => 'Create', - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array(), - ); - - $result = $this->audience_test_class->determine_visibility( $activity ); - $this->assertSame( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should work with minimal activity data' ); - } -} From 698f36de55a625eea1651736b87144b914915380 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 15:43:34 +0200 Subject: [PATCH 17/30] Rename get_recipients to get_local_recipients Refactors the method name from get_recipients to get_local_recipients in Inbox_Controller for clarity. Updates all internal references to use the new method name. --- includes/rest/class-inbox-controller.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 75c79e88d..6757d17e6 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -150,7 +150,7 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { - $recipients = $this->get_recipients( $data ); + $recipients = $this->get_local_recipients( $data ); $visibility = get_activity_visibility( $data ); foreach ( $recipients as $recipient ) { @@ -286,7 +286,7 @@ public function get_item_schema() { * * @return array An array of user IDs who are the recipients of the activity. */ - private function get_recipients( $activity ) { + private function get_local_recipients( $activity ) { $recipients = extract_recipients_from_activity( $activity ); $user_ids = array(); From 2d99fbed4a9634254156a21f222809a07f8275ed Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 15:46:11 +0200 Subject: [PATCH 18/30] Rename test methods to use get_local_recipients Updated test method names and related references from get_recipients to get_local_recipients in Test_Inbox_Controller. This aligns the tests with the updated method name and ensures accurate coverage annotations. --- .../rest/class-test-inbox-controller.php | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/includes/rest/class-test-inbox-controller.php b/tests/includes/rest/class-test-inbox-controller.php index 3470cc870..bcfbfad1b 100644 --- a/tests/includes/rest/class-test-inbox-controller.php +++ b/tests/includes/rest/class-test-inbox-controller.php @@ -454,18 +454,18 @@ public function test_create_item_with_invalid_request() { } /** - * Test get_recipients method with no recipients. + * Test get_local_recipients method with no recipients. * - * @covers ::get_recipients + * @covers ::get_local_recipients */ - public function test_get_recipients_no_recipients() { + public function test_get_local_recipients_no_recipients() { $activity = array( 'type' => 'Create', ); // Use reflection to test the private method. $reflection = new \ReflectionClass( $this->inbox_controller ); - $method = $reflection->getMethod( 'get_recipients' ); + $method = $reflection->getMethod( 'get_local_recipients' ); $method->setAccessible( true ); $result = $method->invoke( $this->inbox_controller, $activity ); @@ -473,11 +473,11 @@ public function test_get_recipients_no_recipients() { } /** - * Test get_recipients with external recipients only. + * Test get_local_recipients with external recipients only. * - * @covers ::get_recipients + * @covers ::get_local_recipients */ - public function test_get_recipients_external_only() { + public function test_get_local_recipients_external_only() { $activity = array( 'type' => 'Create', 'to' => array( 'https://external.example.com/user/123' ), @@ -486,7 +486,7 @@ public function test_get_recipients_external_only() { // Use reflection to test the private method. $reflection = new \ReflectionClass( $this->inbox_controller ); - $method = $reflection->getMethod( 'get_recipients' ); + $method = $reflection->getMethod( 'get_local_recipients' ); $method->setAccessible( true ); $result = $method->invoke( $this->inbox_controller, $activity ); @@ -494,11 +494,11 @@ public function test_get_recipients_external_only() { } /** - * Test get_recipients with actual local actor. + * Test get_local_recipients with actual local actor. * - * @covers ::get_recipients + * @covers ::get_local_recipients */ - public function test_get_recipients_with_local_actor() { + public function test_get_local_recipients_with_local_actor() { // Get the actual actor ID for the user. $actor = Actors::get_by_id( self::$user_id ); $actor_id = $actor->get_id(); @@ -511,7 +511,7 @@ public function test_get_recipients_with_local_actor() { // Use reflection to test the private method. $reflection = new \ReflectionClass( $this->inbox_controller ); - $method = $reflection->getMethod( 'get_recipients' ); + $method = $reflection->getMethod( 'get_local_recipients' ); $method->setAccessible( true ); $result = $method->invoke( $this->inbox_controller, $activity ); @@ -520,11 +520,11 @@ public function test_get_recipients_with_local_actor() { } /** - * Test get_recipients handles malformed actor URLs. + * Test get_local_recipients handles malformed actor URLs. * - * @covers ::get_recipients + * @covers ::get_local_recipients */ - public function test_get_recipients_with_malformed_urls() { + public function test_get_local_recipients_with_malformed_urls() { $activity = array( 'type' => 'Create', 'to' => array( @@ -536,7 +536,7 @@ public function test_get_recipients_with_malformed_urls() { // Use reflection to test the private method. $reflection = new \ReflectionClass( $this->inbox_controller ); - $method = $reflection->getMethod( 'get_recipients' ); + $method = $reflection->getMethod( 'get_local_recipients' ); $method->setAccessible( true ); $result = $method->invoke( $this->inbox_controller, $activity ); From 7838f06fffe6211f9497bc32c2bc70f38c91031f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 16:02:09 +0200 Subject: [PATCH 19/30] Prevent duplicate checkstyle annotations in PHPUnit workflow Adds logic to track processed file:line combinations and avoid generating duplicate checkstyle annotations for the same @covers warning in the GitHub Actions PHPUnit workflow. --- .github/workflows/phpunit.yml | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 4a4241b2f..6189004bc 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -115,22 +115,25 @@ jobs: EOF # Check for PHPUnit warnings and process them - if grep -q "WARNINGS!\|There was.*warning" coverage-output.log; then + if grep -q "WARNINGS!" coverage-output.log; then echo "Processing coverage warnings for checkstyle report..." # Show warnings section for debugging echo "--- Warnings Found ---" - sed -n '/There was.*warning/,/WARNINGS!/p' coverage-output.log | head -10 + sed -n '/There w.*warning/,/WARNINGS!/p' coverage-output.log | head -10 echo "--- End Warnings ---" # Extract warnings to temporary file to avoid subshell issues - sed -n '/There was.*warning/,/WARNINGS!/p' coverage-output.log > temp-warnings.txt + sed -n '/There w.*warning/,/WARNINGS!/p' coverage-output.log > temp-warnings.txt + + # Track processed file:line combinations to avoid duplicates + declare -A processed_annotations # Process each line while IFS= read -r line; do echo "Processing line: $line" # Look for @covers warnings - if [[ "$line" =~ \"@covers.*\"\ is\ invalid ]]; then + if [[ "$line" =~ \"@covers[^\"]*\"\ is\ invalid ]]; then echo "Found @covers warning line: $line" # Extract the @covers target - everything between quotes if [[ "$line" =~ \"@covers\ ([^\"]+)\" ]]; then @@ -146,13 +149,25 @@ jobs: echo "Found test file: $TEST_FILE" if LINE_NUM=$(grep -n "@covers.*$METHOD_NAME" "$TEST_FILE" | head -1 | cut -d: -f1); then echo "Found line number: $LINE_NUM" - # Escape XML characters and add checkstyle entry - MESSAGE=$(echo "Invalid @covers annotation: $COVERS_TARGET" | sed 's/&/\&/g; s//\>/g; s/"/\"/g; s/'"'"'/\'/g') - cat >> coverage-warnings.xml << EOF + + # Create unique key for this file:line combination + FILE_LINE_KEY="$TEST_FILE:$LINE_NUM" + + # Only process if we haven't seen this file:line before + if [[ -z "${processed_annotations[$FILE_LINE_KEY]}" ]]; then + echo "Adding annotation for $FILE_LINE_KEY" + processed_annotations[$FILE_LINE_KEY]=1 + + # Escape XML characters and add checkstyle entry + MESSAGE=$(echo "Invalid @covers annotation: $COVERS_TARGET" | sed 's/&/\&/g; s//\>/g; s/"/\"/g; s/'"'"'/\'/g') + cat >> coverage-warnings.xml << EOF EOF + else + echo "Skipping duplicate annotation for $FILE_LINE_KEY" + fi fi fi fi From 9c4af521a73cc71834885e067f2f4158a2a84473 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 16:08:32 +0200 Subject: [PATCH 20/30] Remove redundant @covers annotations in tests Eliminated unnecessary @covers tags from get_activity_visibility test methods to clean up test documentation. --- tests/includes/class-test-functions.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index 62a6b4429..e3fc6fad0 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -1334,7 +1334,6 @@ public function visibility_data_provider() { * Test get_activity_visibility function. * * @dataProvider visibility_data_provider - * @covers ::get_activity_visibility * * @param array $activity The activity data. * @param string $expected Expected visibility level. @@ -1347,8 +1346,6 @@ public function test_get_activity_visibility( $activity, $expected, $description /** * Test get_activity_visibility with minimal activity data. - * - * @covers ::get_activity_visibility */ public function test_get_activity_visibility_with_minimal_activity() { $activity = array( From 1c6a2744258a91eef55d282cea0d0ac48b4ad989 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 16:58:32 +0200 Subject: [PATCH 21/30] Refactor object_to_uri to remove default empty string Updated the object_to_uri function to no longer return an empty string by default. The function now directly returns the relevant URI fields without fallback to an empty string, ensuring stricter type consistency. --- includes/functions.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index d7e5fb060..615dbb3f2 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -713,7 +713,7 @@ function url_to_commentid( $url ) { * * @param array|string $data The ActivityPub object. * - * @return string The URI of the ActivityPub object or empty string if not found. + * @return string The URI of the ActivityPub object. */ function object_to_uri( $data ) { // Check whether it is already simple. @@ -750,10 +750,10 @@ function object_to_uri( $data ) { $data = object_to_uri( $data['url'] ); break; case 'Link': - $data = $data['href'] ?? ''; + $data = $data['href']; break; default: - $data = $data['id'] ?? $data['url'] ?? ''; + $data = $data['id'] ?? $data['url']; break; } From 0154560c1ae6149130da560a5fa6e07f6cc3d7d5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 Sep 2025 17:22:32 +0200 Subject: [PATCH 22/30] Fix object_to_uri to require 'id' and update related tests Updated object_to_uri to only use the 'id' field, removing fallback to 'url'. Adjusted tests to expect this behavior and improved error handling in moderation tests for missing 'id' keys. --- includes/functions.php | 2 +- tests/includes/class-test-functions.php | 9 ++++++--- tests/includes/class-test-moderation.php | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 615dbb3f2..fe3af1335 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -753,7 +753,7 @@ function object_to_uri( $data ) { $data = $data['href']; break; default: - $data = $data['id'] ?? $data['url']; + $data = $data['id']; break; } diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index e3fc6fad0..96e37d6da 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -1132,17 +1132,20 @@ public function data_provider_extract_recipients() { 'attribute' => 'to', 'expected' => array(), ), - 'object_without_id' => array( + 'object_with_id' => array( 'data' => array( 'to' => array( array( + 'id' => 'https://example.com/users/kate', 'type' => 'Person', 'name' => 'Kate', - ), // No 'id' key. + ), ), ), 'attribute' => 'to', - 'expected' => array(), // Should be ignored. + 'expected' => array( + 'https://example.com/users/kate', + ), // Should be ignored. ), 'public_recipients' => array( 'data' => array( diff --git a/tests/includes/class-test-moderation.php b/tests/includes/class-test-moderation.php index f3b099269..88356d32e 100644 --- a/tests/includes/class-test-moderation.php +++ b/tests/includes/class-test-moderation.php @@ -446,6 +446,7 @@ public function test_hierarchical_blocking() { * Test edge cases with malformed activity data. * * @covers ::activity_is_blocked + * @throws \Exception Thrown when an error occurs. */ public function test_activity_blocking_edge_cases() { // Test with empty activity. @@ -505,7 +506,23 @@ public function test_activity_blocking_edge_cases() { ), ) ); + + // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler + \set_error_handler( + static function ( $errno, $errstr ) { + throw new \Exception( \esc_html( $errstr ), \esc_html( $errno ) ); + }, + E_NOTICE | E_WARNING + ); + // PHP 7.2 uses "Undefined index", PHP 8+ uses "Undefined array key". + if ( version_compare( PHP_VERSION, '8.0.0', '>=' ) ) { + $this->expectExceptionMessage( 'Undefined array key "id"' ); + } else { + $this->expectExceptionMessage( 'Undefined index: id' ); + } $this->assertFalse( Moderation::activity_is_blocked( $activity ) ); + + \restore_error_handler(); } /** From 6d6d923e3f56604ab2879717d6feb4b23bb83595 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 08:28:05 +0200 Subject: [PATCH 23/30] Refactor activity visibility handling in inbox actions Removes the explicit passing of the $visibility parameter in ActivityPub inbox-related actions and instead determines visibility within the handler as needed. This simplifies the action signatures and centralizes visibility logic. --- includes/handler/class-create.php | 8 ++++---- includes/rest/class-actors-inbox-controller.php | 9 ++------- includes/rest/class-inbox-controller.php | 10 +++------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/includes/handler/class-create.php b/includes/handler/class-create.php index fba791ba5..d2344ea19 100644 --- a/includes/handler/class-create.php +++ b/includes/handler/class-create.php @@ -9,6 +9,7 @@ use Activitypub\Collection\Interactions; +use function Activitypub\get_activity_visibility; use function Activitypub\is_activity_reply; use function Activitypub\is_self_ping; use function Activitypub\object_id_to_comment; @@ -21,7 +22,7 @@ class Create { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 4 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 3 ); \add_filter( 'activitypub_validate_object', array( self::class, 'validate_object' ), 10, 3 ); } @@ -31,12 +32,11 @@ public static function init() { * @param array $activity The activity-object. * @param int $user_id The id of the local blog-user. * @param \Activitypub\Activity\Activity $activity_object Optional. The activity object. Default null. - * @param string $visibility The visibility of the activity. */ - public static function handle_create( $activity, $user_id, $activity_object = null, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ) { + public static function handle_create( $activity, $user_id, $activity_object = null ) { // Check if Activity is public or not. if ( - ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility || + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === get_activity_visibility( $activity ) || ! is_activity_reply( $activity ) ) { return; diff --git a/includes/rest/class-actors-inbox-controller.php b/includes/rest/class-actors-inbox-controller.php index c4f494af0..ed22fac84 100644 --- a/includes/rest/class-actors-inbox-controller.php +++ b/includes/rest/class-actors-inbox-controller.php @@ -10,7 +10,6 @@ use Activitypub\Activity\Activity; use Activitypub\Moderation; -use function Activitypub\get_activity_visibility; use function Activitypub\get_context; use function Activitypub\get_masked_wp_version; use function Activitypub\get_rest_url_by_path; @@ -178,8 +177,6 @@ public function create_item( $request ) { */ do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity ); } else { - $visibility = get_activity_visibility( $data ); - /** * ActivityPub inbox action. * @@ -187,9 +184,8 @@ public function create_item( $request ) { * @param int|null $user_id The user ID. * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. - * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity, $visibility ); + \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity ); /** * ActivityPub inbox action for specific activity types. @@ -197,9 +193,8 @@ public function create_item( $request ) { * @param array $data The data array. * @param int|null $user_id The user ID. * @param Activity|\WP_Error $activity The Activity object. - * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity, $visibility ); + \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity ); } $response = \rest_ensure_response( diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 6757d17e6..c5b5b6050 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -12,7 +12,6 @@ use Activitypub\Moderation; use function Activitypub\extract_recipients_from_activity; -use function Activitypub\get_activity_visibility; use function Activitypub\is_same_domain; use function Activitypub\user_can_activitypub; @@ -151,7 +150,6 @@ public function create_item( $request ) { do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity ); } else { $recipients = $this->get_local_recipients( $data ); - $visibility = get_activity_visibility( $data ); foreach ( $recipients as $recipient ) { // Check user-specific blocks for this recipient. @@ -175,9 +173,8 @@ public function create_item( $request ) { * @param int $user_id The user ID. * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. - * @param string $visibility The visibility of the activity. */ - \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity, $visibility ); + \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity ); /** * ActivityPub inbox action for specific activity types. @@ -185,9 +182,8 @@ public function create_item( $request ) { * @param array $data The data array. * @param int $user_id The user ID. * @param Activity|\WP_Error $activity The Activity object. - * @param string $visibility The visibility of the activity. - */ - \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity, $visibility ); + */ + \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); } } From 84c634a58c329537ba16c63d4abb761e1101dfd3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 08:55:30 +0200 Subject: [PATCH 24/30] Improve activity visibility checks and expand Create handler tests Added a check for empty activity type in get_activity_visibility to prevent errors. Enhanced the Create handler test suite with additional cases for missing type, duplicate IDs, duplicate content, and multiple comments to ensure more robust handling of activity creation scenarios. --- includes/functions.php | 5 +- tests/includes/handler/class-test-create.php | 142 ++++++++++++++++++- 2 files changed, 144 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index fe3af1335..e9e052fcb 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -562,19 +562,22 @@ function extract_recipients_from_activity_property( $property, $data ) { */ function get_activity_visibility( $activity ) { // Set default visibility for specific activity types. - if ( in_array( $activity['type'], array( 'Accept', 'Delete', 'Follow', 'Reject', 'Undo' ), true ) ) { + if ( ! empty( $activity['type'] ) && in_array( $activity['type'], array( 'Accept', 'Delete', 'Follow', 'Reject', 'Undo' ), true ) ) { return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; } + // Check 'to' field for public visibility. $to = extract_recipients_from_activity_property( 'to', $activity ); if ( ! empty( array_intersect( $to, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; } + // Check 'cc' field for quiet public visibility. $cc = extract_recipients_from_activity_property( 'cc', $activity ); if ( ! empty( array_intersect( $cc, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ) ) ) { return ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC; } + return ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE; } diff --git a/tests/includes/handler/class-test-create.php b/tests/includes/handler/class-test-create.php index b29abd793..957d3ce28 100644 --- a/tests/includes/handler/class-test-create.php +++ b/tests/includes/handler/class-test-create.php @@ -98,6 +98,7 @@ public static function get_remote_metadata_by_actor( $value, $actor ) { public function create_test_object( $id = 'https://example.com/123' ) { return array( 'actor' => $this->user_url, + 'type' => 'Create', 'id' => 'https://example.com/id/' . microtime( true ), 'to' => array( $this->user_url ), 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), @@ -118,7 +119,7 @@ public function create_test_object( $id = 'https://example.com/123' ) { public function test_handle_create_non_public_rejected() { $object = $this->create_test_object(); $object['cc'] = array(); - $converted = Create::handle_create( $object, $this->user_id, null, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); + $converted = Create::handle_create( $object, $this->user_id ); $this->assertNull( $converted ); } @@ -129,7 +130,7 @@ public function test_handle_create_non_public_rejected() { */ public function test_handle_create_public_accepted() { $object = $this->create_test_object(); - Create::handle_create( $object, $this->user_id, null, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + Create::handle_create( $object, $this->user_id ); $args = array( 'type' => 'comment', @@ -141,5 +142,142 @@ public function test_handle_create_public_accepted() { $this->assertInstanceOf( 'WP_Comment', $result[0] ); $this->assertEquals( 'example', $result[0]->comment_content ); + $this->assertCount( 1, $result ); + } + + /** + * Test handle create. + * + * @covers ::handle_create + */ + public function test_handle_create_public_accepted_without_type() { + $object = $this->create_test_object( 'https://example.com/123456' ); + unset( $object['type'] ); + + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertInstanceOf( 'WP_Comment', $result[0] ); + $this->assertEquals( 'example', $result[0]->comment_content ); + } + + /** + * Test handle create check duplicate ID. + * + * @covers ::handle_create + */ + public function test_handle_create_check_dupplicate_id() { + $id = 'https://example.com/id/' . microtime( true ); + $object = $this->create_test_object( $id ); + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertInstanceOf( 'WP_Comment', $result[0] ); + $this->assertEquals( 'example', $result[0]->comment_content ); + $this->assertCount( 1, $result ); + + $object['object']['content'] = 'example2'; + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertCount( 1, $result ); + } + + /** + * Test handle create check duplicate content. + * + * @covers ::handle_create + */ + public function test_handle_create_check_dupplicate_content() { + $id = 'https://example.com/id/' . microtime( true ); + $object = $this->create_test_object( $id ); + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertInstanceOf( 'WP_Comment', $result[0] ); + $this->assertEquals( 'example', $result[0]->comment_content ); + $this->assertCount( 1, $result ); + + $id = 'https://example.com/id/' . microtime( true ); + $object = $this->create_test_object( $id ); + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertCount( 1, $result ); + } + + /** + * Test handle create multiple comments. + * + * @covers ::handle_create + */ + public function test_handle_create_check_multiple_comments() { + $id = 'https://example.com/id/4711'; + $object = $this->create_test_object( $id ); + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertInstanceOf( 'WP_Comment', $result[0] ); + $this->assertEquals( 'example', $result[0]->comment_content ); + $this->assertCount( 1, $result ); + + $id = 'https://example.com/id/23'; + $object = $this->create_test_object( $id ); + $object['object']['content'] = 'example2'; + Create::handle_create( $object, $this->user_id ); + + $args = array( + 'type' => 'comment', + 'post_id' => $this->post_id, + ); + + $query = new \WP_Comment_Query( $args ); + $result = $query->comments; + + $this->assertInstanceOf( 'WP_Comment', $result[1] ); + $this->assertEquals( 'example2', $result[1]->comment_content ); + $this->assertCount( 2, $result ); } } From 359496d59f9dfecc8d24fd8bc079c50af556eacc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 08:58:19 +0200 Subject: [PATCH 25/30] Update changelog for recipient and visibility handling Revised the changelog entry to clarify improvements in recipient handling and to note enhanced visibility handling of activities. --- .github/changelog/2210-from-description | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/changelog/2210-from-description b/.github/changelog/2210-from-description index c58d319b0..7ea0a108f 100644 --- a/.github/changelog/2210-from-description +++ b/.github/changelog/2210-from-description @@ -1,4 +1,4 @@ Significance: minor Type: changed -Improved recipient handling for clarity and added better inbox support. +Improved recipient handling for clarity and improved visibility handling of activities. From b9e19fc86a74c8f239654558162e033a883aaa74 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 09:05:29 +0200 Subject: [PATCH 26/30] Fix indentation in docblock for do_action hook Corrected the indentation of the docblock for the do_action hook in Inbox_Controller to improve code readability and maintain consistency. --- includes/rest/class-inbox-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index c5b5b6050..19ff7b648 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -182,7 +182,7 @@ public function create_item( $request ) { * @param array $data The data array. * @param int $user_id The user ID. * @param Activity|\WP_Error $activity The Activity object. - */ + */ \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); } } From 7001481ffb0ec717ef3651330729c403ca3ef022 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 12:31:38 +0200 Subject: [PATCH 27/30] Update tests/includes/handler/class-test-create.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/includes/handler/class-test-create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/handler/class-test-create.php b/tests/includes/handler/class-test-create.php index 957d3ce28..a945dc7f3 100644 --- a/tests/includes/handler/class-test-create.php +++ b/tests/includes/handler/class-test-create.php @@ -173,7 +173,7 @@ public function test_handle_create_public_accepted_without_type() { * * @covers ::handle_create */ - public function test_handle_create_check_dupplicate_id() { + public function test_handle_create_check_duplicate_id() { $id = 'https://example.com/id/' . microtime( true ); $object = $this->create_test_object( $id ); Create::handle_create( $object, $this->user_id ); From b05c85ccf66b66a6173f465a09abb1844665fc53 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 Sep 2025 12:32:43 +0200 Subject: [PATCH 28/30] Fix typo in test method name Corrected 'dupplicate' to 'duplicate' in the test_handle_create_check_duplicate_content method name for clarity and consistency. --- tests/includes/handler/class-test-create.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/handler/class-test-create.php b/tests/includes/handler/class-test-create.php index a945dc7f3..817778821 100644 --- a/tests/includes/handler/class-test-create.php +++ b/tests/includes/handler/class-test-create.php @@ -209,7 +209,7 @@ public function test_handle_create_check_duplicate_id() { * * @covers ::handle_create */ - public function test_handle_create_check_dupplicate_content() { + public function test_handle_create_check_duplicate_content() { $id = 'https://example.com/id/' . microtime( true ); $object = $this->create_test_object( $id ); Create::handle_create( $object, $this->user_id ); From d8de9a718a80bf08329ec54f42d231a0c1d0dee0 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 24 Sep 2025 07:51:01 -0500 Subject: [PATCH 29/30] Whitespace changes --- includes/rest/class-actors-inbox-controller.php | 14 +++++++------- includes/rest/class-inbox-controller.php | 14 +++++++------- tests/includes/class-test-moderation.php | 1 + 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/includes/rest/class-actors-inbox-controller.php b/includes/rest/class-actors-inbox-controller.php index ed22fac84..86ed35cd3 100644 --- a/includes/rest/class-actors-inbox-controller.php +++ b/includes/rest/class-actors-inbox-controller.php @@ -180,19 +180,19 @@ public function create_item( $request ) { /** * ActivityPub inbox action. * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. */ \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity ); /** * ActivityPub inbox action for specific activity types. * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. */ \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity ); } diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 19ff7b648..0f36da0f6 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -169,19 +169,19 @@ public function create_item( $request ) { /** * ActivityPub inbox action. * - * @param array $data The data array. - * @param int $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. */ \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity ); /** * ActivityPub inbox action for specific activity types. * - * @param array $data The data array. - * @param int $user_id The user ID. - * @param Activity|\WP_Error $activity The Activity object. + * @param array $data The data array. + * @param int $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. */ \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); } diff --git a/tests/includes/class-test-moderation.php b/tests/includes/class-test-moderation.php index 88356d32e..537e4f350 100644 --- a/tests/includes/class-test-moderation.php +++ b/tests/includes/class-test-moderation.php @@ -514,6 +514,7 @@ static function ( $errno, $errstr ) { }, E_NOTICE | E_WARNING ); + // PHP 7.2 uses "Undefined index", PHP 8+ uses "Undefined array key". if ( version_compare( PHP_VERSION, '8.0.0', '>=' ) ) { $this->expectExceptionMessage( 'Undefined array key "id"' ); From 58d0b5fd507fa686fa4fb533bd3c7accd16c0acf Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 24 Sep 2025 07:54:57 -0500 Subject: [PATCH 30/30] Keep parameter names aligned with hook docs --- includes/rest/class-inbox-controller.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-inbox-controller.php b/includes/rest/class-inbox-controller.php index 0f36da0f6..e6c0fd688 100644 --- a/includes/rest/class-inbox-controller.php +++ b/includes/rest/class-inbox-controller.php @@ -151,9 +151,9 @@ public function create_item( $request ) { } else { $recipients = $this->get_local_recipients( $data ); - foreach ( $recipients as $recipient ) { + foreach ( $recipients as $user_id ) { // Check user-specific blocks for this recipient. - if ( Moderation::activity_is_blocked_for_user( $activity, $recipient ) ) { + if ( Moderation::activity_is_blocked_for_user( $activity, $user_id ) ) { /** * ActivityPub inbox disallowed activity for specific user. * @@ -162,7 +162,7 @@ public function create_item( $request ) { * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_rest_inbox_disallowed', $data, $recipient, $type, $activity ); + \do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity ); continue; } @@ -174,7 +174,7 @@ public function create_item( $request ) { * @param string $type The type of the activity. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_inbox', $data, $recipient, $type, $activity ); + \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity ); /** * ActivityPub inbox action for specific activity types. @@ -183,7 +183,7 @@ public function create_item( $request ) { * @param int $user_id The user ID. * @param Activity|\WP_Error $activity The Activity object. */ - \do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity ); + \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity ); } }