PluginProbe
ActivityPub / 1.0.0
ActivityPub v1.0.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
← All changes | includes/class-scheduler.php +83 -680 9.2.2 → 1.0.0 View file →
@@ -1,754 +1,157 @@
1 1 <?php
2 -/**
3 - * Scheduler class file.
4 - *
5 - * @package Activitypub
6 - */
7 2
8 3 namespace Activitypub;
9 4
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\Collection\Remote_Posts;
17 -use Activitypub\Scheduler\Actor;
18 -use Activitypub\Scheduler\Collection_Sync;
19 -use Activitypub\Scheduler\Comment;
20 -use Activitypub\Scheduler\Post;
21 -use Activitypub\Scheduler\Statistics;
5 +use Activitypub\Collection\Users;
6 +use Activitypub\Collection\Followers;
7 +use Activitypub\Transformer\Post;
22 8
23 9 /**
24 - * Scheduler class.
10 + * ActivityPub Scheduler Class
25 11 *
26 12 * @author Matthias Pfefferle
27 13 */
28 14 class Scheduler {
29 -
30 15 /**
31 - * Scheduled events with their recurrence.
32 - *
33 - * @var array
16 + * Initialize the class, registering WordPress hooks
34 17 */
35 - const SCHEDULES = array(
36 - 'activitypub_update_remote_actors' => 'hourly',
37 - 'activitypub_cleanup_remote_actors' => 'daily',
38 - 'activitypub_reprocess_outbox' => 'hourly',
39 - 'activitypub_outbox_purge' => 'daily',
40 - 'activitypub_inbox_purge' => 'daily',
41 - 'activitypub_ap_post_purge' => 'daily',
42 - 'activitypub_tombstone_purge' => 'daily',
43 - 'activitypub_sync_blocklist_subscriptions' => 'weekly',
44 - );
45 -
46 - /**
47 - * Allowed batch callbacks.
48 - *
49 - * @var array
50 - */
51 - private static $batch_callbacks = array();
52 -
53 - /**
54 - * Get the pause between async batches (in seconds).
55 - *
56 - * @param string|false|null $hook Optional. The async batch hook being scheduled. Default current action.
57 - *
58 - * @return int The pause in seconds.
59 - */
60 - public static function get_retry_delay( $hook = null ) {
61 - if ( null === $hook ) {
62 - $hook = \current_action();
63 - }
64 -
65 - /**
66 - * Filters the pause between async batches (in seconds).
67 - *
68 - * @param int $async_batch_pause The pause in seconds. Default 30.
69 - * @param string|false|null $hook The async batch hook being scheduled.
70 - */
71 - return \apply_filters( 'activitypub_scheduler_async_batch_pause', 30, $hook );
72 - }
73 -
74 - /**
75 - * Initialize the class, registering WordPress hooks.
76 - */
77 18 public static function init() {
78 - self::register_schedulers();
19 + \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
79 20
80 - // Custom cron schedules.
81 - \add_filter( 'cron_schedules', array( self::class, 'add_cron_schedules' ) );
21 + \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
22 + \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
82 23
83 - // Follower Cleanups.
84 - \add_action( 'activitypub_update_remote_actors', array( self::class, 'update_remote_actors' ) );
85 - \add_action( 'activitypub_cleanup_remote_actors', array( self::class, 'cleanup_remote_actors' ) );
86 -
87 - // Event callbacks.
88 - \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
89 - \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
90 - \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
91 - \add_action( 'activitypub_inbox_purge', array( self::class, 'purge_inbox' ) );
92 - \add_action( 'activitypub_ap_post_purge', array( self::class, 'purge_ap_posts' ) );
93 - \add_action( 'activitypub_tombstone_purge', array( self::class, 'purge_tombstones' ) );
94 - \add_action( 'activitypub_inbox_create_item', array( self::class, 'process_inbox_activity' ) );
95 - \add_action( 'activitypub_sync_blocklist_subscriptions', array( Blocklist_Subscriptions::class, 'sync_all' ) );
96 -
97 - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
98 - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
99 -
100 - \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'update_outbox_purge_schedule' ), 10, 2 );
101 - \add_action( 'update_option_activitypub_inbox_purge_days', array( self::class, 'update_inbox_purge_schedule' ), 10, 2 );
102 - \add_action( 'update_option_activitypub_ap_post_purge_days', array( self::class, 'update_ap_post_purge_schedule' ), 10, 2 );
24 + \add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
103 25 }
104 26
105 27 /**
106 - * Register handlers.
107 - */
108 - public static function register_schedulers() {
109 - Post::init();
110 - Actor::init();
111 - Collection_Sync::init();
112 - Comment::init();
113 - Statistics::init();
114 -
115 - /**
116 - * Register additional schedulers.
117 - *
118 - * @since 5.0.0
119 - */
120 - \do_action( 'activitypub_register_schedulers' );
121 - }
122 -
123 - /**
124 - * Add custom cron schedules.
28 + * Schedule all ActivityPub schedules.
125 29 *
126 - * @param array $schedules Existing cron schedules.
127 - *
128 - * @return array Modified cron schedules.
30 + * @return void
129 31 */
130 - public static function add_cron_schedules( $schedules ) {
131 - $schedules['monthly'] = array(
132 - 'interval' => MONTH_IN_SECONDS,
133 - 'display' => \__( 'Once Monthly', 'activitypub' ),
134 - );
135 -
136 - $schedules['yearly'] = array(
137 - 'interval' => YEAR_IN_SECONDS,
138 - 'display' => \__( 'Once Yearly', 'activitypub' ),
139 - );
140 -
141 - return $schedules;
142 - }
143 -
144 - /**
145 - * Register a batch callback for async processing.
146 - *
147 - * @param string $hook The cron event hook name.
148 - * @param callable $callback The callback to execute.
149 - */
150 - public static function register_async_batch_callback( $hook, $callback ) {
151 - if ( \did_action( 'init' ) && ! \doing_action( 'init' ) ) {
152 - \_doing_it_wrong( __METHOD__, 'Async batch callbacks should be registered before or during the init action.', '7.5.0' );
153 - return;
154 - }
155 -
156 - if ( ! \is_callable( $callback ) ) {
157 - return;
158 - }
159 -
160 - self::$batch_callbacks[ $hook ] = $callback;
161 -
162 - // Register the WordPress action hook to trigger async_batch.
163 - \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 );
164 - }
165 -
166 - /**
167 - * Schedule all ActivityPub schedules.
168 - */
169 32 public static function register_schedules() {
170 - foreach ( self::SCHEDULES as $hook => $recurrence ) {
171 - if ( ! \wp_next_scheduled( $hook ) ) {
172 - \wp_schedule_event( \time(), $recurrence, $hook );
173 - }
33 + if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
34 + \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
174 35 }
175 36
176 - // Schedule monthly stats collection for the 1st of each month.
177 - if ( ! \wp_next_scheduled( 'activitypub_collect_monthly_stats' ) ) {
178 - // Calculate next 1st of month at 2:00 AM.
179 - $next_first = self::get_next_first_of_month();
180 - \wp_schedule_event( $next_first, 'monthly', 'activitypub_collect_monthly_stats' );
37 + if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
38 + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
181 39 }
182 -
183 - // Schedule annual stats compilation for December 1st (wrapped notification).
184 - if ( ! \wp_next_scheduled( 'activitypub_compile_annual_stats' ) ) {
185 - $next_december = self::get_next_december_first();
186 - \wp_schedule_event( $next_december, 'yearly', 'activitypub_compile_annual_stats' );
187 - }
188 40 }
189 41
190 42 /**
191 - * Un-schedule all ActivityPub schedules.
43 + * Unscedule all ActivityPub schedules.
192 44 *
193 45 * @return void
194 46 */
195 47 public static function deregister_schedules() {
196 - foreach ( \array_keys( self::SCHEDULES ) as $hook ) {
197 - \wp_unschedule_hook( $hook );
198 - }
199 -
200 - // Statistics schedules.
201 - \wp_unschedule_hook( 'activitypub_collect_monthly_stats' );
202 - \wp_unschedule_hook( 'activitypub_compile_annual_stats' );
48 + wp_unschedule_hook( 'activitypub_update_followers' );
49 + wp_unschedule_hook( 'activitypub_cleanup_followers' );
203 50 }
204 51
205 - /**
206 - * Get the next 1st of month timestamp.
207 - *
208 - * @return int Unix timestamp of next 1st of month at 2:00 AM.
209 - */
210 - private static function get_next_first_of_month() {
211 - $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
212 - $next_month = \strtotime( 'first day of next month 02:00:00', $now );
213 52
214 - return $next_month;
215 - }
216 -
217 53 /**
218 - * Get the next December 1st timestamp for wrapped notification.
54 + * Schedule Activities.
219 55 *
220 - * @return int Unix timestamp of next December 1st at 3:00 AM.
56 + * @param string $new_status New post status.
57 + * @param string $old_status Old post status.
58 + * @param WP_Post $post Post object.
221 59 */
222 - private static function get_next_december_first() {
223 - $now = \current_time( 'timestamp' ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
224 - $year = (int) \gmdate( 'Y', $now );
225 -
226 - // Get December 1st 3:00 AM for this year.
227 - $this_year_dec_first = \strtotime( \sprintf( '%d-12-01 03:00:00', $year ) );
228 -
229 - // If we're already past this year's December 1st, schedule for next year.
230 - if ( $now >= $this_year_dec_first ) {
231 - return \strtotime( \sprintf( '%d-12-01 03:00:00', $year + 1 ) );
60 + public static function schedule_post_activity( $new_status, $old_status, $post ) {
61 + // Do not send activities if post is password protected.
62 + if ( \post_password_required( $post ) ) {
63 + return;
232 64 }
233 65
234 - return $this_year_dec_first;
235 - }
236 -
237 - /**
238 - * Unschedule events for an outbox item.
239 - *
240 - * @param int $outbox_item_id The outbox item ID.
241 - */
242 - public static function unschedule_events_for_item( $outbox_item_id ) {
243 - \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
244 -
245 - $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
246 - \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
247 -
248 - self::unschedule_outbox_delivery_batches( $outbox_item_id );
249 -
250 - // Invalidate any retries for this outbox item.
251 - foreach ( \_get_cron_array() as $timestamp => $cron ) {
252 - if ( ! isset( $cron['activitypub_retry_activity'] ) ) {
253 - continue;
254 - }
255 -
256 - foreach ( $cron['activitypub_retry_activity'] as $event ) {
257 - if ( isset( $event['args'][1] ) && $outbox_item_id === $event['args'][1] ) {
258 - \wp_unschedule_event( $timestamp, 'activitypub_retry_activity', $event['args'] );
259 - }
260 - }
66 + // Check if post-type supports ActivityPub.
67 + $post_types = \get_post_types_by_support( 'activitypub' );
68 + if ( ! \in_array( $post->post_type, $post_types, true ) ) {
69 + return;
261 70 }
262 - }
263 71
264 - /**
265 - * Update remote Actors.
266 - */
267 - public static function update_remote_actors() {
268 - $number = 5;
72 + $type = false;
269 73
270 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
271 - $number = 50;
74 + if ( 'publish' === $new_status && 'publish' !== $old_status ) {
75 + $type = 'Create';
76 + } elseif ( 'publish' === $new_status ) {
77 + $type = 'Update';
78 + } elseif ( 'trash' === $new_status ) {
79 + $type = 'Delete';
272 80 }
273 81
274 - /**
275 - * Filter the number of remote Actors to update.
276 - *
277 - * @param int $number The number of remote Actors to update.
278 - */
279 - $number = \apply_filters( 'activitypub_update_remote_actors_number', $number );
280 - $actors = Remote_Actors::get_outdated( $number );
281 -
282 - foreach ( $actors as $actor ) {
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 );
292 -
293 - if ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
294 - Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
295 - } else {
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 );
327 - if ( \is_wp_error( $id ) ) {
328 - continue;
329 - }
330 - Remote_Actors::clear_errors( $id );
331 - }
82 + if ( ! $type ) {
83 + return;
332 84 }
333 - }
334 85
335 - /**
336 - * Cleanup remote Actors.
337 - */
338 - public static function cleanup_remote_actors() {
339 - $number = 5;
86 + \wp_schedule_single_event(
87 + \time(),
88 + 'activitypub_send_activity',
89 + array( $post, $type )
90 + );
340 91
341 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
342 - $number = 50;
343 - }
344 -
345 - /**
346 - * Filter the number of remote Actors to clean up.
347 - *
348 - * @param int $number The number of remote Actors to clean up.
349 - */
350 - $number = \apply_filters( 'activitypub_cleanup_remote_actors_number', $number );
351 - $actors = Remote_Actors::get_faulty( $number );
352 -
353 - foreach ( $actors as $actor ) {
354 - $meta = get_remote_metadata_by_actor( $actor->guid, false );
355 -
356 - if ( Tombstone::exists( $meta ) ) {
357 - \wp_delete_post( $actor->ID );
358 - } elseif ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
359 - if ( Remote_Actors::count_errors( $actor->ID ) >= 5 ) {
360 - \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_interactions', array( $actor->guid ) );
361 - \wp_schedule_single_event( \time(), 'activitypub_delete_remote_actor_posts', array( $actor->guid ) );
362 - \wp_delete_post( $actor->ID );
363 - } else {
364 - Remote_Actors::add_error( $actor->ID, $meta );
365 - }
366 - } else {
367 - $id = Remote_Actors::upsert( $meta );
368 - if ( \is_wp_error( $id ) ) {
369 - Remote_Actors::add_error( $actor->ID, $id );
370 - } else {
371 - Remote_Actors::clear_errors( $actor->ID );
372 - }
373 - }
374 - }
92 + \wp_schedule_single_event(
93 + \time(),
94 + sprintf(
95 + 'activitypub_send_%s_activity',
96 + \strtolower( $type )
97 + ),
98 + array( $post )
99 + );
375 100 }
376 101
377 102 /**
378 - * Schedule the outbox item for federation.
103 + * Update followers
379 104 *
380 - * @param int $id The ID of the outbox item.
381 - * @param int $offset The offset to add to the scheduled time. Default 3 seconds.
105 + * @return void
382 106 */
383 - public static function schedule_outbox_activity_for_federation( $id, $offset = 3 ) {
384 - $hook = 'activitypub_process_outbox';
385 - $args = array( $id );
107 + public static function update_followers() {
108 + $followers = Followers::get_outdated_followers();
386 109
387 - if ( false === \wp_next_scheduled( $hook, $args ) ) {
388 - \wp_schedule_single_event(
389 - \time() + $offset,
390 - $hook,
391 - $args
392 - );
393 - }
394 - }
110 + foreach ( $followers as $follower ) {
111 + $meta = get_remote_metadata_by_actor( $follower->get_url(), true );
395 112
396 - /**
397 - * Reprocess the outbox.
398 - */
399 - public static function reprocess_outbox() {
400 - $ids = \get_posts(
401 - array(
402 - 'post_type' => Outbox::POST_TYPE,
403 - 'post_status' => 'pending',
404 - 'posts_per_page' => 10,
405 - 'fields' => 'ids',
406 - )
407 - );
408 -
409 - foreach ( $ids as $id ) {
410 - // Bail if there is a pending batch.
411 - $offset = \get_post_meta( $id, '_activitypub_outbox_offset', true ) ?: 0; // phpcs:ignore
412 - if ( self::has_scheduled_outbox_delivery_batch( $id, $offset ) ) {
413 - return;
113 + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
114 + Followers::add_error( $follower->get__id(), $meta );
115 + } else {
116 + $follower->from_array( $meta );
117 + $follower->update();
414 118 }
415 -
416 - // Bail if there is a batch in progress.
417 - $key = \md5( \serialize( $id ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
418 - if ( self::is_locked( $key ) ) {
419 - return;
420 - }
421 -
422 - self::schedule_outbox_activity_for_federation( $id );
423 119 }
424 120 }
425 121
426 122 /**
427 - * Purge outbox items based on a schedule.
428 - */
429 - public static function purge_outbox() {
430 - Outbox::purge( \get_option( 'activitypub_outbox_purge_days', ACTIVITYPUB_OUTBOX_PURGE_DAYS ) );
431 - }
432 -
433 - /**
434 - * Purge inbox items based on a schedule.
435 - */
436 - public static function purge_inbox() {
437 - Inbox::purge( \get_option( 'activitypub_inbox_purge_days', ACTIVITYPUB_INBOX_PURGE_DAYS ) );
438 - }
439 -
440 - /**
441 - * Purge remote posts based on a schedule.
442 - */
443 - public static function purge_ap_posts() {
444 - Remote_Posts::purge( \get_option( 'activitypub_ap_post_purge_days', ACTIVITYPUB_AP_POST_PURGE_DAYS ) );
445 - }
446 -
447 - /**
448 - * Daily cron handler that purges expired tombstones.
123 + * Cleanup followers
449 124 *
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
125 + * @return void
454 126 */
455 - public static function purge_tombstones() {
456 - Tombstone::purge();
457 - }
127 + public static function cleanup_followers() {
128 + $followers = Followers::get_faulty_followers();
458 129
459 - /**
460 - * Process cached inbox activity.
461 - *
462 - * Retrieves all collected user IDs for an activity and processes them together.
463 - *
464 - * @param string $activity_id The activity ID.
465 - */
466 - public static function process_inbox_activity( $activity_id ) {
467 - // Deduplicate if multiple inbox items were created due to race condition.
468 - $inbox_item = Inbox::deduplicate( $activity_id );
469 - if ( ! $inbox_item ) {
470 - return;
471 - }
130 + foreach ( $followers as $follower ) {
131 + $meta = get_remote_metadata_by_actor( $follower->get_url(), true );
472 132
473 - $data = \json_decode( $inbox_item->post_content, true );
474 - // Reconstruct activity from inbox post.
475 - $activity = Activity::init_from_array( $data );
476 - $type = camel_to_snake_case( $activity->get_type() );
477 - $context = Inbox::CONTEXT_INBOX;
478 - $user_ids = Inbox::get_recipients( $inbox_item->ID );
479 -
480 - /**
481 - * Fires after any ActivityPub Inbox activity has been handled, regardless of activity type.
482 - *
483 - * This hook is triggered for all activity types processed by the inbox handler.
484 - *
485 - * @param array $data The data array.
486 - * @param array $user_ids The user IDs.
487 - * @param string $type The type of the activity.
488 - * @param Activity $activity The Activity object.
489 - * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
490 - * @param string $context The context of the request ('inbox' or 'shared_inbox').
491 - */
492 - \do_action( 'activitypub_handled_inbox', $data, $user_ids, $type, $activity, $inbox_item->ID, $context );
493 -
494 - /**
495 - * Fires after an ActivityPub Inbox activity has been handled.
496 - *
497 - * @param array $data The data array.
498 - * @param array $user_ids The user IDs.
499 - * @param Activity $activity The Activity object.
500 - * @param int $result The ID of the inbox item that was created, or WP_Error if failed.
501 - * @param string $context The context of the request ('inbox' or 'shared_inbox').
502 - */
503 - \do_action( 'activitypub_handled_inbox_' . $type, $data, $user_ids, $activity, $inbox_item->ID, $context );
504 - }
505 -
506 - /**
507 - * Update schedules when outbox purge days settings change.
508 - *
509 - * @param int $old_value The old value.
510 - * @param int $value The new value.
511 - */
512 - public static function update_outbox_purge_schedule( $old_value, $value ) {
513 - if ( 0 === (int) $value ) {
514 - \wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
515 - } elseif ( ! \wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
516 - \wp_schedule_event( \time(), 'daily', 'activitypub_outbox_purge' );
517 - }
518 - }
519 -
520 - /**
521 - * Update schedules when inbox purge days settings change.
522 - *
523 - * @param int $old_value The old value.
524 - * @param int $value The new value.
525 - */
526 - public static function update_inbox_purge_schedule( $old_value, $value ) {
527 - if ( 0 === (int) $value ) {
528 - \wp_clear_scheduled_hook( 'activitypub_inbox_purge' );
529 - } elseif ( ! \wp_next_scheduled( 'activitypub_inbox_purge' ) ) {
530 - \wp_schedule_event( \time(), 'daily', 'activitypub_inbox_purge' );
531 - }
532 - }
533 -
534 - /**
535 - * Update schedules when remote posts purge days settings change.
536 - *
537 - * @param int $old_value The old value.
538 - * @param int $value The new value.
539 - */
540 - public static function update_ap_post_purge_schedule( $old_value, $value ) {
541 - if ( 0 === (int) $value ) {
542 - \wp_clear_scheduled_hook( 'activitypub_ap_post_purge' );
543 - } elseif ( ! \wp_next_scheduled( 'activitypub_ap_post_purge' ) ) {
544 - \wp_schedule_event( \time(), 'daily', 'activitypub_ap_post_purge' );
545 - }
546 - }
547 -
548 - /**
549 - * Asynchronously runs batch processing routines.
550 - *
551 - * The batching part is optional and only comes into play if the callback returns anything.
552 - * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
553 - *
554 - * @params mixed ...$args Optional. Parameters that get passed to the callback.
555 - */
556 - public static function async_batch() {
557 - $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
558 - $callback = self::$batch_callbacks[ \current_action() ] ?? $args[0] ?? null;
559 - if ( ! \is_callable( $callback ) ) {
560 - \_doing_it_wrong( __METHOD__, 'There must be a valid callback associated with the current action.', '5.2.0' );
561 - return;
562 - }
563 -
564 - $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
565 -
566 - // Bail if the existing lock is still valid.
567 - if ( self::is_locked( $key ) ) {
568 - \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, \current_action(), $args );
569 - return;
570 - }
571 -
572 - self::lock( $key );
573 -
574 - if ( \is_callable( $args[0] ?? null ) ) {
575 - $callback = \array_shift( $args ); // Remove $callback from arguments.
576 - }
577 - $next = \call_user_func_array( $callback, $args );
578 -
579 - self::unlock( $key );
580 -
581 - if ( ! empty( $next ) ) {
582 - // Schedule the next run, adding the result to the arguments.
583 - \wp_schedule_single_event( \time() + self::get_retry_delay( \current_action() ), \current_action(), \array_values( $next ) );
584 - }
585 - }
586 -
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;
133 + if ( is_tombstone( $meta ) ) {
134 + $follower->delete();
135 + } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
136 + if ( $follower->count_errors() >= 5 ) {
137 + $follower->delete();
138 + } else {
139 + Followers::add_error( $follower->get__id(), $meta );
634 140 }
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 - );
141 + } else {
142 + $follower->reset_errors();
644 143 }
645 144 }
646 -
647 - return $events;
648 145 }
649 146
650 147 /**
651 - * Locks the async batch process for individual callbacks to prevent simultaneous processing.
148 + * Schedule migration if DB-Version is not up to date.
652 149 *
653 - * @param string $key Serialized callback name.
654 - * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
150 + * @return void
655 151 */
656 - public static function lock( $key ) {
657 - global $wpdb;
658 -
659 - // Try to lock.
660 - $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
661 -
662 - if ( ! $lock_result ) {
663 - $lock_result = \get_option( 'activitypub_async_batch_' . $key );
152 + public static function schedule_migration() {
153 + if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) && ! Migration::is_latest_version() ) {
154 + \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
664 155 }
665 -
666 - return $lock_result;
667 - }
668 -
669 - /**
670 - * Unlocks processing for the async batch callback.
671 - *
672 - * @param string $key Serialized callback name.
673 - */
674 - public static function unlock( $key ) {
675 - \delete_option( 'activitypub_async_batch_' . $key );
676 - }
677 -
678 - /**
679 - * Whether the async batch callback is locked.
680 - *
681 - * @param string $key Serialized callback name.
682 - * @return boolean
683 - */
684 - public static function is_locked( $key ) {
685 - $lock = \get_option( 'activitypub_async_batch_' . $key );
686 -
687 - if ( ! $lock ) {
688 - return false;
689 - }
690 -
691 - $lock = (int) $lock;
692 -
693 - if ( $lock < \time() - 1800 ) {
694 - self::unlock( $key );
695 - return false;
696 - }
697 -
698 - return true;
699 - }
700 -
701 - /**
702 - * Send announces.
703 - *
704 - * @param int $outbox_activity_id The outbox activity ID.
705 - * @param Activity $activity The activity object.
706 - * @param int $actor_id The actor ID.
707 - * @param int $content_visibility The content visibility.
708 - */
709 - public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
710 - // Only if we're in both Blog and User modes.
711 - if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
712 - return;
713 - }
714 -
715 - // Only if this isn't the Blog Actor.
716 - if ( Actors::BLOG_USER_ID === $actor_id ) {
717 - return;
718 - }
719 -
720 - // Only if the content is public or quiet public.
721 - if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
722 - return;
723 - }
724 -
725 - // Only if the activity is a Create.
726 - if ( 'Create' !== $activity->get_type() ) {
727 - return;
728 - }
729 -
730 - if ( ! \is_object( $activity->get_object() ) ) {
731 - return;
732 - }
733 -
734 - // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
735 - if ( ! \in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
736 - return;
737 - }
738 -
739 - $announce = new Activity();
740 - $announce->set_type( 'Announce' );
741 - $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
742 - $announce->set_object( $activity );
743 - $announce->add_cc( object_to_uri( $activity->get_actor() ) );
744 -
745 - $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
746 -
747 - if ( ! $outbox_activity_id ) {
748 - return;
749 - }
750 -
751 - // Schedule the outbox item for federation.
752 - self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
753 156 }
754 157 }