PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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
activitypub / includes / class-dispatcher.php

class-dispatcher.php in ActivityPub 7.7.0, at includes/class-dispatcher.php

531 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ActivityPub Dispatcher Class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Collection\Followers;
12 use Activitypub\Collection\Outbox;
13
14 /**
15 * ActivityPub Dispatcher Class.
16 *
17 * @author Matthias Pfefferle
18 *
19 * @see https://www.w3.org/TR/activitypub/
20 */
21 class Dispatcher {
22 /**
23 * Batch size.
24 *
25 * @deprecated 7.6.0 Use {@see Dispatcher::get_batch_size()}.
26 *
27 * @var int
28 */
29 public static $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE;
30
31 /**
32 * Initialize the class, registering WordPress hooks.
33 */
34 public static function init() {
35 \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) );
36
37 \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_immediate_accept' ), 10, 2 );
38
39 // Default filters to add Inboxes to sent to.
40 \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
41 \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
42 \add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 );
43
44 Scheduler::register_async_batch_callback( 'activitypub_send_activity', array( self::class, 'send_to_followers' ) );
45 Scheduler::register_async_batch_callback( 'activitypub_retry_activity', array( self::class, 'retry_send_to_followers' ) );
46 }
47
48 /**
49 * Get the batch size for processing outbox items.
50 *
51 * @return int The batch size.
52 */
53 public static function get_batch_size() {
54 /**
55 * Filters the batch size for processing outbox items.
56 *
57 * @param int $batch_size The batch size. Default ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE.
58 */
59 return apply_filters( 'activitypub_dispatcher_batch_size', ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE );
60 }
61
62 /**
63 * Get the maximum number of retry attempts.
64 *
65 * @return int The maximum number of retry attempts.
66 */
67 public static function get_retry_max_attempts() {
68 /**
69 * Filters the maximum number of retry attempts.
70 *
71 * @param int $retry_max_attempts The maximum number of retry attempts. Default ACTIVITYPUB_OUTBOX_RETRY_MAX_ATTEMPTS.
72 */
73 return apply_filters( 'activitypub_dispatcher_retry_max_attempts', 3 );
74 }
75
76 /**
77 * Get the retry delay unit (in seconds).
78 *
79 * Used to calculate exponential backoff: time() + (attempt * attempt * retry_delay_unit).
80 *
81 * @return int The retry delay unit in seconds.
82 */
83 public static function get_retry_delay() {
84 /**
85 * Filters the retry delay unit (in seconds).
86 *
87 * Used to calculate exponential backoff: time() + (attempt * attempt * retry_delay_unit).
88 *
89 * @param int $retry_delay_unit The retry delay unit in seconds. Default ACTIVITYPUB_OUTBOX_RETRY_DELAY_UNIT.
90 */
91 return apply_filters( 'activitypub_dispatcher_retry_delay', HOUR_IN_SECONDS );
92 }
93
94 /**
95 * Get the error codes that qualify for a retry.
96 *
97 * @see https://github.com/tfredrich/RestApiTutorial.com/blob/fd08b0f67f07450521d143b123cd6e1846cb2e3b/content/advanced/responses/retries.md
98 *
99 * @return int[] The error codes.
100 */
101 public static function get_retry_error_codes() {
102 /**
103 * Filters the error codes that qualify for a retry.
104 *
105 * @param int[] $retry_error_codes The error codes. Default array( 408, 429, 500, 502, 503, 504 ).
106 */
107 return apply_filters( 'activitypub_dispatcher_retry_error_codes', array( 408, 429, 500, 502, 503, 504 ) );
108 }
109
110 /**
111 * Process the outbox.
112 *
113 * @param int $id The outbox ID.
114 */
115 public static function process_outbox( $id ) {
116 $outbox_item = \get_post( $id );
117
118 // If the activity is not a post, return.
119 if ( ! $outbox_item ) {
120 return;
121 }
122
123 $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
124 $actor = Outbox::get_actor( $outbox_item );
125 if ( \is_wp_error( $actor ) && 'Delete' !== $type ) {
126 // If the actor is not found, publish the post and don't try again.
127 \wp_publish_post( $outbox_item );
128 return;
129 }
130
131 $activity = Outbox::get_activity( $outbox_item );
132
133 // Send to mentioned and replied-to users. Everyone other than followers.
134 self::send_to_additional_inboxes( $activity, $outbox_item->post_author, $outbox_item );
135
136 if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) {
137 \do_action(
138 'activitypub_send_activity',
139 $outbox_item->ID,
140 self::get_batch_size(),
141 \get_post_meta( $outbox_item->ID, '_activitypub_outbox_offset', true ) ?: 0 // phpcs:ignore
142 );
143 } else {
144 // No followers to process for this update. We're done.
145 \wp_publish_post( $outbox_item );
146 \delete_post_meta( $outbox_item->ID, '_activitypub_outbox_offset' );
147 }
148 }
149
150 /**
151 * Asynchronously runs batch processing routines.
152 *
153 * @param int $outbox_item_id The Outbox item ID.
154 * @param int|null $batch_size Optional. The batch size. Default null (uses filtered batch size).
155 * @param int $offset Optional. The offset. Default 0.
156 *
157 * @return array|void The next batch of followers to process, or void if done.
158 */
159 public static function send_to_followers( $outbox_item_id, $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE, $offset = 0 ) {
160 if ( null === $batch_size ) {
161 $batch_size = self::get_batch_size();
162 }
163
164 $outbox_item = \get_post( $outbox_item_id );
165 $json = Outbox::get_activity( $outbox_item_id )->to_json();
166 $inboxes = Followers::get_inboxes_for_activity( $json, $outbox_item->post_author, $batch_size, $offset );
167 $retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
168
169 // Retry failed inboxes.
170 if ( ! empty( $retries ) ) {
171 self::schedule_retry( $retries, $outbox_item_id );
172 }
173
174 if ( is_countable( $inboxes ) && count( $inboxes ) < $batch_size ) {
175 \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
176
177 /**
178 * Fires when the followers are complete.
179 *
180 * @param array $inboxes The inboxes.
181 * @param string $json The ActivityPub Activity JSON
182 * @param int $actor_id The actor ID.
183 * @param int $outbox_item_id The Outbox item ID.
184 * @param int $batch_size The batch size.
185 * @param int $offset The offset.
186 */
187 \do_action( 'activitypub_outbox_processing_complete', $inboxes, $json, $outbox_item->post_author, $outbox_item_id, $batch_size, $offset );
188
189 // No more followers to process for this update.
190 \wp_publish_post( $outbox_item_id );
191 } else {
192 \update_post_meta( $outbox_item_id, '_activitypub_outbox_offset', $offset + $batch_size );
193
194 /**
195 * Fires when the batch of followers is complete.
196 *
197 * @param array $inboxes The inboxes.
198 * @param string $json The ActivityPub Activity JSON
199 * @param int $actor_id The actor ID.
200 * @param int $outbox_item_id The Outbox item ID.
201 * @param int $batch_size The batch size.
202 * @param int $offset The offset.
203 */
204 \do_action( 'activitypub_outbox_processing_batch_complete', $inboxes, $json, $outbox_item->post_author, $outbox_item_id, $batch_size, $offset );
205
206 return array( $outbox_item_id, $batch_size, $offset + $batch_size );
207 }
208 }
209
210 /**
211 * Retry sending to followers.
212 *
213 * @param string $transient_key The key to retrieve retry inboxes.
214 * @param int $outbox_item_id The Outbox item ID.
215 * @param int $attempt The attempt number.
216 */
217 public static function retry_send_to_followers( $transient_key, $outbox_item_id, $attempt = 1 ) {
218 $inboxes = \get_transient( $transient_key );
219 if ( false === $inboxes ) {
220 return;
221 }
222
223 // Delete the transient as we no longer need it.
224 \delete_transient( $transient_key );
225
226 $retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
227
228 // Retry failed inboxes.
229 if ( ++$attempt < self::get_retry_max_attempts() && ! empty( $retries ) ) {
230 self::schedule_retry( $retries, $outbox_item_id, $attempt );
231 }
232 }
233
234 /**
235 * Send to inboxes.
236 *
237 * @param array $inboxes The inboxes to notify.
238 * @param int $outbox_item_id The Outbox item ID.
239 * @return array The failed inboxes.
240 */
241 private static function send_to_inboxes( $inboxes, $outbox_item_id ) {
242 $outbox_item = \get_post( $outbox_item_id );
243 $json = Outbox::get_activity( $outbox_item_id )->to_json();
244 $retries = array();
245
246 /**
247 * Fires before sending an Activity to inboxes.
248 *
249 * @param string $json The ActivityPub Activity JSON.
250 * @param array $inboxes The inboxes to send to.
251 * @param int $outbox_item_id The Outbox item ID.
252 */
253 \do_action( 'activitypub_pre_send_to_inboxes', $json, $inboxes, $outbox_item_id );
254
255 foreach ( $inboxes as $inbox ) {
256 // Handle local inboxes via internal REST API, remote via HTTP.
257 if ( is_same_domain( $inbox ) ) {
258 $result = self::send_to_local_inbox( $inbox, $json );
259 } else {
260 $result = safe_remote_post( $inbox, $json, $outbox_item->post_author );
261 }
262
263 if ( \is_wp_error( $result ) && in_array( $result->get_error_code(), self::get_retry_error_codes(), true ) ) {
264 $retries[] = $inbox;
265 }
266
267 /**
268 * Fires after an Activity has been sent to an inbox.
269 *
270 * @param array $result The result of the internal or remote post request.
271 * @param string $inbox The inbox URL.
272 * @param string $json The ActivityPub Activity JSON.
273 * @param int $actor_id The actor ID.
274 * @param int $outbox_item_id The Outbox item ID.
275 */
276 \do_action( 'activitypub_sent_to_inbox', $result, $inbox, $json, $outbox_item->post_author, $outbox_item_id );
277 }
278
279 return $retries;
280 }
281
282 /**
283 * Send an activity to a local inbox via internal REST API request.
284 *
285 * @param string $inbox_url The local inbox URL.
286 * @param string $json The ActivityPub Activity JSON.
287 * @return array|\WP_Error The result in the format of a remote post response, or WP_Error on failure.
288 */
289 private static function send_to_local_inbox( $inbox_url, $json ) {
290 // Parse the inbox URL to extract the REST route.
291 $path = \wp_parse_url( $inbox_url, PHP_URL_PATH ) ?? '';
292 $rest_route = \preg_replace( '#^/' . preg_quote( \rest_get_url_prefix(), '#' ) . '#', '', $path );
293
294 // Create a REST request.
295 $request = new \WP_REST_Request( 'POST', $rest_route );
296 $request->set_header( 'Content-Type', 'application/activity+json' );
297 $request->set_body( $json );
298 $request->get_json_params();
299
300 \add_filter( 'activitypub_defer_signature_verification', '__return_true' );
301 $response = \rest_do_request( $request );
302 \remove_filter( 'activitypub_defer_signature_verification', '__return_true' );
303
304 // Return result in format similar to remote post response.
305 if ( $response->is_error() ) {
306 return $response->as_error();
307 }
308
309 return array(
310 'response' => array(
311 'code' => $response->get_status(),
312 ),
313 'body' => \wp_json_encode( $response->get_data() ),
314 );
315 }
316
317 /**
318 * Schedule a retry.
319 *
320 * @param array $retries The inboxes to retry.
321 * @param int $outbox_item_id The Outbox item ID.
322 * @param int $attempt Optional. The attempt number. Default 1.
323 */
324 private static function schedule_retry( $retries, $outbox_item_id, $attempt = 1 ) {
325 $transient_key = 'activitypub_retry_' . \wp_generate_password( 12, false );
326 \set_transient( $transient_key, $retries, WEEK_IN_SECONDS );
327
328 \wp_schedule_single_event(
329 \time() + ( $attempt * $attempt * self::get_retry_delay() ),
330 'activitypub_retry_activity',
331 array( $transient_key, $outbox_item_id, $attempt )
332 );
333 }
334
335 /**
336 * Send an Activity to a custom list of inboxes, like mentioned users or replied-to posts.
337 *
338 * For all custom implementations, please use the `activitypub_additional_inboxes` filter.
339 *
340 * @param Activity $activity The ActivityPub Activity.
341 * @param int $actor_id The actor ID.
342 * @param \WP_Post $outbox_item The WordPress object.
343 */
344 private static function send_to_additional_inboxes( $activity, $actor_id, $outbox_item = null ) {
345 /**
346 * Filters the list of inboxes to send the Activity to.
347 *
348 * @param array $inboxes The list of inboxes to send to.
349 * @param int $actor_id The actor ID.
350 * @param Activity $activity The ActivityPub Activity.
351 */
352 $inboxes = apply_filters( 'activitypub_additional_inboxes', array(), $actor_id, $activity );
353 $inboxes = array_unique( $inboxes );
354
355 $retries = self::send_to_inboxes( $inboxes, $outbox_item->ID );
356
357 // Retry failed inboxes.
358 if ( ! empty( $retries ) ) {
359 self::schedule_retry( $retries, $outbox_item->ID );
360 }
361 }
362
363 /**
364 * Default filter to add Inboxes of Mentioned Actors
365 *
366 * @param array $inboxes The list of Inboxes.
367 * @param int $actor_id The WordPress Actor-ID.
368 * @param Activity $activity The ActivityPub Activity.
369 *
370 * @return array The filtered Inboxes.
371 */
372 public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) {
373 $cc = $activity->get_cc() ?? array();
374 $to = $activity->get_to() ?? array();
375
376 $audience = array_merge( $cc, $to );
377
378 // Remove "public placeholder" from the audience.
379 $audience = array_diff( $audience, array( 'https://www.w3.org/ns/activitystreams#Public' ) );
380
381 if ( $audience ) {
382 $mentioned_inboxes = Mention::get_inboxes( $audience );
383
384 return array_merge( $inboxes, $mentioned_inboxes );
385 }
386
387 return $inboxes;
388 }
389
390 /**
391 * Default filter to add Inboxes of Posts that are set as `in-reply-to`
392 *
393 * @param array $inboxes The list of Inboxes.
394 * @param int $actor_id The WordPress Actor-ID.
395 * @param Activity $activity The ActivityPub Activity.
396 *
397 * @return array The filtered Inboxes
398 */
399 public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) {
400 $in_reply_to = $activity->get_in_reply_to();
401
402 if ( ! $in_reply_to ) {
403 return $inboxes;
404 }
405
406 if ( ! is_array( $in_reply_to ) ) {
407 $in_reply_to = array( $in_reply_to );
408 }
409
410 foreach ( $in_reply_to as $url ) {
411 // No need to self-notify.
412 if ( is_same_domain( $url ) ) {
413 continue;
414 }
415
416 $object = Http::get_remote_object( $url );
417
418 if (
419 ! $object ||
420 \is_wp_error( $object ) ||
421 empty( $object['attributedTo'] )
422 ) {
423 continue;
424 }
425
426 $actor = object_to_uri( $object['attributedTo'] );
427 $actor = Http::get_remote_object( $actor );
428
429 if ( ! $actor || \is_wp_error( $actor ) ) {
430 continue;
431 }
432
433 if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) {
434 $inboxes[] = $actor['endpoints']['sharedInbox'];
435 } elseif ( ! empty( $actor['inbox'] ) ) {
436 $inboxes[] = $actor['inbox'];
437 }
438 }
439
440 return $inboxes;
441 }
442
443 /**
444 * Check if passed Activity is public.
445 *
446 * @param Activity $activity The Activity object.
447 * @param \Activitypub\Model\User|\Activitypub\Model\Blog $actor The Actor object.
448 * @param \WP_Post $outbox_item The Outbox item.
449 *
450 * @return boolean True if public, false if not.
451 */
452 protected static function should_send_to_followers( $activity, $actor, $outbox_item ) {
453 // Check if follower endpoint is set.
454 $cc = $activity->get_cc() ?? array();
455 $to = $activity->get_to() ?? array();
456
457 $audience = array_merge( $cc, $to );
458
459 $send = (
460 // Check if activity is public.
461 in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ||
462 // ...or check if follower endpoint is set.
463 in_array( $actor->get_followers(), $audience, true )
464 );
465
466 if ( $send ) {
467 $followers = Followers::get_inboxes_for_activity( $activity->to_json(), $outbox_item->post_author );
468
469 // Only send if there are followers to send to.
470 $send = ! is_countable( $followers ) || 0 < count( $followers );
471 }
472
473 /**
474 * Filters whether to send an Activity to followers.
475 *
476 * @param bool $send_activity_to_followers Whether to send the Activity to followers.
477 * @param Activity $activity The ActivityPub Activity.
478 * @param int $actor_id The actor ID.
479 * @param \WP_Post $outbox_item The WordPress object.
480 */
481 return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $outbox_item->post_author, $outbox_item );
482 }
483
484 /**
485 * Add Inboxes of Relays.
486 *
487 * @param array $inboxes The list of Inboxes.
488 * @param int $actor_id The Actor-ID.
489 * @param Activity $activity The ActivityPub Activity.
490 *
491 * @return array The filtered Inboxes.
492 */
493 public static function add_inboxes_of_relays( $inboxes, $actor_id, $activity ) {
494 // Check if follower endpoint is set.
495 $cc = $activity->get_cc() ?? array();
496 $to = $activity->get_to() ?? array();
497
498 $audience = array_merge( $cc, $to );
499
500 // Check if activity is public.
501 if ( ! in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ) {
502 return $inboxes;
503 }
504
505 $relays = \get_option( 'activitypub_relays', array() );
506
507 if ( empty( $relays ) ) {
508 return $inboxes;
509 }
510
511 return array_merge( $inboxes, $relays );
512 }
513
514 /**
515 * Send an immediate Accept activity for the given Outbox item.
516 *
517 * @param int $outbox_id The Outbox item ID.
518 * @param Activity $activity The Activity that was just added to the Outbox.
519 */
520 public static function send_immediate_accept( $outbox_id, $activity ) {
521 $outbox_item = \get_post( $outbox_id );
522
523 if ( ! $outbox_item || 'Accept' !== $activity->get_type() ) {
524 return;
525 }
526
527 // Send to mentioned and replied-to users. Everyone other than followers.
528 self::send_to_additional_inboxes( $activity, $outbox_item->post_author, $outbox_item );
529 }
530 }
531