PluginProbe
ActivityPub / 7.8.2
ActivityPub v7.8.2
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 / class-move.php

class-move.php in ActivityPub 7.8.2, at includes/class-move.php

313 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Move class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Actor;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Model\Blog;
14 use Activitypub\Model\User;
15
16 /**
17 * ActivityPub (Account) Move Class
18 *
19 * @author Matthias Pfefferle
20 */
21 class Move {
22
23 /**
24 * Initialize the Move class.
25 */
26 public static function init() {
27 /**
28 * Filter to enable automatically moving Fediverse accounts when the domain changes.
29 *
30 * @param bool $domain_moves_enabled Whether domain moves are enabled.
31 */
32 $domain_moves_enabled = apply_filters( 'activitypub_enable_primary_domain_moves', false );
33
34 if ( $domain_moves_enabled ) {
35 // Add the filter to change the domain.
36 \add_filter( 'update_option_home', array( self::class, 'change_domain' ), 10, 2 );
37
38 if ( get_option( 'activitypub_old_host' ) ) {
39 \add_action( 'activitypub_construct_model_actor', array( self::class, 'maybe_initiate_old_user' ) );
40 \add_action( 'activitypub_pre_send_to_inboxes', array( self::class, 'pre_send_to_inboxes' ) );
41
42 if ( ! is_user_type_disabled( 'blog' ) ) {
43 \add_filter( 'activitypub_pre_get_by_username', array( self::class, 'old_blog_username' ), 10, 2 );
44 }
45 }
46 }
47 }
48
49 /**
50 * Move an ActivityPub account from one location to another.
51 *
52 * @param string $from The current account URL.
53 * @param string $to The new account URL.
54 *
55 * @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
56 */
57 public static function account( $from, $to ) {
58 if ( is_same_domain( $from ) && is_same_domain( $to ) ) {
59 return self::internally( $from, $to );
60 }
61
62 return self::externally( $from, $to );
63 }
64
65 /**
66 * Move an ActivityPub Actor from one location (internal) to another (external).
67 *
68 * This helps with migrating local profiles to a new external profile:
69 *
70 * `Move::externally( 'https://example.com/?author=123', 'https://mastodon.example/users/foo' );`
71 *
72 * @param string $from The current account URL.
73 * @param string $to The new account URL.
74 *
75 * @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
76 */
77 public static function externally( $from, $to ) {
78 $user = Actors::get_by_various( $from );
79
80 if ( \is_wp_error( $user ) ) {
81 return $user;
82 }
83
84 // Update the movedTo property.
85 if ( $user->get__id() > 0 ) {
86 \update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
87 } else {
88 \update_option( 'activitypub_blog_user_moved_to', $to );
89 }
90
91 $response = Http::get_remote_object( $to );
92
93 if ( \is_wp_error( $response ) ) {
94 return $response;
95 }
96
97 $target_actor = new Actor();
98 $target_actor->from_array( $response );
99
100 // Check if the `Move` Activity is valid.
101 $also_known_as = $target_actor->get_also_known_as() ?? array();
102 if ( ! in_array( $from, $also_known_as, true ) ) {
103 return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) );
104 }
105
106 $activity = new Activity();
107 $activity->set_type( 'Move' );
108 $activity->set_actor( $user->get_id() );
109 $activity->set_origin( $user->get_id() );
110 $activity->set_object( $user->get_id() );
111 $activity->set_target( $target_actor->get_id() );
112
113 // Add to outbox.
114 return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
115 }
116
117 /**
118 * Internal Move.
119 *
120 * Move an ActivityPub Actor from one location (internal) to another (internal).
121 *
122 * This helps with migrating abandoned profiles to `Move` to other profiles:
123 *
124 * `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );`
125 *
126 * ... or to change Actor-IDs like:
127 *
128 * `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );`
129 *
130 * @param string $from The current account URL.
131 * @param string $to The new account URL.
132 *
133 * @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
134 */
135 public static function internally( $from, $to ) {
136 $user = Actors::get_by_various( $from );
137
138 if ( \is_wp_error( $user ) ) {
139 return $user;
140 }
141
142 // Add the old account URL to alsoKnownAs.
143 if ( $user->get__id() > 0 ) {
144 self::update_user_also_known_as( $user->get__id(), $from );
145 \update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
146 } else {
147 self::update_blog_also_known_as( $from );
148 \update_option( 'activitypub_blog_user_moved_to', $to );
149 }
150
151 // check if `$from` is a URL or an ID.
152 if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) {
153 $actor = $from;
154 } else {
155 $actor = $user->get_id();
156 }
157
158 $activity = new Activity();
159 $activity->set_type( 'Move' );
160 $activity->set_actor( $actor );
161 $activity->set_origin( $actor );
162 $activity->set_object( $actor );
163 $activity->set_target( $to );
164
165 return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
166 }
167
168 /**
169 * Update the alsoKnownAs property of a user.
170 *
171 * @param int $user_id The user ID.
172 * @param string $from The current account URL.
173 */
174 private static function update_user_also_known_as( $user_id, $from ) {
175 $also_known_as = \get_user_option( 'activitypub_also_known_as', $user_id ) ?: array();
176 $also_known_as[] = $from;
177
178 \update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
179 }
180
181 /**
182 * Update the alsoKnownAs property of the blog.
183 *
184 * @param string $from The current account URL.
185 */
186 private static function update_blog_also_known_as( $from ) {
187 $also_known_as = \get_option( 'activitypub_blog_user_also_known_as', array() );
188 $also_known_as[] = $from;
189
190 \update_option( 'activitypub_blog_user_also_known_as', $also_known_as );
191 }
192
193 /**
194 * Change domain for all ActivityPub Actors.
195 *
196 * This method handles domain migration according to the ActivityPub Data Portability spec.
197 * It stores the old host and calls Move::internally for each available profile.
198 * It also caches the JSON representation of the old Actor for future lookups.
199 *
200 * @param string $from The old domain.
201 * @param string $to The new domain.
202 *
203 * @return array Array of results from Move::internally calls.
204 */
205 public static function change_domain( $from, $to ) {
206 // Get all actors that need to be migrated.
207 $actors = Actors::get_all();
208
209 $results = array();
210 $to_host = \wp_parse_url( $to, \PHP_URL_HOST );
211 $from_host = \wp_parse_url( $from, \PHP_URL_HOST );
212
213 // Store the old host for future reference.
214 \update_option( 'activitypub_old_host', $from_host );
215
216 // Process each actor.
217 foreach ( $actors as $actor ) {
218 $actor_id = $actor->get_id();
219
220 // Replace the new host with the old host in the actor ID.
221 $old_actor_id = str_replace( $to_host, $from_host, $actor_id );
222
223 // Call Move::internally for this actor.
224 $result = self::internally( $old_actor_id, $actor_id );
225
226 if ( \is_wp_error( $result ) ) {
227 // Log the error and continue with the next actor.
228 Debug::write_log( 'Error moving actor: ' . $actor_id . ' - ' . $result->get_error_message() );
229 continue;
230 }
231
232 $json = str_replace( $to_host, $from_host, $actor->to_json() );
233
234 // Save the current actor data after migration.
235 if ( $actor instanceof Blog ) {
236 \update_option( 'activitypub_blog_user_old_host_data', $json, false );
237 } else {
238 \update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json );
239 }
240
241 $results[] = array(
242 'actor' => $actor_id,
243 'result' => $result,
244 );
245 }
246
247 return $results;
248 }
249
250 /**
251 * Maybe initiate old user.
252 *
253 * This method checks if the current request domain matches the old host.
254 * If it does, it retrieves the cached data for the user and populates the instance.
255 *
256 * @param Blog|User $instance The Blog or User instance to populate.
257 */
258 public static function maybe_initiate_old_user( $instance ) {
259 if ( ! Query::get_instance()->is_old_host_request() ) {
260 return;
261 }
262
263 if ( $instance instanceof Blog ) {
264 $cached_data = \get_option( 'activitypub_blog_user_old_host_data' );
265 } elseif ( $instance instanceof User ) {
266 $cached_data = \get_user_option( 'activitypub_old_host_data', $instance->get__id() );
267 }
268
269 if ( ! empty( $cached_data ) ) {
270 $instance->from_json( $cached_data );
271 }
272 }
273
274 /**
275 * Pre-send to inboxes.
276 *
277 * @param string $json The ActivityPub Activity JSON.
278 */
279 public static function pre_send_to_inboxes( $json ) {
280 $json = json_decode( $json, true );
281
282 if ( 'Move' !== $json['type'] ) {
283 return;
284 }
285
286 if ( is_same_domain( $json['object'] ) ) {
287 return;
288 }
289
290 Query::get_instance()->set_old_host_request();
291 }
292
293 /**
294 * Filter to return the old blog username.
295 *
296 * @param null $pre The pre-existing value.
297 * @param string $username The username to check.
298 *
299 * @return Blog|null The old blog instance or null.
300 */
301 public static function old_blog_username( $pre, $username ) {
302 $old_host = \get_option( 'activitypub_old_host' );
303
304 // Special case for Blog Actor on old host.
305 if ( $old_host === $username && Query::get_instance()->is_old_host_request() ) {
306 // Return a new Blog instance which will load the cached data in its constructor.
307 $pre = new Blog();
308 }
309
310 return $pre;
311 }
312 }
313