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