Fix account move verification and notify followers - #3585
Conversation
There was a problem hiding this comment.
Pull request overview
Improves outgoing ActivityPub account migration (Move) handling to better align with FEP-7628 by verifying targets before persisting movedTo, ensuring link-back validation uses the canonical actor id, and notifying followers via a profile Update after a move.
Changes:
- Adjust
Move::externally()to validate the target’salsoKnownAsagainst the canonical actor id and only persistmovedToafter verification. - Adjust
Move::internally()to record the source URL on the target actor’salsoKnownAs(so the new actor links back). - Add/extend PHPUnit coverage for verified moves, rejected targets, internal moves between distinct local users, and profile-update federation; add changelog entry.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
includes/class-move.php |
Updates external/internal move validation and adds follower-notification via profile Update scheduling. |
tests/phpunit/tests/includes/class-test-move.php |
Expands tests for verification behavior, follower notification, and internal moves across distinct users. |
.github/changelog/fix-account-move-verification |
Adds a patch-level changelog entry describing the migration fix. |
Comments suppressed due to low confidence (4)
includes/class-move.php:193
schedule_profile_update()runs even whenadd_to_outbox()fails, which can result in profile Updates being federated without a corresponding Move activity.
$outbox_id = add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
/*
* Notify followers of the changed profile on both actors by federating an Update (FEP-7628).
* Queued after the Move so a follower that reacts to `movedTo` still processes the migration first.
tests/phpunit/tests/includes/class-test-move.php:109
- This
pre_http_requestfilter callback is registered with the default accepted-args (1), but the closure declares 0 parameters. That produces “too many arguments” warnings on PHP 7.x and can become an ArgumentCountError on PHP 8+.
$filter = function () use ( $from ) {
tests/phpunit/tests/includes/class-test-move.php:149
- This
pre_http_requestfilter callback is registered with the default accepted-args (1), but the closure declares 0 parameters. That produces “too many arguments” warnings on PHP 7.x and can become an ArgumentCountError on PHP 8+.
$filter = function () {
tests/phpunit/tests/includes/class-test-move.php:205
- This
pre_http_requestfilter callback is registered with the default accepted-args (1), but the closure declares 0 parameters. That produces “too many arguments” warnings on PHP 7.x and can become an ArgumentCountError on PHP 8+.
$filter = function () use ( $from ) {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (6)
tests/phpunit/tests/includes/class-test-move.php:113
- The mocked target actor JSON returned via
pre_http_requestis missing required actor fields likeid(and typicallytype). That meansMove::externally()builds a target actor without an id, so the federatedMoveends up with an emptytarget, and the test can still pass. Include at leastid(andtype) in the mock to better match real actor objects and exercise the intended code path.
$filter = function () use ( $from ) {
return array(
'body' => wp_json_encode( array( 'alsoKnownAs' => array( $from ) ) ),
'response' => array( 'code' => 200 ),
);
tests/phpunit/tests/includes/class-test-move.php:153
- The mocked target actor JSON returned via
pre_http_requestis missing required actor fields likeid(and typicallytype). That meansMove::externally()builds a target actor without an id, so the federatedMoveends up with an emptytarget, and the test can still pass. Include at leastid(andtype) in the mock to better match real actor objects and exercise the intended code path.
$http = function () use ( $from ) {
return array(
'body' => wp_json_encode( array( 'alsoKnownAs' => array( $from ) ) ),
'response' => array( 'code' => 200 ),
);
tests/phpunit/tests/includes/class-test-move.php:204
- This
pre_http_requestmock returns an object without anid(andtype).Move::externally()uses the decoded actor’s id as theMoveactivity target; omitting it means the test doesn’t exercise the real-world path where the target has an id. Include at leastid/typein the mocked JSON response.
$filter = function () {
return array(
'body' => wp_json_encode( array( 'alsoKnownAs' => array( 'https://newsite.com/user/999' ) ) ),
'response' => array( 'code' => 200 ),
);
tests/phpunit/tests/includes/class-test-move.php:260
- The mocked target actor JSON returned via
pre_http_requestis missing required actor fields likeid(and typicallytype). That meansMove::externally()builds a target actor without an id, so the federatedMoveends up with an emptytarget, and the test can still pass. Include at leastid(andtype) in the mock to better match real actor objects and exercise the intended code path.
$filter = function () use ( $from ) {
return array(
'body' => wp_json_encode( array( 'alsoKnownAs' => array( $from ) ) ),
'response' => array( 'code' => 200 ),
);
includes/class-move.php:109
externally()persistsmovedTousing the raw$toinput even though the target actor is fetched and the canonical id is available. If$tois a non-canonical URL (or redirects), followers may cache amovedTothat doesn’t match the actor id used in the federatedMove(set_target( $target_actor->get_id() )). Persist the verified/canonical target id instead to match what is federated.
// Advertise the move only after the target is verified, so a failed attempt never leaves the actor pointing at an unverified target.
if ( $user->get__id() > 0 ) {
\update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
} else {
\update_option( 'activitypub_blog_user_moved_to', $to );
tests/phpunit/tests/includes/class-test-move.php:48
- The mocked target actor JSON returned via
pre_http_requestis missing required actor fields likeid(and typicallytype). That meansMove::externally()builds a target actor without an id, so the federatedMoveends up with an emptytarget, and the test can still pass. Include at leastid(andtype) in the mock to better match real actor objects and exercise the intended code path.
This issue also appears in the following locations of the same file:
- line 109
- line 149
- line 200
- line 256
$filter = function () use ( $from ) {
return array(
'body' => wp_json_encode( array( 'alsoKnownAs' => array( $from ) ) ),
'response' => array( 'code' => 200 ),
);
|
Copilot suppressed six comments in the last review. One of them was right, so I fixed it.
So the move was just silently ignored there. I also had to add a guard for a target document without an Two new tests, both verified by breaking the fix again:
Without the guard the second one fails with "Failed asserting that 10 is an instance of WP_Error", so the move really did succeed with an empty target before. Full suite green, 2915 tests. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (1)
tests/phpunit/tests/includes/class-test-move.php:409
- The new follower-notification behavior for internal moves (scheduling profile
Updateactivities for both source and target actors) isn’t asserted anywhere. Adding an assertion here would prevent regressions (e.g., only one actor being updated, or no Update being federated at all).
// The source points at the target.
$this->assertEquals( $to, Actors::get_by_id( self::$user_id )->get_moved_to() );
// The target links back to the source via alsoKnownAs, so receiving servers accept the move.
$this->assertContains( $from, Actors::get_by_id( $target_id )->get_also_known_as() );
Proposed changes:
Reviewed the outgoing
Movehandling (Move::externally()/Move::internally()) against FEP-7628 and fixed four things:externally()setmovedTobefore fetching and checking the target, so a failed or unverifiable move left the actor pointing at a destination with noMovefederated.movedTois now written only after the target is verified.Move'sobject(get_id()) against the target'salsoKnownAs, instead of the possibly non-canonical input URL, which is what receiving servers verify against.alsoKnownAson the target.internally()recorded the old URL on the source actor. It now records it on the target, so the new actor links back to the old one and receiving servers accept the move. For a domain change the two resolve to the same actor, so that path is unchanged; for a move between two different local actors the target now links back.Updateso followers refresh the cachedmovedTo/alsoKnownAs. It is queued after theMoveso a follower that reacts tomovedTostill processes the migration first.Delivery is unchanged and already matches FEP-7628: the
Moveis addressed to the old actor's followers.Other information:
Testing instructions:
wp activitypub move <from> <to>(WP-CLI) or trigger a move via the settings.alsoKnownAsdoes not list your actor is rejected and leavesmovedTounset.movedTo, federates theMoveto followers, and federates a profileUpdate.Changelog entry
The changelog entry is already included in the branch (
.github/changelog/fix-account-move-verification), so the auto-create box below is left unchecked.Changelog Entry Details
Significance
Type
Message
Fix Fediverse account migration so a move is verified before it takes effect and reliably reaches your followers on other servers.