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
← All changes | includes/class-scheduler.php +451 -226 2.0.18.0.2 View file →
@@ -1,16 +1,27 @@
1 1 <?php
2 +/**
3 + * Scheduler class file.
4 + *
5 + * @package Activitypub
6 + */
2 7
3 8 namespace Activitypub;
4 9
5 -use Activitypub\Collection\Users;
6 -use Activitypub\Collection\Followers;
7 -use Activitypub\Transformer\Post;
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;
8 21
9 -use function Activitypub\is_user_type_disabled;
10 -
11 22 /**
12 - * ActivityPub Scheduler Class
23 + * Scheduler class.
13 24 *
14 25 * @author Matthias Pfefferle
15 26 */
16 27 class Scheduler {
@@ -15,206 +26,169 @@
15 26 */
16 27 class Scheduler {
17 28
18 29 /**
19 - * Initialize the class, registering WordPress hooks
30 + * Scheduled events with their recurrence.
31 + *
32 + * @var array
20 33 */
21 - public static function init() {
22 - // Post transitions
23 - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
24 - \add_action(
25 - 'edit_attachment',
26 - function ( $post_id ) {
27 - self::schedule_post_activity( 'publish', 'publish', $post_id );
28 - }
29 - );
30 - \add_action(
31 - 'add_attachment',
32 - function ( $post_id ) {
33 - self::schedule_post_activity( 'publish', '', $post_id );
34 - }
35 - );
36 - \add_action(
37 - 'delete_attachment',
38 - function ( $post_id ) {
39 - self::schedule_post_activity( 'trash', '', $post_id );
40 - }
41 - );
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 + );
42 43
43 - // Comment transitions
44 - \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
45 - \add_action(
46 - 'edit_comment',
47 - function ( $comment_id ) {
48 - self::schedule_comment_activity( 'approved', 'approved', $comment_id );
49 - }
50 - );
51 - \add_action(
52 - 'wp_insert_comment',
53 - function ( $comment_id ) {
54 - self::schedule_comment_activity( 'approved', '', $comment_id );
55 - }
56 - );
44 + /**
45 + * Allowed batch callbacks.
46 + *
47 + * @var array
48 + */
49 + private static $batch_callbacks = array();
57 50
58 - // Follower Cleanups
59 - \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
60 - \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
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 + }
61 64
62 - // Migration
63 - \add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
65 + /**
66 + * Initialize the class, registering WordPress hooks.
67 + */
68 + public static function init() {
69 + self::register_schedulers();
64 70
65 - // profile updates for blog options
66 - if ( ! is_user_type_disabled( 'blog' ) ) {
67 - \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
68 - \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
69 - \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
70 - \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
71 - \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
72 - }
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' ) );
73 74
74 - // profile updates for user options
75 - if ( ! is_user_type_disabled( 'user' ) ) {
76 - \add_action( 'wp_update_user', array( self::class, 'user_update' ) );
77 - \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
78 - // @todo figure out a feasible way of updating the header image since it's not unique to any user.
79 - }
80 - }
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' ) );
81 83
82 - /**
83 - * Schedule all ActivityPub schedules.
84 - *
85 - * @return void
86 - */
87 - public static function register_schedules() {
88 - if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
89 - \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
90 - }
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 );
91 86
92 - if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
93 - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
94 - }
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 );
95 90 }
96 91
97 92 /**
98 - * Unscedule all ActivityPub schedules.
99 - *
100 - * @return void
93 + * Register handlers.
101 94 */
102 - public static function deregister_schedules() {
103 - wp_unschedule_hook( 'activitypub_update_followers' );
104 - wp_unschedule_hook( 'activitypub_cleanup_followers' );
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' );
105 107 }
106 108
107 -
108 109 /**
109 - * Schedule Activities.
110 + * Register a batch callback for async processing.
110 111 *
111 - * @param string $new_status New post status.
112 - * @param string $old_status Old post status.
113 - * @param WP_Post $post Post object.
112 + * @param string $hook The cron event hook name.
113 + * @param callable $callback The callback to execute.
114 114 */
115 - public static function schedule_post_activity( $new_status, $old_status, $post ) {
116 - $post = get_post( $post );
117 -
118 - // Do not send activities if post is password protected.
119 - if ( \post_password_required( $post ) ) {
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' );
120 118 return;
121 119 }
122 120
123 - // Check if post-type supports ActivityPub.
124 - $post_types = \get_post_types_by_support( 'activitypub' );
125 - if ( ! \in_array( $post->post_type, $post_types, true ) ) {
121 + if ( ! \is_callable( $callback ) ) {
126 122 return;
127 123 }
128 124
129 - $type = false;
125 + self::$batch_callbacks[ $hook ] = $callback;
130 126
131 - if ( 'publish' === $new_status && 'publish' !== $old_status ) {
132 - $type = 'Create';
133 - } elseif ( 'publish' === $new_status ) {
134 - $type = 'Update';
135 - } elseif ( 'trash' === $new_status ) {
136 - $type = 'Delete';
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 + }
137 139 }
140 + }
138 141
139 - if ( ! $type ) {
140 - return;
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 );
141 150 }
142 -
143 - \wp_schedule_single_event(
144 - \time(),
145 - 'activitypub_send_activity',
146 - array( $post, $type )
147 - );
148 -
149 - \wp_schedule_single_event(
150 - \time(),
151 - sprintf(
152 - 'activitypub_send_%s_activity',
153 - \strtolower( $type )
154 - ),
155 - array( $post )
156 - );
157 151 }
158 152
159 153 /**
160 - * Schedule Comment Activities
154 + * Unschedule events for an outbox item.
161 155 *
162 - * transition_comment_status()
163 - *
164 - * @param string $new_status New comment status.
165 - * @param string $old_status Old comment status.
166 - * @param WP_Comment $comment Comment object.
156 + * @param int $outbox_item_id The outbox item ID.
167 157 */
168 - public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
169 - $comment = get_comment( $comment );
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 + );
170 164
171 - // Federate only approved comments.
172 - if ( ! $comment->user_id ) {
173 - return;
174 - }
165 + \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
175 166
176 - if (
177 - 'approved' === $new_status &&
178 - 'approved' !== $old_status
179 - ) {
180 - $type = 'Create';
181 - } elseif ( 'approved' === $new_status ) {
182 - $type = 'Update';
183 - \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
184 - } elseif (
185 - 'trash' === $new_status ||
186 - 'spam' === $new_status
187 - ) {
188 - $type = 'Delete';
189 - }
167 + $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
168 + \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
190 169
191 - if ( ! $type ) {
192 - return;
193 - }
170 + $timestamp = \wp_next_scheduled( 'activitypub_send_activity', $event_args );
171 + \wp_unschedule_event( $timestamp, 'activitypub_send_activity', $event_args );
194 172
195 - \wp_schedule_single_event(
196 - \time(),
197 - 'activitypub_send_activity',
198 - array( $comment, $type )
199 - );
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 + }
200 178
201 - \wp_schedule_single_event(
202 - \time(),
203 - sprintf(
204 - 'activitypub_send_%s_activity',
205 - \strtolower( $type )
206 - ),
207 - array( $comment )
208 - );
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 + }
209 185 }
210 186
211 187 /**
212 - * Update followers
213 - *
214 - * @return void
188 + * Update remote Actors.
215 189 */
216 - public static function update_followers() {
190 + public static function update_remote_actors() {
217 191 $number = 5;
218 192
219 193 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
220 194 $number = 50;
@@ -219,28 +193,35 @@
219 193 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
220 194 $number = 50;
221 195 }
222 196
223 - $followers = Followers::get_outdated_followers( $number );
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 );
224 204
225 - foreach ( $followers as $follower ) {
226 - $meta = get_remote_metadata_by_actor( $follower->get_id(), false );
205 + foreach ( $actors as $actor ) {
206 + $meta = get_remote_metadata_by_actor( $actor->guid, false );
227 207
228 208 if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
229 - Followers::add_error( $follower->get__id(), $meta );
209 + Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
230 210 } else {
231 - $follower->from_array( $meta );
232 - $follower->update();
211 + $id = Remote_Actors::upsert( $meta );
212 + if ( \is_wp_error( $id ) ) {
213 + continue;
214 + }
215 + Remote_Actors::clear_errors( $id );
233 216 }
234 217 }
235 218 }
236 219
237 220 /**
238 - * Cleanup followers
239 - *
240 - * @return void
221 + * Cleanup remote Actors.
241 222 */
242 - public static function cleanup_followers() {
223 + public static function cleanup_remote_actors() {
243 224 $number = 5;
244 225
245 226 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
246 227 $number = 50;
@@ -245,98 +226,342 @@
245 226 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
246 227 $number = 50;
247 228 }
248 229
249 - $followers = Followers::get_faulty_followers( $number );
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 );
250 237
251 - foreach ( $followers as $follower ) {
252 - $meta = get_remote_metadata_by_actor( $follower->get_url(), false );
238 + foreach ( $actors as $actor ) {
239 + $meta = get_remote_metadata_by_actor( $actor->guid, false );
253 240
254 - if ( is_tombstone( $meta ) ) {
255 - $follower->delete();
256 - } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
257 - if ( $follower->count_errors() >= 5 ) {
258 - $follower->delete();
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 );
259 248 } else {
260 - Followers::add_error( $follower->get__id(), $meta );
249 + Remote_Actors::add_error( $actor->ID, $meta );
261 250 }
262 251 } else {
263 - $follower->reset_errors();
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 + }
264 258 }
265 259 }
266 260 }
267 261
268 262 /**
269 - * Schedule migration if DB-Version is not up to date.
263 + * Schedule the outbox item for federation.
270 264 *
271 - * @return void
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.
272 267 */
273 - public static function schedule_migration() {
274 - if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) && ! Migration::is_latest_version() ) {
275 - \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
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 + );
276 278 }
277 279 }
278 280
279 281 /**
280 - * Send a profile update when relevant user meta is updated.
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.
281 337 *
282 - * @param int $meta_id Meta ID being updated.
283 - * @param int $user_id User ID being updated.
284 - * @param string $meta_key Meta key being updated.
338 + * Retrieves all collected user IDs for an activity and processes them together.
285 339 *
286 - * @return void
340 + * @param string $activity_id The activity ID.
287 341 */
288 - public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
289 - // don't bother if the user can't publish
290 - if ( ! \user_can( $user_id, 'publish_posts' ) ) {
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 ) {
291 346 return;
292 347 }
293 - // the user meta fields that affect a profile.
294 - $fields = array(
295 - 'activitypub_user_description',
296 - 'description',
297 - 'user_url',
298 - 'display_name',
299 - );
300 - if ( in_array( $meta_key, $fields, true ) ) {
301 - self::schedule_profile_update( $user_id );
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' );
302 393 }
303 394 }
304 395
305 396 /**
306 - * Send a profile update when a user is updated.
397 + * Update schedules when inbox purge days settings change.
307 398 *
308 - * @param int $user_id User ID being updated.
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.
309 412 *
310 - * @return void
413 + * @param int $old_value The old value.
414 + * @param int $value The new value.
311 415 */
312 - public static function user_update( $user_id ) {
313 - // don't bother if the user can't publish
314 - if ( ! \user_can( $user_id, 'publish_posts' ) ) {
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' );
315 437 return;
316 438 }
317 439
318 - self::schedule_profile_update( $user_id );
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 + }
319 461 }
320 462
321 463 /**
322 - * Theme mods only have a dynamic filter so we fudge it like this.
323 - * @param mixed $value
324 - * @return mixed
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.
325 468 */
326 - public static function blog_user_update( $value = null ) {
327 - self::schedule_profile_update( 0 );
328 - return $value;
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;
329 480 }
330 481
331 482 /**
332 - * Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
333 - * @param int $user_id The user ID to update (Could be 0 for Blog-User).
483 + * Unlocks processing for the async batch callback.
484 + *
485 + * @param string $key Serialized callback name.
334 486 */
335 - public static function schedule_profile_update( $user_id ) {
336 - \wp_schedule_single_event(
337 - \time(),
338 - 'activitypub_send_update_profile_activity',
339 - array( $user_id )
340 - );
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 );
341 566 }
342 567 }