PluginProbe
ActivityPub / 9.2.1
ActivityPub v9.2.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / handler / class-move.php

class-move.php in ActivityPub 9.2.1, at includes/handler/class-move.php

208 lines 5.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Move handler file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Handler;
9
10 use Activitypub\Collection\Followers;
11 use Activitypub\Collection\Remote_Actors;
12 use Activitypub\Http;
13
14 use function Activitypub\object_to_uri;
15
16 /**
17 * Handle Move requests.
18 *
19 * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
20 * @see https://docs.joinmastodon.org/user/moving/
21 * @see https://docs.joinmastodon.org/spec/activitypub/#Move
22 */
23 class Move {
24 /**
25 * Initialize the class, registering WordPress hooks.
26 */
27 public static function init() {
28 \add_action( 'activitypub_inbox_move', array( self::class, 'handle_move' ), 10, 2 );
29 }
30
31 /**
32 * Handle Move requests.
33 *
34 * @param array $activity The JSON "Move" Activity.
35 * @param int|int[] $user_ids The user ID(s).
36 */
37 public static function handle_move( $activity, $user_ids ) {
38 $target_uri = self::extract_target( $activity );
39 $origin_uri = self::extract_origin( $activity );
40
41 if ( ! $target_uri || ! $origin_uri ) {
42 return;
43 }
44
45 $target_json = Http::get_remote_object( $target_uri );
46 $origin_json = Http::get_remote_object( $origin_uri );
47
48 $verified = self::verify_move( $target_json, $origin_json );
49
50 if ( ! $verified ) {
51 return;
52 }
53
54 $target_object = Remote_Actors::get_by_uri( $target_uri );
55 $origin_object = Remote_Actors::get_by_uri( $origin_uri );
56 $result = null;
57 $success = false;
58
59 // If the origin is followed but the target is not, update the origin to point to the target.
60 if ( \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) {
61 global $wpdb;
62 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
63 $wpdb->update(
64 $wpdb->posts,
65 array( 'guid' => \sanitize_url( $target_uri ) ),
66 array( 'ID' => \sanitize_key( $origin_object->ID ) )
67 );
68
69 // Clear the cache.
70 \wp_cache_delete( $origin_object->ID, 'posts' );
71
72 $success = true;
73
74 // get_remote_object() already self-confirmed the target, so it is safe to cache.
75 $result = Remote_Actors::upsert( $target_json );
76 }
77
78 // If both the target and origin are followed, merge them.
79 if ( ! \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) {
80 $origin_users = \get_post_meta( $origin_object->ID, Followers::FOLLOWER_META_KEY, false );
81 $target_users = \get_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, false );
82
83 // Get all user ids from $origin_users that are not in $target_users.
84 $users = \array_diff( $origin_users, $target_users );
85
86 foreach ( $users as $follower_user_id ) {
87 \add_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, $follower_user_id );
88 }
89
90 $success = true;
91 $result = \wp_delete_post( $origin_object->ID );
92 }
93
94 /**
95 * Fires after an ActivityPub Move activity has been handled.
96 *
97 * @param array $activity The ActivityPub activity data.
98 * @param int[] $user_ids The local user IDs.
99 * @param bool $success True on success, false otherwise.
100 * @param mixed $result The result of the operation (e.g., post ID, WP_Error, or status).
101 */
102 \do_action( 'activitypub_handled_move', $activity, (array) $user_ids, $success, $result );
103 }
104
105 /**
106 * Extract the target from the activity.
107 *
108 * The ActivityStreams spec define the `target` attribute as the
109 * destination of the activity, but Mastodon uses the `object`
110 * attribute to move profiles.
111 *
112 * @param array $activity The JSON "Move" Activity.
113 *
114 * @return string|null The target URI or null if not found.
115 */
116 private static function extract_target( $activity ) {
117 if ( ! empty( $activity['target'] ) ) {
118 return object_to_uri( $activity['target'] );
119 }
120
121 if ( ! empty( $activity['object'] ) ) {
122 return object_to_uri( $activity['object'] );
123 }
124
125 return null;
126 }
127
128 /**
129 * Extract the origin from the activity.
130 *
131 * The ActivityStreams spec define the `origin` attribute as source
132 * of the activity, but Mastodon uses the `actor` attribute as source
133 * to move profiles.
134 *
135 * @param array $activity The JSON "Move" Activity.
136 *
137 * @return string|null The origin URI or null if not found.
138 */
139 private static function extract_origin( $activity ) {
140 if ( ! empty( $activity['origin'] ) ) {
141 return object_to_uri( $activity['origin'] );
142 }
143
144 if ( ! empty( $activity['actor'] ) ) {
145 return object_to_uri( $activity['actor'] );
146 }
147
148 return null;
149 }
150
151 /**
152 * Verify the move.
153 *
154 * @param array $target_object The target object.
155 * @param array $origin_object The origin object.
156 *
157 * @return bool True if the move is verified, false otherwise.
158 */
159 private static function verify_move( $target_object, $origin_object ) {
160 // Check if both objects are valid.
161 if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) {
162 return false;
163 }
164
165 // Check if both objects are persons.
166 if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) {
167 return false;
168 }
169
170 // Check if the target and origin are not the same.
171 if ( $target_object['id'] === $origin_object['id'] ) {
172 return false;
173 }
174
175 // Normalize alsoKnownAs to an array (some JSON-LD payloads may use a string).
176 $also_known_as = (array) ( $target_object['alsoKnownAs'] ?? array() );
177 if ( empty( $also_known_as ) ) {
178 return false;
179 }
180
181 // Collect all possible origin identifiers (id, url, webfinger).
182 $origin_ids = \array_filter(
183 array(
184 $origin_object['id'] ?? null,
185 $origin_object['url'] ?? null,
186 $origin_object['webfinger'] ?? null,
187 )
188 );
189
190 // Check if any origin identifier is in the alsoKnownAs property of the target.
191 if ( ! \array_intersect( $origin_ids, $also_known_as ) ) {
192 return false;
193 }
194
195 // Check if the origin has a movedTo property.
196 if ( empty( $origin_object['movedTo'] ) ) {
197 return false;
198 }
199
200 // Check if the movedTo property of the origin is the target.
201 if ( $origin_object['movedTo'] !== $target_object['id'] ) {
202 return false;
203 }
204
205 return true;
206 }
207 }
208