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-scheduler.php +145 -30 8.2.19.2.1 View file →
@@ -38,8 +38,9 @@
38 38 'activitypub_reprocess_outbox' => 'hourly',
39 39 'activitypub_outbox_purge' => 'daily',
40 40 'activitypub_inbox_purge' => 'daily',
41 41 'activitypub_ap_post_purge' => 'daily',
42 + 'activitypub_tombstone_purge' => 'daily',
42 43 'activitypub_sync_blocklist_subscriptions' => 'weekly',
43 44 );
44 45
45 46 /**
@@ -51,17 +52,24 @@
51 52
52 53 /**
53 54 * Get the pause between async batches (in seconds).
54 55 *
56 + * @param string|false|null $hook Optional. The async batch hook being scheduled. Default current action.
57 + *
55 58 * @return int The pause in seconds.
56 59 */
57 - public static function get_retry_delay() {
60 + public static function get_retry_delay( $hook = null ) {
61 + if ( null === $hook ) {
62 + $hook = \current_action();
63 + }
64 +
58 65 /**
59 66 * Filters the pause between async batches (in seconds).
60 67 *
61 - * @param int $async_batch_pause The pause in seconds. Default 30.
68 + * @param int $async_batch_pause The pause in seconds. Default 30.
69 + * @param string|false|null $hook The async batch hook being scheduled.
62 70 */
63 - return apply_filters( 'activitypub_scheduler_async_batch_pause', 30 );
71 + return \apply_filters( 'activitypub_scheduler_async_batch_pause', 30, $hook );
64 72 }
65 73
66 74 /**
67 75 * Initialize the class, registering WordPress hooks.
@@ -81,8 +89,9 @@
81 89 \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
82 90 \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
83 91 \add_action( 'activitypub_inbox_purge', array( self::class, 'purge_inbox' ) );
84 92 \add_action( 'activitypub_ap_post_purge', array( self::class, 'purge_ap_posts' ) );
93 + \add_action( 'activitypub_tombstone_purge', array( self::class, 'purge_tombstones' ) );
85 94 \add_action( 'activitypub_inbox_create_item', array( self::class, 'process_inbox_activity' ) );
86 95 \add_action( 'activitypub_sync_blocklist_subscriptions', array( Blocklist_Subscriptions::class, 'sync_all' ) );
87 96
88 97 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
@@ -159,9 +168,9 @@
159 168 */
160 169 public static function register_schedules() {
161 170 foreach ( self::SCHEDULES as $hook => $recurrence ) {
162 171 if ( ! \wp_next_scheduled( $hook ) ) {
163 - \wp_schedule_event( time(), $recurrence, $hook );
172 + \wp_schedule_event( \time(), $recurrence, $hook );
164 173 }
165 174 }
166 175
167 176 // Schedule monthly stats collection for the 1st of each month.
@@ -183,9 +192,9 @@
183 192 *
184 193 * @return void
185 194 */
186 195 public static function deregister_schedules() {
187 - foreach ( array_keys( self::SCHEDULES ) as $hook ) {
196 + foreach ( \array_keys( self::SCHEDULES ) as $hook ) {
188 197 \wp_unschedule_hook( $hook );
189 198 }
190 199
191 200 // Statistics schedules.
@@ -214,13 +223,13 @@
214 223 $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
215 224 $year = (int) \gmdate( 'Y', $now );
216 225
217 226 // Get December 1st 3:00 AM for this year.
218 - $this_year_dec_first = \strtotime( sprintf( '%d-12-01 03:00:00', $year ) );
227 + $this_year_dec_first = \strtotime( \sprintf( '%d-12-01 03:00:00', $year ) );
219 228
220 229 // If we're already past this year's December 1st, schedule for next year.
221 230 if ( $now >= $this_year_dec_first ) {
222 - return \strtotime( sprintf( '%d-12-01 03:00:00', $year + 1 ) );
231 + return \strtotime( \sprintf( '%d-12-01 03:00:00', $year + 1 ) );
223 232 }
224 233
225 234 return $this_year_dec_first;
226 235 }
@@ -230,24 +239,17 @@
230 239 *
231 240 * @param int $outbox_item_id The outbox item ID.
232 241 */
233 242 public static function unschedule_events_for_item( $outbox_item_id ) {
234 - $event_args = array(
235 - $outbox_item_id,
236 - Dispatcher::get_batch_size(),
237 - \get_post_meta( $outbox_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
238 - );
239 -
240 243 \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
241 244
242 245 $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
243 246 \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
244 247
245 - $timestamp = \wp_next_scheduled( 'activitypub_send_activity', $event_args );
246 - \wp_unschedule_event( $timestamp, 'activitypub_send_activity', $event_args );
248 + self::unschedule_outbox_delivery_batches( $outbox_item_id );
247 249
248 250 // Invalidate any retries for this outbox item.
249 - foreach ( _get_cron_array() as $timestamp => $cron ) {
251 + foreach ( \_get_cron_array() as $timestamp => $cron ) {
250 252 if ( ! isset( $cron['activitypub_retry_activity'] ) ) {
251 253 continue;
252 254 }
253 255
@@ -264,9 +266,9 @@
264 266 */
265 267 public static function update_remote_actors() {
266 268 $number = 5;
267 269
268 - if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
270 + if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
269 271 $number = 50;
270 272 }
271 273
272 274 /**
@@ -273,18 +275,56 @@
273 275 * Filter the number of remote Actors to update.
274 276 *
275 277 * @param int $number The number of remote Actors to update.
276 278 */
277 - $number = apply_filters( 'activitypub_update_remote_actors_number', $number );
279 + $number = \apply_filters( 'activitypub_update_remote_actors_number', $number );
278 280 $actors = Remote_Actors::get_outdated( $number );
279 281
280 282 foreach ( $actors as $actor ) {
281 - $meta = get_remote_metadata_by_actor( $actor->guid, false );
283 + /*
284 + * Use Http::get_remote_object() directly here.
285 + * get_remote_metadata_by_actor() short-circuits to the locally
286 + * cached ap_actor CPT via Remote_Actors::fetch_by_uri() and never
287 + * makes an HTTP request when the actor is already cached, so the
288 + * upsert would just rewrite the same stale data and this refresh
289 + * would be a no-op. The Update handler documents the same trap.
290 + */
291 + $meta = Http::get_remote_object( $actor->guid, false );
282 292
283 - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
293 + if ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
284 294 Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
285 295 } else {
286 - $id = Remote_Actors::upsert( $meta );
296 + /*
297 + * Only refresh when the remote still reports the same identity. A
298 + * different (or missing) id means a Move or a malformed response;
299 + * applying it would rewrite the cached guid in place and could
300 + * collide with another cached actor, so leave the record alone.
301 + * Updating by the known post ID otherwise refreshes it without the
302 + * redundant get_by_uri() lookup upsert() would do.
303 + */
304 + $fetched_id = isset( $meta['id'] ) && \is_string( $meta['id'] ) ? \esc_url_raw( $meta['id'] ) : '';
305 + if ( $fetched_id !== $actor->guid ) {
306 + /*
307 + * Bump only the modified date, directly, so the skipped actor
308 + * drops out of the outdated queue and is not re-fetched every
309 + * run. A direct write avoids the save_post hooks wp_update_post()
310 + * fires (which would needlessly clear the cached avatar); the
311 + * record is intentionally left unchanged otherwise.
312 + */
313 + global $wpdb;
314 + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
315 + $wpdb->posts,
316 + array(
317 + 'post_modified' => \current_time( 'mysql' ),
318 + 'post_modified_gmt' => \current_time( 'mysql', true ),
319 + ),
320 + array( 'ID' => $actor->ID )
321 + );
322 + \clean_post_cache( $actor->ID );
323 + continue;
324 + }
325 +
326 + $id = Remote_Actors::update( $actor->ID, $meta );
287 327 if ( \is_wp_error( $id ) ) {
288 328 continue;
289 329 }
290 330 Remote_Actors::clear_errors( $id );
@@ -297,9 +337,9 @@
297 337 */
298 338 public static function cleanup_remote_actors() {
299 339 $number = 5;
300 340
301 - if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
341 + if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
302 342 $number = 50;
303 343 }
304 344
305 345 /**
@@ -306,9 +346,9 @@
306 346 * Filter the number of remote Actors to clean up.
307 347 *
308 348 * @param int $number The number of remote Actors to clean up.
309 349 */
310 - $number = apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
350 + $number = \apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
311 351 $actors = Remote_Actors::get_faulty( $number );
312 352
313 353 foreach ( $actors as $actor ) {
314 354 $meta = get_remote_metadata_by_actor( $actor->guid, false );
@@ -314,9 +354,9 @@
314 354 $meta = get_remote_metadata_by_actor( $actor->guid, false );
315 355
316 356 if ( Tombstone::exists( $meta ) ) {
317 357 \wp_delete_post( $actor->ID );
318 - } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) {
358 + } elseif ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
319 359 if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
320 360 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
321 361 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
322 362 \wp_delete_post( $actor->ID );
@@ -343,9 +383,9 @@
343 383 public static function schedule_outbox_activity_for_federation( $id, $offset = 3 ) {
344 384 $hook = 'activitypub_process_outbox';
345 385 $args = array( $id );
346 386
347 - if ( false === wp_next_scheduled( $hook, $args ) ) {
387 + if ( false === \wp_next_scheduled( $hook, $args ) ) {
348 388 \wp_schedule_single_event(
349 389 \time() + $offset,
350 390 $hook,
351 391 $args
@@ -368,9 +408,9 @@
368 408
369 409 foreach ( $ids as $id ) {
370 410 // Bail if there is a pending batch.
371 411 $offset = \get_post_meta( $id, '_activitypub_outbox_offset', true ) ?: 0; // phpcs:ignore
372 - if ( \wp_next_scheduled( 'activitypub_send_activity', array( $id, Dispatcher::get_batch_size(), $offset ) ) ) {
412 + if ( self::has_scheduled_outbox_delivery_batch( $id, $offset ) ) {
373 413 return;
374 414 }
375 415
376 416 // Bail if there is a batch in progress.
@@ -404,8 +444,20 @@
404 444 Remote_Posts::purge( \get_option( 'activitypub_ap_post_purge_days', ACTIVITYPUB_AP_POST_PURGE_DAYS ) );
405 445 }
406 446
407 447 /**
448 + * Daily cron handler that purges expired tombstones.
449 + *
450 + * Retention is non-urgent: large backlogs (e.g. after retention is first enforced)
451 + * drain across multiple daily runs.
452 + *
453 + * @since 8.3.0
454 + */
455 + public static function purge_tombstones() {
456 + Tombstone::purge();
457 + }
458 +
459 + /**
408 460 * Process cached inbox activity.
409 461 *
410 462 * Retrieves all collected user IDs for an activity and processes them together.
411 463 *
@@ -420,9 +472,9 @@
420 472
421 473 $data = \json_decode( $inbox_item->post_content, true );
422 474 // Reconstruct activity from inbox post.
423 475 $activity = Activity::init_from_array( $data );
424 - $type = \Activitypub\camel_to_snake_case( $activity->get_type() );
476 + $type = camel_to_snake_case( $activity->get_type() );
425 477 $context = Inbox::CONTEXT_INBOX;
426 478 $user_ids = Inbox::get_recipients( $inbox_item->ID );
427 479
428 480 /**
@@ -527,13 +579,76 @@
527 579 self::unlock( $key );
528 580
529 581 if ( ! empty( $next ) ) {
530 582 // Schedule the next run, adding the result to the arguments.
531 - \wp_schedule_single_event( \time() + self::get_retry_delay(), \current_action(), \array_values( $next ) );
583 + \wp_schedule_single_event( \time() + self::get_retry_delay( \current_action() ), \current_action(), \array_values( $next ) );
532 584 }
533 585 }
534 586
535 587 /**
588 + * Whether an outbox item already has a scheduled delivery batch at an offset.
589 + *
590 + * @param int $outbox_item_id The outbox item ID.
591 + * @param int $offset The delivery offset.
592 + *
593 + * @return bool True when a matching delivery batch is scheduled.
594 + */
595 + private static function has_scheduled_outbox_delivery_batch( $outbox_item_id, $offset ) {
596 + return ! empty( self::get_scheduled_outbox_delivery_batches( $outbox_item_id, $offset ) );
597 + }
598 +
599 + /**
600 + * Unschedule all pending delivery batches for an outbox item.
601 + *
602 + * @param int $outbox_item_id The outbox item ID.
603 + */
604 + private static function unschedule_outbox_delivery_batches( $outbox_item_id ) {
605 + foreach ( self::get_scheduled_outbox_delivery_batches( $outbox_item_id ) as $event ) {
606 + \wp_unschedule_event( $event['timestamp'], 'activitypub_send_activity', $event['args'] );
607 + }
608 + }
609 +
610 + /**
611 + * Get scheduled delivery batches for an outbox item.
612 + *
613 + * The batch size is deliberately ignored because scheduled events may
614 + * retain an older value after the admin changes the distribution mode.
615 + *
616 + * @param int $outbox_item_id The outbox item ID.
617 + * @param int|null $offset Optional. Restrict results to this delivery offset.
618 + *
619 + * @return array Scheduled events with timestamp and args.
620 + */
621 + private static function get_scheduled_outbox_delivery_batches( $outbox_item_id, $offset = null ) {
622 + $events = array();
623 +
624 + foreach ( \_get_cron_array() as $timestamp => $cron ) {
625 + if ( empty( $cron['activitypub_send_activity'] ) ) {
626 + continue;
627 + }
628 +
629 + foreach ( $cron['activitypub_send_activity'] as $event ) {
630 + $args = $event['args'] ?? array();
631 +
632 + if ( ! isset( $args[0] ) || (int) $outbox_item_id !== (int) $args[0] ) {
633 + continue;
634 + }
635 +
636 + if ( null !== $offset && (int) ( $args[2] ?? 0 ) !== (int) $offset ) {
637 + continue;
638 + }
639 +
640 + $events[] = array(
641 + 'timestamp' => $timestamp,
642 + 'args' => $args,
643 + );
644 + }
645 + }
646 +
647 + return $events;
648 + }
649 +
650 + /**
536 651 * Locks the async batch process for individual callbacks to prevent simultaneous processing.
537 652 *
538 653 * @param string $key Serialized callback name.
539 654 * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
@@ -611,14 +726,14 @@
611 726 if ( 'Create' !== $activity->get_type() ) {
612 727 return;
613 728 }
614 729
615 - if ( ! is_object( $activity->get_object() ) ) {
730 + if ( ! \is_object( $activity->get_object() ) ) {
616 731 return;
617 732 }
618 733
619 734 // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
620 - if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
735 + if ( ! \in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
621 736 return;
622 737 }
623 738
624 739 $announce = new Activity();