diff --git a/includes/class-migration.php b/includes/class-migration.php index 7c4c7df858..a6b95f7960 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,12 +7,15 @@ 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\Model\User; use Activitypub\Transformer\Factory; /** @@ -227,6 +230,11 @@ public static function maybe_migrate() { */ \add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 ); + 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. Scheduler::register_schedules(); @@ -1093,6 +1101,66 @@ 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 Blog(); + $old_id = \esc_url( \trailingslashit( get_home_url() ) . '@' . $blog->get_preferred_username() ); + + $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( $blog->get_id() ); + + add_to_outbox( $activity, null, 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() ); + + add_to_outbox( $activity, null, $wp_user->ID, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); + } + } + /** * Migrate URLs from the legacy `activitypub_tombstone_urls` option into the * `ap_tombstone` custom post type. diff --git a/includes/class-move.php b/includes/class-move.php index 4f553442bc..149288a702 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 ); } /** diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index edc32c8838..f10eebe852 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -394,8 +394,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 ); diff --git a/includes/model/class-blog.php b/includes/model/class-blog.php index f8ca03c4e0..f903e78abe 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( \home_url( '/@' . $this->get_preferred_username() ) ); - } - return \add_query_arg( 'author', $this->_id, \home_url( '/' ) ); } @@ -594,11 +588,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 7adb272738..a667f5f31f 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( '/' ) ); } @@ -476,11 +470,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; } }