PluginProbe
ActivityPub / 3.2.2
ActivityPub v3.2.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
← All changes | includes/class-migration.php +89 -1082 9.0.23.2.2 View file →
@@ -1,20 +1,10 @@
1 1 <?php
2 -/**
3 - * Migration class file.
4 - *
5 - * @package Activitypub
6 - */
7 -
8 2 namespace Activitypub;
9 3
10 -use Activitypub\Collection\Actors;
11 -use Activitypub\Collection\Extra_Fields;
4 +use Activitypub\Activitypub;
5 +use Activitypub\Model\Blog;
12 6 use Activitypub\Collection\Followers;
13 -use Activitypub\Collection\Following;
14 -use Activitypub\Collection\Outbox;
15 -use Activitypub\Collection\Remote_Actors;
16 -use Activitypub\Transformer\Factory;
17 7
18 8 /**
19 9 * ActivityPub Migration Class
20 10 *
@@ -21,21 +11,26 @@
21 11 * @author Matthias Pfefferle
22 12 */
23 13 class Migration {
24 14 /**
25 - * Initialize the class, registering WordPress hooks.
15 + * Initialize the class, registering WordPress hooks
26 16 */
27 17 public static function init() {
18 + \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
19 +
28 20 self::maybe_migrate();
21 + }
29 22
30 - Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) );
31 - Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) );
32 - Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) );
33 - Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
34 - Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
35 - Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) );
36 - Scheduler::register_async_batch_callback( 'activitypub_backfill_statistics', array( Statistics::class, 'backfill_historical_stats' ) );
37 - Scheduler::register_async_batch_callback( 'activitypub_tombstone_migrate', array( self::class, 'migrate_tombstones_to_cpt' ) );
23 + /**
24 + * Get the target version.
25 + *
26 + * This is the version that the database structure will be updated to.
27 + * It is the same as the plugin version.
28 + *
29 + * @return string The target version.
30 + */
31 + public static function get_target_version() {
32 + return get_plugin_version();
38 33 }
39 34
40 35 /**
41 36 * The current version of the database structure.
@@ -48,25 +43,18 @@
48 43
49 44 /**
50 45 * Locks the database migration process to prevent simultaneous migrations.
51 46 *
52 - * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
47 + * @return void
53 48 */
54 49 public static function lock() {
55 - global $wpdb;
56 -
57 - // Try to lock.
58 - $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
59 -
60 - if ( ! $lock_result ) {
61 - $lock_result = \get_option( 'activitypub_migration_lock' );
62 - }
63 -
64 - return $lock_result;
50 + \update_option( 'activitypub_migration_lock', \time() );
65 51 }
66 52
67 53 /**
68 54 * Unlocks the database migration process.
55 + *
56 + * @return void
69 57 */
70 58 public static function unlock() {
71 59 \delete_option( 'activitypub_migration_lock' );
72 60 }
@@ -98,11 +86,11 @@
98 86 *
99 87 * @return bool True if the database structure is up to date, false otherwise.
100 88 */
101 89 public static function is_latest_version() {
102 - return (bool) \version_compare(
90 + return (bool) version_compare(
103 91 self::get_version(),
104 - ACTIVITYPUB_PLUGIN_VERSION,
92 + self::get_target_version(),
105 93 '=='
106 94 );
107 95 }
108 96
@@ -121,152 +109,61 @@
121 109 self::lock();
122 110
123 111 $version_from_db = self::get_version();
124 112
125 - // Check for initial migration.
113 + // check for inital migration
126 114 if ( ! $version_from_db ) {
127 115 self::add_default_settings();
128 - $version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
116 + $version_from_db = self::get_target_version();
129 117 }
130 118
131 - if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
119 + // schedule the async migration
120 + if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
121 + \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
122 + }
123 + if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
132 124 self::migrate_from_0_16();
133 125 }
134 - if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
135 - \wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' );
136 - }
137 - if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
126 + if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
138 127 self::migrate_from_1_2_0();
139 128 }
140 - if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
129 + if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
141 130 self::migrate_from_2_0_0();
142 131 }
143 - if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
132 + if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
144 133 self::migrate_from_2_2_0();
145 134 }
146 - if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
135 + if ( version_compare( $version_from_db, '3.0.0', '<' ) ) {
147 136 self::migrate_from_2_6_0();
148 137 }
149 - if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
150 - self::migrate_to_4_0_0();
151 - }
152 - if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
153 - self::migrate_to_4_1_0();
154 - }
155 - if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
156 - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
157 - }
158 - if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
159 - self::migrate_to_4_7_1();
160 - }
161 - if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
162 - self::migrate_to_4_7_2();
163 - }
164 - if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
165 - Scheduler::register_schedules();
166 - \wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' );
167 - \wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' );
168 - }
169 - if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
170 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
171 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
172 - }
173 - if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
174 - self::delete_mastodon_api_orphaned_extra_fields();
175 - }
176 - if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
177 - self::update_notification_options();
178 - }
179 - if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) {
180 - self::migrate_followers_to_ap_actor_cpt();
181 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
182 - }
183 - if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) {
184 - self::migrate_followers_to_ap_actor_cpt();
185 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
186 - }
187 - if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) {
188 - wp_unschedule_hook( 'activitypub_update_followers' );
189 - wp_unschedule_hook( 'activitypub_cleanup_followers' );
190 138
191 - if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
192 - \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' );
193 - }
139 + update_option( 'activitypub_db_version', self::get_target_version() );
194 140
195 - if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
196 - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' );
197 - }
141 + self::unlock();
142 + }
143 +
144 + /**
145 + * Asynchronously migrates the database structure.
146 + *
147 + * @param string $version_from_db The version from which to migrate.
148 + */
149 + public static function async_migration( $version_from_db ) {
150 + if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
151 + self::migrate_from_0_17();
198 152 }
199 - if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) {
200 - self::remove_pending_application_user_follow_requests();
201 - }
202 - if ( \version_compare( $version_from_db, '7.5.0', '<' ) ) {
203 - self::sync_jetpack_following_meta();
204 - }
205 - if ( \version_compare( $version_from_db, '7.6.0', '<' ) ) {
206 - self::clean_up_inbox();
207 - \wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
208 - }
209 - if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) {
210 - \wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' );
211 - }
212 - if ( \version_compare( $version_from_db, '8.1.0', '<' ) && ! \wp_next_scheduled( 'activitypub_backfill_statistics' ) ) {
213 - // Backfill historical statistics data (delay + jitter to avoid load spikes on hosts running many sites).
214 - \wp_schedule_single_event( \time() + HOUR_IN_SECONDS + \wp_rand( 0, 6 * HOUR_IN_SECONDS ), 'activitypub_backfill_statistics' );
215 - }
216 - if ( \version_compare( $version_from_db, '8.3.0', '<' ) ) {
217 - if ( ! \wp_next_scheduled( 'activitypub_tombstone_migrate' ) ) {
218 - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_tombstone_migrate' );
219 - }
220 - }
221 -
222 - /*
223 - * Defer the flush to late in the `init` cycle (priority 20). Migration::init
224 - * runs at priority 1, which is earlier than most plugins register their
225 - * rewrite rules. Flushing synchronously here would persist a truncated
226 - * ruleset that omits third-party rules added on `init` at priority 10.
227 - */
228 - \add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 );
229 -
230 - // Ensure all required cron schedules are registered.
231 - Scheduler::register_schedules();
232 -
233 - /*
234 - * Add new update routines above this comment. ^
235 - *
236 - * Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
237 - * The release script will automatically replace it with the actual version number.
238 - * Example:
239 - *
240 - * if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
241 - * // Update routine.
242 - * }
243 - */
244 -
245 - /**
246 - * Fires when the system has to be migrated.
247 - *
248 - * @param string $version_from_db The version from which to migrate.
249 - * @param string $target_version The target version to migrate to.
250 - */
251 - \do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
252 -
253 - \update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
254 -
255 - self::unlock();
256 153 }
257 154
258 155 /**
259 156 * Updates the custom template to use shortcodes instead of the deprecated templates.
157 + *
158 + * @return void
260 159 */
261 160 private static function migrate_from_0_16() {
262 161 // Get the custom template.
263 162 $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
264 163
265 - /*
266 - * If the old content exists but is a blank string, we're going to need a flag to updated it even
267 - * after setting it to the default contents.
268 - */
164 + // If the old content exists but is a blank string, we're going to need a flag to updated it even
165 + // after setting it to the default contents.
269 166 $need_update = false;
270 167
271 168 // If the old contents is blank, use the defaults.
272 169 if ( '' === $old_content ) {
@@ -292,25 +189,31 @@
292 189 }
293 190 }
294 191
295 192 /**
296 - * Updates the DB-schema of the followers-list.
193 + * Updates the DB-schema of the followers-list
194 + *
195 + * @return void
297 196 */
298 197 public static function migrate_from_0_17() {
299 - // Migrate followers.
198 + // migrate followers
300 199 foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
301 200 $followers = get_user_meta( $user_id, 'activitypub_followers', true );
302 201
303 202 if ( $followers ) {
304 203 foreach ( $followers as $actor ) {
305 - Followers::add( $user_id, $actor );
204 + Followers::add_follower( $user_id, $actor );
306 205 }
307 206 }
308 207 }
208 +
209 + Activitypub::flush_rewrite_rules();
309 210 }
310 211
311 212 /**
312 - * Clear the cache after updating to 1.3.0.
213 + * Clear the cache after updating to 1.3.0
214 + *
215 + * @return void
313 216 */
314 217 private static function migrate_from_1_2_0() {
315 218 $user_ids = \get_users(
316 219 array(
@@ -324,9 +227,11 @@
324 227 }
325 228 }
326 229
327 230 /**
328 - * Unschedule Hooks after updating to 2.0.0.
231 + * Unschedule Hooks after updating to 2.0.0
232 + *
233 + * @return void
329 234 */
330 235 private static function migrate_from_2_0_0() {
331 236 wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
332 237 wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
@@ -343,17 +248,21 @@
343 248 }
344 249
345 250 /**
346 251 * Add the ActivityPub capability to all users that can publish posts
347 - * Delete old meta to store followers.
252 + * Delete old meta to store followers
253 + *
254 + * @return void
348 255 */
349 256 private static function migrate_from_2_2_0() {
350 - // Add the ActivityPub capability to all users that can publish posts.
257 + // add the ActivityPub capability to all users that can publish posts
351 258 self::add_activitypub_capability();
352 259 }
353 260
354 261 /**
355 - * Rename DB fields.
262 + * Rename DB fields
263 + *
264 + * @return void
356 265 */
357 266 private static function migrate_from_2_6_0() {
358 267 wp_cache_flush();
359 268
@@ -363,398 +272,25 @@
363 272 self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
364 273 }
365 274
366 275 /**
367 - * * Update actor-mode settings.
368 - * * Get the ID of the latest blog post and save it to the options table.
369 - */
370 - private static function migrate_to_4_0_0() {
371 - $latest_post_id = 0;
372 -
373 - // Get the ID of the latest blog post and save it to the options table.
374 - $latest_post = get_posts(
375 - array(
376 - 'numberposts' => 1,
377 - 'orderby' => 'ID',
378 - 'order' => 'DESC',
379 - 'post_type' => 'any',
380 - 'post_status' => 'publish',
381 - )
382 - );
383 -
384 - if ( $latest_post ) {
385 - $latest_post_id = $latest_post[0]->ID;
386 - }
387 -
388 - \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
389 -
390 - $users = \get_users(
391 - array(
392 - 'capability__in' => array( 'activitypub' ),
393 - )
394 - );
395 -
396 - foreach ( $users as $user ) {
397 - $followers = Followers::get_many( $user->ID );
398 -
399 - if ( $followers ) {
400 - \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
401 - }
402 - }
403 -
404 - $followers = Followers::get_many( Actors::BLOG_USER_ID );
405 -
406 - if ( $followers ) {
407 - \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
408 - }
409 -
410 - self::migrate_actor_mode();
411 - }
412 -
413 - /**
414 - * Update to 4.1.0
276 + * Set the defaults needed for the plugin to work
415 277 *
416 - * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
417 - */
418 - public static function migrate_to_4_1_0() {
419 - $content_type = \get_option( 'activitypub_post_content_type' );
420 -
421 - switch ( $content_type ) {
422 - case 'excerpt':
423 - $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
424 - break;
425 - case 'title':
426 - $template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
427 - break;
428 - case 'content':
429 - $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
430 - break;
431 - case 'custom':
432 - $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
433 - break;
434 - default:
435 - $template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
436 - break;
437 - }
438 -
439 - \update_option( 'activitypub_custom_post_content', $template );
440 -
441 - \delete_option( 'activitypub_post_content_type' );
442 -
443 - $object_type = \get_option( 'activitypub_object_type', false );
444 - if ( ! $object_type ) {
445 - \update_option( 'activitypub_object_type', 'note' );
446 - }
447 -
448 - // Clean up empty visibility meta.
449 - global $wpdb;
450 - $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
451 - "DELETE FROM $wpdb->postmeta
452 - WHERE meta_key = 'activitypub_content_visibility'
453 - AND (meta_value IS NULL OR meta_value = '')"
454 - );
455 - }
456 -
457 - /**
458 - * Updates post meta keys to be prefixed with an underscore.
459 - */
460 - public static function migrate_to_4_7_1() {
461 - global $wpdb;
462 -
463 - $meta_keys = array(
464 - 'activitypub_actor_json',
465 - 'activitypub_canonical_url',
466 - 'activitypub_errors',
467 - 'activitypub_inbox',
468 - 'activitypub_user_id',
469 - );
470 -
471 - foreach ( $meta_keys as $meta_key ) {
472 - // phpcs:ignore WordPress.DB
473 - $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
474 - }
475 - }
476 -
477 - /**
478 - * Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
479 - */
480 - public static function migrate_to_4_7_2() {
481 - global $wpdb;
482 - // phpcs:ignore WordPress.DB
483 - $followers = $wpdb->get_col(
484 - $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE )
485 - );
486 - foreach ( $followers as $id ) {
487 - clean_post_cache( $id );
488 - }
489 - }
490 -
491 - /**
492 - * Update comment counts for posts in batches.
278 + * * Add the ActivityPub capability to all users that can publish posts
493 279 *
494 - * @see Comment::pre_wp_update_comment_count_now()
495 - * @param int $batch_size Optional. Number of posts to process per batch. Default 100.
496 - * @param int $offset Optional. Number of posts to skip. Default 0.
497 - *
498 - * @return int[]|void Array with batch size and offset if there are more posts to process.
280 + * @return void
499 281 */
500 - public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
501 - global $wpdb;
502 -
503 - Comment::register_comment_types();
504 - $comment_types = Comment::get_comment_type_slugs();
505 - $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
506 -
507 - // Get and process this batch.
508 - $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
509 - $wpdb->prepare(
510 - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
511 - "SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
512 - $batch_size,
513 - $offset
514 - )
515 - );
516 -
517 - foreach ( $post_ids as $post_id ) {
518 - \wp_update_comment_count_now( $post_id );
519 - }
520 -
521 - if ( count( $post_ids ) === $batch_size ) {
522 - // Schedule next batch.
523 - return array( $batch_size, $offset + $batch_size );
524 - }
525 - }
526 -
527 - /**
528 - * Create outbox items for posts in batches.
529 - *
530 - * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
531 - * @param int $offset Optional. Number of posts to skip. Default 0.
532 - * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
533 - */
534 - public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
535 - $posts = \get_posts(
536 - array(
537 - // our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
538 - 'post_type' => 'any',
539 - 'posts_per_page' => $batch_size,
540 - 'offset' => $offset,
541 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
542 - 'meta_query' => array(
543 - array(
544 - 'key' => 'activitypub_status',
545 - 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
546 - ),
547 - ),
548 - )
549 - );
550 -
551 - // Avoid multiple queries for post meta.
552 - \update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
553 -
554 - foreach ( $posts as $post ) {
555 - $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
556 -
557 - self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
558 -
559 - // Add Update activity when the post has been modified.
560 - if ( $post->post_modified !== $post->post_date ) {
561 - self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
562 - }
563 - }
564 -
565 - if ( count( $posts ) === $batch_size ) {
566 - return array(
567 - 'batch_size' => $batch_size,
568 - 'offset' => $offset + $batch_size,
569 - );
570 - }
571 -
572 - return null;
573 - }
574 -
575 - /**
576 - * Create outbox items for comments in batches.
577 - *
578 - * @param int $batch_size Optional. Number of posts to process per batch. Default 50.
579 - * @param int $offset Optional. Number of posts to skip. Default 0.
580 - * @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
581 - */
582 - public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
583 - $comments = \get_comments(
584 - array(
585 - 'author__not_in' => array( 0 ), // Limit to comments by registered users.
586 - 'number' => $batch_size,
587 - 'offset' => $offset,
588 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
589 - 'meta_query' => array(
590 - array(
591 - 'key' => 'activitypub_status',
592 - 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
593 - ),
594 - ),
595 - )
596 - );
597 -
598 - foreach ( $comments as $comment ) {
599 - self::add_to_outbox( $comment, 'Create', $comment->user_id );
600 - }
601 -
602 - if ( count( $comments ) === $batch_size ) {
603 - return array(
604 - 'batch_size' => $batch_size,
605 - 'offset' => $offset + $batch_size,
606 - );
607 - }
608 -
609 - return null;
610 - }
611 -
612 - /**
613 - * Update _activitypub_actor_json meta values to ensure they are properly slashed.
614 - *
615 - * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
616 - * @param int $offset Optional. Number of meta values to skip. Default 0.
617 - * @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
618 - */
619 - public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
620 - global $wpdb;
621 -
622 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
623 - $meta_values = $wpdb->get_results(
624 - $wpdb->prepare(
625 - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
626 - $batch_size,
627 - $offset
628 - )
629 - );
630 -
631 - foreach ( $meta_values as $meta ) {
632 - $json = \json_decode( $meta->meta_value, true );
633 -
634 - // If json_decode fails, try adding slashes.
635 - if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
636 - $escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
637 - $json = \json_decode( $escaped_value, true );
638 -
639 - // Update the meta if json_decode succeeds with slashes.
640 - if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
641 - \update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
642 - }
643 - }
644 - }
645 -
646 - if ( \count( $meta_values ) === $batch_size ) {
647 - return array(
648 - 'batch_size' => $batch_size,
649 - 'offset' => $offset + $batch_size,
650 - );
651 - }
652 -
653 - return null;
654 - }
655 -
656 - /**
657 - * Update comment author emails with webfinger addresses for ActivityPub comments.
658 - *
659 - * @param int $batch_size Optional. Number of comments to process per batch. Default 50.
660 - * @param int $offset Optional. Number of comments to skip. Default 0.
661 - * @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
662 - */
663 - public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
664 - $comments = \get_comments(
665 - array(
666 - 'number' => $batch_size,
667 - 'offset' => $offset,
668 - 'orderby' => 'comment_ID',
669 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
670 - 'meta_query' => array(
671 - array(
672 - 'key' => 'protocol',
673 - 'value' => 'activitypub',
674 - ),
675 - ),
676 - )
677 - );
678 -
679 - foreach ( $comments as $comment ) {
680 - $comment_author_url = $comment->comment_author_url;
681 - if ( empty( $comment_author_url ) ) {
682 - continue;
683 - }
684 -
685 - $webfinger = Webfinger::uri_to_acct( $comment_author_url );
686 - if ( \is_wp_error( $webfinger ) ) {
687 - continue;
688 - }
689 -
690 - \wp_update_comment(
691 - array(
692 - 'comment_ID' => $comment->comment_ID,
693 - 'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
694 - )
695 - );
696 - }
697 -
698 - if ( count( $comments ) === $batch_size ) {
699 - return array(
700 - 'batch_size' => $batch_size,
701 - 'offset' => $offset + $batch_size,
702 - );
703 - }
704 -
705 - return null;
706 - }
707 -
708 - /**
709 - * Set the defaults needed for the plugin to work.
710 - *
711 - * Add the ActivityPub capability to all users that can publish posts.
712 - */
713 282 public static function add_default_settings() {
714 283 self::add_activitypub_capability();
715 - self::add_default_extra_field();
716 284 }
717 285
718 286 /**
719 - * Add an activity to the outbox without federating it.
287 + * Add the ActivityPub capability to all users that can publish posts
720 288 *
721 - * @param \WP_Post|\WP_Comment $comment The comment or post object.
722 - * @param string $activity_type The type of activity.
723 - * @param int $user_id The user ID.
724 - * @param string $visibility Optional. The visibility of the content. Default 'public'.
289 + * @return void
725 290 */
726 - private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
727 - $transformer = Factory::get_transformer( $comment );
728 - if ( ! $transformer || \is_wp_error( $transformer ) ) {
729 - return;
730 - }
731 -
732 - $activity = $transformer->to_activity( $activity_type );
733 - if ( ! $activity || \is_wp_error( $activity ) ) {
734 - return;
735 - }
736 -
737 - // If the user is disabled, fall back to the blog user when available.
738 - if ( ! user_can_activitypub( $user_id ) ) {
739 - if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
740 - $user_id = Actors::BLOG_USER_ID;
741 - } else {
742 - return;
743 - }
744 - }
745 -
746 - $post_id = Outbox::add( $activity, $user_id, $visibility );
747 -
748 - // Immediately set to publish, no federation needed.
749 - \wp_publish_post( $post_id );
750 - }
751 -
752 - /**
753 - * Add the ActivityPub capability to all users that can publish posts.
754 - */
755 291 private static function add_activitypub_capability() {
756 - // Get all WP_User objects that can publish posts.
292 + // get all WP_User objects that can publish posts
757 293 $users = \get_users(
758 294 array(
759 295 'capability__in' => array( 'publish_posts' ),
760 296 )
@@ -759,9 +295,9 @@
759 295 'capability__in' => array( 'publish_posts' ),
760 296 )
761 297 );
762 298
763 - // Add ActivityPub capability to all users that can publish posts.
299 + // add ActivityPub capability to all users that can publish posts
764 300 foreach ( $users as $user ) {
765 301 $user->add_cap( 'activitypub' );
766 302 }
767 303 }
@@ -766,57 +302,20 @@
766 302 }
767 303 }
768 304
769 305 /**
770 - * Add a default extra field for the user.
771 - */
772 - private static function add_default_extra_field() {
773 - $users = \get_users(
774 - array(
775 - 'capability__in' => array( 'activitypub' ),
776 - )
777 - );
778 -
779 - $title = \__( 'Powered by', 'activitypub' );
780 - $content = 'WordPress';
781 -
782 - // Add a default extra field for each user.
783 - foreach ( $users as $user ) {
784 - \wp_insert_post(
785 - array(
786 - 'post_type' => Extra_Fields::USER_POST_TYPE,
787 - 'post_author' => $user->ID,
788 - 'post_status' => 'publish',
789 - 'post_title' => $title,
790 - 'post_content' => $content,
791 - )
792 - );
793 - }
794 -
795 - \wp_insert_post(
796 - array(
797 - 'post_type' => Extra_Fields::BLOG_POST_TYPE,
798 - 'post_author' => 0,
799 - 'post_status' => 'publish',
800 - 'post_title' => $title,
801 - 'post_content' => $content,
802 - )
803 - );
804 - }
805 -
806 - /**
807 - * Rename user meta keys.
306 + * Rename meta keys.
808 307 *
809 - * @param string $old_key The old comment meta key.
810 - * @param string $new_key The new comment meta key.
308 + * @param string $old The old commentmeta key
309 + * @param string $new The new commentmeta key
811 310 */
812 - private static function update_usermeta_key( $old_key, $new_key ) {
311 + private static function update_usermeta_key( $old, $new ) { // phpcs:ignore
813 312 global $wpdb;
814 313
815 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
314 + $wpdb->update( // phpcs:ignore
816 315 $wpdb->usermeta,
817 - array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
818 - array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
316 + array( 'meta_key' => $new ), // phpcs:ignore
317 + array( 'meta_key' => $old ), // phpcs:ignore
819 318 array( '%s' ),
820 319 array( '%s' )
821 320 );
822 321 }
@@ -821,513 +320,21 @@
821 320 );
822 321 }
823 322
824 323 /**
825 - * Update post meta keys.
826 - *
827 - * @param string $old_key The old post meta key.
828 - * @param string $new_key The new post meta key.
829 - */
830 - private static function update_postmeta_key( $old_key, $new_key ) {
831 - global $wpdb;
832 -
833 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
834 - $wpdb->postmeta,
835 - array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
836 - array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
837 - array( '%s' ),
838 - array( '%s' )
839 - );
840 - }
841 -
842 - /**
843 324 * Rename option keys.
844 325 *
845 - * @param string $old_key The old option key.
846 - * @param string $new_key The new option key.
326 + * @param string $old The old option key
327 + * @param string $new The new option key
847 328 */
848 - private static function update_options_key( $old_key, $new_key ) {
329 + private static function update_options_key( $old, $new ) { // phpcs:ignore
849 330 global $wpdb;
850 331
851 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
332 + $wpdb->update( // phpcs:ignore
852 333 $wpdb->options,
853 - array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
854 - array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
334 + array( 'option_name' => $new ), // phpcs:ignore
335 + array( 'option_name' => $old ), // phpcs:ignore
855 336 array( '%s' ),
856 337 array( '%s' )
857 338 );
858 - }
859 -
860 - /**
861 - * Migrate the actor mode settings.
862 - */
863 - public static function migrate_actor_mode() {
864 - $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
865 - $author_profiles = \get_option( 'activitypub_enable_users', '1' );
866 -
867 - if (
868 - '1' === $blog_profile &&
869 - '1' === $author_profiles
870 - ) {
871 - \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
872 - } elseif (
873 - '1' === $blog_profile &&
874 - '1' !== $author_profiles
875 - ) {
876 - \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
877 - } elseif (
878 - '1' !== $blog_profile &&
879 - '1' === $author_profiles
880 - ) {
881 - \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
882 - }
883 - }
884 -
885 - /**
886 - * Deletes user extra fields where the author is the blog user.
887 - *
888 - * These extra fields were created when the Enable Mastodon Apps integration passed
889 - * an author_url instead of a user_id to the mastodon_api_account filter. This caused
890 - * Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
891 - * for non-existent users. The result is a number of user extra fields with no author.
892 - *
893 - * @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
894 - */
895 - public static function delete_mastodon_api_orphaned_extra_fields() {
896 - global $wpdb;
897 -
898 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
899 - $wpdb->delete(
900 - $wpdb->posts,
901 - array(
902 - 'post_type' => Extra_Fields::USER_POST_TYPE,
903 - 'post_author' => Actors::BLOG_USER_ID,
904 - )
905 - );
906 - }
907 -
908 - /**
909 - * Update notification options.
910 - */
911 - public static function update_notification_options() {
912 - $new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
913 - $new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
914 -
915 - // Add the blog user notification options.
916 - \add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
917 - \add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
918 - \add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
919 -
920 - $user_ids = \get_users(
921 - array(
922 - 'capability__in' => array( 'activitypub' ),
923 - 'fields' => 'id',
924 - )
925 - );
926 -
927 - // Add the actor notification options.
928 - foreach ( $user_ids as $user_id ) {
929 - \update_user_option( $user_id, 'activitypub_mailer_new_dm', $new_dm );
930 - \update_user_option( $user_id, 'activitypub_mailer_new_follower', $new_follower );
931 - \update_user_option( $user_id, 'activitypub_mailer_new_mention', '1' );
932 - }
933 -
934 - // Delete the old notification options.
935 - \delete_option( 'activitypub_mailer_new_dm' );
936 - \delete_option( 'activitypub_mailer_new_follower' );
937 - }
938 -
939 - /**
940 - * Migrate followers to the new CPT.
941 - */
942 - public static function migrate_followers_to_ap_actor_cpt() {
943 - global $wpdb;
944 -
945 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
946 - $wpdb->posts,
947 - array( 'post_type' => Remote_Actors::POST_TYPE ),
948 - array( 'post_type' => 'ap_follower' ),
949 - array( '%s' ),
950 - array( '%s' )
951 - );
952 -
953 - self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY );
954 - }
955 -
956 - /**
957 - * Update _activitypub_actor_json meta values to ensure they are properly slashed.
958 - *
959 - * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
960 - *
961 - * @return array|void Array with batch size and offset if there are more meta values to process, void otherwise.
962 - */
963 - public static function update_actor_json_storage( $batch_size = 100 ) {
964 - global $wpdb;
965 -
966 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
967 - $meta_values = $wpdb->get_results(
968 - $wpdb->prepare(
969 - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d",
970 - $batch_size
971 - )
972 - );
973 -
974 - $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
975 - if ( $has_kses ) {
976 - // Prevent KSES from corrupting JSON in post_content.
977 - \kses_remove_filters();
978 - }
979 -
980 - foreach ( $meta_values as $meta ) {
981 - $post = \get_post( $meta->post_id );
982 -
983 - if ( ! $post ) {
984 - \delete_post_meta( $meta->post_id, '_activitypub_actor_json' );
985 - continue;
986 - }
987 -
988 - $post_content = \json_decode( $meta->meta_value, true );
989 -
990 - if ( \json_last_error() !== JSON_ERROR_NONE ) {
991 - $post_content = Http::get_remote_object( $post->guid );
992 -
993 - if ( \is_wp_error( $post_content ) ) {
994 - \delete_post_meta( $post->ID, '_activitypub_actor_json' );
995 - continue;
996 - }
997 - }
998 -
999 - \wp_update_post(
1000 - array(
1001 - 'ID' => $post->ID,
1002 - 'post_content' => \wp_slash( \wp_json_encode( $post_content ) ),
1003 - )
1004 - );
1005 -
1006 - \delete_post_meta( $post->ID, '_activitypub_actor_json' );
1007 - }
1008 -
1009 - if ( $has_kses ) {
1010 - // Restore KSES filters.
1011 - \kses_init_filters();
1012 - }
1013 -
1014 - if ( \count( $meta_values ) === $batch_size ) {
1015 - return array(
1016 - 'batch_size' => $batch_size,
1017 - );
1018 - }
1019 - }
1020 -
1021 - /**
1022 - * Removes pending follow requests for the application user.
1023 - */
1024 - public static function remove_pending_application_user_follow_requests() {
1025 - global $wpdb;
1026 -
1027 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1028 - $wpdb->delete(
1029 - $wpdb->postmeta,
1030 - array(
1031 - 'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1032 - 'meta_value' => Actors::APPLICATION_USER_ID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1033 - )
1034 - );
1035 - }
1036 -
1037 - /**
1038 - * Sync Jetpack meta for all followings.
1039 - *
1040 - * Replays the added_post_meta sync action for Jetpack with the Following::FOLLOWING_META_KEY meta key.
1041 - */
1042 - public static function sync_jetpack_following_meta() {
1043 - if ( ! \class_exists( 'Jetpack' ) || ! \Jetpack::is_connection_ready() ) {
1044 - return;
1045 - }
1046 -
1047 - global $wpdb;
1048 -
1049 - // Get all posts that have the following meta key.
1050 - $posts_with_following = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1051 - $wpdb->prepare(
1052 - "SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
1053 - Following::FOLLOWING_META_KEY
1054 - ),
1055 - ARRAY_N
1056 - );
1057 -
1058 - // Trigger the added_post_meta action for each following relationship.
1059 - foreach ( $posts_with_following as $meta ) {
1060 - /**
1061 - * Fires when post meta is added.
1062 - *
1063 - * @param int $meta_id ID of the metadata entry.
1064 - * @param int $object_id Post ID.
1065 - * @param string $meta_key Metadata key.
1066 - * @param mixed $meta_value Metadata value.
1067 - */
1068 - \do_action( 'added_post_meta', ...$meta );
1069 - }
1070 - }
1071 -
1072 - /**
1073 - * Clean up inbox items for shared inbox migration.
1074 - *
1075 - * Deletes all existing inbox items to prepare for the new shared inbox structure
1076 - * where activities are stored once with multiple recipients as metadata.
1077 - */
1078 - private static function clean_up_inbox() {
1079 - global $wpdb;
1080 -
1081 - // Get all inbox post IDs.
1082 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1083 - $inbox_ids = $wpdb->get_col(
1084 - $wpdb->prepare(
1085 - "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
1086 - \Activitypub\Collection\Inbox::POST_TYPE
1087 - )
1088 - );
1089 -
1090 - // Delete all inbox items and their metadata.
1091 - foreach ( $inbox_ids as $post_id ) {
1092 - \wp_delete_post( $post_id, true );
1093 - }
1094 - }
1095 -
1096 - /**
1097 - * Migrate URLs from the legacy `activitypub_tombstone_urls` option into the
1098 - * `ap_tombstone` custom post type.
1099 - *
1100 - * Chunked async migration. Locking and rescheduling is handled by
1101 - * Scheduler::async_batch — the callback returns `array( 'batch_size' => N )`
1102 - * to request another run, or `null` when the option is fully drained.
1103 - *
1104 - * Legacy entries are already-normalized strings (no scheme), so we bypass
1105 - * URL validation and insert directly via wp_insert_post.
1106 - *
1107 - * @since 8.3.0
1108 - *
1109 - * @param int $batch_size Optional. Number of URLs to process per call. Default 500.
1110 - * @return array|null Args for the next run, or null when migration is complete.
1111 - */
1112 - public static function migrate_tombstones_to_cpt( $batch_size = 500 ) {
1113 - global $wpdb;
1114 -
1115 - $urls = \get_option( 'activitypub_tombstone_urls', null );
1116 -
1117 - if ( null === $urls || ! \is_array( $urls ) || empty( $urls ) ) {
1118 - \delete_option( 'activitypub_tombstone_urls' );
1119 - return null;
1120 - }
1121 -
1122 - $chunk = \array_slice( $urls, 0, (int) $batch_size );
1123 - $remaining = \array_slice( $urls, (int) $batch_size );
1124 - $progressed = false;
1125 -
1126 - foreach ( $chunk as $normalized ) {
1127 - if ( ! \is_string( $normalized ) || '' === $normalized ) {
1128 - // Drop garbage entries — counts as progress.
1129 - $progressed = true;
1130 - continue;
1131 - }
1132 -
1133 - $hash = \md5( $normalized );
1134 -
1135 - /*
1136 - * Light existence check. `get_page_by_path()` would hydrate a
1137 - * full `WP_Post` per loop iteration; on a large registry that
1138 - * adds up fast. We only need a boolean here.
1139 - */
1140 - $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1141 - $wpdb->prepare(
1142 - "SELECT 1 FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1",
1143 - Tombstone::POST_TYPE,
1144 - $hash
1145 - )
1146 - );
1147 - if ( $exists ) {
1148 - $progressed = true;
1149 - continue;
1150 - }
1151 -
1152 - /*
1153 - * `guid` is intentionally omitted: the legacy option only kept
1154 - * the normalized (schemeless) form, so we can't reconstruct the
1155 - * original URL. Storing the schemeless string would be mangled
1156 - * by `esc_url()`. Leave WordPress to auto-generate the guid
1157 - * — it's not used for lookups, only for debugging.
1158 - */
1159 - $result = \wp_insert_post(
1160 - array(
1161 - 'post_type' => Tombstone::POST_TYPE,
1162 - 'post_status' => 'publish',
1163 - 'post_name' => $hash,
1164 - 'post_author' => 0,
1165 - ),
1166 - true
1167 - );
1168 -
1169 - if ( \is_wp_error( $result ) || ! $result ) {
1170 - /*
1171 - * Keep failed inserts in the legacy option so the next batch
1172 - * retries them. `Tombstone::exists_local()` still falls back
1173 - * to the option, so the tombstone remains discoverable.
1174 - */
1175 - $remaining[] = $normalized;
1176 - } else {
1177 - $progressed = true;
1178 - }
1179 - }
1180 -
1181 - if ( empty( $remaining ) ) {
1182 - \delete_option( 'activitypub_tombstone_urls' );
1183 - return null;
1184 - }
1185 -
1186 - /*
1187 - * Disable autoload while we drain. The point of the migration is to
1188 - * stop this option from contributing to `alloptions` pressure, so
1189 - * flip the flag immediately rather than waiting for the option to
1190 - * be fully empty before the relief kicks in.
1191 - */
1192 - \update_option( 'activitypub_tombstone_urls', \array_values( $remaining ), false );
1193 -
1194 - /*
1195 - * If nothing in this batch was drained — every insert errored and
1196 - * nothing was already migrated — halt the scheduler so we don't loop
1197 - * forever on a persistent failure. The legacy option still backs
1198 - * exists_local(), so the data isn't lost; an admin can re-trigger
1199 - * the migration via `wp cron event run activitypub_tombstone_migrate`
1200 - * after fixing the underlying cause.
1201 - */
1202 - if ( ! $progressed ) {
1203 - return null;
1204 - }
1205 -
1206 - return array( 'batch_size' => (int) $batch_size );
1207 - }
1208 -
1209 - /**
1210 - * Migrate avatar URLs from comment meta to remote actors in batches.
1211 - *
1212 - * This migration:
1213 - * 1. Finds all comments with ActivityPub protocol and avatar_url meta
1214 - * 2. Looks up the remote actor by comment_author_url
1215 - * 3. Adds _activitypub_remote_actor_id to comment meta
1216 - * 4. Stores avatar_url in remote actor post meta
1217 - *
1218 - * Note: We don't use offset because as we add _activitypub_remote_actor_id,
1219 - * comments are filtered out of the query. We just keep fetching the next
1220 - * batch until no more comments match the criteria.
1221 - *
1222 - * @param int $batch_size Optional. Number of comments to process per batch. Default 50.
1223 - * @return array|null Array with batch size if there are more comments to process, null otherwise.
1224 - */
1225 - public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) {
1226 - global $wpdb;
1227 -
1228 - /*
1229 - * Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet.
1230 - * Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance.
1231 - * Filters meta_key before GROUP BY to reduce rows processed during aggregation.
1232 - * No offset needed - as we process comments, they're filtered out by the HAVING clause.
1233 - */
1234 - $comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1235 - $wpdb->prepare(
1236 - "SELECT c.comment_ID, c.comment_author_url,
1237 - MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url,
1238 - MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol,
1239 - MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id
1240 - FROM {$wpdb->comments} c
1241 - INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
1242 - WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id')
1243 - GROUP BY c.comment_ID, c.comment_author_url
1244 - HAVING protocol = 'activitypub'
1245 - AND avatar_url IS NOT NULL
1246 - AND (remote_actor_id IS NULL OR remote_actor_id = '')
1247 - LIMIT %d",
1248 - $batch_size
1249 - )
1250 - );
1251 -
1252 - foreach ( $comments as $comment ) {
1253 - if ( empty( $comment->comment_author_url ) ) {
1254 - continue;
1255 - }
1256 -
1257 - // Try to get the remote actor by URI.
1258 - $remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url );
1259 -
1260 - // If we have a valid remote actor, store the reference.
1261 - if ( ! \is_wp_error( $remote_actor ) ) {
1262 - // Add _activitypub_remote_actor_id to comment meta.
1263 - \add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true );
1264 -
1265 - // Ensure avatar is stored on remote actor if not already present.
1266 - $existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true );
1267 - if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) {
1268 - \update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) );
1269 - }
1270 - }
1271 - }
1272 -
1273 - // Return batch info if there are more comments to process.
1274 - if ( count( $comments ) === $batch_size ) {
1275 - return array(
1276 - 'batch_size' => $batch_size,
1277 - );
1278 - }
1279 -
1280 - return null;
1281 - }
1282 -
1283 - /**
1284 - * Migrate emoji data from stored actor JSON to post meta.
1285 - *
1286 - * This migration:
1287 - * 1. Finds all remote actor posts without _activitypub_emoji meta
1288 - * 2. Extracts emoji from stored JSON in post_content
1289 - * 3. Stores as _activitypub_emoji post meta
1290 - *
1291 - * @param int $batch_size Optional. Number of actors to process per batch. Default 50.
1292 - * @param int $offset Optional. Offset for pagination. Default 0.
1293 - * @return array|null Array with batch size if there are more actors to process, null otherwise.
1294 - */
1295 - public static function migrate_actor_emoji( $batch_size = 50, $offset = 0 ) {
1296 - $actors = \get_posts(
1297 - array(
1298 - 'post_type' => Remote_Actors::POST_TYPE,
1299 - 'posts_per_page' => $batch_size,
1300 - 'offset' => $offset,
1301 - 'post_status' => 'any',
1302 - 'orderby' => 'ID',
1303 - 'order' => 'ASC',
1304 - )
1305 - );
1306 -
1307 - foreach ( $actors as $actor_post ) {
1308 - if ( empty( $actor_post->post_content ) ) {
1309 - continue;
1310 - }
1311 -
1312 - $actor_data = \json_decode( $actor_post->post_content, true );
1313 - if ( ! $actor_data ) {
1314 - continue;
1315 - }
1316 -
1317 - $emoji_meta = Emoji::prepare_actor_meta( $actor_data );
1318 - if ( ! empty( $emoji_meta['_activitypub_emoji'] ) ) {
1319 - \update_post_meta( $actor_post->ID, '_activitypub_emoji', $emoji_meta['_activitypub_emoji'] );
1320 - }
1321 - }
1322 -
1323 - // Return batch info if there are more actors to process.
1324 - if ( count( $actors ) === $batch_size ) {
1325 - return array(
1326 - 'batch_size' => $batch_size,
1327 - 'offset' => $offset + $batch_size,
1328 - );
1329 - }
1330 -
1331 - return null;
1332 339 }
1333 340 }