PluginProbe
ActivityPub / 2.6.0
ActivityPub v2.6.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 2.6.0, at includes/functions.php

1,046 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub;
3
4 use WP_Query;
5 use WP_Error;
6 use Activitypub\Http;
7 use Activitypub\Comment;
8 use Activitypub\Webfinger;
9 use Activitypub\Activity\Activity;
10 use Activitypub\Collection\Followers;
11 use Activitypub\Collection\Users;
12
13 /**
14 * Returns the ActivityPub default JSON-context
15 *
16 * @return array the activitypub context
17 */
18 function get_context() {
19 $context = Activity::JSON_LD_CONTEXT;
20
21 return \apply_filters( 'activitypub_json_context', $context );
22 }
23
24 function safe_remote_post( $url, $body, $user_id ) {
25 return Http::post( $url, $body, $user_id );
26 }
27
28 function safe_remote_get( $url ) {
29 return Http::get( $url );
30 }
31
32 /**
33 * Returns a users WebFinger "resource"
34 *
35 * @param int $user_id The User-ID.
36 *
37 * @return string The User-Resource.
38 */
39 function get_webfinger_resource( $user_id ) {
40 return Webfinger::get_user_resource( $user_id );
41 }
42
43 /**
44 * Requests the Meta-Data from the Actors profile
45 *
46 * @param string $actor The Actor URL.
47 * @param bool $cached If the result should be cached.
48 *
49 * @return array|WP_Error The Actor profile as array or WP_Error on failure.
50 */
51 function get_remote_metadata_by_actor( $actor, $cached = true ) {
52 $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
53 if ( $pre ) {
54 return $pre;
55 }
56
57 if ( is_array( $actor ) ) {
58 if ( array_key_exists( 'id', $actor ) ) {
59 $actor = $actor['id'];
60 } elseif ( array_key_exists( 'url', $actor ) ) {
61 $actor = $actor['url'];
62 } else {
63 return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $actor ) );
64 }
65 }
66
67 if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
68 $actor = Webfinger::resolve( $actor );
69 }
70
71 if ( ! $actor ) {
72 return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $actor ) );
73 }
74
75 if ( is_wp_error( $actor ) ) {
76 return $actor;
77 }
78
79 $transient_key = 'activitypub_' . $actor;
80
81 // only check the cache if needed.
82 if ( $cached ) {
83 $metadata = \get_transient( $transient_key );
84
85 if ( $metadata ) {
86 return $metadata;
87 }
88 }
89
90 if ( ! \wp_http_validate_url( $actor ) ) {
91 $metadata = new WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
92 return $metadata;
93 }
94
95 $response = Http::get( $actor );
96
97 if ( \is_wp_error( $response ) ) {
98 return $response;
99 }
100
101 $metadata = \wp_remote_retrieve_body( $response );
102 $metadata = \json_decode( $metadata, true );
103
104 if ( ! $metadata ) {
105 $metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
106 return $metadata;
107 }
108
109 \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
110
111 return $metadata;
112 }
113
114 /**
115 * Returns the followers of a given user.
116 *
117 * @param int $user_id The User-ID.
118 *
119 * @return array The followers.
120 */
121 function get_followers( $user_id ) {
122 return Followers::get_followers( $user_id );
123 }
124
125 /**
126 * Count the number of followers for a given user.
127 *
128 * @param int $user_id The User-ID.
129 *
130 * @return int The number of followers.
131 */
132 function count_followers( $user_id ) {
133 return Followers::count_followers( $user_id );
134 }
135
136 /**
137 * Examine a url and try to determine the author ID it represents.
138 *
139 * Checks are supposedly from the hosted site blog.
140 *
141 * @param string $url Permalink to check.
142 *
143 * @return int User ID, or 0 on failure.
144 */
145 function url_to_authorid( $url ) {
146 global $wp_rewrite;
147
148 // check if url hase the same host
149 if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
150 return 0;
151 }
152
153 // first, check to see if there is a 'author=N' to match against
154 if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
155 $id = \absint( $values[1] );
156 if ( $id ) {
157 return $id;
158 }
159 }
160
161 // check to see if we are using rewrite rules
162 $rewrite = $wp_rewrite->wp_rewrite_rules();
163
164 // not using rewrite rules, and 'author=N' method failed, so we're out of options
165 if ( empty( $rewrite ) ) {
166 return 0;
167 }
168
169 // generate rewrite rule for the author url
170 $author_rewrite = $wp_rewrite->get_author_permastruct();
171 $author_regexp = \str_replace( '%author%', '', $author_rewrite );
172
173 // match the rewrite rule with the passed url
174 if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
175 $user = \get_user_by( 'slug', $match[2] );
176 if ( $user ) {
177 return $user->ID;
178 }
179 }
180
181 return 0;
182 }
183
184 /**
185 * Verify if url is a wp_ap_comment,
186 * Or if it is a previously received remote comment
187 *
188 * @return int comment_id
189 */
190 function is_comment() {
191 $comment_id = get_query_var( 'c', null );
192
193 if ( ! is_null( $comment_id ) ) {
194 $comment = \get_comment( $comment_id );
195
196 // Only return local origin comments
197 if ( $comment && $comment->user_id ) {
198 return $comment_id;
199 }
200 }
201
202 return false;
203 }
204
205 /**
206 * Check for Tombstone Objects
207 *
208 * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
209 *
210 * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request
211 *
212 * @return boolean true if HTTP-Code is 410 or 404
213 */
214 function is_tombstone( $wp_error ) {
215 if ( ! is_wp_error( $wp_error ) ) {
216 return false;
217 }
218
219 if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
220 return true;
221 }
222
223 return false;
224 }
225
226 /**
227 * Get the REST URL relative to this plugin's namespace.
228 *
229 * @param string $path Optional. REST route path. Otherwise this plugin's namespaced root.
230 *
231 * @return string REST URL relative to this plugin's namespace.
232 */
233 function get_rest_url_by_path( $path = '' ) {
234 // we'll handle the leading slash.
235 $path = ltrim( $path, '/' );
236 $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
237 return \get_rest_url( null, $namespaced_path );
238 }
239
240 /**
241 * Convert a string from camelCase to snake_case.
242 *
243 * @param string $string The string to convert.
244 *
245 * @return string The converted string.
246 */
247 // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
248 function camel_to_snake_case( $string ) {
249 return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $string ) );
250 }
251
252 /**
253 * Convert a string from snake_case to camelCase.
254 *
255 * @param string $string The string to convert.
256 *
257 * @return string The converted string.
258 */
259 // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
260 function snake_to_camel_case( $string ) {
261 return lcfirst( str_replace( '_', '', ucwords( $string, '_' ) ) );
262 }
263
264 /**
265 * Escapes a Tag, to be used as a hashtag.
266 *
267 * @param string $string The string to escape.
268 *
269 * @return string The escaped hastag.
270 */
271 function esc_hashtag( $string ) {
272
273 $hashtag = \wp_specialchars_decode( $string, ENT_QUOTES );
274 // Remove all characters that are not letters, numbers, or underscores.
275 $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
276
277 // Capitalize every letter that is preceded by an underscore.
278 $hashtag = preg_replace_callback(
279 '/_(.)/',
280 function ( $matches ) {
281 return '' . strtoupper( $matches[1] );
282 },
283 $hashtag
284 );
285
286 // Add a hashtag to the beginning of the string.
287 $hashtag = ltrim( $hashtag, '#' );
288 $hashtag = '#' . $hashtag;
289
290 /**
291 * Allow defining your own custom hashtag generation rules.
292 *
293 * @param string $hashtag The hashtag to be returned.
294 * @param string $string The original string.
295 */
296 $hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $string );
297
298 return esc_html( $hashtag );
299 }
300
301 /**
302 * Check if a request is for an ActivityPub request.
303 *
304 * @return bool False by default.
305 */
306 function is_activitypub_request() {
307 global $wp_query;
308
309 /*
310 * ActivityPub requests are currently only made for
311 * author archives, singular posts, and the homepage.
312 */
313 if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) {
314 return false;
315 }
316
317 // Check if the current post type supports ActivityPub.
318 if ( \is_singular() ) {
319 $queried_object = \get_queried_object();
320 $post_type = \get_post_type( $queried_object );
321
322 if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
323 return false;
324 }
325 }
326
327 // Check if header already sent.
328 if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) {
329 // Send Vary header for Accept header.
330 \header( 'Vary: Accept' );
331 }
332
333 // One can trigger an ActivityPub request by adding ?activitypub to the URL.
334 // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
335 global $wp_query;
336 if ( isset( $wp_query->query_vars['activitypub'] ) ) {
337 return true;
338 }
339
340 /*
341 * The other (more common) option to make an ActivityPub request
342 * is to send an Accept header.
343 */
344 if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
345 $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
346
347 /*
348 * $accept can be a single value, or a comma separated list of values.
349 * We want to support both scenarios,
350 * and return true when the header includes at least one of the following:
351 * - application/activity+json
352 * - application/ld+json
353 * - application/json
354 */
355 if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
356 return true;
357 }
358 }
359
360 return false;
361 }
362
363 /**
364 * This function checks if a user is disabled for ActivityPub.
365 *
366 * @param int $user_id The User-ID.
367 *
368 * @return boolean True if the user is disabled, false otherwise.
369 */
370 function is_user_disabled( $user_id ) {
371 $return = false;
372
373 switch ( $user_id ) {
374 // if the user is the application user, it's always enabled.
375 case \Activitypub\Collection\Users::APPLICATION_USER_ID:
376 $return = false;
377 break;
378 // if the user is the blog user, it's only enabled in single-user mode.
379 case \Activitypub\Collection\Users::BLOG_USER_ID:
380 if ( is_user_type_disabled( 'blog' ) ) {
381 $return = true;
382 break;
383 }
384
385 $return = false;
386 break;
387 // if the user is any other user, it's enabled if it can publish posts.
388 default:
389 if ( ! \get_user_by( 'id', $user_id ) ) {
390 $return = true;
391 break;
392 }
393
394 if ( is_user_type_disabled( 'user' ) ) {
395 $return = true;
396 break;
397 }
398
399 if ( ! \user_can( $user_id, 'activitypub' ) ) {
400 $return = true;
401 break;
402 }
403
404 $return = false;
405 break;
406 }
407
408 return apply_filters( 'activitypub_is_user_disabled', $return, $user_id );
409 }
410
411 /**
412 * Checks if a User-Type is disabled for ActivityPub.
413 *
414 * This function is used to check if the 'blog' or 'user'
415 * type is disabled for ActivityPub.
416 *
417 * @param enum $type Can be 'blog' or 'user'.
418 *
419 * @return boolean True if the user type is disabled, false otherwise.
420 */
421 function is_user_type_disabled( $type ) {
422 switch ( $type ) {
423 case 'blog':
424 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
425 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
426 $return = false;
427 break;
428 }
429 }
430
431 if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
432 $return = ACTIVITYPUB_DISABLE_BLOG_USER;
433 break;
434 }
435
436 if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) {
437 $return = true;
438 break;
439 }
440
441 $return = false;
442 break;
443 case 'user':
444 if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
445 if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
446 $return = true;
447 break;
448 }
449 }
450
451 if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
452 $return = ACTIVITYPUB_DISABLE_USER;
453 break;
454 }
455
456 if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) {
457 $return = true;
458 break;
459 }
460
461 $return = false;
462 break;
463 default:
464 $return = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) );
465 break;
466 }
467
468 return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
469 }
470
471 /**
472 * Check if the blog is in single-user mode.
473 *
474 * @return boolean True if the blog is in single-user mode, false otherwise.
475 */
476 function is_single_user() {
477 if (
478 false === is_user_type_disabled( 'blog' ) &&
479 true === is_user_type_disabled( 'user' )
480 ) {
481 return true;
482 }
483
484 return false;
485 }
486
487 /**
488 * Check if a site supports the block editor.
489 *
490 * @return boolean True if the site supports the block editor, false otherwise.
491 */
492 function site_supports_blocks() {
493 if ( \version_compare( \get_bloginfo( 'version' ), '5.9', '<' ) ) {
494 return false;
495 }
496
497 if ( ! \function_exists( 'register_block_type_from_metadata' ) ) {
498 return false;
499 }
500
501 /**
502 * Allow plugins to disable block editor support,
503 * thus disabling blocks registered by the ActivityPub plugin.
504 *
505 * @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
506 */
507 return apply_filters( 'activitypub_site_supports_blocks', true );
508 }
509
510 /**
511 * Check if data is valid JSON.
512 *
513 * @param string $data The data to check.
514 *
515 * @return boolean True if the data is JSON, false otherwise.
516 */
517 function is_json( $data ) {
518 return \is_array( \json_decode( $data, true ) ) ? true : false;
519 }
520
521 /**
522 * Check if a blog is public based on the `blog_public` option
523 *
524 * @return bollean True if public, false if not
525 */
526 function is_blog_public() {
527 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
528 }
529
530 /**
531 * Sanitize a URL
532 *
533 * @param string $value The URL to sanitize
534 *
535 * @return string|null The sanitized URL or null if invalid
536 */
537 function sanitize_url( $value ) {
538 if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
539 return null;
540 }
541
542 return esc_url_raw( $value );
543 }
544
545 /**
546 * Extract recipient URLs from Activity object
547 *
548 * @param array $data
549 *
550 * @return array The list of user URLs
551 */
552 function extract_recipients_from_activity( $data ) {
553 $recipient_items = array();
554
555 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
556 if ( array_key_exists( $i, $data ) ) {
557 if ( is_array( $data[ $i ] ) ) {
558 $recipient = $data[ $i ];
559 } else {
560 $recipient = array( $data[ $i ] );
561 }
562 $recipient_items = array_merge( $recipient_items, $recipient );
563 }
564
565 if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
566 if ( is_array( $data['object'][ $i ] ) ) {
567 $recipient = $data['object'][ $i ];
568 } else {
569 $recipient = array( $data['object'][ $i ] );
570 }
571 $recipient_items = array_merge( $recipient_items, $recipient );
572 }
573 }
574
575 $recipients = array();
576
577 // flatten array
578 foreach ( $recipient_items as $recipient ) {
579 if ( is_array( $recipient ) ) {
580 // check if recipient is an object
581 if ( array_key_exists( 'id', $recipient ) ) {
582 $recipients[] = $recipient['id'];
583 }
584 } else {
585 $recipients[] = $recipient;
586 }
587 }
588
589 return array_unique( $recipients );
590 }
591
592 /**
593 * Check if passed Activity is Public
594 *
595 * @param array $data The Activity object as array
596 *
597 * @return boolean True if public, false if not
598 */
599 function is_activity_public( $data ) {
600 $recipients = extract_recipients_from_activity( $data );
601
602 return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
603 }
604
605 /**
606 * Get active users based on a given duration
607 *
608 * @param int $duration The duration to check in month(s)
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 $query = "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 )";
621 $query = $wpdb->prepare( $query, $duration );
622 $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
623
624 set_transient( $transient_key, $count, DAY_IN_SECONDS );
625 }
626
627 // if 0 authors where active
628 if ( 0 === $count ) {
629 return 0;
630 }
631
632 // if single user mode
633 if ( is_single_user() ) {
634 return 1;
635 }
636
637 // if blog user is disabled
638 if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
639 return $count;
640 }
641
642 // also count blog user
643 return $count + 1;
644 }
645
646 /**
647 * Get the total number of users
648 *
649 * @return int The total number of users
650 */
651 function get_total_users() {
652 // if single user mode
653 if ( is_single_user() ) {
654 return 1;
655 }
656
657 $users = \get_users(
658 array(
659 'capability__in' => array( 'activitypub' ),
660 )
661 );
662
663 if ( is_array( $users ) ) {
664 $users = count( $users );
665 } else {
666 $users = 1;
667 }
668
669 // if blog user is disabled
670 if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
671 return $users;
672 }
673
674 return $users + 1;
675 }
676
677 /**
678 * Examine a comment ID and look up an existing comment it represents.
679 *
680 * @param string $id ActivityPub object ID (usually a URL) to check.
681 *
682 * @return int|boolean Comment ID, or false on failure.
683 */
684 function object_id_to_comment( $id ) {
685 return Comment::object_id_to_comment( $id );
686 }
687
688 /**
689 * Verify if URL is a local comment,
690 * Or if it is a previously received remote comment
691 * (For threading comments locally)
692 *
693 * @param string $url The URL to check.
694 *
695 * @return int comment_ID or null if not found
696 */
697 function url_to_commentid( $url ) {
698 return Comment::url_to_commentid( $url );
699 }
700
701 /**
702 * Get the URI of an ActivityPub object
703 *
704 * @param array $object The ActivityPub object
705 *
706 * @return string The URI of the ActivityPub object
707 */
708 function object_to_uri( $object ) {
709 // check if it is already simple
710 if ( ! $object || is_string( $object ) ) {
711 return $object;
712 }
713
714 // check if it is a list, then take first item
715 // this plugin does not support collections
716 if ( array_is_list( $object ) ) {
717 $object = $object[0];
718 }
719
720 // check if it is simplified now
721 if ( is_string( $object ) ) {
722 return $object;
723 }
724
725 // return part of Object that makes most sense
726 switch ( $object['type'] ) {
727 case 'Link':
728 $object = $object['href'];
729 break;
730 default:
731 $object = $object['id'];
732 break;
733 }
734
735 return $object;
736 }
737
738 /**
739 * Check if a comment should be federated.
740 *
741 * We consider a comment should be federated if it is authored by a user that is
742 * not disabled for federation and if it is a reply directly to the post or to a
743 * federated comment.
744 *
745 * @param mixed $comment Comment object or ID.
746 *
747 * @return boolean True if the comment should be federated, false otherwise.
748 */
749 function should_comment_be_federated( $comment ) {
750 return Comment::should_be_federated( $comment );
751 }
752
753 /**
754 * Check if a comment was federated.
755 *
756 * This function checks if a comment was federated via ActivityPub.
757 *
758 * @param mixed $comment Comment object or ID.
759 *
760 * @return boolean True if the comment was federated, false otherwise.
761 */
762 function was_comment_sent( $comment ) {
763 return Comment::was_sent( $comment );
764 }
765
766 /**
767 * Check if a comment is federated.
768 *
769 * We consider a comment federated if comment was received via ActivityPub.
770 *
771 * Use this function to check if it is comment that was received via ActivityPub.
772 *
773 * @param mixed $comment Comment object or ID.
774 *
775 * @return boolean True if the comment is federated, false otherwise.
776 */
777 function was_comment_received( $comment ) {
778 return Comment::was_received( $comment );
779 }
780
781 /**
782 * Check if a comment is local only.
783 *
784 * This function checks if a comment is local only and was not sent or received via ActivityPub.
785 *
786 * @param mixed $comment Comment object or ID.
787 *
788 * @return boolean True if the comment is local only, false otherwise.
789 */
790 function is_local_comment( $comment ) {
791 return Comment::is_local( $comment );
792 }
793
794 /**
795 * Mark a WordPress object as federated.
796 *
797 * @param WP_Comment|WP_Post|mixed $wp_object
798 *
799 * @return void
800 */
801 function set_wp_object_state( $wp_object, $state ) {
802 $meta_key = 'activitypub_status';
803
804 if ( $wp_object instanceof \WP_Post ) {
805 \update_post_meta( $wp_object->ID, $meta_key, $state );
806 } elseif ( $wp_object instanceof \WP_Comment ) {
807 \update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
808 } else {
809 \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
810 }
811 }
812
813 /**
814 * Get the federation state of a WordPress object.
815 *
816 * @param WP_Comment|WP_Post|mixed $wp_object
817 *
818 * @return string|false The state of the object or false if not found.
819 */
820 function get_wp_object_state( $wp_object ) {
821 $meta_key = 'activitypub_status';
822
823 if ( $wp_object instanceof \WP_Post ) {
824 return \get_post_meta( $wp_object->ID, $meta_key, true );
825 } elseif ( $wp_object instanceof \WP_Comment ) {
826 return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
827 } else {
828 return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
829 }
830 }
831
832 /**
833 * Get the description of a post type.
834 *
835 * Set some default descriptions for the default post types.
836 *
837 * @param WP_Post_Type $post_type The post type object.
838 *
839 * @return string The description of the post type.
840 */
841 function get_post_type_description( $post_type ) {
842 $description = '';
843
844 switch ( $post_type->name ) {
845 case 'post':
846 $description = '';
847 break;
848 case 'page':
849 $description = '';
850 break;
851 case 'attachment':
852 $description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
853 break;
854 default:
855 if ( ! empty( $post_type->description ) ) {
856 $description = ' - ' . $post_type->description;
857 }
858 }
859
860 return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
861 }
862
863 /**
864 * Get the masked WordPress version to only show the major and minor version.
865 *
866 * @return string The masked version.
867 */
868 function get_masked_wp_version() {
869 // only show the major and minor version
870 $version = get_bloginfo( 'version' );
871 // strip the RC or beta part
872 $version = preg_replace( '/-.*$/', '', $version );
873 $version = explode( '.', $version );
874 $version = array_slice( $version, 0, 2 );
875
876 return implode( '.', $version );
877 }
878
879 /**
880 * Get the enclosures of a post.
881 *
882 * @param int $post_id The post ID.
883 *
884 * @return array The enclosures.
885 */
886 function get_enclosures( $post_id ) {
887 $enclosures = get_post_meta( $post_id, 'enclosure' );
888
889 if ( ! $enclosures ) {
890 return array();
891 }
892
893 $enclosures = array_map(
894 function ( $enclosure ) {
895 $attributes = explode( "\n", $enclosure );
896
897 if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
898 return false;
899 }
900
901 return array(
902 'url' => $attributes[0],
903 'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null,
904 'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null,
905 );
906 },
907 $enclosures
908 );
909
910 return array_filter( $enclosures );
911 }
912
913 /**
914 * Retrieves the IDs of the ancestors of a comment.
915 *
916 * Adaption of `get_post_ancestors` from WordPress core.
917 *
918 * @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
919 *
920 * @param int|WP_Comment $comment Comment ID or comment object.
921 *
922 * @return WP_Comment[] Array of ancestor comments or empty array if there are none.
923 */
924 function get_comment_ancestors( $comment ) {
925 $comment = \get_comment( $comment );
926
927 // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
928 if ( ! $comment || empty( $comment->comment_parent ) || $comment->comment_parent == $comment->comment_ID ) {
929 return array();
930 }
931
932 $ancestors = array();
933
934 $id = (int) $comment->comment_parent;
935 $ancestors[] = $id;
936
937 // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
938 while ( $id > 0 ) {
939 $ancestor = \get_comment( $id );
940 $parent_id = (int) $ancestor->comment_parent;
941
942 // Loop detection: If the ancestor has been seen before, break.
943 if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
944 break;
945 }
946
947 $id = $parent_id;
948 $ancestors[] = $id;
949 }
950
951 return $ancestors;
952 }
953
954 /**
955 * Change the display of large numbers on the site.
956 *
957 * @author Jeremy Herve
958 *
959 * @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
960 *
961 * @param string $formatted Converted number in string format.
962 * @param float $number The number to convert based on locale.
963 * @param int $decimals Precision of the number of decimal places.
964 *
965 * @return string Converted number in string format.
966 */
967 function custom_large_numbers( $formatted, $number, $decimals ) {
968 global $wp_locale;
969
970 $decimals = 0;
971 $decimal_point = '.';
972 $thousands_sep = ',';
973
974 if ( isset( $wp_locale ) ) {
975 $decimals = (int) $wp_locale->number_format['decimal_point'];
976 $decimal_point = $wp_locale->number_format['decimal_point'];
977 $thousands_sep = $wp_locale->number_format['thousands_sep'];
978 }
979
980 if ( $number < 1000 ) { // any number less than a Thousand.
981 return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
982 } elseif ( $number < 1000000 ) { // any number less than a million
983 return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
984 } elseif ( $number < 1000000000 ) { // any number less than a billion
985 return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
986 } else { // at least a billion
987 return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
988 }
989
990 // Default fallback. We should not get here.
991 return $formatted;
992 }
993
994 /**
995 * Normalize a URL.
996 *
997 * @param string $url The URL.
998 *
999 * @return string The normalized URL.
1000 */
1001 function normalize_url( $url ) {
1002 $url = \untrailingslashit( $url );
1003 $url = \str_replace( 'https://', '', $url );
1004 $url = \str_replace( 'http://', '', $url );
1005 $url = \str_replace( 'www.', '', $url );
1006
1007 return $url;
1008 }
1009
1010 /**
1011 * Normalize a host.
1012 *
1013 * @param string $host The host.
1014 *
1015 * @return string The normalized host.
1016 */
1017 function normalize_host( $host ) {
1018 return \str_replace( 'www.', '', $host );
1019 }
1020
1021 /**
1022 * Get the Extra Fields of an Actor
1023 *
1024 * @param int $user_id The User-ID.
1025 *
1026 * @return array The extra fields.
1027 */
1028 function get_actor_extra_fields( $user_id ) {
1029 $extra_fields = new WP_Query(
1030 array(
1031 'post_type' => 'ap_extrafield',
1032 'nopaging' => true,
1033 'status' => 'publish',
1034 'author' => $user_id,
1035 )
1036 );
1037
1038 if ( $extra_fields->have_posts() ) {
1039 $extra_fields = $extra_fields->posts;
1040 } else {
1041 $extra_fields = array();
1042 }
1043
1044 return apply_filters( 'activitypub_get_actor_extra_fields', $extra_fields, $user_id );
1045 }
1046