PluginProbe
ActivityPub / 5.3.1
ActivityPub v5.3.1
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / functions.php

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

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