PluginProbe
ActivityPub / 7.0.0
ActivityPub v7.0.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 / functions.php

functions.php in ActivityPub 7.0.0, at includes/functions.php

1,700 lines 44.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Functions file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Actor;
12 use Activitypub\Activity\Base_Object;
13 use Activitypub\Collection\Actors;
14 use Activitypub\Collection\Outbox;
15 use Activitypub\Collection\Followers;
16 use Activitypub\Collection\Following;
17 use Activitypub\Transformer\Post;
18 use Activitypub\Transformer\Factory as Transformer_Factory;
19
20 /**
21 * Returns the ActivityPub default JSON-context.
22 *
23 * @return array The activitypub context.
24 */
25 function get_context() {
26 $context = Activity::JSON_LD_CONTEXT;
27
28 /**
29 * Filters the ActivityPub JSON-LD context.
30 *
31 * This filter allows developers to modify or extend the JSON-LD context used
32 * in ActivityPub responses. The context defines the vocabulary and terms used
33 * in the ActivityPub JSON objects.
34 *
35 * @param array $context The default ActivityPub JSON-LD context array.
36 */
37 return \apply_filters( 'activitypub_json_context', $context );
38 }
39
40 /**
41 * Send a POST request to a remote server.
42 *
43 * @param string $url The URL endpoint.
44 * @param string $body The Post Body.
45 * @param int $user_id The WordPress user ID.
46 *
47 * @return array|\WP_Error The POST Response or an WP_Error.
48 */
49 function safe_remote_post( $url, $body, $user_id ) {
50 return Http::post( $url, $body, $user_id );
51 }
52
53 /**
54 * Send a GET request to a remote server.
55 *
56 * @param string $url The URL endpoint.
57 *
58 * @return array|\WP_Error The GET Response or an WP_Error.
59 */
60 function safe_remote_get( $url ) {
61 return Http::get( $url );
62 }
63
64 /**
65 * Returns a users WebFinger "resource".
66 *
67 * @param int $user_id The user ID.
68 *
69 * @return string The User resource.
70 */
71 function get_webfinger_resource( $user_id ) {
72 return Webfinger::get_user_resource( $user_id );
73 }
74
75 /**
76 * Requests the Meta-Data from the Actors profile.
77 *
78 * @param array|string $actor The Actor array or URL.
79 * @param bool $cached Optional. Whether the result should be cached. Default true.
80 *
81 * @return array|\WP_Error The Actor profile as array or WP_Error on failure.
82 */
83 function get_remote_metadata_by_actor( $actor, $cached = true ) {
84 /**
85 * Filters the metadata before it is retrieved from a remote actor.
86 *
87 * Passing a non-false value will effectively short-circuit the remote request,
88 * returning that value instead.
89 *
90 * @param mixed $pre The value to return instead of the remote metadata.
91 * Default false to continue with the remote request.
92 * @param string $actor The actor URL.
93 */
94 $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
95 if ( $pre ) {
96 return $pre;
97 }
98
99 return Http::get_remote_object( $actor, $cached );
100 }
101
102 /**
103 * Returns the followers of a given user.
104 *
105 * @param int $user_id The user ID.
106 *
107 * @return array The followers.
108 */
109 function get_followers( $user_id ) {
110 return Followers::get_followers( $user_id );
111 }
112
113 /**
114 * Count the number of followers for a given user.
115 *
116 * @param int $user_id The user ID.
117 *
118 * @return int The number of followers.
119 */
120 function count_followers( $user_id ) {
121 return Followers::count_followers( $user_id );
122 }
123
124 /**
125 * Examine a url and try to determine the author ID it represents.
126 *
127 * Checks are supposedly from the hosted site blog.
128 *
129 * @param string $url Permalink to check.
130 *
131 * @return int|null User ID, or null on failure.
132 */
133 function url_to_authorid( $url ) {
134 global $wp_rewrite;
135
136 // Check if url hase the same host.
137 $request_host = \wp_parse_url( $url, \PHP_URL_HOST );
138 if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== $request_host && get_option( 'activitypub_old_host' ) !== $request_host ) {
139 return null;
140 }
141
142 // First, check to see if there is an 'author=N' to match against.
143 if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
144 return \absint( $values[1] );
145 }
146
147 // Check to see if we are using rewrite rules.
148 $rewrite = $wp_rewrite->wp_rewrite_rules();
149
150 // Not using rewrite rules, and 'author=N' method failed, so we're out of options.
151 if ( empty( $rewrite ) ) {
152 return null;
153 }
154
155 // Generate rewrite rule for the author url.
156 $author_rewrite = $wp_rewrite->get_author_permastruct();
157 $author_regexp = \str_replace( '%author%', '', $author_rewrite );
158
159 // Match the rewrite rule with the passed url.
160 if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
161 $user = \get_user_by( 'slug', $match[2] );
162 if ( $user ) {
163 return $user->ID;
164 }
165 }
166
167 return null;
168 }
169
170 /**
171 * Verify that url is a wp_ap_comment or a previously received remote comment.
172 *
173 * @return int|bool Comment ID or false if not found.
174 */
175 function is_comment() {
176 $comment_id = get_query_var( 'c', null );
177
178 if ( ! is_null( $comment_id ) ) {
179 $comment = \get_comment( $comment_id );
180
181 if ( $comment ) {
182 return $comment_id;
183 }
184 }
185
186 return false;
187 }
188
189 /**
190 * Check for Tombstone Objects.
191 *
192 * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
193 *
194 * @param \WP_Error $wp_error A WP_Error-Response of an HTTP-Request.
195 *
196 * @return boolean True if HTTP-Code is 410 or 404.
197 */
198 function is_tombstone( $wp_error ) {
199 if ( ! is_wp_error( $wp_error ) ) {
200 return false;
201 }
202
203 if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
204 return true;
205 }
206
207 return false;
208 }
209
210 /**
211 * Get the REST URL relative to this plugin's namespace.
212 *
213 * @param string $path Optional. REST route path. Default ''.
214 *
215 * @return string REST URL relative to this plugin's namespace.
216 */
217 function get_rest_url_by_path( $path = '' ) {
218 // We'll handle the leading slash.
219 $path = ltrim( $path, '/' );
220 $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
221 return \get_rest_url( null, $namespaced_path );
222 }
223
224 /**
225 * Convert a string from camelCase to snake_case.
226 *
227 * @param string $input The string to convert.
228 *
229 * @return string The converted string.
230 */
231 function camel_to_snake_case( $input ) {
232 return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $input ) );
233 }
234
235 /**
236 * Convert a string from snake_case to camelCase.
237 *
238 * @param string $input The string to convert.
239 *
240 * @return string The converted string.
241 */
242 function snake_to_camel_case( $input ) {
243 return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
244 }
245
246 /**
247 * Escapes a Tag, to be used as a hashtag.
248 *
249 * @param string $input The string to escape.
250 *
251 * @return string The escaped hashtag.
252 */
253 function esc_hashtag( $input ) {
254
255 $hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
256 // Remove all characters that are not letters, numbers, or underscores.
257 $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
258
259 // Capitalize every letter that is preceded by an underscore.
260 $hashtag = preg_replace_callback(
261 '/_(.)/',
262 function ( $matches ) {
263 return strtoupper( $matches[1] );
264 },
265 $hashtag
266 );
267
268 // Add a hashtag to the beginning of the string.
269 $hashtag = ltrim( $hashtag, '#' );
270 $hashtag = '#' . $hashtag;
271
272 /**
273 * Allow defining your own custom hashtag generation rules.
274 *
275 * @param string $hashtag The hashtag to be returned.
276 * @param string $input The original string.
277 */
278 $hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $input );
279
280 return esc_html( $hashtag );
281 }
282
283 /**
284 * Check if a request is for an ActivityPub request.
285 *
286 * @return bool False by default.
287 */
288 function is_activitypub_request() {
289 return Query::get_instance()->is_activitypub_request();
290 }
291
292 /**
293 * Check if content negotiation is allowed for a request.
294 *
295 * @return bool True if content negotiation is allowed, false otherwise.
296 */
297 function should_negotiate_content() {
298 return Query::get_instance()->should_negotiate_content();
299 }
300
301 /**
302 * Check if a post is disabled for ActivityPub.
303 *
304 * This function checks if the post type supports ActivityPub and if the post is set to be local.
305 *
306 * @param mixed $post The post object or ID.
307 *
308 * @return boolean True if the post is disabled, false otherwise.
309 */
310 function is_post_disabled( $post ) {
311 $post = \get_post( $post );
312 $disabled = false;
313
314 if ( ! $post ) {
315 return true;
316 }
317
318 $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
319
320 if (
321 ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL === $visibility ||
322 ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility ||
323 ! \post_type_supports( $post->post_type, 'activitypub' ) ||
324 'private' === $post->post_status ||
325 ! empty( $post->post_password )
326 ) {
327 $disabled = true;
328 }
329
330 /**
331 * Allow plugins to disable posts for ActivityPub.
332 *
333 * @param boolean $disabled True if the post is disabled, false otherwise.
334 * @param \WP_Post $post The post object.
335 */
336 return \apply_filters( 'activitypub_is_post_disabled', $disabled, $post );
337 }
338
339 /**
340 * This function checks if a user is enabled for ActivityPub.
341 *
342 * @param int|string $user_id The user ID.
343 * @return boolean True if the user is enabled, false otherwise.
344 */
345 function user_can_activitypub( $user_id ) {
346 if ( ! is_numeric( $user_id ) ) {
347 return false;
348 }
349
350 switch ( $user_id ) {
351 case Actors::APPLICATION_USER_ID:
352 $enabled = true; // Application user is always enabled.
353 break;
354
355 case Actors::BLOG_USER_ID:
356 $enabled = ! is_user_type_disabled( 'blog' );
357 break;
358
359 default:
360 if ( ! \get_user_by( 'id', $user_id ) ) {
361 $enabled = false;
362 break;
363 }
364
365 if ( is_user_type_disabled( 'user' ) ) {
366 $enabled = false;
367 break;
368 }
369
370 $enabled = \user_can( $user_id, 'activitypub' );
371 }
372
373 /**
374 * Allow plugins to enable/disable users for ActivityPub.
375 *
376 * @param boolean $enabled True if the user is enabled, false otherwise.
377 * @param int $user_id The user ID.
378 */
379 return apply_filters( 'activitypub_user_can_activitypub', $enabled, $user_id );
380 }
381
382 /**
383 * Checks if a User-Type is disabled for ActivityPub.
384 *
385 * This function is used to check if the 'blog' or 'user'
386 * type is disabled for ActivityPub.
387 *
388 * @param string $type User type. 'blog' or 'user'.
389 *
390 * @return boolean True if the user type is disabled, false otherwise.
391 */
392 function is_user_type_disabled( $type ) {
393 switch ( $type ) {
394 case 'blog':
395 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
396 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
397 $disabled = false;
398 break;
399 }
400 }
401
402 if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
403 $disabled = ACTIVITYPUB_DISABLE_BLOG_USER;
404 break;
405 }
406
407 if ( ACTIVITYPUB_ACTOR_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
408 $disabled = true;
409 break;
410 }
411
412 $disabled = false;
413 break;
414 case 'user':
415 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
416 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
417 $disabled = true;
418 break;
419 }
420 }
421
422 if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
423 $disabled = ACTIVITYPUB_DISABLE_USER;
424 break;
425 }
426
427 if ( ACTIVITYPUB_BLOG_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
428 $disabled = true;
429 break;
430 }
431
432 $disabled = false;
433 break;
434 default:
435 $disabled = new \WP_Error(
436 'activitypub_wrong_user_type',
437 __( 'Wrong user type', 'activitypub' ),
438 array( 'status' => 400 )
439 );
440 break;
441 }
442
443 /**
444 * Allow plugins to disable user types for ActivityPub.
445 *
446 * @param boolean $disabled True if the user type is disabled, false otherwise.
447 * @param string $type The User-Type.
448 */
449 return apply_filters( 'activitypub_is_user_type_disabled', $disabled, $type );
450 }
451
452 /**
453 * Check if the blog is in single-user mode.
454 *
455 * @return boolean True if the blog is in single-user mode, false otherwise.
456 */
457 function is_single_user() {
458 if (
459 false === is_user_type_disabled( 'blog' ) &&
460 true === is_user_type_disabled( 'user' )
461 ) {
462 return true;
463 }
464
465 return false;
466 }
467
468 /**
469 * Check if a site supports the block editor.
470 *
471 * @return boolean True if the site supports the block editor, false otherwise.
472 */
473 function site_supports_blocks() {
474 /**
475 * Allow plugins to disable block editor support,
476 * thus disabling blocks registered by the ActivityPub plugin.
477 *
478 * @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
479 */
480 return apply_filters( 'activitypub_site_supports_blocks', true );
481 }
482
483 /**
484 * Check if data is valid JSON.
485 *
486 * @param string $data The data to check.
487 *
488 * @return boolean True if the data is JSON, false otherwise.
489 */
490 function is_json( $data ) {
491 return \is_array( \json_decode( $data, true ) );
492 }
493
494 /**
495 * Check whether a blog is public based on the `blog_public` option.
496 *
497 * @return bool True if public, false if not
498 */
499 function is_blog_public() {
500 /**
501 * Filter whether the blog is public.
502 *
503 * @param bool $public Whether the blog is public.
504 */
505 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
506 }
507
508 /**
509 * Extract recipient URLs from Activity object.
510 *
511 * @param array $data The Activity object as array.
512 *
513 * @return array The list of user URLs.
514 */
515 function extract_recipients_from_activity( $data ) {
516 $recipient_items = array();
517
518 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
519 if ( array_key_exists( $i, $data ) ) {
520 if ( is_array( $data[ $i ] ) ) {
521 $recipient = $data[ $i ];
522 } else {
523 $recipient = array( $data[ $i ] );
524 }
525 $recipient_items = array_merge( $recipient_items, $recipient );
526 }
527
528 if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
529 if ( is_array( $data['object'][ $i ] ) ) {
530 $recipient = $data['object'][ $i ];
531 } else {
532 $recipient = array( $data['object'][ $i ] );
533 }
534 $recipient_items = array_merge( $recipient_items, $recipient );
535 }
536 }
537
538 $recipients = array();
539
540 // Flatten array.
541 foreach ( $recipient_items as $recipient ) {
542 if ( is_array( $recipient ) ) {
543 // Check if recipient is an object.
544 if ( array_key_exists( 'id', $recipient ) ) {
545 $recipients[] = $recipient['id'];
546 }
547 } else {
548 $recipients[] = $recipient;
549 }
550 }
551
552 return array_unique( $recipients );
553 }
554
555 /**
556 * Check if passed Activity is Public.
557 *
558 * @param array $data The Activity object as array.
559 *
560 * @return boolean True if public, false if not.
561 */
562 function is_activity_public( $data ) {
563 $recipients = extract_recipients_from_activity( $data );
564
565 return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
566 }
567
568 /**
569 * Check if passed Activity is a reply.
570 *
571 * @param array $data The Activity object as array.
572 *
573 * @return boolean True if a reply, false if not.
574 */
575 function is_activity_reply( $data ) {
576 return ! empty( $data['object']['inReplyTo'] );
577 }
578
579 /**
580 * Get active users based on a given duration.
581 *
582 * @param int $duration Optional. The duration to check in month(s). Default 1.
583 *
584 * @return int The number of active users.
585 */
586 function get_active_users( $duration = 1 ) {
587
588 $duration = intval( $duration );
589 $transient_key = sprintf( 'monthly_active_users_%d', $duration );
590 $count = get_transient( $transient_key );
591
592 if ( false === $count ) {
593 global $wpdb;
594
595 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
596 $count = $wpdb->get_var(
597 $wpdb->prepare(
598 "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )",
599 $duration
600 )
601 );
602
603 set_transient( $transient_key, $count, DAY_IN_SECONDS );
604 }
605
606 // If 0 authors where active.
607 if ( 0 === $count ) {
608 return 0;
609 }
610
611 // If single user mode.
612 if ( is_single_user() ) {
613 return 1;
614 }
615
616 // If blog user is disabled.
617 if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
618 return (int) $count;
619 }
620
621 // Also count blog user.
622 return (int) $count + 1;
623 }
624
625 /**
626 * Get the total number of users.
627 *
628 * @return int The total number of users.
629 */
630 function get_total_users() {
631 // If single user mode.
632 if ( is_single_user() ) {
633 return 1;
634 }
635
636 $users = \get_users(
637 array(
638 'capability__in' => array( 'activitypub' ),
639 )
640 );
641
642 if ( is_array( $users ) ) {
643 $users = count( $users );
644 } else {
645 $users = 1;
646 }
647
648 // If blog user is disabled.
649 if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
650 return (int) $users;
651 }
652
653 return (int) $users + 1;
654 }
655
656 /**
657 * Examine a comment ID and look up an existing comment it represents.
658 *
659 * @param string $id ActivityPub object ID (usually a URL) to check.
660 *
661 * @return \WP_Comment|boolean Comment, or false on failure.
662 */
663 function object_id_to_comment( $id ) {
664 return Comment::object_id_to_comment( $id );
665 }
666
667 /**
668 * Verify that URL is a local comment or a previously received remote comment.
669 * (For threading comments locally)
670 *
671 * @param string $url The URL to check.
672 *
673 * @return string|null Comment ID or null if not found
674 */
675 function url_to_commentid( $url ) {
676 return Comment::url_to_commentid( $url );
677 }
678
679 /**
680 * Get the URI of an ActivityPub object.
681 *
682 * @param array|string $data The ActivityPub object.
683 *
684 * @return string The URI of the ActivityPub object
685 */
686 function object_to_uri( $data ) {
687 // Check whether it is already simple.
688 if ( ! $data || is_string( $data ) ) {
689 return $data;
690 }
691
692 if ( is_object( $data ) ) {
693 $data = $data->to_array();
694 }
695
696 /*
697 * Check if it is a list, then take first item.
698 * This plugin does not support collections.
699 */
700 if ( array_is_list( $data ) ) {
701 $data = $data[0];
702 }
703
704 // Check if it is simplified now.
705 if ( is_string( $data ) ) {
706 return $data;
707 }
708
709 $type = 'Object';
710 if ( isset( $data['type'] ) ) {
711 $type = $data['type'];
712 }
713
714 // Return part of Object that makes most sense.
715 switch ( $type ) {
716 case 'Image':
717 $data = $data['url'];
718 break;
719 case 'Link':
720 $data = $data['href'];
721 break;
722 default:
723 $data = $data['id'];
724 break;
725 }
726
727 return $data;
728 }
729
730 /**
731 * Check if a comment should be federated.
732 *
733 * We consider a comment should be federated if it is authored by a user that is
734 * not disabled for federation and if it is a reply directly to the post or to a
735 * federated comment.
736 *
737 * @param mixed $comment Comment object or ID.
738 *
739 * @return boolean True if the comment should be federated, false otherwise.
740 */
741 function should_comment_be_federated( $comment ) {
742 return Comment::should_be_federated( $comment );
743 }
744
745 /**
746 * Check if a comment was federated.
747 *
748 * This function checks if a comment was federated via ActivityPub.
749 *
750 * @param mixed $comment Comment object or ID.
751 *
752 * @return boolean True if the comment was federated, false otherwise.
753 */
754 function was_comment_sent( $comment ) {
755 return Comment::was_sent( $comment );
756 }
757
758 /**
759 * Check if a comment is federated.
760 *
761 * We consider a comment federated if comment was received via ActivityPub.
762 *
763 * Use this function to check if it is comment that was received via ActivityPub.
764 *
765 * @param mixed $comment Comment object or ID.
766 *
767 * @return boolean True if the comment is federated, false otherwise.
768 */
769 function was_comment_received( $comment ) {
770 return Comment::was_received( $comment );
771 }
772
773 /**
774 * Check if a comment is local only.
775 *
776 * This function checks if a comment is local only and was not sent or received via ActivityPub.
777 *
778 * @param mixed $comment Comment object or ID.
779 *
780 * @return boolean True if the comment is local only, false otherwise.
781 */
782 function is_local_comment( $comment ) {
783 return Comment::is_local( $comment );
784 }
785
786 /**
787 * Mark a WordPress object as federated.
788 *
789 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
790 * @param string $state The state of the object.
791 */
792 function set_wp_object_state( $wp_object, $state ) {
793 $meta_key = 'activitypub_status';
794
795 if ( $wp_object instanceof \WP_Post ) {
796 \update_post_meta( $wp_object->ID, $meta_key, $state );
797 } elseif ( $wp_object instanceof \WP_Comment ) {
798 \update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
799 } else {
800 /**
801 * Allow plugins to mark WordPress objects as federated.
802 *
803 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
804 */
805 \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
806 }
807 }
808
809 /**
810 * Get the federation state of a WordPress object.
811 *
812 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
813 *
814 * @return string|false The state of the object or false if not found.
815 */
816 function get_wp_object_state( $wp_object ) {
817 $meta_key = 'activitypub_status';
818
819 if ( $wp_object instanceof \WP_Post ) {
820 return \get_post_meta( $wp_object->ID, $meta_key, true );
821 } elseif ( $wp_object instanceof \WP_Comment ) {
822 return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
823 } else {
824 /**
825 * Allow plugins to get the federation state of a WordPress object.
826 *
827 * @param false $state The state of the object.
828 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
829 */
830 return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
831 }
832 }
833
834 /**
835 * Get the description of a post type.
836 *
837 * Set some default descriptions for the default post types.
838 *
839 * @param \WP_Post_Type $post_type The post type object.
840 *
841 * @return string The description of the post type.
842 */
843 function get_post_type_description( $post_type ) {
844 switch ( $post_type->name ) {
845 case 'post':
846 case 'page':
847 $description = '';
848 break;
849 case 'attachment':
850 $description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
851 break;
852 default:
853 $description = '';
854 if ( ! empty( $post_type->description ) ) {
855 $description = ' - ' . $post_type->description;
856 }
857 }
858
859 /**
860 * Allow plugins to get the description of a post type.
861 *
862 * @param string $description The description of the post type.
863 * @param string $post_type_name The post type name.
864 * @param \WP_Post_Type $post_type The post type object.
865 */
866 return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
867 }
868
869 /**
870 * Get the masked WordPress version to only show the major and minor version.
871 *
872 * @return string The masked version.
873 */
874 function get_masked_wp_version() {
875 // Only show the major and minor version.
876 $version = get_bloginfo( 'version' );
877 // Strip the RC or beta part.
878 $version = preg_replace( '/-.*$/', '', $version );
879 $version = explode( '.', $version );
880 $version = array_slice( $version, 0, 2 );
881
882 return implode( '.', $version );
883 }
884
885 /**
886 * Get the enclosures of a post.
887 *
888 * @param int $post_id The post ID.
889 *
890 * @return array The enclosures.
891 */
892 function get_enclosures( $post_id ) {
893 $enclosures = get_post_meta( $post_id, 'enclosure', false );
894
895 if ( ! $enclosures ) {
896 return array();
897 }
898
899 $enclosures = array_map(
900 function ( $enclosure ) {
901 // Check if the enclosure is a string.
902 if ( ! $enclosure || ! is_string( $enclosure ) ) {
903 return false;
904 }
905
906 $attributes = explode( "\n", $enclosure );
907
908 if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
909 return false;
910 }
911
912 return array(
913 'url' => $attributes[0],
914 'length' => $attributes[1] ?? null,
915 'mediaType' => $attributes[2] ?? 'application/octet-stream',
916 );
917 },
918 $enclosures
919 );
920
921 return array_filter( $enclosures );
922 }
923
924 /**
925 * Retrieves the IDs of the ancestors of a comment.
926 *
927 * Adaption of `get_post_ancestors` from WordPress core.
928 *
929 * @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
930 *
931 * @param int|\WP_Comment $comment Comment ID or comment object.
932 *
933 * @return int[] Array of ancestor IDs.
934 */
935 function get_comment_ancestors( $comment ) {
936 $comment = \get_comment( $comment );
937
938 if ( ! $comment || empty( $comment->comment_parent ) || (int) $comment->comment_parent === (int) $comment->comment_ID ) {
939 return array();
940 }
941
942 $ancestors = array();
943
944 $id = (int) $comment->comment_parent;
945 $ancestors[] = $id;
946
947 while ( $id > 0 ) {
948 $ancestor = \get_comment( $id );
949 $parent_id = (int) $ancestor->comment_parent;
950
951 // Loop detection: If the ancestor has been seen before, break.
952 if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
953 break;
954 }
955
956 $id = $parent_id;
957 $ancestors[] = $id;
958 }
959
960 return $ancestors;
961 }
962
963 /**
964 * Change the display of large numbers on the site.
965 *
966 * @author Jeremy Herve
967 *
968 * @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
969 *
970 * @param string $formatted Converted number in string format.
971 * @param float $number The number to convert based on locale.
972 *
973 * @return string Converted number in string format.
974 */
975 function custom_large_numbers( $formatted, $number ) {
976 global $wp_locale;
977
978 $decimals = 0;
979 $decimal_point = '.';
980 $thousands_sep = ',';
981
982 if ( isset( $wp_locale ) ) {
983 $decimals = (int) $wp_locale->number_format['decimal_point'];
984 $decimal_point = $wp_locale->number_format['decimal_point'];
985 $thousands_sep = $wp_locale->number_format['thousands_sep'];
986 }
987
988 if ( $number < 1000 ) { // Any number less than a Thousand.
989 return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
990 } elseif ( $number < 1000000 ) { // Any number less than a million.
991 return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
992 } elseif ( $number < 1000000000 ) { // Any number less than a billion.
993 return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
994 } else { // At least a billion.
995 return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
996 }
997 }
998
999 /**
1000 * Registers a ActivityPub comment type.
1001 *
1002 * @param string $comment_type Key for comment type.
1003 * @param array $args Optional. Array of arguments for registering a comment type. Default empty array.
1004 *
1005 * @return array The registered Activitypub comment type.
1006 */
1007 function register_comment_type( $comment_type, $args = array() ) {
1008 global $activitypub_comment_types;
1009
1010 if ( ! is_array( $activitypub_comment_types ) ) {
1011 $activitypub_comment_types = array();
1012 }
1013
1014 // Sanitize comment type name.
1015 $comment_type = sanitize_key( $comment_type );
1016
1017 $activitypub_comment_types[ $comment_type ] = $args;
1018
1019 /**
1020 * Fires after a ActivityPub comment type is registered.
1021 *
1022 * @param string $comment_type Comment type.
1023 * @param array $args Arguments used to register the comment type.
1024 */
1025 do_action( 'activitypub_registered_comment_type', $comment_type, $args );
1026
1027 return $args;
1028 }
1029
1030 /**
1031 * Normalize a URL.
1032 *
1033 * @param string $url The URL.
1034 *
1035 * @return string The normalized URL.
1036 */
1037 function normalize_url( $url ) {
1038 $url = \untrailingslashit( $url );
1039 $url = \str_replace( 'https://', '', $url );
1040 $url = \str_replace( 'http://', '', $url );
1041 $url = \str_replace( 'www.', '', $url );
1042
1043 return $url;
1044 }
1045
1046 /**
1047 * Normalize a host.
1048 *
1049 * @param string $host The host.
1050 *
1051 * @return string The normalized host.
1052 */
1053 function normalize_host( $host ) {
1054 return \str_replace( 'www.', '', $host );
1055 }
1056
1057 /**
1058 * Get the reply intent URI as a JavaScript URI.
1059 *
1060 * @return string The reply intent URI.
1061 */
1062 function get_reply_intent_js() {
1063 return sprintf(
1064 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
1065 get_reply_intent_url()
1066 );
1067 }
1068
1069 /**
1070 * Get the reply intent URI.
1071 *
1072 * @return string The reply intent URI.
1073 */
1074 function get_reply_intent_url() {
1075 /**
1076 * Filters the reply intent parameters.
1077 *
1078 * @param array $params The reply intent parameters.
1079 */
1080 $params = \apply_filters( 'activitypub_reply_intent_params', array() );
1081
1082 $params += array( 'in_reply_to' => '' );
1083 $query = \http_build_query( $params );
1084 $path = 'post-new.php?' . $query;
1085 $url = \admin_url( $path );
1086
1087 /**
1088 * Filters the reply intent URL.
1089 *
1090 * @param string $url The reply intent URL.
1091 */
1092 $url = \apply_filters( 'activitypub_reply_intent_url', $url );
1093
1094 return esc_url_raw( $url );
1095 }
1096
1097 /**
1098 * Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
1099 *
1100 * @param string $content The content that should be changed.
1101 * @param string $regex The regex to use.
1102 * @param callable $regex_callback Callback for replacement logic.
1103 *
1104 * @return string The content with links, mentions, hashtags, etc.
1105 */
1106 function enrich_content_data( $content, $regex, $regex_callback ) {
1107 // Small protection against execution timeouts: limit to 1 MB.
1108 if ( mb_strlen( $content ) > MB_IN_BYTES ) {
1109 return $content;
1110 }
1111 $tag_stack = array();
1112 $protected_tags = array(
1113 'pre',
1114 'code',
1115 'textarea',
1116 'style',
1117 'a',
1118 );
1119 $content_with_links = '';
1120 $in_protected_tag = false;
1121 foreach ( wp_html_split( $content ) as $chunk ) {
1122 if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
1123 $content_with_links .= $chunk;
1124 continue;
1125 }
1126
1127 if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
1128 $tag = strtolower( $m[2] );
1129 if ( '/' === $m[1] ) {
1130 // Closing tag.
1131 $i = array_search( $tag, $tag_stack, true );
1132 // We can only remove the tag from the stack if it is in the stack.
1133 if ( false !== $i ) {
1134 $tag_stack = array_slice( $tag_stack, 0, $i );
1135 }
1136 } else {
1137 // Opening tag, add it to the stack.
1138 $tag_stack[] = $tag;
1139 }
1140
1141 // If we're in a protected tag, the tag_stack contains at least one protected tag string.
1142 // The protected tag state can only change when we encounter a start or end tag.
1143 $in_protected_tag = array_intersect( $tag_stack, $protected_tags );
1144
1145 // Never inspect tags.
1146 $content_with_links .= $chunk;
1147 continue;
1148 }
1149
1150 if ( $in_protected_tag ) {
1151 // Don't inspect a chunk inside an inspected tag.
1152 $content_with_links .= $chunk;
1153 continue;
1154 }
1155
1156 // Only reachable when there is no protected tag in the stack.
1157 $content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk );
1158 }
1159
1160 return $content_with_links;
1161 }
1162
1163 /**
1164 * Generate a summary of a post.
1165 *
1166 * This function generates a summary of a post by extracting:
1167 *
1168 * 1. The post excerpt if it exists.
1169 * 2. The first part of the post content if it contains the <!--more--> tag.
1170 * 3. An excerpt of the post content if it is longer than the specified length.
1171 *
1172 * @param int|\WP_Post $post The post ID or post object.
1173 * @param integer $length The maximum length of the summary.
1174 * Default is 500. It will be ignored if the post excerpt
1175 * and the content above the <!--more--> tag.
1176 *
1177 * @return string The generated post summary.
1178 */
1179 function generate_post_summary( $post, $length = 500 ) {
1180 $post = get_post( $post );
1181
1182 if ( ! $post ) {
1183 return '';
1184 }
1185
1186 /**
1187 * Filters the excerpt more value.
1188 *
1189 * @param string $excerpt_more The excerpt more.
1190 */
1191 $excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
1192 $length = $length - strlen( $excerpt_more );
1193
1194 $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
1195
1196 if ( $content ) {
1197 // Ignore length if excerpt is set.
1198 $length = null;
1199 } else {
1200 $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
1201 $content_parts = \get_extended( $content );
1202
1203 // Check for the <!--more--> tag.
1204 if (
1205 ! empty( $content_parts['extended'] ) &&
1206 ! empty( $content_parts['main'] )
1207 ) {
1208 $content = \trim( $content_parts['main'] ) . ' ' . $excerpt_more;
1209 $length = null;
1210 }
1211 }
1212
1213 $content = \strip_shortcodes( $content );
1214 $content = \html_entity_decode( $content );
1215 $content = \wp_strip_all_tags( $content );
1216 $content = \trim( $content );
1217 $content = \preg_replace( '/\R+/m', "\n\n", $content );
1218 $content = \preg_replace( '/[\r\t]/', '', $content );
1219
1220 if ( $length && \strlen( $content ) > $length ) {
1221 $content = \wordwrap( $content, $length, '</activitypub-summary>' );
1222 $content = \explode( '</activitypub-summary>', $content, 2 );
1223 $content = $content[0] . ' ' . $excerpt_more;
1224 }
1225
1226 // This filter is documented in wp-includes/post-template.php.
1227 return \apply_filters( 'the_excerpt', $content );
1228 }
1229
1230 /**
1231 * Get the content warning of a post.
1232 *
1233 * @param int|\WP_Post $post_id The post ID or post object.
1234 *
1235 * @return string|false The content warning or false if not found.
1236 */
1237 function get_content_warning( $post_id ) {
1238 $post = get_post( $post_id );
1239 if ( ! $post ) {
1240 return false;
1241 }
1242
1243 $warning = get_post_meta( $post->ID, 'activitypub_content_warning', true );
1244 if ( empty( $warning ) ) {
1245 return false;
1246 }
1247
1248 return $warning;
1249 }
1250
1251 /**
1252 * Get the ActivityPub ID of a User by the WordPress User ID.
1253 *
1254 * @param int $id The WordPress User ID.
1255 *
1256 * @return string The ActivityPub ID (a URL) of the User.
1257 */
1258 function get_user_id( $id ) {
1259 $user = Actors::get_by_id( $id );
1260
1261 if ( \is_wp_error( $user ) ) {
1262 return false;
1263 }
1264
1265 return $user->get_id();
1266 }
1267
1268 /**
1269 * Get the ActivityPub ID of a Post by the WordPress Post ID.
1270 *
1271 * @param int $id The WordPress Post ID.
1272 *
1273 * @return string The ActivityPub ID (a URL) of the Post.
1274 */
1275 function get_post_id( $id ) {
1276 $post = get_post( $id );
1277
1278 if ( ! $post ) {
1279 return false;
1280 }
1281
1282 $transformer = new Post( $post );
1283 return $transformer->get_id();
1284 }
1285
1286 /**
1287 * Check if a URL is from the same domain as the site.
1288 *
1289 * @param string $url The URL to check.
1290 *
1291 * @return boolean True if the URL is from the same domain, false otherwise.
1292 */
1293 function is_same_domain( $url ) {
1294 $remote = \wp_parse_url( $url, PHP_URL_HOST );
1295
1296 if ( ! $remote ) {
1297 return false;
1298 }
1299
1300 $remote = normalize_host( $remote );
1301 $self = normalize_host( home_host() );
1302
1303 return $remote === $self;
1304 }
1305
1306 /**
1307 * Get the visibility of a post.
1308 *
1309 * @param int $post_id The post ID.
1310 *
1311 * @return string|false The visibility of the post or false if not found.
1312 */
1313 function get_content_visibility( $post_id ) {
1314 $post = get_post( $post_id );
1315 if ( ! $post ) {
1316 return false;
1317 }
1318
1319 $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
1320 $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
1321 $options = array(
1322 ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC,
1323 ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE,
1324 ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
1325 );
1326
1327 if ( in_array( $visibility, $options, true ) ) {
1328 $_visibility = $visibility;
1329 }
1330
1331 /**
1332 * Filters the visibility of a post.
1333 *
1334 * @param string $_visibility The visibility of the post. Possible values are:
1335 * - 'public': Post is public and federated.
1336 * - 'quiet_public': Post is public but not federated.
1337 * - 'local': Post is only visible locally.
1338 * @param \WP_Post $post The post object.
1339 */
1340 return \apply_filters( 'activitypub_content_visibility', $_visibility, $post );
1341 }
1342
1343 /**
1344 * Retrieves the Host for the current site where the front end is accessible.
1345 *
1346 * @return string The host for the current site.
1347 */
1348 function home_host() {
1349 return \wp_parse_url( \home_url(), PHP_URL_HOST );
1350 }
1351
1352 /**
1353 * Returns the website hosts allowed to credit this blog.
1354 *
1355 * @return array|null The attribution domains or null if not found.
1356 */
1357 function get_attribution_domains() {
1358 if ( '1' !== \get_option( 'activitypub_use_opengraph', '1' ) ) {
1359 return null;
1360 }
1361
1362 $domains = \get_option( 'activitypub_attribution_domains', home_host() );
1363 $domains = explode( PHP_EOL, $domains );
1364
1365 if ( ! $domains ) {
1366 $domains = null;
1367 }
1368
1369 return $domains;
1370 }
1371
1372 /**
1373 * Get the base URL for uploads.
1374 *
1375 * @return string The upload base URL.
1376 */
1377 function get_upload_baseurl() {
1378 /**
1379 * Early filter to allow plugins to set the upload base URL.
1380 *
1381 * @param string|false $maybe_upload_dir The upload base URL or false if not set.
1382 */
1383 $maybe_upload_dir = apply_filters( 'pre_activitypub_get_upload_baseurl', false );
1384 if ( false !== $maybe_upload_dir ) {
1385 return $maybe_upload_dir;
1386 }
1387
1388 $upload_dir = \wp_get_upload_dir();
1389
1390 /**
1391 * Filters the upload base URL.
1392 *
1393 * @param string $upload_dir The upload base URL. Default \wp_get_upload_dir()['baseurl']
1394 */
1395 return apply_filters( 'activitypub_get_upload_baseurl', $upload_dir['baseurl'] );
1396 }
1397
1398 /**
1399 * Check if Authorized-Fetch is enabled.
1400 *
1401 * @see https://docs.joinmastodon.org/admin/config/#authorized_fetch
1402 *
1403 * @return boolean True if Authorized-Fetch is enabled, false otherwise.
1404 */
1405 function use_authorized_fetch() {
1406 $use = (bool) \get_option( 'activitypub_authorized_fetch' );
1407
1408 /**
1409 * Filters whether to use Authorized-Fetch.
1410 *
1411 * @param boolean $use_authorized_fetch True if Authorized-Fetch is enabled, false otherwise.
1412 */
1413 return apply_filters( 'activitypub_use_authorized_fetch', $use );
1414 }
1415
1416 /**
1417 * Check if an ID is from the same domain as the site.
1418 *
1419 * @param string $id The ID URI to check.
1420 *
1421 * @return boolean True if the ID is a self-pint, false otherwise.
1422 */
1423 function is_self_ping( $id ) {
1424 $query_string = \wp_parse_url( $id, PHP_URL_QUERY );
1425
1426 if ( ! $query_string ) {
1427 return false;
1428 }
1429
1430 $query = array();
1431 \parse_str( $query_string, $query );
1432
1433 if (
1434 is_same_domain( $id ) &&
1435 in_array( 'c', array_keys( $query ), true )
1436 ) {
1437 return true;
1438 }
1439
1440 return false;
1441 }
1442
1443 /**
1444 * Add an object to the outbox.
1445 *
1446 * @param mixed $data The object to add to the outbox.
1447 * @param string|null $activity_type Optional. The type of the Activity or null if `$data` is an Activity. Default null.
1448 * @param integer $user_id Optional. The User-ID. Default 0.
1449 * @param string $content_visibility Optional. The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. Default null.
1450 *
1451 * @return boolean|int The ID of the outbox item or false on failure.
1452 */
1453 function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_visibility = null ) {
1454 // If the user is disabled, fall back to the blog user when available.
1455 if ( ! user_can_activitypub( $user_id ) ) {
1456 if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
1457 $user_id = Actors::BLOG_USER_ID;
1458 } else {
1459 return false;
1460 }
1461 }
1462
1463 $transformer = Transformer_Factory::get_transformer( $data );
1464
1465 if ( ! $transformer || is_wp_error( $transformer ) ) {
1466 return false;
1467 }
1468
1469 if ( $content_visibility ) {
1470 $transformer->set_content_visibility( $content_visibility );
1471 } else {
1472 $content_visibility = $transformer->get_content_visibility();
1473 }
1474
1475 if ( $activity_type ) {
1476 $activity = $transformer->to_activity( $activity_type );
1477 $activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
1478 } else {
1479 $activity = $transformer->to_object();
1480 }
1481
1482 if ( ! $activity || \is_wp_error( $activity ) ) {
1483 /**
1484 * Action triggered when adding an object to the outbox fails.
1485 *
1486 * @param \WP_Error $activity The error object or false.
1487 * @param mixed $data The object that failed to be added to the outbox.
1488 * @param string|null $activity_type The type of the Activity or null if `$data` is an Activity.
1489 * @param int $user_id The User ID.
1490 * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
1491 */
1492 \do_action( 'activitypub_add_to_outbox_failed', $activity, $data, $activity_type, $user_id, $content_visibility );
1493
1494 return false;
1495 }
1496
1497 $outbox_activity_id = Outbox::add( $activity, $user_id, $content_visibility );
1498
1499 if ( ! $outbox_activity_id || \is_wp_error( $outbox_activity_id ) ) {
1500 /**
1501 * Action triggered when adding an object to the outbox fails.
1502 *
1503 * @param false|\WP_Error $outbox_activity_id The error object or false.
1504 * @param mixed $data The object that failed to be added to the outbox.
1505 * @param string|null $activity_type The type of the Activity or null if `$data` is an Activity.
1506 * @param int $user_id The User ID.
1507 * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
1508 */
1509 \do_action( 'activitypub_add_to_outbox_failed', $outbox_activity_id, $data, $activity_type, $user_id, $content_visibility );
1510
1511 return false;
1512 }
1513
1514 /**
1515 * Action triggered after an object has been added to the outbox.
1516 *
1517 * @param int $outbox_activity_id The ID of the outbox item.
1518 * @param Activity $activity The activity object.
1519 * @param int $user_id The User-ID.
1520 * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
1521 */
1522 \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity, $user_id, $content_visibility );
1523
1524 set_wp_object_state( $data, 'federated' );
1525
1526 return $outbox_activity_id;
1527 }
1528
1529 /**
1530 * Follow a user.
1531 *
1532 * @param string $remote_actor The Actor URL.
1533 * @param int $user_id The ID of the WordPress User.
1534 *
1535 * @return int|\WP_Error The ID of the Outbox item or a WP_Error.
1536 */
1537 function follow( $remote_actor, $user_id ) {
1538 $remote_actor_post = Actors::fetch_remote_by_uri( $remote_actor );
1539
1540 if ( \is_wp_error( $remote_actor_post ) ) {
1541 return $remote_actor_post;
1542 }
1543
1544 $actor = Actors::get_by_id( $user_id );
1545
1546 if ( \is_wp_error( $actor ) ) {
1547 return $actor;
1548 }
1549
1550 $result = Following::follow( $remote_actor_post, $user_id );
1551 if ( \is_wp_error( $result ) ) {
1552 return $result;
1553 }
1554
1555 $follow = new Activity();
1556 $follow->set_type( 'Follow' );
1557 $follow->set_actor( $actor->get_id() );
1558 $follow->set_object( $remote_actor );
1559 $follow->set_to( array( $remote_actor ) );
1560
1561 return add_to_outbox( $follow, null, $user_id );
1562 }
1563
1564 /**
1565 * Check if an `$data` is an Activity.
1566 *
1567 * @see https://www.w3.org/ns/activitystreams#activities
1568 *
1569 * @param array|object|string $data The data to check.
1570 *
1571 * @return boolean True if the `$data` is an Activity, false otherwise.
1572 */
1573 function is_activity( $data ) {
1574 /**
1575 * Filters the activity types.
1576 *
1577 * @param array $types The activity types.
1578 */
1579 $types = apply_filters( 'activitypub_activity_types', Activity::TYPES );
1580
1581 return _is_type_of( $data, $types );
1582 }
1583
1584 /**
1585 * Check if an `$data` is an Activity Object.
1586 *
1587 * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
1588 *
1589 * @param array|object|string $data The data to check.
1590 *
1591 * @return boolean True if the `$data` is an Activity Object, false otherwise.
1592 */
1593 function is_activity_object( $data ) {
1594 /**
1595 * Filters the activity object types.
1596 *
1597 * @param array $types The activity object types.
1598 */
1599 $types = \apply_filters( 'activitypub_activity_object_types', Base_Object::TYPES );
1600
1601 return _is_type_of( $data, $types );
1602 }
1603
1604 /**
1605 * Check if an `$data` is an Actor.
1606 *
1607 * @see https://www.w3.org/ns/activitystreams#actor
1608 *
1609 * @param array|object|string $data The data to check.
1610 *
1611 * @return boolean True if the `$data` is an Actor, false otherwise.
1612 */
1613 function is_actor( $data ) {
1614 /**
1615 * Filters the actor types.
1616 *
1617 * @param array $types The actor types.
1618 */
1619 $types = apply_filters( 'activitypub_actor_types', Actor::TYPES );
1620
1621 return _is_type_of( $data, $types );
1622 }
1623
1624 /**
1625 * Private helper to check if $data is of a given type set.
1626 *
1627 * @param array|object|string $data The data to check.
1628 * @param array $types The types to check against.
1629 *
1630 * @return boolean True if $data is of one of the types, false otherwise.
1631 */
1632 function _is_type_of( $data, $types ) {
1633 if ( is_string( $data ) ) {
1634 return in_array( $data, $types, true );
1635 }
1636
1637 if ( is_array( $data ) && isset( $data['type'] ) ) {
1638 return in_array( $data['type'], $types, true );
1639 }
1640
1641 if ( $data instanceof Base_Object ) {
1642 return in_array( $data->get_type(), $types, true );
1643 }
1644
1645 return false;
1646 }
1647
1648 /**
1649 * Get an ActivityPub embed HTML for a URL.
1650 *
1651 * @param string $url The URL to get the embed for.
1652 * @param boolean $inline_css Whether to inline CSS. Default true.
1653 *
1654 * @return string|false The embed HTML or false if not found.
1655 */
1656 function get_embed_html( $url, $inline_css = true ) {
1657 return Embed::get_html( $url, $inline_css );
1658 }
1659
1660 /**
1661 * Infer a shortname from the Actor ID or URL. Used only for fallbacks,
1662 * we will try to use what's supplied.
1663 *
1664 * @param string $uri The URI.
1665 *
1666 * @return string Hopefully the name of the Follower.
1667 */
1668 function extract_name_from_uri( $uri ) {
1669 $name = $uri;
1670
1671 if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) {
1672 $name = \rtrim( $name, '/' );
1673 $path = \wp_parse_url( $name, PHP_URL_PATH );
1674 if ( $path ) {
1675 if ( \strpos( $name, '@' ) !== false ) {
1676 // Expected: https://example.com/@user (default URL pattern).
1677 $name = \preg_replace( '|^/@?|', '', $path );
1678 } else {
1679 // Expected: https://example.com/users/user (default ID pattern).
1680 $parts = \explode( '/', $path );
1681 $name = \array_pop( $parts );
1682 }
1683 }
1684 } elseif (
1685 \is_email( $name ) ||
1686 \strpos( $name, 'acct' ) === 0 ||
1687 \strpos( $name, '@' ) === 0
1688 ) {
1689 // Expected: user@example.com or acct:user@example (WebFinger).
1690 $name = \ltrim( $name, '@' );
1691 if ( str_starts_with( $name, 'acct:' ) ) {
1692 $name = \substr( $name, 5 );
1693 }
1694 $parts = \explode( '@', $name );
1695 $name = $parts[0];
1696 }
1697
1698 return $name;
1699 }
1700