PluginProbe
ActivityPub / 5.3.1
ActivityPub v5.3.1
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 +314 -229 2.1.05.3.1 View file →
@@ -1,19 +1,22 @@
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;
10 +use Activitypub\Scheduler\Post;
11 +use Activitypub\Scheduler\Actor;
12 +use Activitypub\Scheduler\Comment;
13 +use Activitypub\Collection\Actors;
14 +use Activitypub\Collection\Outbox;
7 15 use Activitypub\Collection\Followers;
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 -
16 +use Activitypub\Transformer\Factory;
14 17 /**
15 - * ActivityPub Scheduler Class
18 + * Scheduler class.
16 19 *
17 20 * @author Matthias Pfefferle
18 21 */
19 22 class Scheduler {
@@ -18,77 +21,58 @@
18 21 */
19 22 class Scheduler {
20 23
21 24 /**
22 - * Initialize the class, registering WordPress hooks
25 + * Allowed batch callbacks.
26 + *
27 + * @var array
23 28 */
29 + private static $batch_callbacks = array();
30 +
31 + /**
32 + * Initialize the class, registering WordPress hooks.
33 + */
24 34 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 - }
35 + self::register_schedulers();
36 +
37 + self::$batch_callbacks = array(
38 + Dispatcher::$callback,
39 + array( Dispatcher::class, 'retry_send_to_followers' ),
32 40 );
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 41
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 - }
62 -
63 - // Follower Cleanups
42 + // Follower Cleanups.
64 43 \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
65 44 \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
66 45
67 - // Migration
68 - \add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
46 + // Event callbacks.
47 + \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
48 + \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
49 + \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
69 50
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 - }
51 + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
52 + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
78 53
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 - }
54 + \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'handle_outbox_purge_days_update' ), 10, 2 );
85 55 }
86 56
87 57 /**
58 + * Register handlers.
59 + */
60 + public static function register_schedulers() {
61 + Post::init();
62 + Actor::init();
63 + Comment::init();
64 +
65 + /**
66 + * Register additional schedulers.
67 + *
68 + * @since 5.0.0
69 + */
70 + do_action( 'activitypub_register_schedulers' );
71 + }
72 +
73 + /**
88 74 * Schedule all ActivityPub schedules.
89 - *
90 - * @return void
91 75 */
92 76 public static function register_schedules() {
93 77 if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
94 78 \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
@@ -96,12 +80,20 @@
96 80
97 81 if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
98 82 \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
99 83 }
84 +
85 + if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
86 + \wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
87 + }
88 +
89 + if ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
90 + wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
91 + }
100 92 }
101 93
102 94 /**
103 - * Unscedule all ActivityPub schedules.
95 + * Un-schedule all ActivityPub schedules.
104 96 *
105 97 * @return void
106 98 */
107 99 public static function deregister_schedules() {
@@ -106,128 +98,15 @@
106 98 */
107 99 public static function deregister_schedules() {
108 100 wp_unschedule_hook( 'activitypub_update_followers' );
109 101 wp_unschedule_hook( 'activitypub_cleanup_followers' );
102 + wp_unschedule_hook( 'activitypub_reprocess_outbox' );
103 + wp_unschedule_hook( 'activitypub_outbox_purge' );
110 104 }
111 105
112 -
113 106 /**
114 - * Schedule Activities.
115 - *
116 - * @param string $new_status New post status.
117 - * @param string $old_status Old post status.
118 - * @param WP_Post $post Post object.
107 + * Update followers.
119 108 */
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 ) ) {
125 - return;
126 - }
127 -
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 ) ) {
131 - return;
132 - }
133 -
134 - $type = false;
135 -
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';
142 - }
143 -
144 - if ( empty( $type ) ) {
145 - return;
146 - }
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 - }
163 -
164 - /**
165 - * Schedule Comment Activities
166 - *
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.
172 - */
173 - public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
174 - $comment = get_comment( $comment );
175 -
176 - // federate only comments that are written by a registered user.
177 - if ( ! $comment->user_id ) {
178 - return;
179 - }
180 -
181 - $type = false;
182 -
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 - }
197 -
198 - if ( empty( $type ) ) {
199 - return;
200 - }
201 -
202 - // check if comment should be federated or not
203 - if ( ! should_comment_be_federated( $comment ) ) {
204 - return;
205 - }
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 - }
224 -
225 - /**
226 - * Update followers
227 - *
228 - * @return void
229 - */
230 109 public static function update_followers() {
231 110 $number = 5;
232 111
233 112 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
@@ -233,8 +112,13 @@
233 112 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
234 113 $number = 50;
235 114 }
236 115
116 + /**
117 + * Filter the number of followers to update.
118 + *
119 + * @param int $number The number of followers to update.
120 + */
237 121 $number = apply_filters( 'activitypub_update_followers_number', $number );
238 122 $followers = Followers::get_outdated_followers( $number );
239 123
240 124 foreach ( $followers as $follower ) {
@@ -249,11 +133,9 @@
249 133 }
250 134 }
251 135
252 136 /**
253 - * Cleanup followers
254 - *
255 - * @return void
137 + * Cleanup followers.
256 138 */
257 139 public static function cleanup_followers() {
258 140 $number = 5;
259 141
@@ -260,8 +142,13 @@
260 142 if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
261 143 $number = 50;
262 144 }
263 145
146 + /**
147 + * Filter the number of followers to clean up.
148 + *
149 + * @param int $number The number of followers to clean up.
150 + */
264 151 $number = apply_filters( 'activitypub_update_followers_number', $number );
265 152 $followers = Followers::get_faulty_followers( $number );
266 153
267 154 foreach ( $followers as $follower ) {
@@ -271,8 +158,13 @@
271 158 $follower->delete();
272 159 } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
273 160 if ( $follower->count_errors() >= 5 ) {
274 161 $follower->delete();
162 + \wp_schedule_single_event(
163 + \time(),
164 + 'activitypub_delete_actor_interactions',
165 + array( $follower->get_id() )
166 + );
275 167 } else {
276 168 Followers::add_error( $follower->get__id(), $meta );
277 169 }
278 170 } else {
@@ -281,78 +173,271 @@
281 173 }
282 174 }
283 175
284 176 /**
285 - * Schedule migration if DB-Version is not up to date.
177 + * Schedule the outbox item for federation.
286 178 *
287 - * @return void
179 + * @param int $id The ID of the outbox item.
180 + * @param int $offset The offset to add to the scheduled time.
288 181 */
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' );
182 + public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) {
183 + $hook = 'activitypub_process_outbox';
184 + $args = array( $id );
185 +
186 + if ( false === wp_next_scheduled( $hook, $args ) ) {
187 + \wp_schedule_single_event(
188 + \time() + $offset,
189 + $hook,
190 + $args
191 + );
292 192 }
293 193 }
294 194
295 195 /**
296 - * Send a profile update when relevant user meta is updated.
196 + * Reprocess the outbox.
197 + */
198 + public static function reprocess_outbox() {
199 + // Bail if there is a pending batch.
200 + if ( self::next_scheduled_hook( 'activitypub_async_batch' ) ) {
201 + return;
202 + }
203 +
204 + // Bail if there is a batch in progress.
205 + $key = \md5( \serialize( Dispatcher::$callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
206 + if ( self::is_locked( $key ) ) {
207 + return;
208 + }
209 +
210 + $ids = \get_posts(
211 + array(
212 + 'post_type' => Outbox::POST_TYPE,
213 + 'post_status' => 'pending',
214 + 'posts_per_page' => 10,
215 + 'fields' => 'ids',
216 + )
217 + );
218 +
219 + foreach ( $ids as $id ) {
220 + self::schedule_outbox_activity_for_federation( $id );
221 + }
222 + }
223 +
224 + /**
225 + * Purge outbox items based on a schedule.
226 + */
227 + public static function purge_outbox() {
228 + $total_posts = (int) wp_count_posts( Outbox::POST_TYPE )->publish;
229 + if ( $total_posts <= 20 ) {
230 + return;
231 + }
232 +
233 + $days = (int) get_option( 'activitypub_outbox_purge_days', 180 );
234 + $timezone = new \DateTimeZone( 'UTC' );
235 + $date = new \DateTime( 'now', $timezone );
236 +
237 + $date->sub( \DateInterval::createFromDateString( "$days days" ) );
238 +
239 + $post_ids = get_posts(
240 + array(
241 + 'post_type' => Outbox::POST_TYPE,
242 + 'post_status' => 'any',
243 + 'fields' => 'ids',
244 + 'numberposts' => -1,
245 + 'date_query' => array(
246 + array(
247 + 'before' => $date->format( 'Y-m-d' ),
248 + ),
249 + ),
250 + )
251 + );
252 +
253 + foreach ( $post_ids as $post_id ) {
254 + \wp_delete_post( $post_id, true );
255 + }
256 + }
257 +
258 + /**
259 + * Update schedules when outbox purge days settings change.
297 260 *
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.
261 + * @param int $old_value The old value.
262 + * @param int $value The new value.
263 + */
264 + public static function handle_outbox_purge_days_update( $old_value, $value ) {
265 + if ( 0 === (int) $value ) {
266 + wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
267 + } elseif ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
268 + wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
269 + }
270 + }
271 +
272 + /**
273 + * Asynchronously runs batch processing routines.
301 274 *
302 - * @return void
275 + * The batching part is optional and only comes into play if the callback returns anything.
276 + * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
277 + *
278 + * @param callable $callback Callable processing routine.
279 + * @params mixed ...$args Optional. Parameters that get passed to the callback.
303 280 */
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' ) ) {
281 + public static function async_batch( $callback ) {
282 + if ( ! in_array( $callback, self::$batch_callbacks, true ) || ! \is_callable( $callback ) ) {
283 + _doing_it_wrong( __METHOD__, 'The first argument must be a valid callback.', '5.2.0' );
307 284 return;
308 285 }
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 );
286 +
287 + $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
288 + $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
289 +
290 + // Bail if the existing lock is still valid.
291 + if ( self::is_locked( $key ) ) {
292 + \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', $args );
293 + return;
318 294 }
295 +
296 + self::lock( $key );
297 +
298 + $callback = array_shift( $args ); // Remove $callback from arguments.
299 + $next = \call_user_func_array( $callback, $args );
300 +
301 + self::unlock( $key );
302 +
303 + if ( ! empty( $next ) ) {
304 + // Schedule the next run, adding the result to the arguments.
305 + \wp_schedule_single_event(
306 + \time() + 30,
307 + 'activitypub_async_batch',
308 + \array_merge( array( $callback ), \array_values( $next ) )
309 + );
310 + }
319 311 }
320 312
313 +
321 314 /**
322 - * Send a profile update when a user is updated.
315 + * Locks the async batch process for individual callbacks to prevent simultaneous processing.
323 316 *
324 - * @param int $user_id User ID being updated.
317 + * @param string $key Serialized callback name.
318 + * @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
319 + */
320 + public static function lock( $key ) {
321 + global $wpdb;
322 +
323 + // Try to lock.
324 + $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
325 +
326 + if ( ! $lock_result ) {
327 + $lock_result = \get_option( 'activitypub_async_batch_' . $key );
328 + }
329 +
330 + return $lock_result;
331 + }
332 +
333 + /**
334 + * Unlocks processing for the async batch callback.
325 335 *
326 - * @return void
336 + * @param string $key Serialized callback name.
327 337 */
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' ) ) {
331 - return;
338 + public static function unlock( $key ) {
339 + \delete_option( 'activitypub_async_batch_' . $key );
340 + }
341 +
342 + /**
343 + * Whether the async batch callback is locked.
344 + *
345 + * @param string $key Serialized callback name.
346 + * @return boolean
347 + */
348 + public static function is_locked( $key ) {
349 + $lock = \get_option( 'activitypub_async_batch_' . $key );
350 +
351 + if ( ! $lock ) {
352 + return false;
332 353 }
333 354
334 - self::schedule_profile_update( $user_id );
355 + $lock = (int) $lock;
356 +
357 + if ( $lock < \time() - 1800 ) {
358 + self::unlock( $key );
359 + return false;
360 + }
361 +
362 + return true;
335 363 }
336 364
337 365 /**
338 - * Theme mods only have a dynamic filter so we fudge it like this.
339 - * @param mixed $value
340 - * @return mixed
366 + * Get the next scheduled hook.
367 + *
368 + * @param string $hook The hook name.
369 + * @return int|bool The timestamp of the next scheduled hook, or false if none found.
341 370 */
342 - public static function blog_user_update( $value = null ) {
343 - self::schedule_profile_update( 0 );
344 - return $value;
371 + private static function next_scheduled_hook( $hook ) {
372 + $crons = _get_cron_array();
373 + if ( empty( $crons ) ) {
374 + return false;
375 + }
376 +
377 + // Get next event.
378 + $next = false;
379 + foreach ( $crons as $timestamp => $cron ) {
380 + if ( isset( $cron[ $hook ] ) ) {
381 + $next = $timestamp;
382 + break;
383 + }
384 + }
385 +
386 + return $next;
345 387 }
346 388
347 389 /**
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).
390 + * Send announces.
391 + *
392 + * @param int $outbox_activity_id The outbox activity ID.
393 + * @param Activity $activity_object The activity object.
394 + * @param int $actor_id The actor ID.
395 + * @param int $content_visibility The content visibility.
350 396 */
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 - );
397 + public static function schedule_announce_activity( $outbox_activity_id, $activity_object, $actor_id, $content_visibility ) {
398 + // Only if we're in both Blog and User modes.
399 + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
400 + return;
401 + }
402 +
403 + // Only if this isn't the Blog Actor.
404 + if ( Actors::BLOG_USER_ID === $actor_id ) {
405 + return;
406 + }
407 +
408 + // Only if the content is public or quiet public.
409 + if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
410 + return;
411 + }
412 +
413 + $activity_type = \get_post_meta( $outbox_activity_id, '_activitypub_activity_type', true );
414 +
415 + // Only if the activity is a Create, Update or Delete.
416 + if ( ! in_array( $activity_type, array( 'Create', 'Update', 'Delete' ), true ) ) {
417 + return;
418 + }
419 +
420 + // Check if the object is an article, image, audio, video, event or document and ignore profile updates and other activities.
421 + if ( ! in_array( $activity_object->get_type(), array( 'Note', 'Article', 'Image', 'Audio', 'Video', 'Event', 'Document' ), true ) ) {
422 + return;
423 + }
424 +
425 + $transformer = Factory::get_transformer( $activity_object );
426 + if ( ! $transformer || \is_wp_error( $transformer ) ) {
427 + return;
428 + }
429 +
430 + $post = get_post( $outbox_activity_id );
431 + $activity = $transformer->to_activity( $activity_type );
432 + $activity->set_id( $post->guid );
433 +
434 + $outbox_activity_id = Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID );
435 +
436 + if ( ! $outbox_activity_id ) {
437 + return;
438 + }
439 +
440 + // Schedule the outbox item for federation.
441 + self::schedule_outbox_activity_for_federation( $outbox_activity_id, 30 );
357 442 }
358 443 }