PluginProbe
ActivityPub / 2.6.0
ActivityPub v2.6.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 +225 -631 9.2.02.6.0 View file →
@@ -1,28 +1,19 @@
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\Transformer\Post;
6 +use Activitypub\Collection\Users;
7 +use Activitypub\Collection\Followers;
22 8
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 +
23 14 /**
24 - * Scheduler class.
15 + * ActivityPub Scheduler Class
25 16 *
26 17 * @author Matthias Pfefferle
27 18 */
28 19 class Scheduler {
@@ -27,728 +18,331 @@
27 18 */
28 19 class Scheduler {
29 20
30 21 /**
31 - * Scheduled events with their recurrence.
32 - *
33 - * @var array
22 + * Initialize the class, registering WordPress hooks
34 23 */
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 - );
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 + );
45 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();
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 + );
63 61 }
64 62
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 - }
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' ) );
73 66
74 - /**
75 - * Initialize the class, registering WordPress hooks.
76 - */
77 - public static function init() {
78 - self::register_schedulers();
67 + // profile updates for blog options
68 + if ( ! is_user_type_disabled( 'blog' ) ) {
69 + \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
70 + \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
71 + \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
72 + \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
73 + \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
74 + }
79 75
80 - // Custom cron schedules.
81 - \add_filter( 'cron_schedules', array( self::class, 'add_cron_schedules' ) );
82 -
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 );
76 + // profile updates for user options
77 + if ( ! is_user_type_disabled( 'user' ) ) {
78 + \add_action( 'wp_update_user', array( self::class, 'user_update' ) );
79 + \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
80 + // @todo figure out a feasible way of updating the header image since it's not unique to any user.
81 + }
103 82 }
104 83
105 84 /**
106 - * Register handlers.
85 + * Schedule all ActivityPub schedules.
86 + *
87 + * @return void
107 88 */
108 - public static function register_schedulers() {
109 - Post::init();
110 - Actor::init();
111 - Collection_Sync::init();
112 - Comment::init();
113 - Statistics::init();
89 + public static function register_schedules() {
90 + if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
91 + \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
92 + }
114 93
115 - /**
116 - * Register additional schedulers.
117 - *
118 - * @since 5.0.0
119 - */
120 - \do_action( 'activitypub_register_schedulers' );
94 + if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
95 + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
96 + }
121 97 }
122 98
123 99 /**
124 - * Add custom cron schedules.
100 + * Unscedule all ActivityPub schedules.
125 101 *
126 - * @param array $schedules Existing cron schedules.
127 - *
128 - * @return array Modified cron schedules.
102 + * @return void
129 103 */
130 - public static function add_cron_schedules( $schedules ) {
131 - $schedules['monthly'] = array(
132 - 'interval' => MONTH_IN_SECONDS,
133 - 'display' => \__( 'Once Monthly', 'activitypub' ),
134 - );
104 + public static function deregister_schedules() {
105 + wp_unschedule_hook( 'activitypub_update_followers' );
106 + wp_unschedule_hook( 'activitypub_cleanup_followers' );
107 + }
135 108
136 - $schedules['yearly'] = array(
137 - 'interval' => YEAR_IN_SECONDS,
138 - 'display' => \__( 'Once Yearly', 'activitypub' ),
139 - );
140 109
141 - return $schedules;
142 - }
143 -
144 110 /**
145 - * Register a batch callback for async processing.
111 + * Schedule Activities.
146 112 *
147 - * @param string $hook The cron event hook name.
148 - * @param callable $callback The callback to execute.
113 + * @param string $new_status New post status.
114 + * @param string $old_status Old post status.
115 + * @param WP_Post $post Post object.
149 116 */
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' );
117 + public static function schedule_post_activity( $new_status, $old_status, $post ) {
118 + $post = get_post( $post );
119 +
120 + if ( 'ap_extrafield' === $post->post_type ) {
121 + self::schedule_profile_update( $post->post_author );
153 122 return;
154 123 }
155 124
156 - if ( ! \is_callable( $callback ) ) {
125 + // Do not send activities if post is password protected.
126 + if ( \post_password_required( $post ) ) {
157 127 return;
158 128 }
159 129
160 - self::$batch_callbacks[ $hook ] = $callback;
130 + // Check if post-type supports ActivityPub.
131 + $post_types = \get_post_types_by_support( 'activitypub' );
132 + if ( ! \in_array( $post->post_type, $post_types, true ) ) {
133 + return;
134 + }
161 135
162 - // Register the WordPress action hook to trigger async_batch.
163 - \add_action( $hook, array( self::class, 'async_batch' ), 10, 99 );
164 - }
136 + $type = false;
165 137
166 - /**
167 - * Schedule all ActivityPub schedules.
168 - */
169 - 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 - }
138 + if (
139 + 'publish' === $new_status &&
140 + 'publish' !== $old_status
141 + ) {
142 + $type = 'Create';
143 + } elseif (
144 + 'publish' === $new_status ||
145 + ( 'draft' === $new_status &&
146 + 'draft' !== $old_status )
147 + ) {
148 + $type = 'Update';
149 + } elseif ( 'trash' === $new_status ) {
150 + $type = 'Delete';
174 151 }
175 152
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' );
153 + if ( empty( $type ) ) {
154 + return;
181 155 }
182 156
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 - }
157 + $hook = 'activitypub_send_post';
158 + $args = array( $post->ID, $type );
189 159
190 - /**
191 - * Un-schedule all ActivityPub schedules.
192 - *
193 - * @return void
194 - */
195 - public static function deregister_schedules() {
196 - foreach ( \array_keys( self::SCHEDULES ) as $hook ) {
197 - \wp_unschedule_hook( $hook );
160 + if ( false === wp_next_scheduled( $hook, $args ) ) {
161 + set_wp_object_state( $post, 'federate' );
162 + \wp_schedule_single_event( \time(), $hook, $args );
198 163 }
199 -
200 - // Statistics schedules.
201 - \wp_unschedule_hook( 'activitypub_collect_monthly_stats' );
202 - \wp_unschedule_hook( 'activitypub_compile_annual_stats' );
203 164 }
204 165
205 166 /**
206 - * Get the next 1st of month timestamp.
167 + * Schedule Comment Activities
207 168 *
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 -
214 - return $next_month;
215 - }
216 -
217 - /**
218 - * Get the next December 1st timestamp for wrapped notification.
169 + * transition_comment_status()
219 170 *
220 - * @return int Unix timestamp of next December 1st at 3:00 AM.
171 + * @param string $new_status New comment status.
172 + * @param string $old_status Old comment status.
173 + * @param WP_Comment $comment Comment object.
221 174 */
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 );
175 + public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
176 + $comment = get_comment( $comment );
225 177
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 ) );
178 + // federate only comments that are written by a registered user.
179 + if ( ! $comment->user_id ) {
180 + return;
232 181 }
233 182
234 - return $this_year_dec_first;
235 - }
183 + $type = false;
236 184
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' );
185 + if (
186 + 'approved' === $new_status &&
187 + 'approved' !== $old_status
188 + ) {
189 + $type = 'Create';
190 + } elseif ( 'approved' === $new_status ) {
191 + $type = 'Update';
192 + \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
193 + } elseif (
194 + 'trash' === $new_status ||
195 + 'spam' === $new_status
196 + ) {
197 + $type = 'Delete';
198 + }
244 199
245 - $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $outbox_item_id ) );
246 - \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $outbox_item_id ) );
200 + if ( empty( $type ) ) {
201 + return;
202 + }
247 203
248 - self::unschedule_outbox_delivery_batches( $outbox_item_id );
204 + // check if comment should be federated or not
205 + if ( ! should_comment_be_federated( $comment ) ) {
206 + return;
207 + }
249 208
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 - }
209 + $hook = 'activitypub_send_comment';
210 + $args = array( $comment->comment_ID, $type );
255 211
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 - }
212 + if ( false === wp_next_scheduled( $hook, $args ) ) {
213 + set_wp_object_state( $comment, 'federate' );
214 + \wp_schedule_single_event( \time(), $hook, $args );
261 215 }
262 216 }
263 217
264 218 /**
265 - * Update remote Actors.
219 + * Update followers
220 + *
221 + * @return void
266 222 */
267 - public static function update_remote_actors() {
223 + public static function update_followers() {
268 224 $number = 5;
269 225
270 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
226 + if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
271 227 $number = 50;
272 228 }
273 229
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 );
230 + $number = apply_filters( 'activitypub_update_followers_number', $number );
231 + $followers = Followers::get_outdated_followers( $number );
281 232
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 );
233 + foreach ( $followers as $follower ) {
234 + $meta = get_remote_metadata_by_actor( $follower->get_id(), false );
292 235
293 - if ( empty( $meta ) || ! \is_array( $meta ) || \is_wp_error( $meta ) ) {
294 - Remote_Actors::add_error( $actor->ID, 'Failed to fetch or parse metadata' );
236 + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
237 + Followers::add_error( $follower->get__id(), $meta );
295 238 } 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 );
239 + $follower->from_array( $meta );
240 + $follower->update();
331 241 }
332 242 }
333 243 }
334 244
335 245 /**
336 - * Cleanup remote Actors.
246 + * Cleanup followers
247 + *
248 + * @return void
337 249 */
338 - public static function cleanup_remote_actors() {
250 + public static function cleanup_followers() {
339 251 $number = 5;
340 252
341 - if ( \defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
253 + if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
342 254 $number = 50;
343 255 }
344 256
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 );
257 + $number = apply_filters( 'activitypub_update_followers_number', $number );
258 + $followers = Followers::get_faulty_followers( $number );
352 259
353 - foreach ( $actors as $actor ) {
354 - $meta = get_remote_metadata_by_actor( $actor->guid, false );
260 + foreach ( $followers as $follower ) {
261 + $meta = get_remote_metadata_by_actor( $follower->get_url(), false );
355 262
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 );
263 + if ( is_tombstone( $meta ) ) {
264 + $follower->delete();
265 + } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
266 + if ( $follower->count_errors() >= 5 ) {
267 + $follower->delete();
268 + \wp_schedule_single_event(
269 + \time(),
270 + 'activitypub_delete_actor_interactions',
271 + array( $follower->get_id() )
272 + );
363 273 } else {
364 - Remote_Actors::add_error( $actor->ID, $meta );
274 + Followers::add_error( $follower->get__id(), $meta );
365 275 }
366 276 } 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 - }
277 + $follower->reset_errors();
373 278 }
374 279 }
375 280 }
376 281
377 282 /**
378 - * Schedule the outbox item for federation.
283 + * Send a profile update when relevant user meta is updated.
379 284 *
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.
382 - */
383 - public static function schedule_outbox_activity_for_federation( $id, $offset = 3 ) {
384 - $hook = 'activitypub_process_outbox';
385 - $args = array( $id );
386 -
387 - if ( false === \wp_next_scheduled( $hook, $args ) ) {
388 - \wp_schedule_single_event(
389 - \time() + $offset,
390 - $hook,
391 - $args
392 - );
393 - }
394 - }
395 -
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;
414 - }
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 - }
424 - }
425 -
426 - /**
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.
285 + * @param int $meta_id Meta ID being updated.
286 + * @param int $user_id User ID being updated.
287 + * @param string $meta_key Meta key being updated.
449 288 *
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
289 + * @return void
454 290 */
455 - public static function purge_tombstones() {
456 - Tombstone::purge();
457 - }
458 -
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 ) {
291 + public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
292 + // don't bother if the user can't publish
293 + if ( ! \user_can( $user_id, 'activitypub' ) ) {
470 294 return;
471 295 }
472 -
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' );
296 + // the user meta fields that affect a profile.
297 + $fields = array(
298 + 'activitypub_user_description',
299 + 'description',
300 + 'user_url',
301 + 'display_name',
302 + );
303 + if ( in_array( $meta_key, $fields, true ) ) {
304 + self::schedule_profile_update( $user_id );
517 305 }
518 306 }
519 307
520 308 /**
521 - * Update schedules when inbox purge days settings change.
309 + * Send a profile update when a user is updated.
522 310 *
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.
311 + * @param int $user_id User ID being updated.
536 312 *
537 - * @param int $old_value The old value.
538 - * @param int $value The new value.
313 + * @return void
539 314 */
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' );
315 + public static function user_update( $user_id ) {
316 + // don't bother if the user can't publish
317 + if ( ! \user_can( $user_id, 'activitypub' ) ) {
561 318 return;
562 319 }
563 320
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 - }
321 + self::schedule_profile_update( $user_id );
585 322 }
586 323
587 324 /**
588 - * Whether an outbox item already has a scheduled delivery batch at an offset.
325 + * Theme mods only have a dynamic filter so we fudge it like this.
589 326 *
590 - * @param int $outbox_item_id The outbox item ID.
591 - * @param int $offset The delivery offset.
327 + * @param mixed $value
592 328 *
593 - * @return bool True when a matching delivery batch is scheduled.
329 + * @return mixed
594 330 */
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 ) );
331 + public static function blog_user_update( $value = null ) {
332 + self::schedule_profile_update( 0 );
333 + return $value;
597 334 }
598 335
599 336 /**
600 - * Unschedule all pending delivery batches for an outbox item.
337 + * Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
601 338 *
602 - * @param int $outbox_item_id The outbox item ID.
339 + * @param int $user_id The user ID to update (Could be 0 for Blog-User).
603 340 */
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;
634 - }
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 - );
644 - }
645 - }
646 -
647 - return $events;
648 - }
649 -
650 - /**
651 - * Locks the async batch process for individual callbacks to prevent simultaneous processing.
652 - *
653 - * @param string $key Serialized callback name.
654 - * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
655 - */
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 );
664 - }
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 );
341 + public static function schedule_profile_update( $user_id ) {
342 + \wp_schedule_single_event(
343 + \time(),
344 + 'activitypub_send_update_profile_activity',
345 + array( $user_id )
346 + );
753 347 }
754 348 }