PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.7.0
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 +138 -609 9.2.05.7.0 View file →
@@ -9,12 +9,9 @@
9 9
10 10 use Activitypub\Collection\Actors;
11 11 use Activitypub\Collection\Extra_Fields;
12 12 use Activitypub\Collection\Followers;
13 -use Activitypub\Collection\Following;
14 -use Activitypub\Collection\Inbox;
15 13 use Activitypub\Collection\Outbox;
16 -use Activitypub\Collection\Remote_Actors;
17 14 use Activitypub\Transformer\Factory;
18 15
19 16 /**
20 17 * ActivityPub Migration Class
@@ -25,18 +22,29 @@
25 22 /**
26 23 * Initialize the class, registering WordPress hooks.
27 24 */
28 25 public static function init() {
26 + \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
27 + \add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
28 + \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
29 +
29 30 self::maybe_migrate();
31 + }
30 32
31 - Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) );
32 - Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) );
33 - Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) );
34 - Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
35 - Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
36 - Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) );
37 - Scheduler::register_async_batch_callback( 'activitypub_backfill_statistics', array( Statistics::class, 'backfill_historical_stats' ) );
38 - Scheduler::register_async_batch_callback( 'activitypub_tombstone_migrate', array( self::class, 'migrate_tombstones_to_cpt' ) );
33 + /**
34 + * Get the target version.
35 + *
36 + * This is the version that the database structure will be updated to.
37 + * It is the same as the plugin version.
38 + *
39 + * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
40 + *
41 + * @return string The target version.
42 + */
43 + public static function get_target_version() {
44 + _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
45 +
46 + return ACTIVITYPUB_PLUGIN_VERSION;
39 47 }
40 48
41 49 /**
42 50 * The current version of the database structure.
@@ -43,9 +51,9 @@
43 51 *
44 52 * @return string The current version.
45 53 */
46 54 public static function get_version() {
47 - return \get_option( 'activitypub_db_version', 0 );
55 + return get_option( 'activitypub_db_version', 0 );
48 56 }
49 57
50 58 /**
51 59 * Locks the database migration process to prevent simultaneous migrations.
@@ -128,14 +136,15 @@
128 136 self::add_default_settings();
129 137 $version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
130 138 }
131 139
140 + // Schedule the async migration.
141 + if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
142 + \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
143 + }
132 144 if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
133 145 self::migrate_from_0_16();
134 146 }
135 - if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
136 - \wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' );
137 - }
138 147 if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
139 148 self::migrate_from_1_2_0();
140 149 }
141 150 if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
@@ -161,83 +170,30 @@
161 170 }
162 171 if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
163 172 self::migrate_to_4_7_2();
164 173 }
174 + if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
175 + add_action( 'init', 'flush_rewrite_rules', 20 );
176 + }
165 177 if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
166 178 Scheduler::register_schedules();
167 - \wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' );
168 - \wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' );
179 + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
180 + \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
181 + add_action( 'init', 'flush_rewrite_rules', 20 );
169 182 }
183 + if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
184 + Scheduler::register_schedules();
185 + }
170 186 if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
171 187 \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
172 188 \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
189 + \add_action( 'init', 'flush_rewrite_rules', 20 );
173 190 }
174 191 if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
175 192 self::delete_mastodon_api_orphaned_extra_fields();
176 193 }
177 - if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
178 - self::update_notification_options();
179 - }
180 - if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) {
181 - self::migrate_followers_to_ap_actor_cpt();
182 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
183 - }
184 - if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) {
185 - self::migrate_followers_to_ap_actor_cpt();
186 - \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
187 - }
188 - if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) {
189 - \wp_unschedule_hook( 'activitypub_update_followers' );
190 - \wp_unschedule_hook( 'activitypub_cleanup_followers' );
191 194
192 - if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
193 - \wp_schedule_event( \time(), 'hourly', 'activitypub_update_remote_actors' );
194 - }
195 -
196 - if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
197 - \wp_schedule_event( \time(), 'daily', 'activitypub_cleanup_remote_actors' );
198 - }
199 - }
200 - if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) {
201 - self::remove_pending_application_user_follow_requests();
202 - }
203 - if ( \version_compare( $version_from_db, '7.5.0', '<' ) ) {
204 - self::sync_jetpack_following_meta();
205 - }
206 - if ( \version_compare( $version_from_db, '7.6.0', '<' ) ) {
207 - self::clean_up_inbox();
208 - \wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' );
209 - }
210 - if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) {
211 - \wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' );
212 - }
213 - if ( \version_compare( $version_from_db, '8.1.0', '<' ) && ! \wp_next_scheduled( 'activitypub_backfill_statistics' ) ) {
214 - // Backfill historical statistics data (delay + jitter to avoid load spikes on hosts running many sites).
215 - \wp_schedule_single_event( \time() + HOUR_IN_SECONDS + \wp_rand( 0, 6 * HOUR_IN_SECONDS ), 'activitypub_backfill_statistics' );
216 - }
217 -
218 - if ( \version_compare( $version_from_db, '8.3.0', '<' ) ) {
219 - if ( ! \wp_next_scheduled( 'activitypub_tombstone_migrate' ) ) {
220 - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_tombstone_migrate' );
221 - }
222 - }
223 - if ( \version_compare( $version_from_db, '9.1.0', '<' ) ) {
224 - self::migrate_application_keypair_option();
225 - self::delete_application_outbox_items();
226 - }
227 -
228 195 /*
229 - * Defer the flush to late in the `init` cycle (priority 20). Migration::init
230 - * runs at priority 1, which is earlier than most plugins register their
231 - * rewrite rules. Flushing synchronously here would persist a truncated
232 - * ruleset that omits third-party rules added on `init` at priority 10.
233 - */
234 - \add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 );
235 -
236 - // Ensure all required cron schedules are registered.
237 - Scheduler::register_schedules();
238 -
239 - /*
240 196 * Add new update routines above this comment. ^
241 197 *
242 198 * Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
243 199 * The release script will automatically replace it with the actual version number.
@@ -261,8 +217,51 @@
261 217 self::unlock();
262 218 }
263 219
264 220 /**
221 + * Asynchronously migrates the database structure.
222 + *
223 + * @param string $version_from_db The version from which to migrate.
224 + */
225 + public static function async_migration( $version_from_db ) {
226 + if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
227 + self::migrate_from_0_17();
228 + }
229 + }
230 +
231 + /**
232 + * Asynchronously runs upgrade routines.
233 + *
234 + * @param callable $callback Callable upgrade routine. Must be a method of this class.
235 + * @params mixed ...$args Optional. Parameters that get passed to the callback.
236 + */
237 + public static function async_upgrade( $callback ) {
238 + $args = \func_get_args();
239 +
240 + // Bail if the existing lock is still valid.
241 + if ( self::is_locked() ) {
242 + \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
243 + return;
244 + }
245 +
246 + self::lock();
247 +
248 + $callback = array_shift( $args ); // Remove $callback from arguments.
249 + $next = \call_user_func_array( array( self::class, $callback ), $args );
250 +
251 + self::unlock();
252 +
253 + if ( ! empty( $next ) ) {
254 + // Schedule the next run, adding the result to the arguments.
255 + \wp_schedule_single_event(
256 + \time() + 30,
257 + 'activitypub_upgrade',
258 + \array_merge( array( $callback ), \array_values( $next ) )
259 + );
260 + }
261 + }
262 +
263 + /**
265 264 * Updates the custom template to use shortcodes instead of the deprecated templates.
266 265 */
267 266 private static function migrate_from_0_16() {
268 267 // Get the custom template.
@@ -302,17 +301,19 @@
302 301 * Updates the DB-schema of the followers-list.
303 302 */
304 303 public static function migrate_from_0_17() {
305 304 // Migrate followers.
306 - foreach ( \get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
307 - $followers = \get_user_meta( $user_id, 'activitypub_followers', true );
305 + foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
306 + $followers = get_user_meta( $user_id, 'activitypub_followers', true );
308 307
309 308 if ( $followers ) {
310 309 foreach ( $followers as $actor ) {
311 - Followers::add( $user_id, $actor );
310 + Followers::add_follower( $user_id, $actor );
312 311 }
313 312 }
314 313 }
314 +
315 + Activitypub::flush_rewrite_rules();
315 316 }
316 317
317 318 /**
318 319 * Clear the cache after updating to 1.3.0.
@@ -325,9 +326,9 @@
325 326 )
326 327 );
327 328
328 329 foreach ( $user_ids as $user_id ) {
329 - \wp_cache_delete( \sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
330 + wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
330 331 }
331 332 }
332 333
333 334 /**
@@ -333,15 +334,15 @@
333 334 /**
334 335 * Unschedule Hooks after updating to 2.0.0.
335 336 */
336 337 private static function migrate_from_2_0_0() {
337 - \wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
338 - \wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
339 - \wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
338 + wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
339 + wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
340 + wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
340 341
341 - \wp_unschedule_hook( 'activitypub_send_post_activity' );
342 - \wp_unschedule_hook( 'activitypub_send_update_activity' );
343 - \wp_unschedule_hook( 'activitypub_send_delete_activity' );
342 + wp_unschedule_hook( 'activitypub_send_post_activity' );
343 + wp_unschedule_hook( 'activitypub_send_update_activity' );
344 + wp_unschedule_hook( 'activitypub_send_delete_activity' );
344 345
345 346 $object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
346 347 if ( 'article' === $object_type ) {
347 348 \update_option( 'activitypub_object_type', 'wordpress-post-format' );
@@ -360,9 +361,9 @@
360 361 /**
361 362 * Rename DB fields.
362 363 */
363 364 private static function migrate_from_2_6_0() {
364 - \wp_cache_flush();
365 + wp_cache_flush();
365 366
366 367 self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
367 368
368 369 self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
@@ -376,9 +377,9 @@
376 377 private static function migrate_to_4_0_0() {
377 378 $latest_post_id = 0;
378 379
379 380 // Get the ID of the latest blog post and save it to the options table.
380 - $latest_post = \get_posts(
381 + $latest_post = get_posts(
381 382 array(
382 383 'numberposts' => 1,
383 384 'orderby' => 'ID',
384 385 'order' => 'DESC',
@@ -399,9 +400,9 @@
399 400 )
400 401 );
401 402
402 403 foreach ( $users as $user ) {
403 - $followers = Followers::get_many( $user->ID );
404 + $followers = Followers::get_followers( $user->ID );
404 405
405 406 if ( $followers ) {
406 407 \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
407 408 }
@@ -406,9 +407,9 @@
406 407 \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
407 408 }
408 409 }
409 410
410 - $followers = Followers::get_many( Actors::BLOG_USER_ID );
411 + $followers = Followers::get_followers( Actors::BLOG_USER_ID );
411 412
412 413 if ( $followers ) {
413 414 \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
414 415 }
@@ -416,9 +417,9 @@
416 417 self::migrate_actor_mode();
417 418 }
418 419
419 420 /**
420 - * Update to 4.1.0
421 + * Upate to 4.1.0
421 422 *
422 423 * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
423 424 */
424 425 public static function migrate_to_4_1_0() {
@@ -486,12 +487,12 @@
486 487 public static function migrate_to_4_7_2() {
487 488 global $wpdb;
488 489 // phpcs:ignore WordPress.DB
489 490 $followers = $wpdb->get_col(
490 - $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE )
491 + $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
491 492 );
492 493 foreach ( $followers as $id ) {
493 - \clean_post_cache( $id );
494 + clean_post_cache( $id );
494 495 }
495 496 }
496 497
497 498 /**
@@ -499,17 +500,30 @@
499 500 *
500 501 * @see Comment::pre_wp_update_comment_count_now()
501 502 * @param int $batch_size Optional. Number of posts to process per batch. Default 100.
502 503 * @param int $offset Optional. Number of posts to skip. Default 0.
503 - *
504 - * @return int[]|void Array with batch size and offset if there are more posts to process.
505 504 */
506 505 public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
507 506 global $wpdb;
508 507
508 + // Bail if the existing lock is still valid.
509 + if ( self::is_locked() ) {
510 + \wp_schedule_single_event(
511 + time() + ( 5 * MINUTE_IN_SECONDS ),
512 + 'activitypub_update_comment_counts',
513 + array(
514 + 'batch_size' => $batch_size,
515 + 'offset' => $offset,
516 + )
517 + );
518 + return;
519 + }
520 +
521 + self::lock();
522 +
509 523 Comment::register_comment_types();
510 524 $comment_types = Comment::get_comment_type_slugs();
511 - $type_inclusion = "AND comment_type IN ('" . \implode( "','", $comment_types ) . "')";
525 + $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
512 526
513 527 // Get and process this batch.
514 528 $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
515 529 $wpdb->prepare(
@@ -523,12 +537,21 @@
523 537 foreach ( $post_ids as $post_id ) {
524 538 \wp_update_comment_count_now( $post_id );
525 539 }
526 540
527 - if ( \count( $post_ids ) === $batch_size ) {
541 + if ( count( $post_ids ) === $batch_size ) {
528 542 // Schedule next batch.
529 - return array( $batch_size, $offset + $batch_size );
543 + \wp_schedule_single_event(
544 + time() + MINUTE_IN_SECONDS,
545 + 'activitypub_update_comment_counts',
546 + array(
547 + 'batch_size' => $batch_size,
548 + 'offset' => $offset + $batch_size,
549 + )
550 + );
530 551 }
552 +
553 + self::unlock();
531 554 }
532 555
533 556 /**
534 557 * Create outbox items for posts in batches.
@@ -547,9 +570,9 @@
547 570 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
548 571 'meta_query' => array(
549 572 array(
550 573 'key' => 'activitypub_status',
551 - 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
574 + 'value' => 'federated',
552 575 ),
553 576 ),
554 577 )
555 578 );
@@ -567,9 +590,9 @@
567 590 self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
568 591 }
569 592 }
570 593
571 - if ( \count( $posts ) === $batch_size ) {
594 + if ( count( $posts ) === $batch_size ) {
572 595 return array(
573 596 'batch_size' => $batch_size,
574 597 'offset' => $offset + $batch_size,
575 598 );
@@ -594,9 +617,9 @@
594 617 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
595 618 'meta_query' => array(
596 619 array(
597 620 'key' => 'activitypub_status',
598 - 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED,
621 + 'value' => 'federated',
599 622 ),
600 623 ),
601 624 )
602 625 );
@@ -604,9 +627,9 @@
604 627 foreach ( $comments as $comment ) {
605 628 self::add_to_outbox( $comment, 'Create', $comment->user_id );
606 629 }
607 630
608 - if ( \count( $comments ) === $batch_size ) {
631 + if ( count( $comments ) === $batch_size ) {
609 632 return array(
610 633 'batch_size' => $batch_size,
611 634 'offset' => $offset + $batch_size,
612 635 );
@@ -670,9 +693,8 @@
670 693 $comments = \get_comments(
671 694 array(
672 695 'number' => $batch_size,
673 696 'offset' => $offset,
674 - 'orderby' => 'comment_ID',
675 697 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
676 698 'meta_query' => array(
677 699 array(
678 700 'key' => 'protocol',
@@ -700,9 +722,9 @@
700 722 )
701 723 );
702 724 }
703 725
704 - if ( \count( $comments ) === $batch_size ) {
726 + if ( count( $comments ) === $batch_size ) {
705 727 return array(
706 728 'batch_size' => $batch_size,
707 729 'offset' => $offset + $batch_size,
708 730 );
@@ -717,8 +739,9 @@
717 739 * Add the ActivityPub capability to all users that can publish posts.
718 740 */
719 741 public static function add_default_settings() {
720 742 self::add_activitypub_capability();
743 + self::add_notification_defaults();
721 744 self::add_default_extra_field();
722 745 }
723 746
724 747 /**
@@ -772,8 +795,16 @@
772 795 }
773 796 }
774 797
775 798 /**
799 + * Add default notification settings.
800 + */
801 + private static function add_notification_defaults() {
802 + \add_option( 'activitypub_mailer_new_follower', '1' );
803 + \add_option( 'activitypub_mailer_new_dm', '1' );
804 + }
805 +
806 + /**
776 807 * Add a default extra field for the user.
777 808 */
778 809 private static function add_default_extra_field() {
779 810 $users = \get_users(
@@ -809,9 +840,9 @@
809 840 );
810 841 }
811 842
812 843 /**
813 - * Rename user meta keys.
844 + * Rename meta keys.
814 845 *
815 846 * @param string $old_key The old comment meta key.
816 847 * @param string $new_key The new comment meta key.
817 848 */
@@ -827,26 +858,8 @@
827 858 );
828 859 }
829 860
830 861 /**
831 - * Update post meta keys.
832 - *
833 - * @param string $old_key The old post meta key.
834 - * @param string $new_key The new post meta key.
835 - */
836 - private static function update_postmeta_key( $old_key, $new_key ) {
837 - global $wpdb;
838 -
839 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
840 - $wpdb->postmeta,
841 - array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
842 - array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
843 - array( '%s' ),
844 - array( '%s' )
845 - );
846 - }
847 -
848 - /**
849 862 * Rename option keys.
850 863 *
851 864 * @param string $old_key The old option key.
852 865 * @param string $new_key The new option key.
@@ -908,490 +921,6 @@
908 921 'post_type' => Extra_Fields::USER_POST_TYPE,
909 922 'post_author' => Actors::BLOG_USER_ID,
910 923 )
911 924 );
912 - }
913 -
914 - /**
915 - * Update notification options.
916 - */
917 - public static function update_notification_options() {
918 - $new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
919 - $new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
920 -
921 - // Add the blog user notification options.
922 - \add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
923 - \add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
924 - \add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
925 -
926 - $user_ids = \get_users(
927 - array(
928 - 'capability__in' => array( 'activitypub' ),
929 - 'fields' => 'id',
930 - )
931 - );
932 -
933 - // Add the actor notification options.
934 - foreach ( $user_ids as $user_id ) {
935 - \update_user_option( $user_id, 'activitypub_mailer_new_dm', $new_dm );
936 - \update_user_option( $user_id, 'activitypub_mailer_new_follower', $new_follower );
937 - \update_user_option( $user_id, 'activitypub_mailer_new_mention', '1' );
938 - }
939 -
940 - // Delete the old notification options.
941 - \delete_option( 'activitypub_mailer_new_dm' );
942 - \delete_option( 'activitypub_mailer_new_follower' );
943 - }
944 -
945 - /**
946 - * Migrate followers to the new CPT.
947 - */
948 - public static function migrate_followers_to_ap_actor_cpt() {
949 - global $wpdb;
950 -
951 - $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
952 - $wpdb->posts,
953 - array( 'post_type' => Remote_Actors::POST_TYPE ),
954 - array( 'post_type' => 'ap_follower' ),
955 - array( '%s' ),
956 - array( '%s' )
957 - );
958 -
959 - self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY );
960 - }
961 -
962 - /**
963 - * Update _activitypub_actor_json meta values to ensure they are properly slashed.
964 - *
965 - * @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
966 - *
967 - * @return array|void Array with batch size and offset if there are more meta values to process, void otherwise.
968 - */
969 - public static function update_actor_json_storage( $batch_size = 100 ) {
970 - global $wpdb;
971 -
972 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
973 - $meta_values = $wpdb->get_results(
974 - $wpdb->prepare(
975 - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d",
976 - $batch_size
977 - )
978 - );
979 -
980 - $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
981 - if ( $has_kses ) {
982 - // Prevent KSES from corrupting JSON in post_content.
983 - \kses_remove_filters();
984 - }
985 -
986 - foreach ( $meta_values as $meta ) {
987 - $post = \get_post( $meta->post_id );
988 -
989 - if ( ! $post ) {
990 - \delete_post_meta( $meta->post_id, '_activitypub_actor_json' );
991 - continue;
992 - }
993 -
994 - $post_content = \json_decode( $meta->meta_value, true );
995 -
996 - if ( \json_last_error() !== JSON_ERROR_NONE ) {
997 - $post_content = Http::get_remote_object( $post->guid );
998 -
999 - if ( \is_wp_error( $post_content ) ) {
1000 - \delete_post_meta( $post->ID, '_activitypub_actor_json' );
1001 - continue;
1002 - }
1003 - }
1004 -
1005 - \wp_update_post(
1006 - array(
1007 - 'ID' => $post->ID,
1008 - 'post_content' => \wp_slash( \wp_json_encode( $post_content ) ),
1009 - )
1010 - );
1011 -
1012 - \delete_post_meta( $post->ID, '_activitypub_actor_json' );
1013 - }
1014 -
1015 - if ( $has_kses ) {
1016 - // Restore KSES filters.
1017 - \kses_init_filters();
1018 - }
1019 -
1020 - if ( \count( $meta_values ) === $batch_size ) {
1021 - return array(
1022 - 'batch_size' => $batch_size,
1023 - );
1024 - }
1025 - }
1026 -
1027 - /**
1028 - * Removes pending follow requests for the application user.
1029 - */
1030 - public static function remove_pending_application_user_follow_requests() {
1031 - global $wpdb;
1032 -
1033 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1034 - $wpdb->delete(
1035 - $wpdb->postmeta,
1036 - array(
1037 - 'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1038 - 'meta_value' => -1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1039 - )
1040 - );
1041 - }
1042 -
1043 - /**
1044 - * Sync Jetpack meta for all followings.
1045 - *
1046 - * Replays the added_post_meta sync action for Jetpack with the Following::FOLLOWING_META_KEY meta key.
1047 - */
1048 - public static function sync_jetpack_following_meta() {
1049 - if ( ! \class_exists( 'Jetpack' ) || ! \Jetpack::is_connection_ready() ) {
1050 - return;
1051 - }
1052 -
1053 - global $wpdb;
1054 -
1055 - // Get all posts that have the following meta key.
1056 - $posts_with_following = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1057 - $wpdb->prepare(
1058 - "SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s",
1059 - Following::FOLLOWING_META_KEY
1060 - ),
1061 - ARRAY_N
1062 - );
1063 -
1064 - // Trigger the added_post_meta action for each following relationship.
1065 - foreach ( $posts_with_following as $meta ) {
1066 - /**
1067 - * Fires when post meta is added.
1068 - *
1069 - * @param int $meta_id ID of the metadata entry.
1070 - * @param int $object_id Post ID.
1071 - * @param string $meta_key Metadata key.
1072 - * @param mixed $meta_value Metadata value.
1073 - */
1074 - \do_action( 'added_post_meta', ...$meta );
1075 - }
1076 - }
1077 -
1078 - /**
1079 - * Clean up inbox items for shared inbox migration.
1080 - *
1081 - * Deletes all existing inbox items to prepare for the new shared inbox structure
1082 - * where activities are stored once with multiple recipients as metadata.
1083 - */
1084 - private static function clean_up_inbox() {
1085 - global $wpdb;
1086 -
1087 - // Get all inbox post IDs.
1088 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1089 - $inbox_ids = $wpdb->get_col(
1090 - $wpdb->prepare(
1091 - "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
1092 - Inbox::POST_TYPE
1093 - )
1094 - );
1095 -
1096 - // Delete all inbox items and their metadata.
1097 - foreach ( $inbox_ids as $post_id ) {
1098 - \wp_delete_post( $post_id, true );
1099 - }
1100 - }
1101 -
1102 - /**
1103 - * Migrate URLs from the legacy `activitypub_tombstone_urls` option into the
1104 - * `ap_tombstone` custom post type.
1105 - *
1106 - * Chunked async migration. Locking and rescheduling is handled by
1107 - * Scheduler::async_batch — the callback returns `array( 'batch_size' => N )`
1108 - * to request another run, or `null` when the option is fully drained.
1109 - *
1110 - * Legacy entries are already-normalized strings (no scheme), so we bypass
1111 - * URL validation and insert directly via wp_insert_post.
1112 - *
1113 - * @since 8.3.0
1114 - *
1115 - * @param int $batch_size Optional. Number of URLs to process per call. Default 500.
1116 - * @return array|null Args for the next run, or null when migration is complete.
1117 - */
1118 - public static function migrate_tombstones_to_cpt( $batch_size = 500 ) {
1119 - global $wpdb;
1120 -
1121 - $urls = \get_option( 'activitypub_tombstone_urls', null );
1122 -
1123 - if ( null === $urls || ! \is_array( $urls ) || empty( $urls ) ) {
1124 - \delete_option( 'activitypub_tombstone_urls' );
1125 - return null;
1126 - }
1127 -
1128 - $chunk = \array_slice( $urls, 0, (int) $batch_size );
1129 - $remaining = \array_slice( $urls, (int) $batch_size );
1130 - $progressed = false;
1131 -
1132 - foreach ( $chunk as $normalized ) {
1133 - if ( ! \is_string( $normalized ) || '' === $normalized ) {
1134 - // Drop garbage entries — counts as progress.
1135 - $progressed = true;
1136 - continue;
1137 - }
1138 -
1139 - $hash = \md5( $normalized );
1140 -
1141 - /*
1142 - * Light existence check. `get_page_by_path()` would hydrate a
1143 - * full `WP_Post` per loop iteration; on a large registry that
1144 - * adds up fast. We only need a boolean here.
1145 - */
1146 - $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1147 - $wpdb->prepare(
1148 - "SELECT 1 FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1",
1149 - Tombstone::POST_TYPE,
1150 - $hash
1151 - )
1152 - );
1153 - if ( $exists ) {
1154 - $progressed = true;
1155 - continue;
1156 - }
1157 -
1158 - /*
1159 - * `guid` is intentionally omitted: the legacy option only kept
1160 - * the normalized (schemeless) form, so we can't reconstruct the
1161 - * original URL. Storing the schemeless string would be mangled
1162 - * by `esc_url()`. Leave WordPress to auto-generate the guid
1163 - * — it's not used for lookups, only for debugging.
1164 - */
1165 - $result = \wp_insert_post(
1166 - array(
1167 - 'post_type' => Tombstone::POST_TYPE,
1168 - 'post_status' => 'publish',
1169 - 'post_name' => $hash,
1170 - 'post_author' => 0,
1171 - ),
1172 - true
1173 - );
1174 -
1175 - if ( \is_wp_error( $result ) || ! $result ) {
1176 - /*
1177 - * Keep failed inserts in the legacy option so the next batch
1178 - * retries them. `Tombstone::exists_local()` still falls back
1179 - * to the option, so the tombstone remains discoverable.
1180 - */
1181 - $remaining[] = $normalized;
1182 - } else {
1183 - $progressed = true;
1184 - }
1185 - }
1186 -
1187 - if ( empty( $remaining ) ) {
1188 - \delete_option( 'activitypub_tombstone_urls' );
1189 - return null;
1190 - }
1191 -
1192 - /*
1193 - * Disable autoload while we drain. The point of the migration is to
1194 - * stop this option from contributing to `alloptions` pressure, so
1195 - * flip the flag immediately rather than waiting for the option to
1196 - * be fully empty before the relief kicks in.
1197 - */
1198 - \update_option( 'activitypub_tombstone_urls', \array_values( $remaining ), false );
1199 -
1200 - /*
1201 - * If nothing in this batch was drained — every insert errored and
1202 - * nothing was already migrated — halt the scheduler so we don't loop
1203 - * forever on a persistent failure. The legacy option still backs
1204 - * exists_local(), so the data isn't lost; an admin can re-trigger
1205 - * the migration via `wp cron event run activitypub_tombstone_migrate`
1206 - * after fixing the underlying cause.
1207 - */
1208 - if ( ! $progressed ) {
1209 - return null;
1210 - }
1211 -
1212 - return array( 'batch_size' => (int) $batch_size );
1213 - }
1214 -
1215 - /**
1216 - * Migrate avatar URLs from comment meta to remote actors in batches.
1217 - *
1218 - * This migration:
1219 - * 1. Finds all comments with ActivityPub protocol and avatar_url meta
1220 - * 2. Looks up the remote actor by comment_author_url
1221 - * 3. Adds _activitypub_remote_actor_id to comment meta
1222 - * 4. Stores avatar_url in remote actor post meta
1223 - *
1224 - * Note: We don't use offset because as we add _activitypub_remote_actor_id,
1225 - * comments are filtered out of the query. We just keep fetching the next
1226 - * batch until no more comments match the criteria.
1227 - *
1228 - * @param int $batch_size Optional. Number of comments to process per batch. Default 50.
1229 - * @return array|null Array with batch size if there are more comments to process, null otherwise.
1230 - */
1231 - public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) {
1232 - global $wpdb;
1233 -
1234 - /*
1235 - * Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet.
1236 - * Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance.
1237 - * Filters meta_key before GROUP BY to reduce rows processed during aggregation.
1238 - * No offset needed - as we process comments, they're filtered out by the HAVING clause.
1239 - */
1240 - $comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1241 - $wpdb->prepare(
1242 - "SELECT c.comment_ID, c.comment_author_url,
1243 - MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url,
1244 - MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol,
1245 - MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id
1246 - FROM {$wpdb->comments} c
1247 - INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id
1248 - WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id')
1249 - GROUP BY c.comment_ID, c.comment_author_url
1250 - HAVING protocol = 'activitypub'
1251 - AND avatar_url IS NOT NULL
1252 - AND (remote_actor_id IS NULL OR remote_actor_id = '')
1253 - LIMIT %d",
1254 - $batch_size
1255 - )
1256 - );
1257 -
1258 - foreach ( $comments as $comment ) {
1259 - if ( empty( $comment->comment_author_url ) ) {
1260 - continue;
1261 - }
1262 -
1263 - // Try to get the remote actor by URI.
1264 - $remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url );
1265 -
1266 - // If we have a valid remote actor, store the reference.
1267 - if ( ! \is_wp_error( $remote_actor ) ) {
1268 - // Add _activitypub_remote_actor_id to comment meta.
1269 - \add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true );
1270 -
1271 - // Ensure avatar is stored on remote actor if not already present.
1272 - $existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true );
1273 - if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) {
1274 - \update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) );
1275 - }
1276 - }
1277 - }
1278 -
1279 - // Return batch info if there are more comments to process.
1280 - if ( \count( $comments ) === $batch_size ) {
1281 - return array(
1282 - 'batch_size' => $batch_size,
1283 - );
1284 - }
1285 -
1286 - return null;
1287 - }
1288 -
1289 - /**
1290 - * Migrate emoji data from stored actor JSON to post meta.
1291 - *
1292 - * This migration:
1293 - * 1. Finds all remote actor posts without _activitypub_emoji meta
1294 - * 2. Extracts emoji from stored JSON in post_content
1295 - * 3. Stores as _activitypub_emoji post meta
1296 - *
1297 - * @param int $batch_size Optional. Number of actors to process per batch. Default 50.
1298 - * @param int $offset Optional. Offset for pagination. Default 0.
1299 - * @return array|null Array with batch size if there are more actors to process, null otherwise.
1300 - */
1301 - public static function migrate_actor_emoji( $batch_size = 50, $offset = 0 ) {
1302 - $actors = \get_posts(
1303 - array(
1304 - 'post_type' => Remote_Actors::POST_TYPE,
1305 - 'posts_per_page' => $batch_size,
1306 - 'offset' => $offset,
1307 - 'post_status' => 'any',
1308 - 'orderby' => 'ID',
1309 - 'order' => 'ASC',
1310 - )
1311 - );
1312 -
1313 - foreach ( $actors as $actor_post ) {
1314 - if ( empty( $actor_post->post_content ) ) {
1315 - continue;
1316 - }
1317 -
1318 - $actor_data = \json_decode( $actor_post->post_content, true );
1319 - if ( ! $actor_data ) {
1320 - continue;
1321 - }
1322 -
1323 - $emoji_meta = Emoji::prepare_actor_meta( $actor_data );
1324 - if ( ! empty( $emoji_meta['_activitypub_emoji'] ) ) {
1325 - \update_post_meta( $actor_post->ID, '_activitypub_emoji', $emoji_meta['_activitypub_emoji'] );
1326 - }
1327 - }
1328 -
1329 - // Return batch info if there are more actors to process.
1330 - if ( \count( $actors ) === $batch_size ) {
1331 - return array(
1332 - 'batch_size' => $batch_size,
1333 - 'offset' => $offset + $batch_size,
1334 - );
1335 - }
1336 -
1337 - return null;
1338 - }
1339 -
1340 - /**
1341 - * Migrate the Application key pair option from the old name to the new name.
1342 - *
1343 - * Renames `activitypub_keypair_for_-1` to `activitypub_application_keypair`.
1344 - * Older separate key options (activitypub_application_user_public_key /
1345 - * activitypub_application_user_private_key) are migrated lazily on first read.
1346 - *
1347 - * @since 9.1.0
1348 - */
1349 - public static function migrate_application_keypair_option() {
1350 - self::update_options_key( 'activitypub_keypair_for_-1', Application::KEYPAIR_OPTION_KEY );
1351 -
1352 - // The raw rename bypasses the options API, so drop only the two stale option caches (plus the autoload bucket) instead of flushing everything.
1353 - \wp_cache_delete( 'activitypub_keypair_for_-1', 'options' );
1354 - \wp_cache_delete( Application::KEYPAIR_OPTION_KEY, 'options' );
1355 - \wp_cache_delete( 'alloptions', 'options' );
1356 -
1357 - /*
1358 - * If an early Application::get_keypair() read already created the destination
1359 - * option, the rename above is a no-op blocked by the unique `option_name`,
1360 - * leaving the legacy row behind. Drop it once the destination is in place.
1361 - */
1362 - if ( false !== \get_option( Application::KEYPAIR_OPTION_KEY, false ) ) {
1363 - \delete_option( 'activitypub_keypair_for_-1' );
1364 - }
1365 - }
1366 -
1367 - /**
1368 - * Delete outbox items that belonged to the Application actor.
1369 - *
1370 - * The Application used to queue Reject activities through the Outbox as user
1371 - * ID -1. It no longer dispatches activities, so any pending items are
1372 - * undeliverable and are removed.
1373 - *
1374 - * @since 9.1.0
1375 - */
1376 - public static function delete_application_outbox_items() {
1377 - $items = \get_posts(
1378 - array(
1379 - 'post_type' => Outbox::POST_TYPE,
1380 - 'post_status' => 'any',
1381 - 'nopaging' => true,
1382 - 'fields' => 'ids',
1383 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
1384 - 'meta_query' => array(
1385 - array(
1386 - 'key' => '_activitypub_activity_actor',
1387 - 'value' => 'application',
1388 - ),
1389 - ),
1390 - )
1391 - );
1392 -
1393 - foreach ( $items as $item_id ) {
1394 - \wp_delete_post( $item_id, true );
1395 - }
1396 925 }
1397 926 }