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

213 lines 6.1 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 \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
30 }
31
32 /**
33 * Handle Move requests.
34 *
35 * @param array $activity The JSON "Move" Activity.
36 * @param int|int[] $user_ids The user ID(s).
37 */
38 public static function handle_move( $activity, $user_ids ) {
39 $target_uri = self::extract_target( $activity );
40 $origin_uri = self::extract_origin( $activity );
41
42 if ( ! $target_uri || ! $origin_uri ) {
43 return;
44 }
45
46 $target_json = Http::get_remote_object( $target_uri );
47 $origin_json = Http::get_remote_object( $origin_uri );
48
49 $verified = self::verify_move( $target_json, $origin_json );
50
51 if ( ! $verified ) {
52 return;
53 }
54
55 $target_object = Remote_Actors::get_by_uri( $target_uri );
56 $origin_object = Remote_Actors::get_by_uri( $origin_uri );
57 $result = null;
58 $success = false;
59
60 // If the origin is followed but the target is not, update the origin to point to the target.
61 if ( \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) {
62 global $wpdb;
63 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
64 $wpdb->update(
65 $wpdb->posts,
66 array( 'guid' => sanitize_url( $target_uri ) ),
67 array( 'ID' => sanitize_key( $origin_object->ID ) )
68 );
69
70 // Clear the cache.
71 \wp_cache_delete( $origin_object->ID, 'posts' );
72
73 $success = true;
74 $result = Remote_Actors::upsert( $target_json );
75 }
76
77 // If both the target and origin are followed, merge them.
78 if ( ! \is_wp_error( $target_object ) && ! \is_wp_error( $origin_object ) ) {
79 $origin_users = \get_post_meta( $origin_object->ID, Followers::FOLLOWER_META_KEY, false );
80 $target_users = \get_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, false );
81
82 // Get all user ids from $origin_users that are not in $target_users.
83 $users = \array_diff( $origin_users, $target_users );
84
85 foreach ( $users as $follower_user_id ) {
86 \add_post_meta( $target_object->ID, Followers::FOLLOWER_META_KEY, $follower_user_id );
87 }
88
89 $success = true;
90 $result = \wp_delete_post( $origin_object->ID );
91 }
92
93 /**
94 * Fires after an ActivityPub Move activity has been handled.
95 *
96 * @param array $activity The ActivityPub activity data.
97 * @param int[] $user_ids The local user IDs.
98 * @param bool $success True on success, false otherwise.
99 * @param mixed $result The result of the operation (e.g., post ID, WP_Error, or status).
100 */
101 \do_action( 'activitypub_handled_move', $activity, (array) $user_ids, $success, $result );
102 }
103
104 /**
105 * Convert the object and origin to the correct format.
106 *
107 * @param \Activitypub\Activity\Activity $activity The Activity object.
108 * @return \Activitypub\Activity\Activity The filtered Activity object.
109 */
110 public static function outbox_activity( $activity ) {
111 if ( 'Move' === $activity->get_type() ) {
112 $activity->set_object( object_to_uri( $activity->get_object() ) );
113 $activity->set_origin( $activity->get_actor() );
114 $activity->set_target( $activity->get_object() );
115 }
116
117 return $activity;
118 }
119
120 /**
121 * Extract the target from the activity.
122 *
123 * The ActivityStreams spec define the `target` attribute as the
124 * destination of the activity, but Mastodon uses the `object`
125 * attribute to move profiles.
126 *
127 * @param array $activity The JSON "Move" Activity.
128 *
129 * @return string|null The target URI or null if not found.
130 */
131 private static function extract_target( $activity ) {
132 if ( ! empty( $activity['target'] ) ) {
133 return object_to_uri( $activity['target'] );
134 }
135
136 if ( ! empty( $activity['object'] ) ) {
137 return object_to_uri( $activity['object'] );
138 }
139
140 return null;
141 }
142
143 /**
144 * Extract the origin from the activity.
145 *
146 * The ActivityStreams spec define the `origin` attribute as source
147 * of the activity, but Mastodon uses the `actor` attribute as source
148 * to move profiles.
149 *
150 * @param array $activity The JSON "Move" Activity.
151 *
152 * @return string|null The origin URI or null if not found.
153 */
154 private static function extract_origin( $activity ) {
155 if ( ! empty( $activity['origin'] ) ) {
156 return object_to_uri( $activity['origin'] );
157 }
158
159 if ( ! empty( $activity['actor'] ) ) {
160 return object_to_uri( $activity['actor'] );
161 }
162
163 return null;
164 }
165
166 /**
167 * Verify the move.
168 *
169 * @param array $target_object The target object.
170 * @param array $origin_object The origin object.
171 *
172 * @return bool True if the move is verified, false otherwise.
173 */
174 private static function verify_move( $target_object, $origin_object ) {
175 // Check if both objects are valid.
176 if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) {
177 return false;
178 }
179
180 // Check if both objects are persons.
181 if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) {
182 return false;
183 }
184
185 // Check if the target and origin are not the same.
186 if ( $target_object['id'] === $origin_object['id'] ) {
187 return false;
188 }
189
190 // Check if the target has an alsoKnownAs property.
191 if ( empty( $target_object['also_known_as'] ) ) {
192 return false;
193 }
194
195 // Check if the origin is in the alsoKnownAs property of the target.
196 if ( ! in_array( $origin_object['id'], $target_object['also_known_as'], true ) ) {
197 return false;
198 }
199
200 // Check if the origin has a movedTo property.
201 if ( empty( $origin_object['movedTo'] ) ) {
202 return false;
203 }
204
205 // Check if the movedTo property of the origin is the target.
206 if ( $origin_object['movedTo'] !== $target_object['id'] ) {
207 return false;
208 }
209
210 return true;
211 }
212 }
213