PluginProbe
ActivityPub / 3.2.4
ActivityPub v3.2.4
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
← All changes | includes/functions.php +682 -13 1.2.03.2.4 View file →
@@ -1,12 +1,16 @@
1 1 <?php
2 2 namespace Activitypub;
3 3
4 +use WP_Query;
4 5 use WP_Error;
5 6 use Activitypub\Http;
7 +use Activitypub\Comment;
8 +use Activitypub\Webfinger;
6 9 use Activitypub\Activity\Activity;
7 10 use Activitypub\Collection\Followers;
8 11 use Activitypub\Collection\Users;
12 +use Activitypub\Collection\Extra_Fields;
9 13
10 14 /**
11 15 * Returns the ActivityPub default JSON-context
12 16 *
@@ -12,9 +16,9 @@
12 16 *
13 17 * @return array the activitypub context
14 18 */
15 19 function get_context() {
16 - $context = Activity::CONTEXT;
20 + $context = Activity::JSON_LD_CONTEXT;
17 21
18 22 return \apply_filters( 'activitypub_json_context', $context );
19 23 }
20 24
@@ -49,14 +53,33 @@
49 53 $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
50 54 if ( $pre ) {
51 55 return $pre;
52 56 }
57 +
58 + if ( is_array( $actor ) ) {
59 + if ( array_key_exists( 'id', $actor ) ) {
60 + $actor = $actor['id'];
61 + } elseif ( array_key_exists( 'url', $actor ) ) {
62 + $actor = $actor['url'];
63 + } else {
64 + return new WP_Error(
65 + 'activitypub_no_valid_actor_identifier',
66 + \__( 'The "actor" identifier is not valid', 'activitypub' ),
67 + array( 'status' => 404, 'actor' => $actor )
68 + );
69 + }
70 + }
71 +
53 72 if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
54 73 $actor = Webfinger::resolve( $actor );
55 74 }
56 75
57 76 if ( ! $actor ) {
58 - return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $actor ) );
77 + return new WP_Error(
78 + 'activitypub_no_valid_actor_identifier',
79 + \__( 'The "actor" identifier is not valid', 'activitypub' ),
80 + array( 'status' => 404, 'actor' => $actor )
81 + );
59 82 }
60 83
61 84 if ( is_wp_error( $actor ) ) {
62 85 return $actor;
@@ -73,9 +96,13 @@
73 96 }
74 97 }
75 98
76 99 if ( ! \wp_http_validate_url( $actor ) ) {
77 - $metadata = new WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
100 + $metadata = new WP_Error(
101 + 'activitypub_no_valid_actor_url',
102 + \__( 'The "actor" is no valid URL', 'activitypub' ),
103 + array( 'status' => 400, 'actor' => $actor )
104 + );
78 105 return $metadata;
79 106 }
80 107
81 108 $response = Http::get( $actor );
@@ -87,9 +114,13 @@
87 114 $metadata = \wp_remote_retrieve_body( $response );
88 115 $metadata = \json_decode( $metadata, true );
89 116
90 117 if ( ! $metadata ) {
91 - $metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
118 + $metadata = new WP_Error(
119 + 'activitypub_invalid_json',
120 + \__( 'No valid JSON data', 'activitypub' ),
121 + array( 'status' => 400, 'actor' => $actor )
122 + );
92 123 return $metadata;
93 124 }
94 125
95 126 \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
@@ -131,9 +162,9 @@
131 162 function url_to_authorid( $url ) {
132 163 global $wp_rewrite;
133 164
134 165 // check if url hase the same host
135 - if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
166 + if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
136 167 return 0;
137 168 }
138 169
139 170 // first, check to see if there is a 'author=N' to match against
@@ -167,8 +198,29 @@
167 198 return 0;
168 199 }
169 200
170 201 /**
202 + * Verify if url is a wp_ap_comment,
203 + * Or if it is a previously received remote comment
204 + *
205 + * @return int comment_id
206 + */
207 +function is_comment() {
208 + $comment_id = get_query_var( 'c', null );
209 +
210 + if ( ! is_null( $comment_id ) ) {
211 + $comment = \get_comment( $comment_id );
212 +
213 + // Only return local origin comments
214 + if ( $comment && $comment->user_id ) {
215 + return $comment_id;
216 + }
217 + }
218 +
219 + return false;
220 +}
221 +
222 +/**
171 223 * Check for Tombstone Objects
172 224 *
173 225 * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
174 226 *
@@ -278,8 +330,24 @@
278 330 if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) {
279 331 return false;
280 332 }
281 333
334 + // Check if the current post type supports ActivityPub.
335 + if ( \is_singular() ) {
336 + $queried_object = \get_queried_object();
337 + $post_type = \get_post_type( $queried_object );
338 +
339 + if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
340 + return false;
341 + }
342 + }
343 +
344 + // Check if header already sent.
345 + if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) {
346 + // Send Vary header for Accept header.
347 + \header( 'Vary: Accept' );
348 + }
349 +
282 350 // One can trigger an ActivityPub request by adding ?activitypub to the URL.
283 351 // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
284 352 global $wp_query;
285 353 if ( isset( $wp_query->query_vars['activitypub'] ) ) {
@@ -344,9 +412,9 @@
344 412 $return = true;
345 413 break;
346 414 }
347 415
348 - if ( ! \user_can( $user_id, 'publish_posts' ) ) {
416 + if ( ! \user_can( $user_id, 'activitypub' ) ) {
349 417 $return = true;
350 418 break;
351 419 }
352 420
@@ -409,9 +477,13 @@
409 477
410 478 $return = false;
411 479 break;
412 480 default:
413 - $return = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) );
481 + $return = new WP_Error(
482 + 'activitypub_wrong_user_type',
483 + __( 'Wrong user type', 'activitypub' ),
484 + array( 'status' => 400 )
485 + );
414 486 break;
415 487 }
416 488
417 489 return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
@@ -476,8 +548,83 @@
476 548 return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
477 549 }
478 550
479 551 /**
552 + * Sanitize a URL
553 + *
554 + * @param string $value The URL to sanitize
555 + *
556 + * @return string|null The sanitized URL or null if invalid
557 + */
558 +function sanitize_url( $value ) {
559 + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
560 + return null;
561 + }
562 +
563 + return esc_url_raw( $value );
564 +}
565 +
566 +/**
567 + * Extract recipient URLs from Activity object
568 + *
569 + * @param array $data
570 + *
571 + * @return array The list of user URLs
572 + */
573 +function extract_recipients_from_activity( $data ) {
574 + $recipient_items = array();
575 +
576 + foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
577 + if ( array_key_exists( $i, $data ) ) {
578 + if ( is_array( $data[ $i ] ) ) {
579 + $recipient = $data[ $i ];
580 + } else {
581 + $recipient = array( $data[ $i ] );
582 + }
583 + $recipient_items = array_merge( $recipient_items, $recipient );
584 + }
585 +
586 + if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
587 + if ( is_array( $data['object'][ $i ] ) ) {
588 + $recipient = $data['object'][ $i ];
589 + } else {
590 + $recipient = array( $data['object'][ $i ] );
591 + }
592 + $recipient_items = array_merge( $recipient_items, $recipient );
593 + }
594 + }
595 +
596 + $recipients = array();
597 +
598 + // flatten array
599 + foreach ( $recipient_items as $recipient ) {
600 + if ( is_array( $recipient ) ) {
601 + // check if recipient is an object
602 + if ( array_key_exists( 'id', $recipient ) ) {
603 + $recipients[] = $recipient['id'];
604 + }
605 + } else {
606 + $recipients[] = $recipient;
607 + }
608 + }
609 +
610 + return array_unique( $recipients );
611 +}
612 +
613 +/**
614 + * Check if passed Activity is Public
615 + *
616 + * @param array $data The Activity object as array
617 + *
618 + * @return boolean True if public, false if not
619 + */
620 +function is_activity_public( $data ) {
621 + $recipients = extract_recipients_from_activity( $data );
622 +
623 + return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
624 +}
625 +
626 +/**
480 627 * Get active users based on a given duration
481 628 *
482 629 * @param int $duration The duration to check in month(s)
483 630 *
@@ -492,9 +639,9 @@
492 639 if ( false === $count ) {
493 640 global $wpdb;
494 641 $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 )";
495 642 $query = $wpdb->prepare( $query, $duration );
496 - $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
643 + $count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
497 644
498 645 set_transient( $transient_key, $count, DAY_IN_SECONDS );
499 646 }
500 647
@@ -509,13 +656,13 @@
509 656 }
510 657
511 658 // if blog user is disabled
512 659 if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
513 - return $count;
660 + return (int) $count;
514 661 }
515 662
516 663 // also count blog user
517 - return $count + 1;
664 + return (int) $count + 1;
518 665 }
519 666
520 667 /**
521 668 * Get the total number of users
@@ -529,9 +676,9 @@
529 676 }
530 677
531 678 $users = \get_users(
532 679 array(
533 - 'capability__in' => array( 'publish_posts' ),
680 + 'capability__in' => array( 'activitypub' ),
534 681 )
535 682 );
536 683
537 684 if ( is_array( $users ) ) {
@@ -541,9 +688,531 @@
541 688 }
542 689
543 690 // if blog user is disabled
544 691 if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
545 - return $users;
692 + return (int) $users;
546 693 }
547 694
548 - return $users + 1;
695 + return (int) $users + 1;
696 +}
697 +
698 +/**
699 + * Examine a comment ID and look up an existing comment it represents.
700 + *
701 + * @param string $id ActivityPub object ID (usually a URL) to check.
702 + *
703 + * @return int|boolean Comment ID, or false on failure.
704 + */
705 +function object_id_to_comment( $id ) {
706 + return Comment::object_id_to_comment( $id );
707 +}
708 +
709 +/**
710 + * Verify if URL is a local comment,
711 + * Or if it is a previously received remote comment
712 + * (For threading comments locally)
713 + *
714 + * @param string $url The URL to check.
715 + *
716 + * @return int comment_ID or null if not found
717 + */
718 +function url_to_commentid( $url ) {
719 + return Comment::url_to_commentid( $url );
720 +}
721 +
722 +/**
723 + * Get the URI of an ActivityPub object
724 + *
725 + * @param array $object The ActivityPub object
726 + *
727 + * @return string The URI of the ActivityPub object
728 + */
729 +function object_to_uri( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
730 + // check if it is already simple
731 + if ( ! $object || is_string( $object ) ) {
732 + return $object;
733 + }
734 +
735 + // check if it is a list, then take first item
736 + // this plugin does not support collections
737 + if ( array_is_list( $object ) ) {
738 + $object = $object[0];
739 + }
740 +
741 + // check if it is simplified now
742 + if ( is_string( $object ) ) {
743 + return $object;
744 + }
745 +
746 + $type = 'Object';
747 + if ( isset( $object['type'] ) ) {
748 + $type = $object['type'];
749 + }
750 +
751 + // return part of Object that makes most sense
752 + switch ( $type ) {
753 + case 'Link':
754 + $object = $object['href'];
755 + break;
756 + default:
757 + $object = $object['id'];
758 + break;
759 + }
760 +
761 + return $object;
762 +}
763 +
764 +/**
765 + * Check if a comment should be federated.
766 + *
767 + * We consider a comment should be federated if it is authored by a user that is
768 + * not disabled for federation and if it is a reply directly to the post or to a
769 + * federated comment.
770 + *
771 + * @param mixed $comment Comment object or ID.
772 + *
773 + * @return boolean True if the comment should be federated, false otherwise.
774 + */
775 +function should_comment_be_federated( $comment ) {
776 + return Comment::should_be_federated( $comment );
777 +}
778 +
779 +/**
780 + * Check if a comment was federated.
781 + *
782 + * This function checks if a comment was federated via ActivityPub.
783 + *
784 + * @param mixed $comment Comment object or ID.
785 + *
786 + * @return boolean True if the comment was federated, false otherwise.
787 + */
788 +function was_comment_sent( $comment ) {
789 + return Comment::was_sent( $comment );
790 +}
791 +
792 +/**
793 + * Check if a comment is federated.
794 + *
795 + * We consider a comment federated if comment was received via ActivityPub.
796 + *
797 + * Use this function to check if it is comment that was received via ActivityPub.
798 + *
799 + * @param mixed $comment Comment object or ID.
800 + *
801 + * @return boolean True if the comment is federated, false otherwise.
802 + */
803 +function was_comment_received( $comment ) {
804 + return Comment::was_received( $comment );
805 +}
806 +
807 +/**
808 + * Check if a comment is local only.
809 + *
810 + * This function checks if a comment is local only and was not sent or received via ActivityPub.
811 + *
812 + * @param mixed $comment Comment object or ID.
813 + *
814 + * @return boolean True if the comment is local only, false otherwise.
815 + */
816 +function is_local_comment( $comment ) {
817 + return Comment::is_local( $comment );
818 +}
819 +
820 +/**
821 + * Mark a WordPress object as federated.
822 + *
823 + * @param WP_Comment|WP_Post|mixed $wp_object
824 + *
825 + * @return void
826 + */
827 +function set_wp_object_state( $wp_object, $state ) {
828 + $meta_key = 'activitypub_status';
829 +
830 + if ( $wp_object instanceof \WP_Post ) {
831 + \update_post_meta( $wp_object->ID, $meta_key, $state );
832 + } elseif ( $wp_object instanceof \WP_Comment ) {
833 + \update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
834 + } else {
835 + \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
836 + }
837 +}
838 +
839 +/**
840 + * Get the federation state of a WordPress object.
841 + *
842 + * @param WP_Comment|WP_Post|mixed $wp_object
843 + *
844 + * @return string|false The state of the object or false if not found.
845 + */
846 +function get_wp_object_state( $wp_object ) {
847 + $meta_key = 'activitypub_status';
848 +
849 + if ( $wp_object instanceof \WP_Post ) {
850 + return \get_post_meta( $wp_object->ID, $meta_key, true );
851 + } elseif ( $wp_object instanceof \WP_Comment ) {
852 + return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
853 + } else {
854 + return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
855 + }
856 +}
857 +
858 +/**
859 + * Get the description of a post type.
860 + *
861 + * Set some default descriptions for the default post types.
862 + *
863 + * @param WP_Post_Type $post_type The post type object.
864 + *
865 + * @return string The description of the post type.
866 + */
867 +function get_post_type_description( $post_type ) {
868 + $description = '';
869 +
870 + switch ( $post_type->name ) {
871 + case 'post':
872 + $description = '';
873 + break;
874 + case 'page':
875 + $description = '';
876 + break;
877 + case 'attachment':
878 + $description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
879 + break;
880 + default:
881 + if ( ! empty( $post_type->description ) ) {
882 + $description = ' - ' . $post_type->description;
883 + }
884 + }
885 +
886 + return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
887 +}
888 +
889 +/**
890 + * Get the masked WordPress version to only show the major and minor version.
891 + *
892 + * @return string The masked version.
893 + */
894 +function get_masked_wp_version() {
895 + // only show the major and minor version
896 + $version = get_bloginfo( 'version' );
897 + // strip the RC or beta part
898 + $version = preg_replace( '/-.*$/', '', $version );
899 + $version = explode( '.', $version );
900 + $version = array_slice( $version, 0, 2 );
901 +
902 + return implode( '.', $version );
903 +}
904 +
905 +/**
906 + * Get the enclosures of a post.
907 + *
908 + * @param int $post_id The post ID.
909 + *
910 + * @return array The enclosures.
911 + */
912 +function get_enclosures( $post_id ) {
913 + $enclosures = get_post_meta( $post_id, 'enclosure' );
914 +
915 + if ( ! $enclosures ) {
916 + return array();
917 + }
918 +
919 + $enclosures = array_map(
920 + function ( $enclosure ) {
921 + $attributes = explode( "\n", $enclosure );
922 +
923 + if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
924 + return false;
925 + }
926 +
927 + return array(
928 + 'url' => $attributes[0],
929 + 'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null,
930 + 'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null,
931 + );
932 + },
933 + $enclosures
934 + );
935 +
936 + return array_filter( $enclosures );
937 +}
938 +
939 +/**
940 + * Retrieves the IDs of the ancestors of a comment.
941 + *
942 + * Adaption of `get_post_ancestors` from WordPress core.
943 + *
944 + * @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
945 + *
946 + * @param int|WP_Comment $comment Comment ID or comment object.
947 + *
948 + * @return WP_Comment[] Array of ancestor comments or empty array if there are none.
949 + */
950 +function get_comment_ancestors( $comment ) {
951 + $comment = \get_comment( $comment );
952 +
953 + // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
954 + if ( ! $comment || empty( $comment->comment_parent ) || $comment->comment_parent == $comment->comment_ID ) {
955 + return array();
956 + }
957 +
958 + $ancestors = array();
959 +
960 + $id = (int) $comment->comment_parent;
961 + $ancestors[] = $id;
962 +
963 + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
964 + while ( $id > 0 ) {
965 + $ancestor = \get_comment( $id );
966 + $parent_id = (int) $ancestor->comment_parent;
967 +
968 + // Loop detection: If the ancestor has been seen before, break.
969 + if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
970 + break;
971 + }
972 +
973 + $id = $parent_id;
974 + $ancestors[] = $id;
975 + }
976 +
977 + return $ancestors;
978 +}
979 +
980 +/**
981 + * Change the display of large numbers on the site.
982 + *
983 + * @author Jeremy Herve
984 + *
985 + * @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
986 + *
987 + * @param string $formatted Converted number in string format.
988 + * @param float $number The number to convert based on locale.
989 + * @param int $decimals Precision of the number of decimal places.
990 + *
991 + * @return string Converted number in string format.
992 + */
993 +function custom_large_numbers( $formatted, $number, $decimals ) {
994 + global $wp_locale;
995 +
996 + $decimals = 0;
997 + $decimal_point = '.';
998 + $thousands_sep = ',';
999 +
1000 + if ( isset( $wp_locale ) ) {
1001 + $decimals = (int) $wp_locale->number_format['decimal_point'];
1002 + $decimal_point = $wp_locale->number_format['decimal_point'];
1003 + $thousands_sep = $wp_locale->number_format['thousands_sep'];
1004 + }
1005 +
1006 + if ( $number < 1000 ) { // any number less than a Thousand.
1007 + return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
1008 + } elseif ( $number < 1000000 ) { // any number less than a million
1009 + return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
1010 + } elseif ( $number < 1000000000 ) { // any number less than a billion
1011 + return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
1012 + } else { // at least a billion
1013 + return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
1014 + }
1015 +
1016 + // Default fallback. We should not get here.
1017 + return $formatted;
1018 +}
1019 +
1020 +/**
1021 + * Registers a ActivityPub comment type.
1022 + *
1023 + *
1024 + * @param string $comment_type Key for comment type.
1025 + * @param array $args Arguments.
1026 + *
1027 + * @return array The registered Activitypub comment type.
1028 + */
1029 +function register_comment_type( $comment_type, $args = array() ) {
1030 + global $activitypub_comment_types;
1031 +
1032 + if ( ! is_array( $activitypub_comment_types ) ) {
1033 + $activitypub_comment_types = array();
1034 + }
1035 +
1036 + // Sanitize comment type name.
1037 + $comment_type = sanitize_key( $comment_type );
1038 +
1039 + $activitypub_comment_types[ $comment_type ] = $args;
1040 +
1041 + /**
1042 + * Fires after a ActivityPub comment type is registered.
1043 + *
1044 + *
1045 + * @param string $comment_type Comment type.
1046 + * @param array $args Arguments used to register the comment type.
1047 + */
1048 + do_action( 'activitypub_registered_comment_type', $comment_type, $args );
1049 +
1050 + return $args;
1051 +}
1052 +
1053 +/**
1054 + * Normalize a URL.
1055 + *
1056 + * @param string $url The URL.
1057 + *
1058 + * @return string The normalized URL.
1059 + */
1060 +function normalize_url( $url ) {
1061 + $url = \untrailingslashit( $url );
1062 + $url = \str_replace( 'https://', '', $url );
1063 + $url = \str_replace( 'http://', '', $url );
1064 + $url = \str_replace( 'www.', '', $url );
1065 +
1066 + return $url;
1067 +}
1068 +
1069 +/**
1070 + * Normalize a host.
1071 + *
1072 + * @param string $host The host.
1073 + *
1074 + * @return string The normalized host.
1075 + */
1076 +function normalize_host( $host ) {
1077 + return \str_replace( 'www.', '', $host );
1078 +}
1079 +
1080 +/**
1081 + * Get the reply intent URI.
1082 + *
1083 + * @return string The reply intent URI.
1084 + */
1085 +function get_reply_intent_uri() {
1086 + return sprintf(
1087 + 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
1088 + esc_url( \admin_url( 'post-new.php?in_reply_to=' ) )
1089 + );
1090 +}
1091 +
1092 +/**
1093 + * Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
1094 + *
1095 + * @param $content string The content that should be changed
1096 + * @param $regex string The regex to use
1097 + * @param $regex_callback callable Callback for replacement logic
1098 + *
1099 + * @return string The content with links, mentions, hashtags, etc.
1100 + */
1101 +function enrich_content_data( $content, $regex, $regex_callback ) {
1102 + // small protection against execution timeouts: limit to 1 MB
1103 + if ( mb_strlen( $content ) > MB_IN_BYTES ) {
1104 + return $content;
1105 + }
1106 + $tag_stack = array();
1107 + $protected_tags = array(
1108 + 'pre',
1109 + 'code',
1110 + 'textarea',
1111 + 'style',
1112 + 'a',
1113 + );
1114 + $content_with_links = '';
1115 + $in_protected_tag = false;
1116 + foreach ( wp_html_split( $content ) as $chunk ) {
1117 + if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
1118 + $content_with_links .= $chunk;
1119 + continue;
1120 + }
1121 +
1122 + if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
1123 + $tag = strtolower( $m[2] );
1124 + if ( '/' === $m[1] ) {
1125 + // Closing tag.
1126 + $i = array_search( $tag, $tag_stack, true );
1127 + // We can only remove the tag from the stack if it is in the stack.
1128 + if ( false !== $i ) {
1129 + $tag_stack = array_slice( $tag_stack, 0, $i );
1130 + }
1131 + } else {
1132 + // Opening tag, add it to the stack.
1133 + $tag_stack[] = $tag;
1134 + }
1135 +
1136 + // If we're in a protected tag, the tag_stack contains at least one protected tag string.
1137 + // The protected tag state can only change when we encounter a start or end tag.
1138 + $in_protected_tag = array_intersect( $tag_stack, $protected_tags );
1139 +
1140 + // Never inspect tags.
1141 + $content_with_links .= $chunk;
1142 + continue;
1143 + }
1144 +
1145 + if ( $in_protected_tag ) {
1146 + // Don't inspect a chunk inside an inspected tag.
1147 + $content_with_links .= $chunk;
1148 + continue;
1149 + }
1150 +
1151 + // Only reachable when there is no protected tag in the stack.
1152 + $content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk );
1153 + }
1154 +
1155 + return $content_with_links;
1156 +}
1157 +
1158 +/**
1159 + * Generate a summary of a post.
1160 + *
1161 + * This function generates a summary of a post by extracting:
1162 + *
1163 + * 1. The post excerpt if it exists.
1164 + * 2. The first part of the post content if it contains the <!--more--> tag.
1165 + * 3. An excerpt of the post content if it is longer than the specified length.
1166 + *
1167 + * @param int|WP_Post $post The post ID or post object.
1168 + * @param integer $length The maximum length of the summary.
1169 + * Default is 500. It will ne ignored if the post excerpt
1170 + * and the content above the <!--more--> tag.
1171 + *
1172 + * @return string The generated post summary.
1173 + */
1174 +function generate_post_summary( $post, $length = 500 ) {
1175 + $post = get_post( $post );
1176 +
1177 + if ( ! $post ) {
1178 + return '';
1179 + }
1180 +
1181 + $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
1182 +
1183 + if ( $content ) {
1184 + return \apply_filters( 'the_excerpt', $content );
1185 + }
1186 +
1187 + $content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
1188 + $content_parts = \get_extended( $content );
1189 +
1190 + $excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
1191 + $length = $length - strlen( $excerpt_more );
1192 +
1193 + // Check for the <!--more--> tag.
1194 + if (
1195 + ! empty( $content_parts['extended'] ) &&
1196 + ! empty( $content_parts['main'] )
1197 + ) {
1198 + $content = $content_parts['main'] . ' ' . $excerpt_more;
1199 + $length = null;
1200 + }
1201 +
1202 + $content = \html_entity_decode( $content );
1203 + $content = \wp_strip_all_tags( $content );
1204 + $content = \trim( $content );
1205 + $content = \preg_replace( '/\R+/m', "\n\n", $content );
1206 + $content = \preg_replace( '/[\r\t]/', '', $content );
1207 +
1208 + if ( $length && \strlen( $content ) > $length ) {
1209 + $content = \wordwrap( $content, $length, '</activitypub-summary>' );
1210 + $content = \explode( '</activitypub-summary>', $content, 2 );
1211 + $content = $content[0] . ' ' . $excerpt_more;
1212 + }
1213 +
1214 + /* Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
1215 + return \apply_filters( 'the_excerpt', $content );
1216 + */
1217 + return $content;
549 1218 }