From 02b9090f9bdf0c86ca6a5b6960deb5434568ab2a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 11:12:52 +0100 Subject: [PATCH 1/9] Migrate profile IDs from permalink to query param format Change actor IDs to always use the query param format (?author=ID). When accessed via the old permalink URL, show movedTo pointing to the new ID. Add migration to send Move activity for blog user. --- includes/class-migration.php | 34 ++++++++++++++++++++++++++++++++++ includes/model/class-blog.php | 30 ++++++++++++++++++++++-------- includes/model/class-user.php | 30 ++++++++++++++++++++++-------- 3 files changed, 78 insertions(+), 16 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 7c35eafabb..f26a92c356 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -215,6 +215,10 @@ public static function maybe_migrate() { \wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' ); } + if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) { + self::migrate_blog_user_to_query_param_id(); + } + // Ensure all required cron schedules are registered. Scheduler::register_schedules(); @@ -1083,6 +1087,36 @@ private static function clean_up_inbox() { } } + /** + * Migrate blog user from permalink-based ID to query param ID. + * + * This sends a Move activity from the old @username URL to the new ?author= URL + * for the blog actor, allowing followers to update their records. + */ + private static function migrate_blog_user_to_query_param_id() { + $use_permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); + + if ( ! $use_permalink ) { + return; + } + + $blog = new Model\Blog(); + + // Old ID was the @username format. + $old_id = \esc_url( \trailingslashit( \get_home_url() ) . '@' . $blog->get_preferred_username() ); + // New ID is the query param format. + $new_id = \add_query_arg( 'author', Actors::BLOG_USER_ID, \home_url( '/' ) ); + + $activity = new Activity\Activity(); + $activity->set_type( 'Move' ); + $activity->set_actor( $old_id ); + $activity->set_origin( $old_id ); + $activity->set_object( $old_id ); + $activity->set_target( $new_id ); + + add_to_outbox( $activity, null, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); + } + /** * Migrate avatar URLs from comment meta to remote actors in batches. * diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php index 326c3bba43..8ff30a8908 100644 --- a/includes/model/class-blog.php +++ b/includes/model/class-blog.php @@ -90,12 +90,6 @@ public function get_id() { return $id; } - $permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); - - if ( $permalink ) { - return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); - } - return \add_query_arg( 'author', $this->_id, \home_url( '/' ) ); } @@ -555,11 +549,31 @@ public function get_also_known_as() { /** * Returns the movedTo. * - * @return string The movedTo. + * @return string|null The movedTo URL or null. */ public function get_moved_to() { $moved_to = \get_option( 'activitypub_blog_user_moved_to' ); - return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + if ( $moved_to && $moved_to !== $this->get_id() ) { + return $moved_to; + } + + // If the blog had the old permalink-as-id setting and is being accessed + // via the old permalink URL (no author in query string), return the new ID. + $use_permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); + + if ( $use_permalink ) { + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? \esc_url_raw( \wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + $query_string = \wp_parse_url( $request_uri, \PHP_URL_QUERY ); + $query_params = array(); + + \wp_parse_str( $query_string ?? '', $query_params ); + + if ( ! isset( $query_params['author'] ) ) { + return $this->get_id(); + } + } + + return null; } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index a987d5b8fd..b150953a5a 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -116,12 +116,6 @@ public function get_id() { return $id; } - $permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id ); - - if ( '1' === $permalink ) { - return $this->get_url(); - } - return \add_query_arg( 'author', $this->_id, \home_url( '/' ) ); } @@ -460,11 +454,31 @@ public function get_also_known_as() { /** * Returns the movedTo. * - * @return string The movedTo. + * @return string|null The movedTo URL or null. */ public function get_moved_to() { $moved_to = \get_user_option( 'activitypub_moved_to', $this->_id ); - return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + if ( $moved_to && $moved_to !== $this->get_id() ) { + return $moved_to; + } + + // If this user had the old permalink-as-id setting and is being accessed + // via the old permalink URL (no author in query string), return the new ID. + $use_permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id ); + + if ( '1' === $use_permalink ) { + $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? \esc_url_raw( \wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + $query_string = \wp_parse_url( $request_uri, \PHP_URL_QUERY ); + $query_params = array(); + + \wp_parse_str( $query_string ?? '', $query_params ); + + if ( ! isset( $query_params['author'] ) ) { + return $this->get_id(); + } + } + + return null; } } From 709ecd14242665836f614ec475d25e057cda30f7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 11:23:31 +0100 Subject: [PATCH 2/9] Fix add_to_outbox to use imported function --- includes/class-migration.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/class-migration.php b/includes/class-migration.php index f26a92c356..4662cf0bad 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -15,6 +15,8 @@ use Activitypub\Collection\Remote_Actors; use Activitypub\Transformer\Factory; +use function Activitypub\add_to_outbox; + /** * ActivityPub Migration Class * From 7f5717dba5a0ff93d45b4bd4a0e88d43c4c556a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 11:31:58 +0100 Subject: [PATCH 3/9] Send Move activities to all known inboxes --- includes/class-migration.php | 24 ++++++++++-------------- includes/collection/class-followers.php | 5 +++-- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 4662cf0bad..ae5b9041f9 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,16 +7,16 @@ namespace Activitypub; +use Activitypub\Activity\Activity; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; use Activitypub\Collection\Following; use Activitypub\Collection\Outbox; use Activitypub\Collection\Remote_Actors; +use Activitypub\Model\Blog; use Activitypub\Transformer\Factory; -use function Activitypub\add_to_outbox; - /** * ActivityPub Migration Class * @@ -1102,21 +1102,17 @@ private static function migrate_blog_user_to_query_param_id() { return; } - $blog = new Model\Blog(); - - // Old ID was the @username format. - $old_id = \esc_url( \trailingslashit( \get_home_url() ) . '@' . $blog->get_preferred_username() ); - // New ID is the query param format. - $new_id = \add_query_arg( 'author', Actors::BLOG_USER_ID, \home_url( '/' ) ); + $blog = new Blog(); - $activity = new Activity\Activity(); + $activity = new Activity(); $activity->set_type( 'Move' ); - $activity->set_actor( $old_id ); - $activity->set_origin( $old_id ); - $activity->set_object( $old_id ); - $activity->set_target( $new_id ); + $activity->set_actor( $blog->get_url() ); + $activity->set_origin( $blog->get_url() ); + $activity->set_object( $blog->get_url() ); + $activity->set_target( $blog->get_id() ); + $activity->set_to( Remote_Actors::get_inboxes() ); - add_to_outbox( $activity, null, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); + Outbox::add( $activity, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } /** diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index bcad4a0ab0..0184829ad0 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -403,8 +403,9 @@ public static function get_inboxes( $user_id ) { */ public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) { $activity = \json_decode( $json, true ); - // Only if this is a Delete. Create handles its own "Announce" in dual user mode. - if ( 'Delete' === ( $activity['type'] ?? null ) ) { + // Delete and Move activities should be sent to all known inboxes. + // Create handles its own "Announce" in dual user mode. + if ( \in_array( $activity['type'] ?? null, array( 'Delete', 'Move' ), true ) ) { $inboxes = Remote_Actors::get_inboxes(); } else { $inboxes = self::get_inboxes( $actor_id ); From 2f89fd7a34849988a92e231dc541b765cc7d4d1a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 11:51:13 +0100 Subject: [PATCH 4/9] Remove set_to call from migration activity Eliminated the call to set_to with Remote_Actors::get_inboxes() when creating migration activities. This may be to adjust how recipients are handled or to prevent unnecessary inbox notifications. --- includes/class-migration.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index ae5b9041f9..44a1537cc3 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1110,7 +1110,6 @@ private static function migrate_blog_user_to_query_param_id() { $activity->set_origin( $blog->get_url() ); $activity->set_object( $blog->get_url() ); $activity->set_target( $blog->get_id() ); - $activity->set_to( Remote_Actors::get_inboxes() ); Outbox::add( $activity, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } From a0d8dc1e7cd120f08f28789d4d8ce5f518e5d17f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 11:57:59 +0100 Subject: [PATCH 5/9] Use old blog ID for Move activity actor fields Updated the 'Move' activity to set actor, origin, and object fields to the old blog ID format using the preferred username, ensuring correct identification during migration. --- includes/class-migration.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 44a1537cc3..31b0191bfb 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1102,13 +1102,14 @@ private static function migrate_blog_user_to_query_param_id() { return; } - $blog = new Blog(); + $blog = new Blog(); + $old_id = \esc_url( \trailingslashit( get_home_url() ) . '@' . $blog->get_preferred_username() ); $activity = new Activity(); $activity->set_type( 'Move' ); - $activity->set_actor( $blog->get_url() ); - $activity->set_origin( $blog->get_url() ); - $activity->set_object( $blog->get_url() ); + $activity->set_actor( $old_id ); + $activity->set_origin( $old_id ); + $activity->set_object( $old_id ); $activity->set_target( $blog->get_id() ); Outbox::add( $activity, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); From 484f6a1ca5c13f60249d90ecc17e9657445409fd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 12:05:55 +0100 Subject: [PATCH 6/9] Add migration for all users to query param ID format --- includes/class-migration.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/includes/class-migration.php b/includes/class-migration.php index 31b0191bfb..4cce2c95ac 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -15,6 +15,7 @@ use Activitypub\Collection\Outbox; use Activitypub\Collection\Remote_Actors; use Activitypub\Model\Blog; +use Activitypub\Model\User; use Activitypub\Transformer\Factory; /** @@ -219,6 +220,7 @@ public static function maybe_migrate() { if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) { self::migrate_blog_user_to_query_param_id(); + self::migrate_users_to_query_param_id(); } // Ensure all required cron schedules are registered. @@ -1115,6 +1117,40 @@ private static function migrate_blog_user_to_query_param_id() { Outbox::add( $activity, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } + /** + * Migrate users from permalink-based ID to query param ID. + * + * This sends a Move activity from the old author URL to the new ?author= URL + * for each user that had the permalink-as-id setting. + */ + private static function migrate_users_to_query_param_id() { + $users = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + ) + ); + + foreach ( $users as $wp_user ) { + $use_permalink = \get_user_option( 'activitypub_use_permalink_as_id', $wp_user->ID ); + + if ( '1' !== $use_permalink ) { + continue; + } + + $user = new User( $wp_user->ID ); + $old_id = $user->get_url(); + + $activity = new Activity(); + $activity->set_type( 'Move' ); + $activity->set_actor( $old_id ); + $activity->set_origin( $old_id ); + $activity->set_object( $old_id ); + $activity->set_target( $user->get_id() ); + + Outbox::add( $activity, $wp_user->ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); + } + } + /** * Migrate avatar URLs from comment meta to remote actors in batches. * From 57549b0877cd2b2ed0a2e0d0be32888e2a3d951a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 12:08:54 +0100 Subject: [PATCH 7/9] Replace Outbox::add with add_to_outbox in migration Updated calls to Outbox::add to use the add_to_outbox function with an added null parameter. This change likely reflects a refactor or update in the outbox handling API. --- includes/class-migration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 4cce2c95ac..91b2b4004a 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1114,7 +1114,7 @@ private static function migrate_blog_user_to_query_param_id() { $activity->set_object( $old_id ); $activity->set_target( $blog->get_id() ); - Outbox::add( $activity, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); + add_to_outbox( $activity, null, Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } /** @@ -1147,7 +1147,7 @@ private static function migrate_users_to_query_param_id() { $activity->set_object( $old_id ); $activity->set_target( $user->get_id() ); - Outbox::add( $activity, $wp_user->ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); + add_to_outbox( $activity, null, $wp_user->ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } } From f16d419515207dcc35d5560bafa72ba193d2e07f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jan 2026 12:19:18 +0100 Subject: [PATCH 8/9] Use ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE for Move activities. --- includes/class-move.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-move.php b/includes/class-move.php index c7afca6461..9022638bca 100644 --- a/includes/class-move.php +++ b/includes/class-move.php @@ -111,7 +111,7 @@ public static function externally( $from, $to ) { $activity->set_target( $target_actor->get_id() ); // Add to outbox. - return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } /** @@ -162,7 +162,7 @@ public static function internally( $from, $to ) { $activity->set_object( $actor ); $activity->set_target( $to ); - return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); + return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } /** From 48261e85bbf193fc2b76182845fe1a2b6c829f1d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jan 2026 10:56:29 +0100 Subject: [PATCH 9/9] Set default offset to 5 in schedule_outbox_activity_for_federation Changed the default value of the $offset parameter from 0 to 5 in the schedule_outbox_activity_for_federation method to adjust the scheduling behavior. --- includes/class-scheduler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 19ea1ba22b..8ebabc9ec4 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -276,7 +276,7 @@ public static function cleanup_remote_actors() { * @param int $id The ID of the outbox item. * @param int $offset The offset to add to the scheduled time. */ - public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) { + public static function schedule_outbox_activity_for_federation( $id, $offset = 5 ) { $hook = 'activitypub_process_outbox'; $args = array( $id );