update( $wpdb->posts, array( 'guid' => sanitize_url( $target_uri ) ), array( 'ID' => sanitize_key( $origin_object->ID ) ) ); // Clear the cache. \wp_cache_delete( $origin_object->ID, 'posts' ); $success = true; $result = Remote_Actors::upsert( $target_json ); } // If both the target and origin are followed, merge them. if ( ! \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) { $origin_users = \get_post_meta( $origin_object->ID, Followers::FOLLOWER_META_KEY, false ); $target_users = \get_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, false ); // Get all user ids from $origin_users that are not in $target_users. $users = \array_diff( $origin_users, $target_users ); foreach ( $users as $follower_user_id ) { \add_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, $follower_user_id ); } $success = true; $result = \wp_delete_post( $origin_object->ID ); } /** * Fires after an ActivityPub Move activity has been handled. * * @param array $activity The ActivityPub activity data. * @param int[] $user_ids The local user IDs. * @param bool $success True on success, false otherwise. * @param mixed $result The result of the operation (e.g., post ID, WP_Error, or status). */ \do_action( 'activitypub_handled_move', $activity, (array) $user_ids, $success, $result ); } /** * Extract the target from the activity. * * The ActivityStreams spec define the `target` attribute as the * destination of the activity, but Mastodon uses the `object` * attribute to move profiles. * * @param array $activity The JSON "Move" Activity. * * @return string|null The target URI or null if not found. */ private static function extract_target( $activity ) { if ( ! empty( $activity['target'] ) ) { return object_to_uri( $activity['target'] ); } if ( ! empty( $activity['object'] ) ) { return object_to_uri( $activity['object'] ); } return null; } /** * Extract the origin from the activity. * * The ActivityStreams spec define the `origin` attribute as source * of the activity, but Mastodon uses the `actor` attribute as source * to move profiles. * * @param array $activity The JSON "Move" Activity. * * @return string|null The origin URI or null if not found. */ private static function extract_origin( $activity ) { if ( ! empty( $activity['origin'] ) ) { return object_to_uri( $activity['origin'] ); } if ( ! empty( $activity['actor'] ) ) { return object_to_uri( $activity['actor'] ); } return null; } /** * Verify the move. * * @param array $target_object The target object. * @param array $origin_object The origin object. * * @return bool True if the move is verified, false otherwise. */ private static function verify_move( $target_object, $origin_object ) { // Check if both objects are valid. if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) { return false; } // Check if both objects are persons. if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) { return false; } // Check if the target and origin are not the same. if ( $target_object['id'] === $origin_object['id'] ) { return false; } // 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; } // 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; } // Check if the origin has a movedTo property. if ( empty( $origin_object['movedTo'] ) ) { return false; } // Check if the movedTo property of the origin is the target. if ( $origin_object['movedTo'] !== $target_object['id'] ) { return false; } return true; } }