PluginProbe
ActivityPub / 5.9.0
ActivityPub v5.9.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 5.9.0, at includes/functions.php

1,630 lines 41.6 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 WP_Error;
11 use Activitypub\Activity\Activity;
12 use Activitypub\Activity\Actor;
13 use Activitypub\Activity\Base_Object;
14 use Activitypub\Collection\Actors;
15 use Activitypub\Collection\Outbox;
16 use Activitypub\Collection\Followers;
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 disable users for ActivityPub.
375 *
376 * @deprecated 5.7.0 Use the `activitypub_user_can_activitypub` filter instead.
377 *
378 * @param boolean $disabled True if the user is disabled, false otherwise.
379 * @param int $user_id The user ID.
380 */
381 $enabled = ! \apply_filters_deprecated( 'activitypub_is_user_disabled', array( ! $enabled, $user_id ), '5.7.0', 'activitypub_user_can_activitypub' );
382
383 /**
384 * Allow plugins to enable/disable users for ActivityPub.
385 *
386 * @param boolean $enabled True if the user is enabled, false otherwise.
387 * @param int $user_id The user ID.
388 */
389 return apply_filters( 'activitypub_user_can_activitypub', $enabled, $user_id );
390 }
391
392 /**
393 * This function checks if a user is disabled for ActivityPub.
394 *
395 * @deprecated 5.7.0 Use the `user_can_activitypub` function instead.
396 *
397 * @param int $user_id The user ID.
398 *
399 * @return boolean True if the user is disabled, false otherwise.
400 */
401 function is_user_disabled( $user_id ) {
402 _deprecated_function( __FUNCTION__, 'unreleased', 'user_can_activitypub' );
403
404 return ! user_can_activitypub( $user_id );
405 }
406
407 /**
408 * Checks if a User-Type is disabled for ActivityPub.
409 *
410 * This function is used to check if the 'blog' or 'user'
411 * type is disabled for ActivityPub.
412 *
413 * @param string $type User type. 'blog' or 'user'.
414 *
415 * @return boolean True if the user type is disabled, false otherwise.
416 */
417 function is_user_type_disabled( $type ) {
418 switch ( $type ) {
419 case 'blog':
420 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
421 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
422 $disabled = false;
423 break;
424 }
425 }
426
427 if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
428 $disabled = ACTIVITYPUB_DISABLE_BLOG_USER;
429 break;
430 }
431
432 if ( ACTIVITYPUB_ACTOR_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
433 $disabled = true;
434 break;
435 }
436
437 $disabled = false;
438 break;
439 case 'user':
440 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
441 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
442 $disabled = true;
443 break;
444 }
445 }
446
447 if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
448 $disabled = ACTIVITYPUB_DISABLE_USER;
449 break;
450 }
451
452 if ( ACTIVITYPUB_BLOG_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
453 $disabled = true;
454 break;
455 }
456
457 $disabled = false;
458 break;
459 default:
460 $disabled = new WP_Error(
461 'activitypub_wrong_user_type',
462 __( 'Wrong user type', 'activitypub' ),
463 array( 'status' => 400 )
464 );
465 break;
466 }
467
468 /**
469 * Allow plugins to disable user types for ActivityPub.
470 *
471 * @param boolean $disabled True if the user type is disabled, false otherwise.
472 * @param string $type The User-Type.
473 */
474 return apply_filters( 'activitypub_is_user_type_disabled', $disabled, $type );
475 }
476
477 /**
478 * Check if the blog is in single-user mode.
479 *
480 * @return boolean True if the blog is in single-user mode, false otherwise.
481 */
482 function is_single_user() {
483 if (
484 false === is_user_type_disabled( 'blog' ) &&
485 true === is_user_type_disabled( 'user' )
486 ) {
487 return true;
488 }
489
490 return false;
491 }
492
493 /**
494 * Check if a site supports the block editor.
495 *
496 * @return boolean True if the site supports the block editor, false otherwise.
497 */
498 function site_supports_blocks() {
499 /**
500 * Allow plugins to disable block editor support,
501 * thus disabling blocks registered by the ActivityPub plugin.
502 *
503 * @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
504 */
505 return apply_filters( 'activitypub_site_supports_blocks', true );
506 }
507
508 /**
509 * Check if data is valid JSON.
510 *
511 * @param string $data The data to check.
512 *
513 * @return boolean True if the data is JSON, false otherwise.
514 */
515 function is_json( $data ) {
516 return \is_array( \json_decode( $data, true ) );
517 }
518
519 /**
520 * Check whether a blog is public based on the `blog_public` option.
521 *
522 * @return bool True if public, false if not
523 */
524 function is_blog_public() {
525 /**
526 * Filter whether the blog is public.
527 *
528 * @param bool $public Whether the blog is public.
529 */
530 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
531 }
532
533 /**
534 * Extract recipient URLs from Activity object.
535 *
536 * @param array $data The Activity object as array.
537 *
538 * @return array The list of user URLs.
539 */
540 function extract_recipients_from_activity( $data ) {
541 $recipient_items = array();
542
543 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
544 if ( array_key_exists( $i, $data ) ) {
545 if ( is_array( $data[ $i ] ) ) {
546 $recipient = $data[ $i ];
547 } else {
548 $recipient = array( $data[ $i ] );
549 }
550 $recipient_items = array_merge( $recipient_items, $recipient );
551 }
552
553 if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
554 if ( is_array( $data['object'][ $i ] ) ) {
555 $recipient = $data['object'][ $i ];
556 } else {
557 $recipient = array( $data['object'][ $i ] );
558 }
559 $recipient_items = array_merge( $recipient_items, $recipient );
560 }
561 }
562
563 $recipients = array();
564
565 // Flatten array.
566 foreach ( $recipient_items as $recipient ) {
567 if ( is_array( $recipient ) ) {
568 // Check if recipient is an object.
569 if ( array_key_exists( 'id', $recipient ) ) {
570 $recipients[] = $recipient['id'];
571 }
572 } else {
573 $recipients[] = $recipient;
574 }
575 }
576
577 return array_unique( $recipients );
578 }
579
580 /**
581 * Check if passed Activity is Public.
582 *
583 * @param array $data The Activity object as array.
584 *
585 * @return boolean True if public, false if not.
586 */
587 function is_activity_public( $data ) {
588 $recipients = extract_recipients_from_activity( $data );
589
590 return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
591 }
592
593 /**
594 * Check if passed Activity is a reply.
595 *
596 * @param array $data The Activity object as array.
597 *
598 * @return boolean True if a reply, false if not.
599 */
600 function is_activity_reply( $data ) {
601 return ! empty( $data['object']['inReplyTo'] );
602 }
603
604 /**
605 * Get active users based on a given duration.
606 *
607 * @param int $duration Optional. The duration to check in month(s). Default 1.
608 *
609 * @return int The number of active users.
610 */
611 function get_active_users( $duration = 1 ) {
612
613 $duration = intval( $duration );
614 $transient_key = sprintf( 'monthly_active_users_%d', $duration );
615 $count = get_transient( $transient_key );
616
617 if ( false === $count ) {
618 global $wpdb;
619
620 // phpcs:ignore WordPress.DB.DirectDatabaseQuery
621 $count = $wpdb->get_var(
622 $wpdb->prepare(
623 "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 )",
624 $duration
625 )
626 );
627
628 set_transient( $transient_key, $count, DAY_IN_SECONDS );
629 }
630
631 // If 0 authors where active.
632 if ( 0 === $count ) {
633 return 0;
634 }
635
636 // If single user mode.
637 if ( is_single_user() ) {
638 return 1;
639 }
640
641 // If blog user is disabled.
642 if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
643 return (int) $count;
644 }
645
646 // Also count blog user.
647 return (int) $count + 1;
648 }
649
650 /**
651 * Get the total number of users.
652 *
653 * @return int The total number of users.
654 */
655 function get_total_users() {
656 // If single user mode.
657 if ( is_single_user() ) {
658 return 1;
659 }
660
661 $users = \get_users(
662 array(
663 'capability__in' => array( 'activitypub' ),
664 )
665 );
666
667 if ( is_array( $users ) ) {
668 $users = count( $users );
669 } else {
670 $users = 1;
671 }
672
673 // If blog user is disabled.
674 if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
675 return (int) $users;
676 }
677
678 return (int) $users + 1;
679 }
680
681 /**
682 * Examine a comment ID and look up an existing comment it represents.
683 *
684 * @param string $id ActivityPub object ID (usually a URL) to check.
685 *
686 * @return \WP_Comment|boolean Comment, or false on failure.
687 */
688 function object_id_to_comment( $id ) {
689 return Comment::object_id_to_comment( $id );
690 }
691
692 /**
693 * Verify that URL is a local comment or a previously received remote comment.
694 * (For threading comments locally)
695 *
696 * @param string $url The URL to check.
697 *
698 * @return string|null Comment ID or null if not found
699 */
700 function url_to_commentid( $url ) {
701 return Comment::url_to_commentid( $url );
702 }
703
704 /**
705 * Get the URI of an ActivityPub object.
706 *
707 * @param array|string $data The ActivityPub object.
708 *
709 * @return string The URI of the ActivityPub object
710 */
711 function object_to_uri( $data ) {
712 // Check whether it is already simple.
713 if ( ! $data || is_string( $data ) ) {
714 return $data;
715 }
716
717 if ( is_object( $data ) ) {
718 $data = $data->to_array();
719 }
720
721 /*
722 * Check if it is a list, then take first item.
723 * This plugin does not support collections.
724 */
725 if ( array_is_list( $data ) ) {
726 $data = $data[0];
727 }
728
729 // Check if it is simplified now.
730 if ( is_string( $data ) ) {
731 return $data;
732 }
733
734 $type = 'Object';
735 if ( isset( $data['type'] ) ) {
736 $type = $data['type'];
737 }
738
739 // Return part of Object that makes most sense.
740 switch ( $type ) {
741 case 'Image':
742 $data = $data['url'];
743 break;
744 case 'Link':
745 $data = $data['href'];
746 break;
747 default:
748 $data = $data['id'];
749 break;
750 }
751
752 return $data;
753 }
754
755 /**
756 * Check if a comment should be federated.
757 *
758 * We consider a comment should be federated if it is authored by a user that is
759 * not disabled for federation and if it is a reply directly to the post or to a
760 * federated comment.
761 *
762 * @param mixed $comment Comment object or ID.
763 *
764 * @return boolean True if the comment should be federated, false otherwise.
765 */
766 function should_comment_be_federated( $comment ) {
767 return Comment::should_be_federated( $comment );
768 }
769
770 /**
771 * Check if a comment was federated.
772 *
773 * This function checks if a comment was federated via ActivityPub.
774 *
775 * @param mixed $comment Comment object or ID.
776 *
777 * @return boolean True if the comment was federated, false otherwise.
778 */
779 function was_comment_sent( $comment ) {
780 return Comment::was_sent( $comment );
781 }
782
783 /**
784 * Check if a comment is federated.
785 *
786 * We consider a comment federated if comment was received via ActivityPub.
787 *
788 * Use this function to check if it is comment that was received via ActivityPub.
789 *
790 * @param mixed $comment Comment object or ID.
791 *
792 * @return boolean True if the comment is federated, false otherwise.
793 */
794 function was_comment_received( $comment ) {
795 return Comment::was_received( $comment );
796 }
797
798 /**
799 * Check if a comment is local only.
800 *
801 * This function checks if a comment is local only and was not sent or received via ActivityPub.
802 *
803 * @param mixed $comment Comment object or ID.
804 *
805 * @return boolean True if the comment is local only, false otherwise.
806 */
807 function is_local_comment( $comment ) {
808 return Comment::is_local( $comment );
809 }
810
811 /**
812 * Mark a WordPress object as federated.
813 *
814 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
815 * @param string $state The state of the object.
816 */
817 function set_wp_object_state( $wp_object, $state ) {
818 $meta_key = 'activitypub_status';
819
820 if ( $wp_object instanceof \WP_Post ) {
821 \update_post_meta( $wp_object->ID, $meta_key, $state );
822 } elseif ( $wp_object instanceof \WP_Comment ) {
823 \update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
824 } else {
825 /**
826 * Allow plugins to mark WordPress objects as federated.
827 *
828 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
829 */
830 \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
831 }
832 }
833
834 /**
835 * Get the federation state of a WordPress object.
836 *
837 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
838 *
839 * @return string|false The state of the object or false if not found.
840 */
841 function get_wp_object_state( $wp_object ) {
842 $meta_key = 'activitypub_status';
843
844 if ( $wp_object instanceof \WP_Post ) {
845 return \get_post_meta( $wp_object->ID, $meta_key, true );
846 } elseif ( $wp_object instanceof \WP_Comment ) {
847 return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
848 } else {
849 /**
850 * Allow plugins to get the federation state of a WordPress object.
851 *
852 * @param false $state The state of the object.
853 * @param \WP_Comment|\WP_Post $wp_object The WordPress object.
854 */
855 return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
856 }
857 }
858
859 /**
860 * Get the description of a post type.
861 *
862 * Set some default descriptions for the default post types.
863 *
864 * @param \WP_Post_Type $post_type The post type object.
865 *
866 * @return string The description of the post type.
867 */
868 function get_post_type_description( $post_type ) {
869 switch ( $post_type->name ) {
870 case 'post':
871 case 'page':
872 $description = '';
873 break;
874 case 'attachment':
875 $description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
876 break;
877 default:
878 $description = '';
879 if ( ! empty( $post_type->description ) ) {
880 $description = ' - ' . $post_type->description;
881 }
882 }
883
884 /**
885 * Allow plugins to get the description of a post type.
886 *
887 * @param string $description The description of the post type.
888 * @param string $post_type_name The post type name.
889 * @param \WP_Post_Type $post_type The post type object.
890 */
891 return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
892 }
893
894 /**
895 * Get the masked WordPress version to only show the major and minor version.
896 *
897 * @return string The masked version.
898 */
899 function get_masked_wp_version() {
900 // Only show the major and minor version.
901 $version = get_bloginfo( 'version' );
902 // Strip the RC or beta part.
903 $version = preg_replace( '/-.*$/', '', $version );
904 $version = explode( '.', $version );
905 $version = array_slice( $version, 0, 2 );
906
907 return implode( '.', $version );
908 }
909
910 /**
911 * Get the enclosures of a post.
912 *
913 * @param int $post_id The post ID.
914 *
915 * @return array The enclosures.
916 */
917 function get_enclosures( $post_id ) {
918 $enclosures = get_post_meta( $post_id, 'enclosure', false );
919
920 if ( ! $enclosures ) {
921 return array();
922 }
923
924 $enclosures = array_map(
925 function ( $enclosure ) {
926 // Check if the enclosure is a string.
927 if ( ! $enclosure || ! is_string( $enclosure ) ) {
928 return false;
929 }
930
931 $attributes = explode( "\n", $enclosure );
932
933 if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
934 return false;
935 }
936
937 return array(
938 'url' => $attributes[0],
939 'length' => $attributes[1] ?? null,
940 'mediaType' => $attributes[2] ?? 'application/octet-stream',
941 );
942 },
943 $enclosures
944 );
945
946 return array_filter( $enclosures );
947 }
948
949 /**
950 * Retrieves the IDs of the ancestors of a comment.
951 *
952 * Adaption of `get_post_ancestors` from WordPress core.
953 *
954 * @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
955 *
956 * @param int|\WP_Comment $comment Comment ID or comment object.
957 *
958 * @return int[] Array of ancestor IDs.
959 */
960 function get_comment_ancestors( $comment ) {
961 $comment = \get_comment( $comment );
962
963 if ( ! $comment || empty( $comment->comment_parent ) || (int) $comment->comment_parent === (int) $comment->comment_ID ) {
964 return array();
965 }
966
967 $ancestors = array();
968
969 $id = (int) $comment->comment_parent;
970 $ancestors[] = $id;
971
972 while ( $id > 0 ) {
973 $ancestor = \get_comment( $id );
974 $parent_id = (int) $ancestor->comment_parent;
975
976 // Loop detection: If the ancestor has been seen before, break.
977 if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
978 break;
979 }
980
981 $id = $parent_id;
982 $ancestors[] = $id;
983 }
984
985 return $ancestors;
986 }
987
988 /**
989 * Change the display of large numbers on the site.
990 *
991 * @author Jeremy Herve
992 *
993 * @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
994 *
995 * @param string $formatted Converted number in string format.
996 * @param float $number The number to convert based on locale.
997 *
998 * @return string Converted number in string format.
999 */
1000 function custom_large_numbers( $formatted, $number ) {
1001 global $wp_locale;
1002
1003 $decimals = 0;
1004 $decimal_point = '.';
1005 $thousands_sep = ',';
1006
1007 if ( isset( $wp_locale ) ) {
1008 $decimals = (int) $wp_locale->number_format['decimal_point'];
1009 $decimal_point = $wp_locale->number_format['decimal_point'];
1010 $thousands_sep = $wp_locale->number_format['thousands_sep'];
1011 }
1012
1013 if ( $number < 1000 ) { // Any number less than a Thousand.
1014 return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
1015 } elseif ( $number < 1000000 ) { // Any number less than a million.
1016 return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
1017 } elseif ( $number < 1000000000 ) { // Any number less than a billion.
1018 return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
1019 } else { // At least a billion.
1020 return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
1021 }
1022 }
1023
1024 /**
1025 * Registers a ActivityPub comment type.
1026 *
1027 * @param string $comment_type Key for comment type.
1028 * @param array $args Optional. Array of arguments for registering a comment type. Default empty array.
1029 *
1030 * @return array The registered Activitypub comment type.
1031 */
1032 function register_comment_type( $comment_type, $args = array() ) {
1033 global $activitypub_comment_types;
1034
1035 if ( ! is_array( $activitypub_comment_types ) ) {
1036 $activitypub_comment_types = array();
1037 }
1038
1039 // Sanitize comment type name.
1040 $comment_type = sanitize_key( $comment_type );
1041
1042 $activitypub_comment_types[ $comment_type ] = $args;
1043
1044 /**
1045 * Fires after a ActivityPub comment type is registered.
1046 *
1047 * @param string $comment_type Comment type.
1048 * @param array $args Arguments used to register the comment type.
1049 */
1050 do_action( 'activitypub_registered_comment_type', $comment_type, $args );
1051
1052 return $args;
1053 }
1054
1055 /**
1056 * Normalize a URL.
1057 *
1058 * @param string $url The URL.
1059 *
1060 * @return string The normalized URL.
1061 */
1062 function normalize_url( $url ) {
1063 $url = \untrailingslashit( $url );
1064 $url = \str_replace( 'https://', '', $url );
1065 $url = \str_replace( 'http://', '', $url );
1066 $url = \str_replace( 'www.', '', $url );
1067
1068 return $url;
1069 }
1070
1071 /**
1072 * Normalize a host.
1073 *
1074 * @param string $host The host.
1075 *
1076 * @return string The normalized host.
1077 */
1078 function normalize_host( $host ) {
1079 return \str_replace( 'www.', '', $host );
1080 }
1081
1082 /**
1083 * Get the reply intent URI as a JavaScript URI.
1084 *
1085 * @return string The reply intent URI.
1086 */
1087 function get_reply_intent_js() {
1088 return sprintf(
1089 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
1090 get_reply_intent_url()
1091 );
1092 }
1093
1094 /**
1095 * Get the reply intent URI.
1096 *
1097 * @return string The reply intent URI.
1098 */
1099 function get_reply_intent_url() {
1100 /**
1101 * Filters the reply intent parameters.
1102 *
1103 * @param array $params The reply intent parameters.
1104 */
1105 $params = \apply_filters( 'activitypub_reply_intent_params', array() );
1106
1107 $params += array( 'in_reply_to' => '' );
1108 $query = \http_build_query( $params );
1109 $path = 'post-new.php?' . $query;
1110 $url = \admin_url( $path );
1111
1112 /**
1113 * Filters the reply intent URL.
1114 *
1115 * @param string $url The reply intent URL.
1116 */
1117 $url = \apply_filters( 'activitypub_reply_intent_url', $url );
1118
1119 return esc_url_raw( $url );
1120 }
1121
1122 /**
1123 * Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
1124 *
1125 * @param string $content The content that should be changed.
1126 * @param string $regex The regex to use.
1127 * @param callable $regex_callback Callback for replacement logic.
1128 *
1129 * @return string The content with links, mentions, hashtags, etc.
1130 */
1131 function enrich_content_data( $content, $regex, $regex_callback ) {
1132 // Small protection against execution timeouts: limit to 1 MB.
1133 if ( mb_strlen( $content ) > MB_IN_BYTES ) {
1134 return $content;
1135 }
1136 $tag_stack = array();
1137 $protected_tags = array(
1138 'pre',
1139 'code',
1140 'textarea',
1141 'style',
1142 'a',
1143 );
1144 $content_with_links = '';
1145 $in_protected_tag = false;
1146 foreach ( wp_html_split( $content ) as $chunk ) {
1147 if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
1148 $content_with_links .= $chunk;
1149 continue;
1150 }
1151
1152 if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
1153 $tag = strtolower( $m[2] );
1154 if ( '/' === $m[1] ) {
1155 // Closing tag.
1156 $i = array_search( $tag, $tag_stack, true );
1157 // We can only remove the tag from the stack if it is in the stack.
1158 if ( false !== $i ) {
1159 $tag_stack = array_slice( $tag_stack, 0, $i );
1160 }
1161 } else {
1162 // Opening tag, add it to the stack.
1163 $tag_stack[] = $tag;
1164 }
1165
1166 // If we're in a protected tag, the tag_stack contains at least one protected tag string.
1167 // The protected tag state can only change when we encounter a start or end tag.
1168 $in_protected_tag = array_intersect( $tag_stack, $protected_tags );
1169
1170 // Never inspect tags.
1171 $content_with_links .= $chunk;
1172 continue;
1173 }
1174
1175 if ( $in_protected_tag ) {
1176 // Don't inspect a chunk inside an inspected tag.
1177 $content_with_links .= $chunk;
1178 continue;
1179 }
1180
1181 // Only reachable when there is no protected tag in the stack.
1182 $content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk );
1183 }
1184
1185 return $content_with_links;
1186 }
1187
1188 /**
1189 * Generate a summary of a post.
1190 *
1191 * This function generates a summary of a post by extracting:
1192 *
1193 * 1. The post excerpt if it exists.
1194 * 2. The first part of the post content if it contains the <!--more--> tag.
1195 * 3. An excerpt of the post content if it is longer than the specified length.
1196 *
1197 * @param int|\WP_Post $post The post ID or post object.
1198 * @param integer $length The maximum length of the summary.
1199 * Default is 500. It will be ignored if the post excerpt
1200 * and the content above the <!--more--> tag.
1201 *
1202 * @return string The generated post summary.
1203 */
1204 function generate_post_summary( $post, $length = 500 ) {
1205 $post = get_post( $post );
1206
1207 if ( ! $post ) {
1208 return '';
1209 }
1210
1211 $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
1212
1213 if ( $content ) {
1214 /** This filter is documented in wp-includes/post-template.php */
1215 return \apply_filters( 'the_excerpt', $content );
1216 }
1217
1218 $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
1219 $content_parts = \get_extended( $content );
1220
1221 /**
1222 * Filters the excerpt more value.
1223 *
1224 * @param string $excerpt_more The excerpt more.
1225 */
1226 $excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
1227 $length = $length - strlen( $excerpt_more );
1228
1229 // Check for the <!--more--> tag.
1230 if (
1231 ! empty( $content_parts['extended'] ) &&
1232 ! empty( $content_parts['main'] )
1233 ) {
1234 $content = $content_parts['main'] . ' ' . $excerpt_more;
1235 $length = null;
1236 }
1237
1238 $content = \html_entity_decode( $content );
1239 $content = \wp_strip_all_tags( $content );
1240 $content = \trim( $content );
1241 $content = \preg_replace( '/\R+/m', "\n\n", $content );
1242 $content = \preg_replace( '/[\r\t]/', '', $content );
1243
1244 if ( $length && \strlen( $content ) > $length ) {
1245 $content = \wordwrap( $content, $length, '</activitypub-summary>' );
1246 $content = \explode( '</activitypub-summary>', $content, 2 );
1247 $content = $content[0] . ' ' . $excerpt_more;
1248 }
1249
1250 /*
1251 Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
1252 /** This filter is documented in wp-includes/post-template.php
1253 return \apply_filters( 'the_excerpt', $content );
1254 */
1255 return $content;
1256 }
1257
1258 /**
1259 * Get the content warning of a post.
1260 *
1261 * @param int|\WP_Post $post_id The post ID or post object.
1262 *
1263 * @return string|false The content warning or false if not found.
1264 */
1265 function get_content_warning( $post_id ) {
1266 $post = get_post( $post_id );
1267 if ( ! $post ) {
1268 return false;
1269 }
1270
1271 $warning = get_post_meta( $post->ID, 'activitypub_content_warning', true );
1272 if ( empty( $warning ) ) {
1273 return false;
1274 }
1275
1276 return $warning;
1277 }
1278
1279 /**
1280 * Get the ActivityPub ID of a User by the WordPress User ID.
1281 *
1282 * @param int $id The WordPress User ID.
1283 *
1284 * @return string The ActivityPub ID (a URL) of the User.
1285 */
1286 function get_user_id( $id ) {
1287 $user = Actors::get_by_id( $id );
1288
1289 if ( ! $user ) {
1290 return false;
1291 }
1292
1293 return $user->get_id();
1294 }
1295
1296 /**
1297 * Get the ActivityPub ID of a Post by the WordPress Post ID.
1298 *
1299 * @param int $id The WordPress Post ID.
1300 *
1301 * @return string The ActivityPub ID (a URL) of the Post.
1302 */
1303 function get_post_id( $id ) {
1304 $post = get_post( $id );
1305
1306 if ( ! $post ) {
1307 return false;
1308 }
1309
1310 $transformer = new Post( $post );
1311 return $transformer->get_id();
1312 }
1313
1314 /**
1315 * Check if a URL is from the same domain as the site.
1316 *
1317 * @param string $url The URL to check.
1318 *
1319 * @return boolean True if the URL is from the same domain, false otherwise.
1320 */
1321 function is_same_domain( $url ) {
1322 $remote = \wp_parse_url( $url, PHP_URL_HOST );
1323
1324 if ( ! $remote ) {
1325 return false;
1326 }
1327
1328 $remote = normalize_host( $remote );
1329 $self = normalize_host( home_host() );
1330
1331 return $remote === $self;
1332 }
1333
1334 /**
1335 * Get the visibility of a post.
1336 *
1337 * @param int $post_id The post ID.
1338 *
1339 * @return string|false The visibility of the post or false if not found.
1340 */
1341 function get_content_visibility( $post_id ) {
1342 $post = get_post( $post_id );
1343 if ( ! $post ) {
1344 return false;
1345 }
1346
1347 $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
1348 $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
1349 $options = array(
1350 ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC,
1351 ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE,
1352 ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
1353 );
1354
1355 if ( in_array( $visibility, $options, true ) ) {
1356 $_visibility = $visibility;
1357 }
1358
1359 /**
1360 * Filters the visibility of a post.
1361 *
1362 * @param string $_visibility The visibility of the post. Possible values are:
1363 * - 'public': Post is public and federated.
1364 * - 'quiet_public': Post is public but not federated.
1365 * - 'local': Post is only visible locally.
1366 * @param \WP_Post $post The post object.
1367 */
1368 return \apply_filters( 'activitypub_content_visibility', $_visibility, $post );
1369 }
1370
1371 /**
1372 * Retrieves the Host for the current site where the front end is accessible.
1373 *
1374 * @return string The host for the current site.
1375 */
1376 function home_host() {
1377 return \wp_parse_url( \home_url(), PHP_URL_HOST );
1378 }
1379
1380 /**
1381 * Returns the website hosts allowed to credit this blog.
1382 *
1383 * @return array|null The attribution domains or null if not found.
1384 */
1385 function get_attribution_domains() {
1386 if ( '1' !== \get_option( 'activitypub_use_opengraph', '1' ) ) {
1387 return null;
1388 }
1389
1390 $domains = \get_option( 'activitypub_attribution_domains', home_host() );
1391 $domains = explode( PHP_EOL, $domains );
1392
1393 if ( ! $domains ) {
1394 $domains = null;
1395 }
1396
1397 return $domains;
1398 }
1399
1400 /**
1401 * Get the base URL for uploads.
1402 *
1403 * @return string The upload base URL.
1404 */
1405 function get_upload_baseurl() {
1406 /**
1407 * Early filter to allow plugins to set the upload base URL.
1408 *
1409 * @param string|false $maybe_upload_dir The upload base URL or false if not set.
1410 */
1411 $maybe_upload_dir = apply_filters( 'pre_activitypub_get_upload_baseurl', false );
1412 if ( false !== $maybe_upload_dir ) {
1413 return $maybe_upload_dir;
1414 }
1415
1416 $upload_dir = \wp_get_upload_dir();
1417
1418 /**
1419 * Filters the upload base URL.
1420 *
1421 * @param string $upload_dir The upload base URL. Default \wp_get_upload_dir()['baseurl']
1422 */
1423 return apply_filters( 'activitypub_get_upload_baseurl', $upload_dir['baseurl'] );
1424 }
1425
1426 /**
1427 * Check if Authorized-Fetch is enabled.
1428 *
1429 * @see https://docs.joinmastodon.org/admin/config/#authorized_fetch
1430 *
1431 * @return boolean True if Authorized-Fetch is enabled, false otherwise.
1432 */
1433 function use_authorized_fetch() {
1434 $use = (bool) \get_option( 'activitypub_authorized_fetch' );
1435
1436 /**
1437 * Filters whether to use Authorized-Fetch.
1438 *
1439 * @param boolean $use_authorized_fetch True if Authorized-Fetch is enabled, false otherwise.
1440 */
1441 return apply_filters( 'activitypub_use_authorized_fetch', $use );
1442 }
1443
1444 /**
1445 * Check if an ID is from the same domain as the site.
1446 *
1447 * @param string $id The ID URI to check.
1448 *
1449 * @return boolean True if the ID is a self-pint, false otherwise.
1450 */
1451 function is_self_ping( $id ) {
1452 $query_string = \wp_parse_url( $id, PHP_URL_QUERY );
1453
1454 if ( ! $query_string ) {
1455 return false;
1456 }
1457
1458 $query = array();
1459 \parse_str( $query_string, $query );
1460
1461 if (
1462 is_same_domain( $id ) &&
1463 in_array( 'c', array_keys( $query ), true )
1464 ) {
1465 return true;
1466 }
1467
1468 return false;
1469 }
1470
1471 /**
1472 * Add an object to the outbox.
1473 *
1474 * @param mixed $data The object to add to the outbox.
1475 * @param string|null $activity_type Optional. The type of the Activity or null if `$data` is an Activity. Default null.
1476 * @param integer $user_id Optional. The User-ID. Default 0.
1477 * @param string $content_visibility Optional. The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. Default null.
1478 *
1479 * @return boolean|int The ID of the outbox item or false on failure.
1480 */
1481 function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_visibility = null ) {
1482 // If the user is disabled, fall back to the blog user when available.
1483 if ( ! user_can_activitypub( $user_id ) ) {
1484 if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
1485 $user_id = Actors::BLOG_USER_ID;
1486 } else {
1487 return false;
1488 }
1489 }
1490
1491 $transformer = Transformer_Factory::get_transformer( $data );
1492
1493 if ( ! $transformer || is_wp_error( $transformer ) ) {
1494 return false;
1495 }
1496
1497 if ( $content_visibility ) {
1498 $transformer->set_content_visibility( $content_visibility );
1499 } else {
1500 $content_visibility = $transformer->get_content_visibility();
1501 }
1502
1503 if ( $activity_type ) {
1504 $activity = $transformer->to_activity( $activity_type );
1505 $activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
1506 } else {
1507 $activity = $transformer->to_object();
1508 }
1509
1510 if ( ! $activity || \is_wp_error( $activity ) ) {
1511 return false;
1512 }
1513
1514 $outbox_activity_id = Outbox::add( $activity, $user_id, $content_visibility );
1515
1516 if ( ! $outbox_activity_id ) {
1517 return false;
1518 }
1519
1520 /**
1521 * Action triggered after an object has been added to the outbox.
1522 *
1523 * @param int $outbox_activity_id The ID of the outbox item.
1524 * @param Activity $activity The activity object.
1525 * @param int $user_id The User-ID.
1526 * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
1527 */
1528 \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity, $user_id, $content_visibility );
1529
1530 set_wp_object_state( $data, 'federated' );
1531
1532 return $outbox_activity_id;
1533 }
1534
1535 /**
1536 * Check if an `$data` is an Activity.
1537 *
1538 * @see https://www.w3.org/ns/activitystreams#activities
1539 *
1540 * @param array|object|string $data The data to check.
1541 *
1542 * @return boolean True if the `$data` is an Activity, false otherwise.
1543 */
1544 function is_activity( $data ) {
1545 /**
1546 * Filters the activity types.
1547 *
1548 * @param array $types The activity types.
1549 */
1550 $types = apply_filters( 'activitypub_activity_types', Activity::TYPES );
1551
1552 return _is_type_of( $data, $types );
1553 }
1554
1555 /**
1556 * Check if an `$data` is an Activity Object.
1557 *
1558 * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
1559 *
1560 * @param array|object|string $data The data to check.
1561 *
1562 * @return boolean True if the `$data` is an Activity Object, false otherwise.
1563 */
1564 function is_activity_object( $data ) {
1565 /**
1566 * Filters the activity object types.
1567 *
1568 * @param array $types The activity object types.
1569 */
1570 $types = \apply_filters( 'activitypub_activity_object_types', Base_Object::TYPES );
1571
1572 return _is_type_of( $data, $types );
1573 }
1574
1575 /**
1576 * Check if an `$data` is an Actor.
1577 *
1578 * @see https://www.w3.org/ns/activitystreams#actor
1579 *
1580 * @param array|object|string $data The data to check.
1581 *
1582 * @return boolean True if the `$data` is an Actor, false otherwise.
1583 */
1584 function is_actor( $data ) {
1585 /**
1586 * Filters the actor types.
1587 *
1588 * @param array $types The actor types.
1589 */
1590 $types = apply_filters( 'activitypub_actor_types', Actor::TYPES );
1591
1592 return _is_type_of( $data, $types );
1593 }
1594
1595 /**
1596 * Private helper to check if $data is of a given type set.
1597 *
1598 * @param array|object|string $data The data to check.
1599 * @param array $types The types to check against.
1600 *
1601 * @return boolean True if $data is of one of the types, false otherwise.
1602 */
1603 function _is_type_of( $data, $types ) {
1604 if ( is_string( $data ) ) {
1605 return in_array( $data, $types, true );
1606 }
1607
1608 if ( is_array( $data ) && isset( $data['type'] ) ) {
1609 return in_array( $data['type'], $types, true );
1610 }
1611
1612 if ( $data instanceof Base_Object ) {
1613 return in_array( $data->get_type(), $types, true );
1614 }
1615
1616 return false;
1617 }
1618
1619 /**
1620 * Get an ActivityPub embed HTML for a URL.
1621 *
1622 * @param string $url The URL to get the embed for.
1623 * @param boolean $inline_css Whether to inline CSS. Default true.
1624 *
1625 * @return string|false The embed HTML or false if not found.
1626 */
1627 function get_embed_html( $url, $inline_css = true ) {
1628 return Embed::get_html( $url, $inline_css );
1629 }
1630