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

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

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