PluginProbe
ActivityPub / 2.5.0
ActivityPub v2.5.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.5.0, at includes/functions.php

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