PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.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
activitypub / includes / class-scheduler.php

class-scheduler.php in ActivityPub 7.8.0, at includes/class-scheduler.php

579 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Scheduler class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Base_Object;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Collection\Inbox;
14 use Activitypub\Collection\Outbox;
15 use Activitypub\Collection\Posts;
16 use Activitypub\Collection\Remote_Actors;
17 use Activitypub\Scheduler\Actor;
18 use Activitypub\Scheduler\Collection_Sync;
19 use Activitypub\Scheduler\Comment;
20 use Activitypub\Scheduler\Post;
21
22 /**
23 * Scheduler class.
24 *
25 * @author Matthias Pfefferle
26 */
27 class Scheduler {
28
29 /**
30 * Allowed batch callbacks.
31 *
32 * @var array
33 */
34 private static $batch_callbacks = array();
35
36 /**
37 * Get the pause between async batches (in seconds).
38 *
39 * @return int The pause in seconds.
40 */
41 public static function get_retry_delay() {
42 /**
43 * Filters the pause between async batches (in seconds).
44 *
45 * @param int $async_batch_pause The pause in seconds. Default 30.
46 */
47 return apply_filters( 'activitypub_scheduler_async_batch_pause', 30 );
48 }
49
50 /**
51 * Initialize the class, registering WordPress hooks.
52 */
53 public static function init() {
54 self::register_schedulers();
55
56 // Follower Cleanups.
57 \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) );
58 \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) );
59
60 // Event callbacks.
61 \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
62 \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
63 \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
64 \add_action( 'activitypub_inbox_purge', array( self::class, 'purge_inbox' ) );
65 \add_action( 'activitypub_ap_post_purge', array( self::class, 'purge_ap_posts' ) );
66 \add_action( 'activitypub_inbox_create_item', array( self::class, 'process_inbox_activity' ) );
67 \add_action( 'activitypub_sync_blocklist_subscriptions', array( Blocklist_Subscriptions::class, 'sync_all' ) );
68
69 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
70 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
71
72 \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'update_outbox_purge_schedule' ), 10, 2 );
73 \add_action( 'update_option_activitypub_inbox_purge_days', array( self::class, 'update_inbox_purge_schedule' ), 10, 2 );
74 \add_action( 'update_option_activitypub_ap_post_purge_days', array( self::class, 'update_ap_post_purge_schedule' ), 10, 2 );
75 }
76
77 /**
78 * Register handlers.
79 */
80 public static function register_schedulers() {
81 Post::init();
82 Actor::init();
83 Collection_Sync::init();
84 Comment::init();
85
86 /**
87 * Register additional schedulers.
88 *
89 * @since 5.0.0
90 */
91 \do_action( 'activitypub_register_schedulers' );
92 }
93
94 /**
95 * Register a batch callback for async processing.
96 *
97 * @param string $hook The cron event hook name.
98 * @param callable $callback The callback to execute.
99 */
100 public static function register_async_batch_callback( $hook, $callback ) {
101 if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) {
102 \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '7.5.0' );
103 return;
104 }
105
106 if ( ! \is_callable( $callback ) ) {
107 return;
108 }
109
110 self::$batch_callbacks[ $hook ] = $callback;
111
112 // Register the WordPress action hook to trigger async_batch.
113 \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 );
114 }
115
116 /**
117 * Schedule all ActivityPub schedules.
118 */
119 public static function register_schedules() {
120 if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) {
121 \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' );
122 }
123
124 if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) {
125 \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' );
126 }
127
128 if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
129 \wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
130 }
131
132 if ( ! \wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
133 \wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
134 }
135
136 if ( ! \wp_next_scheduled( 'activitypub_inbox_purge' ) ) {
137 \wp_schedule_event( time(), 'daily', 'activitypub_inbox_purge' );
138 }
139
140 if ( ! \wp_next_scheduled( 'activitypub_ap_post_purge' ) ) {
141 \wp_schedule_event( time(), 'daily', 'activitypub_ap_post_purge' );
142 }
143
144 if ( ! \wp_next_scheduled( 'activitypub_sync_blocklist_subscriptions' ) ) {
145 \wp_schedule_event( time(), 'weekly', 'activitypub_sync_blocklist_subscriptions' );
146 }
147 }
148
149 /**
150 * Un-schedule all ActivityPub schedules.
151 *
152 * @return void
153 */
154 public static function deregister_schedules() {
155 \wp_unschedule_hook( 'activitypub_update_remote_actors' );
156 \wp_unschedule_hook( 'activitypub_cleanup_remote_actors' );
157 \wp_unschedule_hook( 'activitypub_reprocess_outbox' );
158 \wp_unschedule_hook( 'activitypub_outbox_purge' );
159 \wp_unschedule_hook( 'activitypub_inbox_purge' );
160 \wp_unschedule_hook( 'activitypub_ap_post_purge' );
161 \wp_unschedule_hook( 'activitypub_sync_blocklist_subscriptions' );
162 }
163
164 /**
165 * Unschedule events for an outbox item.
166 *
167 * @param int $outbox_item_id The outbox item ID.
168 */
169 public static function unschedule_events_for_item( $outbox_item_id ) {
170 $event_args = array(
171 $outbox_item_id,
172 Dispatcher::get_batch_size(),
173 \get_post_meta( $outbox_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
174 );
175
176 \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
177
178 $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
179 \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
180
181 $timestamp = \wp_next_scheduled( 'activitypub_send_activity', $event_args );
182 \wp_unschedule_event( $timestamp, 'activitypub_send_activity', $event_args );
183
184 // Invalidate any retries for this outbox item.
185 foreach ( _get_cron_array() as $timestamp => $cron ) {
186 if ( ! isset( $cron['activitypub_retry_activity'] ) ) {
187 continue;
188 }
189
190 foreach ( $cron['activitypub_retry_activity'] as $event ) {
191 if ( isset( $event['args'][1] ) && $outbox_item_id === $event['args'][1] ) {
192 \wp_unschedule_event( $timestamp, 'activitypub_retry_activity', $event['args'] );
193 }
194 }
195 }
196 }
197
198 /**
199 * Update remote Actors.
200 */
201 public static function update_remote_actors() {
202 $number = 5;
203
204 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
205 $number = 50;
206 }
207
208 /**
209 * Filter the number of remote Actors to update.
210 *
211 * @param int $number The number of remote Actors to update.
212 */
213 $number = apply_filters( 'activitypub_update_remote_actors_number', $number );
214 $actors = Remote_Actors::get_outdated( $number );
215
216 foreach ( $actors as $actor ) {
217 $meta = get_remote_metadata_by_actor( $actor->guid, false );
218
219 if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
220 Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
221 } else {
222 $id = Remote_Actors::upsert( $meta );
223 if ( \is_wp_error( $id ) ) {
224 continue;
225 }
226 Remote_Actors::clear_errors( $id );
227 }
228 }
229 }
230
231 /**
232 * Cleanup remote Actors.
233 */
234 public static function cleanup_remote_actors() {
235 $number = 5;
236
237 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
238 $number = 50;
239 }
240
241 /**
242 * Filter the number of remote Actors to clean up.
243 *
244 * @param int $number The number of remote Actors to clean up.
245 */
246 $number = apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
247 $actors = Remote_Actors::get_faulty( $number );
248
249 foreach ( $actors as $actor ) {
250 $meta = get_remote_metadata_by_actor( $actor->guid, false );
251
252 if ( Tombstone::exists( $meta ) ) {
253 \wp_delete_post( $actor->ID );
254 } elseif ( empty( $meta ) || ! is_array( $meta ) || \is_wp_error( $meta ) ) {
255 if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
256 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
257 \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
258 \wp_delete_post( $actor->ID );
259 } else {
260 Remote_Actors::add_error( $actor->ID, $meta );
261 }
262 } else {
263 $id = Remote_Actors::upsert( $meta );
264 if ( \is_wp_error( $id ) ) {
265 Remote_Actors::add_error( $actor->ID, $id );
266 } else {
267 Remote_Actors::clear_errors( $actor->ID );
268 }
269 }
270 }
271 }
272
273 /**
274 * Schedule the outbox item for federation.
275 *
276 * @param int $id The ID of the outbox item.
277 * @param int $offset The offset to add to the scheduled time.
278 */
279 public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) {
280 $hook = 'activitypub_process_outbox';
281 $args = array( $id );
282
283 if ( false === wp_next_scheduled( $hook, $args ) ) {
284 \wp_schedule_single_event(
285 \time() + $offset,
286 $hook,
287 $args
288 );
289 }
290 }
291
292 /**
293 * Reprocess the outbox.
294 */
295 public static function reprocess_outbox() {
296 $ids = \get_posts(
297 array(
298 'post_type' => Outbox::POST_TYPE,
299 'post_status' => 'pending',
300 'posts_per_page' => 10,
301 'fields' => 'ids',
302 )
303 );
304
305 foreach ( $ids as $id ) {
306 // Bail if there is a pending batch.
307 $offset = \get_post_meta( $id, '_activitypub_outbox_offset', true ) ?: 0; // phpcs:ignore
308 if ( \wp_next_scheduled( 'activitypub_send_activity', array( $id, Dispatcher::get_batch_size(), $offset ) ) ) {
309 return;
310 }
311
312 // Bail if there is a batch in progress.
313 $key = \md5( \serialize( $id ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
314 if ( self::is_locked( $key ) ) {
315 return;
316 }
317
318 self::schedule_outbox_activity_for_federation( $id );
319 }
320 }
321
322 /**
323 * Purge outbox items based on a schedule.
324 */
325 public static function purge_outbox() {
326 $days = (int) \get_option( 'activitypub_outbox_purge_days', 180 );
327 Outbox::purge( $days );
328 }
329
330 /**
331 * Purge inbox items based on a schedule.
332 */
333 public static function purge_inbox() {
334 $days = (int) \get_option( 'activitypub_inbox_purge_days', 180 );
335 Inbox::purge( $days );
336 }
337
338 /**
339 * Purge remote posts based on a schedule.
340 */
341 public static function purge_ap_posts() {
342 $days = (int) \get_option( 'activitypub_ap_post_purge_days', 30 );
343 Posts::purge( $days );
344 }
345
346 /**
347 * Process cached inbox activity.
348 *
349 * Retrieves all collected user IDs for an activity and processes them together.
350 *
351 * @param string $activity_id The activity ID.
352 */
353 public static function process_inbox_activity( $activity_id ) {
354 // Deduplicate if multiple inbox items were created due to race condition.
355 $inbox_item = Inbox::deduplicate( $activity_id );
356 if ( ! $inbox_item ) {
357 return;
358 }
359
360 $data = \json_decode( $inbox_item->post_content, true );
361 // Reconstruct activity from inbox post.
362 $activity = Activity::init_from_array( $data );
363 $type = \Activitypub\camel_to_snake_case( $activity->get_type() );
364 $context = Inbox::CONTEXT_INBOX;
365 $user_ids = Inbox::get_recipients( $inbox_item->ID );
366
367 /**
368 * Fires after any ActivityPub Inbox activity has been handled, regardless of activity type.
369 *
370 * This hook is triggered for all activity types processed by the inbox handler.
371 *
372 * @param array $data The data array.
373 * @param array $user_ids The user IDs.
374 * @param string $type The type of the activity.
375 * @param Activity $activity The Activity object.
376 * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
377 * @param string $context The context of the request ('inbox' or 'shared_inbox').
378 */
379 \do_action( 'activitypub_handled_inbox', $data, $user_ids, $type, $activity, $inbox_item->ID, $context );
380
381 /**
382 * Fires after an ActivityPub Inbox activity has been handled.
383 *
384 * @param array $data The data array.
385 * @param array $user_ids The user IDs.
386 * @param Activity $activity The Activity object.
387 * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
388 * @param string $context The context of the request ('inbox' or 'shared_inbox').
389 */
390 \do_action( 'activitypub_handled_inbox_' . $type, $data, $user_ids, $activity, $inbox_item->ID, $context );
391 }
392
393 /**
394 * Update schedules when outbox purge days settings change.
395 *
396 * @param int $old_value The old value.
397 * @param int $value The new value.
398 */
399 public static function update_outbox_purge_schedule( $old_value, $value ) {
400 if ( 0 === (int) $value ) {
401 \wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
402 } elseif ( ! \wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
403 \wp_schedule_event( \time(), 'daily', 'activitypub_outbox_purge' );
404 }
405 }
406
407 /**
408 * Update schedules when inbox purge days settings change.
409 *
410 * @param int $old_value The old value.
411 * @param int $value The new value.
412 */
413 public static function update_inbox_purge_schedule( $old_value, $value ) {
414 if ( 0 === (int) $value ) {
415 \wp_clear_scheduled_hook( 'activitypub_inbox_purge' );
416 } elseif ( ! \wp_next_scheduled( 'activitypub_inbox_purge' ) ) {
417 \wp_schedule_event( \time(), 'daily', 'activitypub_inbox_purge' );
418 }
419 }
420
421 /**
422 * Update schedules when remote posts purge days settings change.
423 *
424 * @param int $old_value The old value.
425 * @param int $value The new value.
426 */
427 public static function update_ap_post_purge_schedule( $old_value, $value ) {
428 if ( 0 === (int) $value ) {
429 \wp_clear_scheduled_hook( 'activitypub_ap_post_purge' );
430 } elseif ( ! \wp_next_scheduled( 'activitypub_ap_post_purge' ) ) {
431 \wp_schedule_event( \time(), 'daily', 'activitypub_ap_post_purge' );
432 }
433 }
434
435 /**
436 * Asynchronously runs batch processing routines.
437 *
438 * The batching part is optional and only comes into play if the callback returns anything.
439 * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
440 *
441 * @params mixed ...$args Optional. Parameters that get passed to the callback.
442 */
443 public static function async_batch() {
444 $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
445 $callback = self::$batch_callbacks[ \current_action() ] ?? $args[0] ?? null;
446 if ( ! \is_callable( $callback ) ) {
447 \_doing_it_wrong( __METHOD__, 'There must be a valid callback associated with the current action.', '5.2.0' );
448 return;
449 }
450
451 $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
452
453 // Bail if the existing lock is still valid.
454 if ( self::is_locked( $key ) ) {
455 \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, \current_action(), $args );
456 return;
457 }
458
459 self::lock( $key );
460
461 if ( \is_callable( $args[0] ?? null ) ) {
462 $callback = \array_shift( $args ); // Remove $callback from arguments.
463 }
464 $next = \call_user_func_array( $callback, $args );
465
466 self::unlock( $key );
467
468 if ( ! empty( $next ) ) {
469 // Schedule the next run, adding the result to the arguments.
470 \wp_schedule_single_event( \time() + self::get_retry_delay(), \current_action(), \array_values( $next ) );
471 }
472 }
473
474 /**
475 * Locks the async batch process for individual callbacks to prevent simultaneous processing.
476 *
477 * @param string $key Serialized callback name.
478 * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
479 */
480 public static function lock( $key ) {
481 global $wpdb;
482
483 // Try to lock.
484 $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB
485
486 if ( ! $lock_result ) {
487 $lock_result = \get_option( 'activitypub_async_batch_' . $key );
488 }
489
490 return $lock_result;
491 }
492
493 /**
494 * Unlocks processing for the async batch callback.
495 *
496 * @param string $key Serialized callback name.
497 */
498 public static function unlock( $key ) {
499 \delete_option( 'activitypub_async_batch_' . $key );
500 }
501
502 /**
503 * Whether the async batch callback is locked.
504 *
505 * @param string $key Serialized callback name.
506 * @return boolean
507 */
508 public static function is_locked( $key ) {
509 $lock = \get_option( 'activitypub_async_batch_' . $key );
510
511 if ( ! $lock ) {
512 return false;
513 }
514
515 $lock = (int) $lock;
516
517 if ( $lock < \time() - 1800 ) {
518 self::unlock( $key );
519 return false;
520 }
521
522 return true;
523 }
524
525 /**
526 * Send announces.
527 *
528 * @param int $outbox_activity_id The outbox activity ID.
529 * @param Activity $activity The activity object.
530 * @param int $actor_id The actor ID.
531 * @param int $content_visibility The content visibility.
532 */
533 public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
534 // Only if we're in both Blog and User modes.
535 if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
536 return;
537 }
538
539 // Only if this isn't the Blog Actor.
540 if ( Actors::BLOG_USER_ID === $actor_id ) {
541 return;
542 }
543
544 // Only if the content is public or quiet public.
545 if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
546 return;
547 }
548
549 // Only if the activity is a Create.
550 if ( 'Create' !== $activity->get_type() ) {
551 return;
552 }
553
554 if ( ! is_object( $activity->get_object() ) ) {
555 return;
556 }
557
558 // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
559 if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
560 return;
561 }
562
563 $announce = new Activity();
564 $announce->set_type( 'Announce' );
565 $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
566 $announce->set_object( $activity );
567 $announce->add_cc( object_to_uri( $activity->get_actor() ) );
568
569 $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
570
571 if ( ! $outbox_activity_id ) {
572 return;
573 }
574
575 // Schedule the outbox item for federation.
576 self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
577 }
578 }
579