PluginProbe
ActivityPub / 9.2.1
ActivityPub v9.2.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
← All changes | includes/class-migration.php +212 -29 8.2.19.2.1 View file →
@@ -10,8 +10,9 @@
10 10 use Activitypub\Collection\Actors;
11 11 use Activitypub\Collection\Extra_Fields;
12 12 use Activitypub\Collection\Followers;
13 13 use Activitypub\Collection\Following;
14 +use Activitypub\Collection\Inbox;
14 15 use Activitypub\Collection\Outbox;
15 16 use Activitypub\Collection\Remote_Actors;
16 17 use Activitypub\Transformer\Factory;
17 18
@@ -33,8 +34,9 @@
33 34 Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) );
34 35 Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) );
35 36 Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) );
36 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' ) );
37 39 }
38 40
39 41 /**
40 42 * The current version of the database structure.
@@ -41,9 +43,9 @@
41 43 *
42 44 * @return string The current version.
43 45 */
44 46 public static function get_version() {
45 - return get_option( 'activitypub_db_version', 0 );
47 + return \get_option( 'activitypub_db_version', 0 );
46 48 }
47 49
48 50 /**
49 51 * Locks the database migration process to prevent simultaneous migrations.
@@ -183,17 +185,17 @@
183 185 self::migrate_followers_to_ap_actor_cpt();
184 186 \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) );
185 187 }
186 188 if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) {
187 - wp_unschedule_hook( 'activitypub_update_followers' );
188 - wp_unschedule_hook( 'activitypub_cleanup_followers' );
189 + \wp_unschedule_hook( 'activitypub_update_followers' );
190 + \wp_unschedule_hook( 'activitypub_cleanup_followers' );
189 191
190 192 if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
191 - \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' );
193 + \wp_schedule_event( \time(), 'hourly', 'activitypub_update_remote_actors' );
192 194 }
193 195
194 196 if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
195 - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' );
197 + \wp_schedule_event( \time(), 'daily', 'activitypub_cleanup_remote_actors' );
196 198 }
197 199 }
198 200 if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) {
199 201 self::remove_pending_application_user_follow_requests();
@@ -207,13 +209,23 @@
207 209 }
208 210 if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) {
209 211 \wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' );
210 212 }
211 - if ( \version_compare( $version_from_db, '8.1.0', '<' ) ) {
212 - // Backfill historical statistics data (delay to avoid load immediately after upgrade).
213 - \wp_schedule_single_event( \time() + HOUR_IN_SECONDS, 'activitypub_backfill_statistics' );
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' );
214 216 }
215 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 +
216 228 /*
217 229 * Defer the flush to late in the `init` cycle (priority 20). Migration::init
218 230 * runs at priority 1, which is earlier than most plugins register their
219 231 * rewrite rules. Flushing synchronously here would persist a truncated
@@ -290,10 +302,10 @@
290 302 * Updates the DB-schema of the followers-list.
291 303 */
292 304 public static function migrate_from_0_17() {
293 305 // Migrate followers.
294 - foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
295 - $followers = get_user_meta( $user_id, 'activitypub_followers', true );
306 + foreach ( \get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
307 + $followers = \get_user_meta( $user_id, 'activitypub_followers', true );
296 308
297 309 if ( $followers ) {
298 310 foreach ( $followers as $actor ) {
299 311 Followers::add( $user_id, $actor );
@@ -313,9 +325,9 @@
313 325 )
314 326 );
315 327
316 328 foreach ( $user_ids as $user_id ) {
317 - wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
329 + \wp_cache_delete( \sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
318 330 }
319 331 }
320 332
321 333 /**
@@ -321,15 +333,15 @@
321 333 /**
322 334 * Unschedule Hooks after updating to 2.0.0.
323 335 */
324 336 private static function migrate_from_2_0_0() {
325 - wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
326 - wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
327 - wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
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' );
328 340
329 - wp_unschedule_hook( 'activitypub_send_post_activity' );
330 - wp_unschedule_hook( 'activitypub_send_update_activity' );
331 - wp_unschedule_hook( 'activitypub_send_delete_activity' );
341 + \wp_unschedule_hook( 'activitypub_send_post_activity' );
342 + \wp_unschedule_hook( 'activitypub_send_update_activity' );
343 + \wp_unschedule_hook( 'activitypub_send_delete_activity' );
332 344
333 345 $object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
334 346 if ( 'article' === $object_type ) {
335 347 \update_option( 'activitypub_object_type', 'wordpress-post-format' );
@@ -348,9 +360,9 @@
348 360 /**
349 361 * Rename DB fields.
350 362 */
351 363 private static function migrate_from_2_6_0() {
352 - wp_cache_flush();
364 + \wp_cache_flush();
353 365
354 366 self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
355 367
356 368 self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
@@ -364,9 +376,9 @@
364 376 private static function migrate_to_4_0_0() {
365 377 $latest_post_id = 0;
366 378
367 379 // Get the ID of the latest blog post and save it to the options table.
368 - $latest_post = get_posts(
380 + $latest_post = \get_posts(
369 381 array(
370 382 'numberposts' => 1,
371 383 'orderby' => 'ID',
372 384 'order' => 'DESC',
@@ -477,9 +489,9 @@
477 489 $followers = $wpdb->get_col(
478 490 $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE )
479 491 );
480 492 foreach ( $followers as $id ) {
481 - clean_post_cache( $id );
493 + \clean_post_cache( $id );
482 494 }
483 495 }
484 496
485 497 /**
@@ -495,9 +507,9 @@
495 507 global $wpdb;
496 508
497 509 Comment::register_comment_types();
498 510 $comment_types = Comment::get_comment_type_slugs();
499 - $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
511 + $type_inclusion = "AND comment_type IN ('" . \implode( "','", $comment_types ) . "')";
500 512
501 513 // Get and process this batch.
502 514 $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
503 515 $wpdb->prepare(
@@ -511,9 +523,9 @@
511 523 foreach ( $post_ids as $post_id ) {
512 524 \wp_update_comment_count_now( $post_id );
513 525 }
514 526
515 - if ( count( $post_ids ) === $batch_size ) {
527 + if ( \count( $post_ids ) === $batch_size ) {
516 528 // Schedule next batch.
517 529 return array( $batch_size, $offset + $batch_size );
518 530 }
519 531 }
@@ -555,9 +567,9 @@
555 567 self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
556 568 }
557 569 }
558 570
559 - if ( count( $posts ) === $batch_size ) {
571 + if ( \count( $posts ) === $batch_size ) {
560 572 return array(
561 573 'batch_size' => $batch_size,
562 574 'offset' => $offset + $batch_size,
563 575 );
@@ -592,9 +604,9 @@
592 604 foreach ( $comments as $comment ) {
593 605 self::add_to_outbox( $comment, 'Create', $comment->user_id );
594 606 }
595 607
596 - if ( count( $comments ) === $batch_size ) {
608 + if ( \count( $comments ) === $batch_size ) {
597 609 return array(
598 610 'batch_size' => $batch_size,
599 611 'offset' => $offset + $batch_size,
600 612 );
@@ -688,9 +700,9 @@
688 700 )
689 701 );
690 702 }
691 703
692 - if ( count( $comments ) === $batch_size ) {
704 + if ( \count( $comments ) === $batch_size ) {
693 705 return array(
694 706 'batch_size' => $batch_size,
695 707 'offset' => $offset + $batch_size,
696 708 );
@@ -1022,9 +1034,9 @@
1022 1034 $wpdb->delete(
1023 1035 $wpdb->postmeta,
1024 1036 array(
1025 1037 'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
1026 - 'meta_value' => Actors::APPLICATION_USER_ID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1038 + 'meta_value' => -1, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
1027 1039 )
1028 1040 );
1029 1041 }
1030 1042
@@ -1076,9 +1088,9 @@
1076 1088 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1077 1089 $inbox_ids = $wpdb->get_col(
1078 1090 $wpdb->prepare(
1079 1091 "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s",
1080 - \Activitypub\Collection\Inbox::POST_TYPE
1092 + Inbox::POST_TYPE
1081 1093 )
1082 1094 );
1083 1095
1084 1096 // Delete all inbox items and their metadata.
@@ -1087,8 +1099,121 @@
1087 1099 }
1088 1100 }
1089 1101
1090 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 + /**
1091 1216 * Migrate avatar URLs from comment meta to remote actors in batches.
1092 1217 *
1093 1218 * This migration:
1094 1219 * 1. Finds all comments with ActivityPub protocol and avatar_url meta
@@ -1151,9 +1276,9 @@
1151 1276 }
1152 1277 }
1153 1278
1154 1279 // Return batch info if there are more comments to process.
1155 - if ( count( $comments ) === $batch_size ) {
1280 + if ( \count( $comments ) === $batch_size ) {
1156 1281 return array(
1157 1282 'batch_size' => $batch_size,
1158 1283 );
1159 1284 }
@@ -1201,9 +1326,9 @@
1201 1326 }
1202 1327 }
1203 1328
1204 1329 // Return batch info if there are more actors to process.
1205 - if ( count( $actors ) === $batch_size ) {
1330 + if ( \count( $actors ) === $batch_size ) {
1206 1331 return array(
1207 1332 'batch_size' => $batch_size,
1208 1333 'offset' => $offset + $batch_size,
1209 1334 );
@@ -1209,6 +1334,64 @@
1209 1334 );
1210 1335 }
1211 1336
1212 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 + }
1213 1396 }
1214 1397 }