Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
523defe
Refactor recipient extraction and add unit tests
pfefferle Sep 18, 2025
51fe673
Rename parameter from attribute to property in function
pfefferle Sep 18, 2025
3b79444
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 18, 2025
d25fd5f
Refactor recipient extraction and object_to_uri handling
pfefferle Sep 18, 2025
70ce80f
Filter empty values from recipients array
pfefferle Sep 18, 2025
79429da
Improve object_to_uri fallback and update related tests
pfefferle Sep 18, 2025
858672f
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 19, 2025
35dbe06
Refactor recipient extraction and add Audience trait
pfefferle Sep 19, 2025
615bd19
Add visibility parameter to ActivityPub inbox actions
pfefferle Sep 19, 2025
93cf677
Refactor handle_create to use visibility parameter
pfefferle Sep 19, 2025
5e823d1
Set default private visibility for certain activity types
pfefferle Sep 19, 2025
17fb6f9
Reorder activity types in visibility check
pfefferle Sep 19, 2025
2e62107
Add changelog
matticbot Sep 19, 2025
be0b67f
Add unit tests for Audience trait in REST API
pfefferle Sep 19, 2025
4d6bb2f
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 19, 2025
17ca93c
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 19, 2025
23b92ea
Use global namespace for array functions
pfefferle Sep 19, 2025
4ed4a4a
Update object_to_uri to return empty string on failure
pfefferle Sep 19, 2025
8a731d8
Move recipient extraction logic to Inbox_Controller
pfefferle Sep 19, 2025
eb12652
Refactor activity visibility logic and remove Audience trait
pfefferle Sep 22, 2025
698f36d
Rename get_recipients to get_local_recipients
pfefferle Sep 22, 2025
2d99fbe
Rename test methods to use get_local_recipients
pfefferle Sep 22, 2025
7838f06
Prevent duplicate checkstyle annotations in PHPUnit workflow
pfefferle Sep 22, 2025
9c4af52
Remove redundant @covers annotations in tests
pfefferle Sep 22, 2025
1c6a274
Refactor object_to_uri to remove default empty string
pfefferle Sep 22, 2025
4d40df6
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 22, 2025
0154560
Fix object_to_uri to require 'id' and update related tests
pfefferle Sep 22, 2025
107e0f6
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 22, 2025
6d6d923
Refactor activity visibility handling in inbox actions
pfefferle Sep 24, 2025
84c634a
Improve activity visibility checks and expand Create handler tests
pfefferle Sep 24, 2025
359496d
Update changelog for recipient and visibility handling
pfefferle Sep 24, 2025
b9e19fc
Fix indentation in docblock for do_action hook
pfefferle Sep 24, 2025
7001481
Update tests/includes/handler/class-test-create.php
pfefferle Sep 24, 2025
b05c85c
Fix typo in test method name
pfefferle Sep 24, 2025
d8de9a7
Whitespace changes
obenland Sep 24, 2025
fc945d2
Merge branch 'trunk' into add/inbox-visibility
pfefferle Sep 24, 2025
58d0b5f
Keep parameter names aligned with hook docs
obenland Sep 24, 2025
90d935a
Merge branch 'add/inbox-visibility' of https://github.com/Automattic/…
obenland Sep 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/changelog/2210-from-description
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Improved recipient handling for clarity and added better inbox support.
54 changes: 23 additions & 31 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,40 +525,32 @@ 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 );
}

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 );
}
$recipient_items = \array_merge( $recipient_items, extract_recipients_from_activity_property( $i, $data ) );
}

return \array_unique( $recipient_items );
}

/**
* Extract recipient URLs from a specific property of an Activity object.
*
* @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( $property, $data ) {
$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 {
$recipients[] = $recipient;
}
if ( ! empty( $data[ $property ] ) ) {
$recipients = $data[ $property ];
} elseif ( ! empty( $data['object'][ $property ] ) ) {
$recipients = $data['object'][ $property ];
}

return array_unique( $recipients );
$recipients = \array_map( '\Activitypub\object_to_uri', (array) $recipients );

return \array_unique( \array_filter( $recipients ) );
}

/**
Expand Down Expand Up @@ -696,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.
Expand Down Expand Up @@ -733,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'] ?? $data['url'] ?? false;
break;
}

Expand Down
21 changes: 5 additions & 16 deletions includes/handler/class-create.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 );
}

/**
Expand All @@ -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;
Expand Down
23 changes: 14 additions & 9 deletions includes/rest/class-actors-inbox-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*/
class Actors_Inbox_Controller extends Actors_Controller {
use Collection;
use Audience;

/**
* Register routes.
Expand Down Expand Up @@ -177,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(
Expand Down
48 changes: 17 additions & 31 deletions includes/rest/class-inbox-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -149,25 +146,12 @@ 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_local_recipients( $data );
$visibility = $this->determine_visibility( $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.
*
Expand All @@ -176,28 +160,30 @@ 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;
}

/**
* 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, $user_id, $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, $user_id, $activity );
\do_action( 'activitypub_inbox_' . $type, $data, $recipient, $activity, $visibility );
}
}

Expand Down
83 changes: 83 additions & 0 deletions includes/rest/trait-audience.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
/**
* Audience Trait file.
*
* @package Activitypub
*/

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.
*
* 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.
*
* @param array $activity The activity data.
*
* @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( '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;
}
}
Loading