PluginProbe
ActivityPub / 8.1.1
ActivityPub v8.1.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 8.1.1, at includes/handler/class-move.php

206 lines 5.7 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 $result = Remote_Actors::upsert( $target_json );
74 }
75
76 // If both the target and origin are followed, merge them.
77 if ( ! \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) {
78 $origin_users = \get_post_meta( $origin_object->ID, Followers::FOLLOWER_META_KEY, false );
79 $target_users = \get_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, false );
80
81 // Get all user ids from $origin_users that are not in $target_users.
82 $users = \array_diff( $origin_users, $target_users );
83
84 foreach ( $users as $follower_user_id ) {
85 \add_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, $follower_user_id );
86 }
87
88 $success = true;
89 $result = \wp_delete_post( $origin_object->ID );
90 }
91
92 /**
93 * Fires after an ActivityPub Move activity has been handled.
94 *
95 * @param array $activity The ActivityPub activity data.
96 * @param int[] $user_ids The local user IDs.
97 * @param bool $success True on success, false otherwise.
98 * @param mixed $result The result of the operation (e.g., post ID, WP_Error, or status).
99 */
100 \do_action( 'activitypub_handled_move', $activity, (array) $user_ids, $success, $result );
101 }
102
103 /**
104 * Extract the target from the activity.
105 *
106 * The ActivityStreams spec define the `target` attribute as the
107 * destination of the activity, but Mastodon uses the `object`
108 * attribute to move profiles.
109 *
110 * @param array $activity The JSON "Move" Activity.
111 *
112 * @return string|null The target URI or null if not found.
113 */
114 private static function extract_target( $activity ) {
115 if ( ! empty( $activity['target'] ) ) {
116 return object_to_uri( $activity['target'] );
117 }
118
119 if ( ! empty( $activity['object'] ) ) {
120 return object_to_uri( $activity['object'] );
121 }
122
123 return null;
124 }
125
126 /**
127 * Extract the origin from the activity.
128 *
129 * The ActivityStreams spec define the `origin` attribute as source
130 * of the activity, but Mastodon uses the `actor` attribute as source
131 * to move profiles.
132 *
133 * @param array $activity The JSON "Move" Activity.
134 *
135 * @return string|null The origin URI or null if not found.
136 */
137 private static function extract_origin( $activity ) {
138 if ( ! empty( $activity['origin'] ) ) {
139 return object_to_uri( $activity['origin'] );
140 }
141
142 if ( ! empty( $activity['actor'] ) ) {
143 return object_to_uri( $activity['actor'] );
144 }
145
146 return null;
147 }
148
149 /**
150 * Verify the move.
151 *
152 * @param array $target_object The target object.
153 * @param array $origin_object The origin object.
154 *
155 * @return bool True if the move is verified, false otherwise.
156 */
157 private static function verify_move( $target_object, $origin_object ) {
158 // Check if both objects are valid.
159 if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) {
160 return false;
161 }
162
163 // Check if both objects are persons.
164 if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) {
165 return false;
166 }
167
168 // Check if the target and origin are not the same.
169 if ( $target_object['id'] === $origin_object['id'] ) {
170 return false;
171 }
172
173 // Normalize alsoKnownAs to an array (some JSON-LD payloads may use a string).
174 $also_known_as = (array) ( $target_object['alsoKnownAs'] ?? array() );
175 if ( empty( $also_known_as ) ) {
176 return false;
177 }
178
179 // Collect all possible origin identifiers (id, url, webfinger).
180 $origin_ids = array_filter(
181 array(
182 $origin_object['id'] ?? null,
183 $origin_object['url'] ?? null,
184 $origin_object['webfinger'] ?? null,
185 )
186 );
187
188 // Check if any origin identifier is in the alsoKnownAs property of the target.
189 if ( ! array_intersect( $origin_ids, $also_known_as ) ) {
190 return false;
191 }
192
193 // Check if the origin has a movedTo property.
194 if ( empty( $origin_object['movedTo'] ) ) {
195 return false;
196 }
197
198 // Check if the movedTo property of the origin is the target.
199 if ( $origin_object['movedTo'] !== $target_object['id'] ) {
200 return false;
201 }
202
203 return true;
204 }
205 }
206