PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.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
activitypub / includes / class-scheduler.php

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

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