Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/fix-move-activity-target
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Fix account migration (Move) not working when moving back to an external account.
35 changes: 14 additions & 21 deletions includes/handler/class-move.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class Move {
*/
public static function init() {
\add_action( 'activitypub_inbox_move', array( self::class, 'handle_move' ), 10, 2 );
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
}

/**
Expand Down Expand Up @@ -101,22 +100,6 @@ public static function handle_move( $activity, $user_ids ) {
\do_action( 'activitypub_handled_move', $activity, (array) $user_ids, $success, $result );
}

/**
* Convert the object and origin to the correct format.
*
* @param \Activitypub\Activity\Activity $activity The Activity object.
* @return \Activitypub\Activity\Activity The filtered Activity object.
*/
public static function outbox_activity( $activity ) {
if ( 'Move' === $activity->get_type() ) {
$activity->set_object( object_to_uri( $activity->get_object() ) );
$activity->set_origin( $activity->get_actor() );
$activity->set_target( $activity->get_object() );
}

return $activity;
}

/**
* Extract the target from the activity.
*
Expand Down Expand Up @@ -187,13 +170,23 @@ private static function verify_move( $target_object, $origin_object ) {
return false;
}

// Check if the target has an alsoKnownAs property.
if ( empty( $target_object['also_known_as'] ) ) {
// Normalize alsoKnownAs to an array (some JSON-LD payloads may use a string).
$also_known_as = (array) ( $target_object['alsoKnownAs'] ?? array() );
if ( empty( $also_known_as ) ) {
return false;
}

// Check if the origin is in the alsoKnownAs property of the target.
if ( ! in_array( $origin_object['id'], $target_object['also_known_as'], true ) ) {
// Collect all possible origin identifiers (id, url, webfinger).
$origin_ids = array_filter(
array(
$origin_object['id'] ?? null,
$origin_object['url'] ?? null,
$origin_object['webfinger'] ?? null,
)
);

// Check if any origin identifier is in the alsoKnownAs property of the target.
if ( ! array_intersect( $origin_ids, $also_known_as ) ) {
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/phpunit/tests/includes/class-test-move.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public function test_account_with_duplicate_moves() {

$filter = function () use ( $from ) {
return array(
'body' => wp_json_encode( array( 'also_known_as' => array( $from ) ) ),
'body' => wp_json_encode( array( 'alsoKnownAs' => array( $from ) ) ),
'response' => array( 'code' => 200 ),
);
};
Expand Down
155 changes: 137 additions & 18 deletions tests/phpunit/tests/includes/handler/class-test-move.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ public function test_handle_move_with_target_and_origin() {

// Mock the HTTP response for the target object.
$target_object = array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'also_known_as' => array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'alsoKnownAs' => array(
$origin,
),
);
Expand Down Expand Up @@ -234,12 +234,12 @@ public function test_handle_move_with_existing_target_but_missing_origin() {
return array(
'body' => \wp_json_encode(
array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'also_known_as' => array( $origin ),
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'alsoKnownAs' => array( $origin ),
)
),
'response' => array( 'code' => 200 ),
Expand Down Expand Up @@ -322,12 +322,12 @@ public function test_handle_move_with_existing_target_and_origin() {

if ( $url === $target ) {
return array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'also_known_as' => array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'alsoKnownAs' => array(
$origin,
),
);
Expand Down Expand Up @@ -369,4 +369,123 @@ public function test_handle_move_with_existing_target_and_origin() {

\remove_filter( 'activitypub_pre_http_get_remote_object', $filter );
}

/**
* Test that verify_move matches origin by url when id is not in alsoKnownAs.
*/
public function test_handle_move_matches_origin_by_url() {
$target = 'https://example.com/new-profile';
$origin = 'https://example.com/old-profile';

$origin_object = array(
'type' => 'Person',
'id' => $origin,
'url' => 'https://example.com/@oldprofile',
'name' => 'Old Profile',
'inbox' => 'https://example.com/old-profile/inbox',
'movedTo' => $target,
);

// Target's alsoKnownAs contains the origin's url, not its id.
$target_object = array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile/inbox',
'alsoKnownAs' => array(
'https://example.com/@oldprofile',
),
);

$id = Remote_Actors::upsert( $origin_object );
\add_post_meta( $id, Followers::FOLLOWER_META_KEY, $this->user_id );

$filter = function ( $pre, $url_or_object ) use ( $target, $target_object, $origin, $origin_object ) {
$url = object_to_uri( $url_or_object );
if ( $url === $target ) {
return $target_object;
}
if ( $url === $origin ) {
return $origin_object;
}
return $pre;
};

\add_filter( 'activitypub_pre_http_get_remote_object', $filter, 10, 2 );

$activity = array(
'type' => 'Move',
'actor' => $origin,
'object' => $target,
);

Move::handle_move( $activity, 1 );

$updated_follower = Remote_Actors::get_by_uri( $target );
$this->assertNotNull( $updated_follower );
$this->assertEquals( $target, $updated_follower->guid );

\remove_filter( 'activitypub_pre_http_get_remote_object', $filter );
}

/**
* Test that verify_move matches origin by webfinger when id is not in alsoKnownAs.
*/
public function test_handle_move_matches_origin_by_webfinger() {
$target = 'https://example.com/new-profile2';
$origin = 'https://example.com/old-profile2';

$origin_object = array(
'type' => 'Person',
'id' => $origin,
'url' => $origin,
'name' => 'Old Profile',
'inbox' => 'https://example.com/old-profile2/inbox',
'movedTo' => $target,
'webfinger' => 'olduser@example.com',
);

// Target's alsoKnownAs contains the origin's webfinger, not its id.
$target_object = array(
'type' => 'Person',
'id' => $target,
'url' => $target,
'name' => 'New Profile',
'inbox' => 'https://example.com/new-profile2/inbox',
'alsoKnownAs' => array(
'olduser@example.com',
),
);

$id = Remote_Actors::upsert( $origin_object );
\add_post_meta( $id, Followers::FOLLOWER_META_KEY, $this->user_id );

$filter = function ( $pre, $url_or_object ) use ( $target, $target_object, $origin, $origin_object ) {
$url = object_to_uri( $url_or_object );
if ( $url === $target ) {
return $target_object;
}
if ( $url === $origin ) {
return $origin_object;
}
return $pre;
};

\add_filter( 'activitypub_pre_http_get_remote_object', $filter, 10, 2 );

$activity = array(
'type' => 'Move',
'actor' => $origin,
'object' => $target,
);

Move::handle_move( $activity, 1 );

$updated_follower = Remote_Actors::get_by_uri( $target );
$this->assertNotNull( $updated_follower );
$this->assertEquals( $target, $updated_follower->guid );

\remove_filter( 'activitypub_pre_http_get_remote_object', $filter );
}
}
Loading