From 5200eb24636238657ce6177ebdd09b637577e849 Mon Sep 17 00:00:00 2001 From: Edward Ficklin Date: Tue, 14 Mar 2023 13:34:50 -0400 Subject: [PATCH 01/60] define const for fedi bio meta key --- activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/activitypub.php b/activitypub.php index b72901b..1a23d8d 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

    1. ' );
       	\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

      [ap_title]

      \n\n[ap_content]\n\n

      [ap_hashtags]

      \n\n

      [ap_shortlink]

      " ); + \defined( 'ACTIVITYPUB_USER_DESCRIPTION_KEY' ) || \define( 'ACTIVITYPUB_USER_DESCRIPTION_KEY', 'activitypub_user_description' ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); From 3ed96471de419114eb5606cd38d0bca34ddaff6e Mon Sep 17 00:00:00 2001 From: Edward Ficklin Date: Tue, 14 Mar 2023 13:36:12 -0400 Subject: [PATCH 02/60] add profile field and save handling for fediverse specific bio --- includes/class-admin.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/includes/class-admin.php b/includes/class-admin.php index e98d547..4acef35 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -14,6 +14,7 @@ class Admin { \add_action( 'admin_menu', array( '\Activitypub\Admin', 'admin_menu' ) ); \add_action( 'admin_init', array( '\Activitypub\Admin', 'register_settings' ) ); \add_action( 'show_user_profile', array( '\Activitypub\Admin', 'add_fediverse_profile' ) ); + \add_action( 'personal_options_update', array( '\Activitypub\Admin', 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( '\Activitypub\Admin', 'enqueue_scripts' ) ); } @@ -153,10 +154,27 @@ class Admin { } public static function add_fediverse_profile( $user ) { + $ap_description = get_user_meta( $user->ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); ?>

      ID ); + ?> + + + + + + + Date: Tue, 14 Mar 2023 13:36:47 -0400 Subject: [PATCH 03/60] template helper function for displaying fedi bio, if set --- includes/functions.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index 8f2e5d9..9b00e8f 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -320,3 +320,17 @@ function url_to_authorid( $url ) { return 0; } + +/** + * Return the custom Activity Pub description, if set, or default author description. + * + * @param int $user_id The user ID. + * @return string + */ +function get_author_description( $user_id ) { + $description = get_user_meta( $user_id, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); + if ( empty( $description ) ) { + $description = get_user_meta( $user_id, 'description', true ); + } + return $description; +} From 277c7ba10f4a83610f058b04e29da9f46121b377 Mon Sep 17 00:00:00 2001 From: Edward Ficklin Date: Tue, 14 Mar 2023 13:37:20 -0400 Subject: [PATCH 04/60] output fedi bio if set, default if not --- templates/author-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/author-json.php b/templates/author-json.php index 1003989..3b355b9 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -8,7 +8,7 @@ $json->id = \get_author_posts_url( $author_id ); $json->type = 'Person'; $json->name = \get_the_author_meta( 'display_name', $author_id ); $json->summary = \html_entity_decode( - \get_the_author_meta( 'description', $author_id ), + \Activitypub\get_author_description( $author_id ), \ENT_QUOTES, 'UTF-8' ); From 8b92e9d47e8485189713ba0338127100fb479205 Mon Sep 17 00:00:00 2001 From: Edward Ficklin Date: Tue, 14 Mar 2023 20:35:14 -0400 Subject: [PATCH 05/60] escape output --- includes/class-admin.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 4acef35..c677246 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -162,9 +162,9 @@ class Admin { ?> - - + + Date: Tue, 14 Mar 2023 20:47:30 -0400 Subject: [PATCH 06/60] nonce verification --- includes/class-admin.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/class-admin.php b/includes/class-admin.php index c677246..8287259 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -165,12 +165,16 @@ class Admin {

      + Date: Fri, 21 Apr 2023 08:42:51 +0200 Subject: [PATCH 07/60] count only users that can `publish_posts` --- includes/rest/class-nodeinfo.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 100fdd3..980c24b 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -75,13 +75,24 @@ class Nodeinfo { 'version' => \get_bloginfo( 'version' ), ); - $users = \count_users(); + $users = \get_users( + array( + 'capability__in' => array( 'publish_posts' ), + ) + ); + + if ( is_array( $users ) ) { + $users = count( $users ); + } else { + $users = 1; + } + $posts = \wp_count_posts(); $comments = \wp_count_comments(); $nodeinfo['usage'] = array( 'users' => array( - 'total' => (int) $users['total_users'], + 'total' => $users, ), 'localPosts' => (int) $posts->publish, 'localComments' => (int) $comments->approved, From 7769d76849d6aa1a72d5ca51868679d0c1f6a2f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 14:56:22 +0200 Subject: [PATCH 08/60] use a taxonomy to save the list of followers --- activitypub.php | 5 +- includes/class-admin.php | 2 +- includes/collection/class-followers.php | 244 ++++++++++++++++++++++++ includes/rest/class-inbox.php | 2 - includes/table/class-followers.php | 84 ++++++++ includes/table/followers-list.php | 36 ---- templates/followers-list.php | 6 +- 7 files changed, 336 insertions(+), 43 deletions(-) create mode 100644 includes/collection/class-followers.php create mode 100644 includes/table/class-followers.php delete mode 100644 includes/table/followers-list.php diff --git a/activitypub.php b/activitypub.php index 35e6de8..c1cb823 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,7 +29,7 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); - require_once \dirname( __FILE__ ) . '/includes/table/followers-list.php'; + require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; @@ -44,6 +44,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php'; Activitypub::init(); + require_once \dirname( __FILE__ ) . '/includes/collection/class-followers.php'; + Collection\Followers::init(); + // Configure the REST API route require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php'; Rest\Outbox::init(); diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..fbb953a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -31,7 +31,7 @@ class Admin { \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) ); - $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); + $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php new file mode 100644 index 0000000..7d7f8eb --- /dev/null +++ b/includes/collection/class-followers.php @@ -0,0 +1,244 @@ + array( + 'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ), + 'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ), + 'menu_name' => __( 'Followers', 'activitypub' ), + ), + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_nav_menus' => false, + 'show_admin_column' => false, + 'query_var' => false, + 'rewrite' => false, + 'public' => false, + 'capabilities' => array( + 'edit_terms' => null, + ), + ); + + register_taxonomy( self::TAXONOMY, 'user', $args ); + register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); + + register_term_meta( + self::TAXONOMY, + 'user_id', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_username' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'name', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_displayname' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'username', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_username' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'avatar', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_avatar' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'created_at', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_created_at' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'inbox', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_created_at' ), + ) + ); + + do_action( 'activitypub_after_register_taxonomy' ); + } + + /** + * Handle the "Follow" Request + * + * @param array $object The JSON "Follow" Activity + * @param int $user_id The ID of the WordPress User + * + * @return void + */ + public static function handle_follow_request( $object, $user_id ) { + // save follower + self::add_follower( $user_id, $object['actor'] ); + } + + /** + * Handles "Unfollow" requests + * + * @param array $object The JSON "Undo" Activity + * @param int $user_id The ID of the WordPress User + */ + public static function handle_undo_request( $object, $user_id ) { + if ( + isset( $object['object'] ) && + isset( $object['object']['type'] ) && + 'Follow' === $object['object']['type'] + ) { + self::remove_follower( $user_id, $object['actor'] ); + } + } + + /** + * Undocumented function + * + * @return void + */ + public static function add_follower( $user_id, $actor ) { + $remote_data = get_remote_metadata_by_actor( $actor ); + + if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { + $remote_data = array(); + } + + $term = term_exists( $actor, self::TAXONOMY ); + + if ( ! $term ) { + $term = wp_insert_term( + $actor, + self::TAXONOMY, + array( + 'slug' => sanitize_title( $actor ), + 'description' => wp_json_encode( $remote_data ), + ) + ); + } + + $term_id = $term['term_id']; + + $map_meta = array( + 'name' => 'name', + 'preferredUsername' => 'username', + 'inbox' => 'inbox', + ); + + foreach ( $map_meta as $remote => $internal ) { + if ( ! empty( $remote_data[ $remote ] ) ) { + update_term_meta( $term_id, $internal, esc_html( $remote_data[ $remote ] ), true ); + } + } + + if ( ! empty( $remote_data['icon']['url'] ) ) { + update_term_meta( $term_id, 'avatar', esc_url_raw( $remote_data['icon']['url'] ), true ); + } + + wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); + } + + public static function send_ack() { + // get inbox + $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); + + // send "Accept" activity + $activity = new Activity( 'Accept' ); + $activity->set_object( $object ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_to( $object['actor'] ); + $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); + + $activity = $activity->to_simple_json(); + $response = safe_remote_post( $inbox, $activity, $user_id ); + } + + public static function get_followers( $user_id, $number = null, $offset = null ) { + //self::migrate_followers( $user_id ); + + $terms = new WP_Term_Query( + array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => $number, + 'offset' => $offset, + ) + ); + + return $terms->get_terms(); + } + + public static function count_followers( $user_id ) { + return count( self::get_followers( $user_id ) ); + } + + public static function migrate_followers( $user_id ) { + $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + + if ( $followes ) { + foreach ( $followes as $follower ) { + self::add_follower( $user_id, $follower ); + } + } + } +} diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1a63108..761bfd6 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,8 +17,6 @@ class Inbox { public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); - \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); - \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 ); \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php new file mode 100644 index 0000000..bba3208 --- /dev/null +++ b/includes/table/class-followers.php @@ -0,0 +1,84 @@ + '', + 'avatar' => \__( 'Avatar', 'activitypub' ), + 'name' => \__( 'Name', 'activitypub' ), + 'username' => \__( 'Username', 'activitypub' ), + 'identifier' => \__( 'Identifier', 'activitypub' ), + ); + } + + public function get_sortable_columns() { + return array(); + } + + public function prepare_items() { + $columns = $this->get_columns(); + $hidden = array(); + + $this->process_action(); + $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); + + $page_num = $this->get_pagenum(); + $per_page = 20; + + $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( \get_current_user_id() ); + + $this->items = array(); + $this->set_pagination_args( + array( + 'total_items' => $counter, + 'total_pages' => round( $counter / $per_page ), + 'per_page' => $per_page, + ) + ); + + foreach ( $follower as $follower ) { + $item = array( + 'avatar' => esc_attr( get_term_meta( $follower->term_id, 'avatar', true ) ), + 'name' => esc_attr( get_term_meta( $follower->term_id, 'name', true ) ), + 'username' => esc_attr( get_term_meta( $follower->term_id, 'username', true ) ), + 'identifier' => esc_attr( $follower->name ), + ); + + $this->items[] = $item; + } + } + + public function get_bulk_actions() { + return array( + 'revoke' => __( 'Revoke', 'activitypub' ), + 'verify' => __( 'Verify', 'activitypub' ), + ); + } + + public function column_default( $item, $column_name ) { + if ( ! array_key_exists( $column_name, $item ) ) { + return __( 'None', 'activitypub' ); + } + return $item[ $column_name ]; + } + + public function column_avatar( $item ) { + return sprintf( + '', + $item['avatar'] + ); + } + + public function column_cb( $item ) { + return sprintf( '', esc_attr( $item['identifier'] ) ); + } +} diff --git a/includes/table/followers-list.php b/includes/table/followers-list.php deleted file mode 100644 index 81444ee..0000000 --- a/includes/table/followers-list.php +++ /dev/null @@ -1,36 +0,0 @@ - \__( 'Identifier', 'activitypub' ), - ); - } - - public function get_sortable_columns() { - return array(); - } - - public function prepare_items() { - $columns = $this->get_columns(); - $hidden = array(); - - $this->process_action(); - $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); - - $this->items = array(); - - foreach ( \Activitypub\Peer\Followers::get_followers( \get_current_user_id() ) as $follower ) { - $this->items[]['identifier'] = \esc_attr( $follower ); - } - } - - public function column_default( $item, $column_name ) { - return $item[ $column_name ]; - } -} diff --git a/templates/followers-list.php b/templates/followers-list.php index 057f498..261808f 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,10 +1,10 @@
      -

      +

      -

      +

      - +
      From 75e9b1e2819726faf7cde87953fd3fd2485041c6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:21 +0200 Subject: [PATCH 09/60] deprecate old functions --- includes/peer/class-followers.php | 68 +++++-------------------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index abc18e3..d5caf10 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -9,76 +9,30 @@ namespace Activitypub\Peer; class Followers { public static function get_followers( $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' ); - if ( ! $followers ) { - return array(); + $items = array(); // phpcs:ignore + foreach ( \Activitypub\Collection\Followers::get_followers( $author_id ) as $follower ) { + $items[] = $follower->name; // phpcs:ignore } - - foreach ( $followers as $key => $follower ) { - if ( - \is_array( $follower ) && - isset( $follower['type'] ) && - 'Person' === $follower['type'] && - isset( $follower['id'] ) && - false !== \filter_var( $follower['id'], \FILTER_VALIDATE_URL ) - ) { - $followers[ $key ] = $follower['id']; - } - } - - return $followers; + return $items; } public static function count_followers( $author_id ) { - $followers = self::get_followers( $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::count_followers' ); - return \count( $followers ); + return \Activitypub\Collection\Followers::count_followers( $author_id ); } public static function add_follower( $actor, $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' ); - if ( ! \is_string( $actor ) ) { - if ( - \is_array( $actor ) && - isset( $actor['type'] ) && - 'Person' === $actor['type'] && - isset( $actor['id'] ) && - false !== \filter_var( $actor['id'], \FILTER_VALIDATE_URL ) - ) { - $actor = $actor['id']; - } - - return new \WP_Error( - 'invalid_actor_object', - \__( 'Unknown Actor schema', 'activitypub' ), - array( - 'status' => 404, - ) - ); - } - - if ( ! \is_array( $followers ) ) { - $followers = array( $actor ); - } else { - $followers[] = $actor; - } - - $followers = \array_unique( $followers ); - - \update_user_meta( $author_id, 'activitypub_followers', $followers ); + return \Activitypub\Collection\Followers::add_followers( $author_id, $actor ); } public static function remove_follower( $actor, $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::remove_follower' ); - foreach ( $followers as $key => $value ) { - if ( $value === $actor ) { - unset( $followers[ $key ] ); - } - } - - \update_user_meta( $author_id, 'activitypub_followers', $followers ); + return \Activitypub\Collection\Followers::remove_follower( $author_id, $actor ); } } From 734750b7968ad30b713e1d2ca8b43650eabdd194 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:41 +0200 Subject: [PATCH 10/60] use collection also for rest endpoints --- includes/rest/class-followers.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 3b1e146..ee7fe9d 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -1,6 +1,12 @@ \d+)/followers', array( array( - 'methods' => \WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::READABLE, 'callback' => array( self::class, 'get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', @@ -46,7 +52,7 @@ class Followers { $user = \get_user_by( 'ID', $user_id ); if ( ! $user ) { - return new \WP_Error( + return new WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( @@ -63,7 +69,7 @@ class Followers { */ \do_action( 'activitypub_outbox_pre' ); - $json = new \stdClass(); + $json = new stdClass(); $json->{'@context'} = \Activitypub\get_context(); @@ -73,14 +79,14 @@ class Followers { $json->type = 'OrderedCollectionPage'; $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore - $json->totalItems = \Activitypub\count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = \Activitypub\Peer\Followers::get_followers( $user_id ); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore + $json->orderedItems = array(); // phpcs:ignore + foreach ( FollowerCollection::get_followers( $user_id ) as $follower ) { + $json->orderedItems[] = $follower->name; // phpcs:ignore + } - $json->first = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); - - $response = new \WP_REST_Response( $json, 200 ); + $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); return $response; From 32194c31df392a3384e786e80ec9a7d1a6f2d26c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:49 +0200 Subject: [PATCH 11/60] phpDoc --- includes/collection/class-followers.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 7d7f8eb..e583b31 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -152,8 +152,10 @@ class Followers { } /** - * Undocumented function + * Add a new Follower * + * @param int $user_id The WordPress user + * @param string $actor The Actor URL * @return void */ public static function add_follower( $user_id, $actor ) { @@ -197,6 +199,11 @@ class Followers { wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); } + /** + * Undocumented function + * + * @return void + */ public static function send_ack() { // get inbox $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); @@ -212,6 +219,14 @@ class Followers { $response = safe_remote_post( $inbox, $activity, $user_id ); } + /** + * Get the Followers of a given user + * + * @param int $user_id + * @param int $number + * @param int $offset + * @return array The Term list of followers + */ public static function get_followers( $user_id, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); @@ -228,6 +243,12 @@ class Followers { return $terms->get_terms(); } + /** + * Count the total number of followers + * + * @param int $user_id The WordPress user + * @return int The number of Followers + */ public static function count_followers( $user_id ) { return count( self::get_followers( $user_id ) ); } From 3c86e94d9a79603aac2b809fbc54cb677c87928a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 16:25:15 +0200 Subject: [PATCH 12/60] remove followers --- includes/collection/class-followers.php | 6 +++++- includes/table/class-followers.php | 15 ++++++++++++--- templates/followers-list.php | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index e583b31..9848c5e 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -199,6 +199,10 @@ class Followers { wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); } + public static function remove_follower( $user_id, $actor ) { + wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + } + /** * Undocumented function * @@ -225,7 +229,7 @@ class Followers { * @param int $user_id * @param int $number * @param int $offset - * @return array The Term list of followers + * @return array The Term list of Followers */ public static function get_followers( $user_id, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index bba3208..dbd2647 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -59,8 +59,7 @@ class Followers extends WP_List_Table { public function get_bulk_actions() { return array( - 'revoke' => __( 'Revoke', 'activitypub' ), - 'verify' => __( 'Verify', 'activitypub' ), + 'delete' => __( 'Delete', 'activitypub' ), ); } @@ -79,6 +78,16 @@ class Followers extends WP_List_Table { } public function column_cb( $item ) { - return sprintf( '', esc_attr( $item['identifier'] ) ); + return sprintf( '', esc_attr( $item['identifier'] ) ); + } + + public function process_action() { + $followers = isset( $_REQUEST['followers'] ) ? $_REQUEST['followers'] : array(); // phpcs:ignore + + switch ( $this->current_action() ) { + case 'delete': + FollowerCollection::remove_follower( \get_current_user_id(), $followers ); + break; + } } } diff --git a/templates/followers-list.php b/templates/followers-list.php index 261808f..e76a45d 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -7,7 +7,7 @@ - + prepare_items(); $token_table->display(); From ebc9b6ac8d1479194a8234871734f3de21c863dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 16:34:47 +0200 Subject: [PATCH 13/60] naming improvements --- includes/class-admin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index fbb953a..c8c5813 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,7 +13,7 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); - \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) ); + \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } @@ -152,7 +152,7 @@ class Admin { // todo } - public static function add_fediverse_profile( $user ) { + public static function add_profile( $user ) { ?>

      Date: Fri, 21 Apr 2023 16:40:46 +0200 Subject: [PATCH 14/60] verify requests --- includes/table/class-followers.php | 14 +++++++++++++- templates/followers-list.php | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index dbd2647..3cf4946 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -82,7 +82,19 @@ class Followers extends WP_List_Table { } public function process_action() { - $followers = isset( $_REQUEST['followers'] ) ? $_REQUEST['followers'] : array(); // phpcs:ignore + if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_apnonce'] ) ) { + return false; + } + + if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-followers-list' ) ) { + return false; + } + + if ( ! current_user_can( 'edit_user', \get_current_user_id() ) ) { + return false; + } + + $followers = $_REQUEST['followers']; // phpcs:ignore switch ( $this->current_action() ) { case 'delete': diff --git a/templates/followers-list.php b/templates/followers-list.php index e76a45d..c79c961 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -12,5 +12,6 @@ $token_table->prepare_items(); $token_table->display(); ?> +
      From 45ae73bb0660ed6062751ff1ca452e10e7a868eb Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 21 Apr 2023 17:20:48 +0200 Subject: [PATCH 15/60] Add Vary header --- includes/class-activitypub.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 4730515..b20eb71 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -39,6 +39,8 @@ class Activitypub { return $template; } + header( 'Vary: Accept' ); + // check if user can publish posts if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { return $template; From 4ed4d06fd5f651377551fa6860d2b5028cec00ca Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 21 Apr 2023 17:41:04 +0200 Subject: [PATCH 16/60] Add comment --- includes/class-activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index b20eb71..16a1f22 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -39,6 +39,7 @@ class Activitypub { return $template; } + // Ensure that edge caches know that this page can deliver both HTML and JSON. header( 'Vary: Accept' ); // check if user can publish posts From 28c077e422645d84ea4fc8ff41157e5555fe59a2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 23 Apr 2023 22:56:45 +0200 Subject: [PATCH 17/60] Add URL --- includes/model/class-post.php | 43 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index cfe49b6..76d4d03 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -28,6 +28,13 @@ class Post { */ private $id; + /** + * The Object URL. + * + * @var string + */ + private $url; + /** * The Object Summary. * @@ -179,6 +186,7 @@ class Post { $array = array( 'id' => $this->get_id(), + 'url' => $this->get_url(), 'type' => $this->get_object_type(), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), 'attributedTo' => \get_author_posts_url( $post->post_author ), @@ -206,6 +214,29 @@ class Post { return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } + /** + * Returns the URL of an Activity Object + * + * @return string + */ + public function get_url() { + if ( $this->url ) { + return $this->url; + } + + $post = $this->post; + + if ( 'trash' === get_post_status( $post ) ) { + $permalink = \get_post_meta( $post->url, 'activitypub_canonical_url', true ); + } else { + $permalink = \get_permalink( $post ); + } + + $this->url = $permalink; + + return $permalink; + } + /** * Returns the ID of an Activity Object * @@ -216,17 +247,9 @@ class Post { return $this->id; } - $post = $this->post; + $this->id = $this->get_url(); - if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); - } else { - $permalink = \get_permalink( $post ); - } - - $this->id = $permalink; - - return $permalink; + return $this->id; } /** From 77415ef510a1ebea33d567c354e87e02cf0df824 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 23 Apr 2023 22:57:03 +0200 Subject: [PATCH 18/60] Remove "(Fediverse)" --- includes/class-admin.php | 2 +- templates/followers-list.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..fbb953a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -31,7 +31,7 @@ class Admin { \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) ); - $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); + $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); } diff --git a/templates/followers-list.php b/templates/followers-list.php index 057f498..a7136ce 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,5 +1,5 @@
      -

      +

      From 47dc2f72d1095eb02fe0975c95936c82a139ddc5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Apr 2023 09:49:06 +0200 Subject: [PATCH 19/60] fix "bulk replace" issue --- includes/model/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 76d4d03..470ad70 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -227,7 +227,7 @@ class Post { $post = $this->post; if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->url, 'activitypub_canonical_url', true ); + $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); } else { $permalink = \get_permalink( $post ); } From 3f4c44db05e7a6b8df24f7891a177b865972e4d8 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 24 Apr 2023 09:15:48 +0200 Subject: [PATCH 20/60] Compatibility: do not serve images with Jetpack CDN when active When Jetpack's image CDN is active, core calls to retrieve images return an image served by the CDN. Since Fediverse instances usually fetch and cache the data themselves, we do not need to use the CDN for those images when returned by the ActivityPub plugin. In fact, we really do not want that to happen, as Fediverse instances may get errors when trying to fetch images from the CDN (they may get blocked / rate-limited / ...). Let's hook into Jetpack's CDN to avoid that. --- includes/model/class-post.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 470ad70..6f33cc3 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -306,7 +306,24 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + + /** + * If you use the Jetpack plugin and its Image CDN, aka Photon, + * the image strings returned will use the Photon URL. + * We don't want that since Fediverse instances already do caching on their end. + * Let the CDN only be used for visitors of the site. + */ + if ( class_exists( 'Jetpack_Photon' ) ) { + \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); + } + $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); + + // Re-enable Photon now that the image URL has been built. + if ( class_exists( 'Jetpack_Photon' ) ) { + \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); + } + $mimetype = \get_post_mime_type( $id ); if ( $thumbnail ) { From 56d2b7e8be6b6765138dbd6a4170a056c7e95794 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 24 Apr 2023 09:49:05 +0200 Subject: [PATCH 21/60] Update to handle both old and new versions of Jetpack See https://github.com/Automattic/jetpack/pull/30050/ --- includes/model/class-post.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 6f33cc3..a69e139 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -312,15 +312,23 @@ class Post { * the image strings returned will use the Photon URL. * We don't want that since Fediverse instances already do caching on their end. * Let the CDN only be used for visitors of the site. + * + * Old versions of Jetpack used the Jetpack_Photon class to do this. + * New versions use the Image_CDN class. + * Let's handle both. */ - if ( class_exists( 'Jetpack_Photon' ) ) { + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); } $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); // Re-enable Photon now that the image URL has been built. - if ( class_exists( 'Jetpack_Photon' ) ) { + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); } From 84a82c2ac41a44bb4278d46072b193193c18a1c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Apr 2023 20:46:51 +0200 Subject: [PATCH 22/60] added follower model --- activitypub.php | 3 + includes/class-activity-dispatcher.php | 7 +- includes/collection/class-followers.php | 181 ++++++++++++++--------- includes/functions.php | 96 +----------- includes/model/class-follower.php | 187 ++++++++++++++++++++++++ includes/peer/class-followers.php | 6 +- includes/rest/class-followers.php | 5 +- includes/rest/class-inbox.php | 40 +---- includes/table/class-followers.php | 10 +- 9 files changed, 320 insertions(+), 215 deletions(-) create mode 100644 includes/model/class-follower.php diff --git a/activitypub.php b/activitypub.php index c1cb823..6439ec8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,6 +29,8 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); + \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); + require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; @@ -37,6 +39,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; + require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; Activity_Dispatcher::init(); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 57fee1a..73e49b5 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use Activitypub\Collection\Followers; /** * ActivityPub Activity_Dispatcher Class @@ -66,11 +67,9 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); + $inboxes = FollowerCollection::get_inboxes( $user_id ); - foreach ( $inboxes as $inbox => $cc ) { - $cc = array_values( array_unique( $cc ) ); - $activitypub_activity->add_cc( $cc ); + foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 9848c5e..5964756 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -5,10 +5,10 @@ use WP_Error; use WP_Term_Query; use Activitypub\Webfinger; use Activitypub\Model\Activity; +use Activitypub\Model\Follower; use function Activitypub\safe_remote_get; use function Activitypub\safe_remote_post; -use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection @@ -29,6 +29,8 @@ class Followers { \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 ); + + \add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 ); } /** @@ -59,16 +61,6 @@ class Followers { register_taxonomy( self::TAXONOMY, 'user', $args ); register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); - register_term_meta( - self::TAXONOMY, - 'user_id', - array( - 'type' => 'string', - 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_username' ), - ) - ); - register_term_meta( self::TAXONOMY, 'name', @@ -101,21 +93,21 @@ class Followers { register_term_meta( self::TAXONOMY, - 'created_at', + 'inbox', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_inbox' ), ) ); register_term_meta( self::TAXONOMY, - 'inbox', + 'updated_at', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_updated_at' ), ) ); @@ -127,12 +119,13 @@ class Followers { * * @param array $object The JSON "Follow" Activity * @param int $user_id The ID of the WordPress User - * * @return void */ public static function handle_follow_request( $object, $user_id ) { // save follower - self::add_follower( $user_id, $object['actor'] ); + $follower = self::add_follower( $user_id, $object['actor'] ); + + do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower ); } /** @@ -156,68 +149,67 @@ class Followers { * * @param int $user_id The WordPress user * @param string $actor The Actor URL - * @return void + * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { - $remote_data = get_remote_metadata_by_actor( $actor ); + $follower = new Follower( $actor ); + $follower->upsert(); - if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { - $remote_data = array(); + $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); + + if ( is_wp_error( $result ) ) { + return $result; + } else { + return $follower; } - - $term = term_exists( $actor, self::TAXONOMY ); - - if ( ! $term ) { - $term = wp_insert_term( - $actor, - self::TAXONOMY, - array( - 'slug' => sanitize_title( $actor ), - 'description' => wp_json_encode( $remote_data ), - ) - ); - } - - $term_id = $term['term_id']; - - $map_meta = array( - 'name' => 'name', - 'preferredUsername' => 'username', - 'inbox' => 'inbox', - ); - - foreach ( $map_meta as $remote => $internal ) { - if ( ! empty( $remote_data[ $remote ] ) ) { - update_term_meta( $term_id, $internal, esc_html( $remote_data[ $remote ] ), true ); - } - } - - if ( ! empty( $remote_data['icon']['url'] ) ) { - update_term_meta( $term_id, 'avatar', esc_url_raw( $remote_data['icon']['url'] ), true ); - } - - wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); - } - - public static function remove_follower( $user_id, $actor ) { - wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); } /** - * Undocumented function + * Remove a Follower * + * @param int $user_id The WordPress user_id + * @param string $actor The Actor URL + * @return bool|WP_Error True on success, false or WP_Error on failure. + */ + public static function remove_follower( $user_id, $actor ) { + return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + } + + /** + * Remove a Follower + * + * @param string $actor The Actor URL + * @return \Activitypub\Model\Follower The Follower object + */ + public static function get_follower( $actor ) { + $term = get_term_by( 'name', $actor, self::TAXONOMY ); + + return new Follower( $term->name ); + } + + /** + * Send Accept response + * + * @param string $actor The Actor URL + * @param array $object The Activity object + * @param int $user_id The WordPress user_id + * @param Activitypub\Model\Follower $follower The Follower object * @return void */ - public static function send_ack() { + public static function send_follow_response( $actor, $object, $user_id, $follower ) { + //if ( is_wp_error( $follower ) ) { + // @todo send error message + //} + // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); + $inbox = $follower->get_inbox(); // send "Accept" activity $activity = new Activity( 'Accept' ); $activity->set_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); + $activity->set_to( $actor ); + $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity = $activity->to_simple_json(); $response = safe_remote_post( $inbox, $activity, $user_id ); @@ -226,12 +218,13 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id - * @param int $number - * @param int $offset - * @return array The Term list of Followers + * @param int $user_id The WordPress user_id + * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param int $number Limts the result + * @param int $offset Offset + * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $offset = null ) { + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); $terms = new WP_Term_Query( @@ -244,7 +237,24 @@ class Followers { ) ); - return $terms->get_terms(); + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } } /** @@ -257,6 +267,39 @@ class Followers { return count( self::get_followers( $user_id ) ); } + public static function get_inboxes( $user_id ) { + // get all Followers of a WordPress user + $terms = new WP_Term_Query( + array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'fields' => 'ids', + ) + ); + + $terms = $terms->get_terms(); + + global $wpdb; + $results = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT meta_value FROM {$wpdb->termmeta} + WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ") + AND meta_key = 'shared_inbox' + AND meta_value IS NOT NULL", + $terms + ) + ); + + return array_filter( $results ); + } + + /** + * Undocumented function + * + * @param [type] $user_id + * @return void + */ public static function migrate_followers( $user_id ) { $followes = get_user_meta( $user_id, 'activitypub_followers', true ); diff --git a/includes/functions.php b/includes/functions.php index 77508c5..74b0c2e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -63,7 +63,7 @@ function safe_remote_post( $url, $body, $user_id ) { function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date ); + $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); @@ -95,15 +95,14 @@ function safe_remote_get( $url, $user_id ) { * @return string The user-resource */ function get_webfinger_resource( $user_id ) { - return \Activitypub\Webfinger::get_user_resource( $user_id ); + return Webfinger::get_user_resource( $user_id ); } /** - * [get_metadata_by_actor description] + * Requests the Meta-Data from the Actors profile * - * @param string $actor - * - * @return array + * @param string $actor The Actor URL + * @return array The Actor profile as array */ function get_remote_metadata_by_actor( $actor ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); @@ -165,82 +164,9 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - return $metadata; } -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_inbox_by_actor( $actor ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) { - return $metadata['endpoints']['sharedInbox']; - } - - if ( \array_key_exists( 'inbox', $metadata ) ) { - return $metadata['inbox']; - } - - return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); -} - -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_publickey_by_actor( $actor, $key_id ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( - isset( $metadata['publicKey'] ) && - isset( $metadata['publicKey']['id'] ) && - isset( $metadata['publicKey']['owner'] ) && - isset( $metadata['publicKey']['publicKeyPem'] ) && - $key_id === $metadata['publicKey']['id'] && - $actor === $metadata['publicKey']['owner'] - ) { - return $metadata['publicKey']['publicKeyPem']; - } - - return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); -} - -function get_follower_inboxes( $user_id, $cc = array() ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - $followers = array_merge( $followers, $cc ); - $followers = array_unique( $followers ); - - $inboxes = array(); - - foreach ( $followers as $follower ) { - $inbox = \Activitypub\get_inbox_by_actor( $follower ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $follower; - } - - return $inboxes; -} - function get_identifier_settings( $user_id ) { ?> @@ -261,19 +187,11 @@ function get_identifier_settings( $user_id ) { } function get_followers( $user_id ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - - if ( ! $followers ) { - return array(); - } - - return $followers; + return Collection\Followers::get_followers( $user_id ); } function count_followers( $user_id ) { - $followers = \Activitypub\get_followers( $user_id ); - - return \count( $followers ); + return Collection\Followers::count_followers( $user_id ); } /** diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php new file mode 100644 index 0000000..169e5f2 --- /dev/null +++ b/includes/model/class-follower.php @@ -0,0 +1,187 @@ + 'name', + 'preferredUsername' => 'username', + 'inbox' => 'inbox', + ); + + /** + * Constructor + * + * @param WP_Post $post + */ + public function __construct( $actor ) { + $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); + + $this->actor = $actor; + + if ( $term ) { + $this->id = $term->term_id; + $this->slug = $term->slug; + $this->meta = json_decode( $term->meta ); + } else { + $this->slug = sanitize_title( $actor ); + } + } + + /** + * Magic function to implement getter and setter + * + * @param string $method + * @param string $params + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( empty( $this->$var ) ) { + return $this->get( $var ); + } + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->$var = $params[0]; + } + } + + public function get( $attribute ) { + if ( $this->$attribute ) { + return $this->$attribute; + } + + if ( ! $this->id ) { + $this->$attribute = $this->get_meta_by( $attribute ); + return $this->$attribute; + } + + $this->$attribute = get_term_meta( $this->id, $attribute, true ); + return $this->$attribute; + } + + public function get_meta_by( $attribute, $force = false ) { + $meta = $this->get_meta( $force ); + + foreach ( $this->map_meta as $remote => $local ) { + if ( $attribute === $local && isset( $meta[ $remote ] ) ) { + return $meta[ $remote ]; + } + } + + return null; + } + + public function get_meta( $force = false ) { + if ( $this->meta && false === (bool) $force ) { + return $this->meta; + } + + $remote_data = get_remote_metadata_by_actor( $this->actor ); + + if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { + $remote_data = array(); + } + + $this->meta = $remote_data; + + return $this->meta; + } + + public function update() { + $term = wp_update_term( + $this->id, + Followers::TAXONOMY, + array( + 'description' => wp_json_encode( $this->get_meta( true ) ), + ) + ); + + $this->update_term_meta(); + } + + public function save() { + $term = wp_insert_term( + $this->actor, + Followers::TAXONOMY, + array( + 'slug' => sanitize_title( $this->get_actor() ), + 'description' => wp_json_encode( $this->get_meta() ), + ) + ); + + $this->id = $term['term_id']; + + $this->update_term_meta(); + } + + public function upsert() { + if ( $this->id ) { + $this->update(); + } else { + $this->save(); + } + } + + protected function update_term_meta() { + $meta = $this->get_meta(); + + foreach ( $this->map_meta as $remote => $internal ) { + if ( ! empty( $meta[ $remote ] ) ) { + update_term_meta( $this->id, $internal, esc_html( $meta[ $remote ] ), true ); + $this->$internal = $meta[ $remote ]; + } + } + + if ( ! empty( $meta['icon']['url'] ) ) { + update_term_meta( $this->id, 'avatar', esc_url_raw( $meta['icon']['url'] ), true ); + $this->avatar = $meta['icon']['url']; + } + + if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['endpoints']['sharedInbox'] ), true ); + $this->shared_inbox = $meta['endpoints']['sharedInbox']; + } elseif ( ! empty( $meta['inbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['inbox'] ), true ); + $this->shared_inbox = $meta['inbox']; + } + + update_term_meta( $this->id, 'updated_at', \strtotime( 'now' ), true ); + } +} diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index d5caf10..8941cc4 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -11,11 +11,7 @@ class Followers { public static function get_followers( $author_id ) { _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' ); - $items = array(); // phpcs:ignore - foreach ( \Activitypub\Collection\Followers::get_followers( $author_id ) as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + return \Activitypub\Collection\Followers::get_followers( $author_id ); } public static function count_followers( $author_id ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index ee7fe9d..9a8b9b6 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -81,10 +81,7 @@ class Followers { $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = array(); // phpcs:ignore - foreach ( FollowerCollection::get_followers( $user_id ) as $follower ) { - $json->orderedItems[] = $follower->name; // phpcs:ignore - } + $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 761bfd6..5a23d02 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,8 +17,7 @@ class Inbox { public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); - //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); - //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); } @@ -342,43 +341,6 @@ class Inbox { return $params; } - /** - * Handles "Follow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_follow( $object, $user_id ) { - // save follower - \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); - - // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); - - // send "Accept" activity - $activity = new Activity( 'Accept' ); - $activity->set_object( $object ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); - - $activity = $activity->to_simple_json(); - - $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - - /** - * Handles "Unfollow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_unfollow( $object, $user_id ) { - if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) { - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); - } - } - /** * Handles "Reaction" requests * diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 3cf4946..13d6a91 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -33,7 +33,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page ); $counter = FollowerCollection::count_followers( \get_current_user_id() ); $this->items = array(); @@ -47,10 +47,10 @@ class Followers extends WP_List_Table { foreach ( $follower as $follower ) { $item = array( - 'avatar' => esc_attr( get_term_meta( $follower->term_id, 'avatar', true ) ), - 'name' => esc_attr( get_term_meta( $follower->term_id, 'name', true ) ), - 'username' => esc_attr( get_term_meta( $follower->term_id, 'username', true ) ), - 'identifier' => esc_attr( $follower->name ), + 'avatar' => esc_attr( $follower->get_avatar() ), + 'name' => esc_attr( $follower->get_name() ), + 'username' => esc_attr( $follower->get_username() ), + 'identifier' => esc_attr( $follower->get_actor() ), ); $this->items[] = $item; From 377fc9416198b371d53ab6d231afbf170853d528 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 09:09:07 +0200 Subject: [PATCH 23/60] php doc --- includes/collection/class-followers.php | 52 ++++++++++++------- includes/model/class-follower.php | 67 ++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 5964756..78d0139 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -118,7 +118,8 @@ class Followers { * Handle the "Follow" Request * * @param array $object The JSON "Follow" Activity - * @param int $user_id The ID of the WordPress User + * @param int $user_id The ID of the ID of the WordPress User + * * @return void */ public static function handle_follow_request( $object, $user_id ) { @@ -131,8 +132,8 @@ class Followers { /** * Handles "Unfollow" requests * - * @param array $object The JSON "Undo" Activity - * @param int $user_id The ID of the WordPress User + * @param array $object The JSON "Undo" Activity + * @param int $user_id The ID of the ID of the WordPress User */ public static function handle_undo_request( $object, $user_id ) { if ( @@ -147,8 +148,9 @@ class Followers { /** * Add a new Follower * - * @param int $user_id The WordPress user + * @param int $user_id The ID of the WordPress User * @param string $actor The Actor URL + * * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { @@ -167,9 +169,10 @@ class Followers { /** * Remove a Follower * - * @param int $user_id The WordPress user_id - * @param string $actor The Actor URL - * @return bool|WP_Error True on success, false or WP_Error on failure. + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL + * + * @return bool|WP_Error True on success, false or WP_Error on failure. */ public static function remove_follower( $user_id, $actor ) { return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); @@ -178,7 +181,8 @@ class Followers { /** * Remove a Follower * - * @param string $actor The Actor URL + * @param string $actor The Actor URL + * * @return \Activitypub\Model\Follower The Follower object */ public static function get_follower( $actor ) { @@ -192,8 +196,9 @@ class Followers { * * @param string $actor The Actor URL * @param array $object The Activity object - * @param int $user_id The WordPress user_id + * @param int $user_id The ID of the WordPress User * @param Activitypub\Model\Follower $follower The Follower object + * * @return void */ public static function send_follow_response( $actor, $object, $user_id, $follower ) { @@ -218,11 +223,12 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id The WordPress user_id - * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT - * @param int $number Limts the result - * @param int $offset Offset - * @return array The Term list of Followers, the format depends on $output + * @param int $user_id The ID of the WordPress User + * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param int $number Limts the result + * @param int $offset Offset + * + * @return array The Term list of Followers, the format depends on $output */ public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); @@ -260,15 +266,23 @@ class Followers { /** * Count the total number of followers * - * @param int $user_id The WordPress user - * @return int The number of Followers + * @param int $user_id The ID of the WordPress User + * + * @return int The number of Followers */ public static function count_followers( $user_id ) { return count( self::get_followers( $user_id ) ); } + /** + * Returns all Inboxes fo a Users Followers + * + * @param int $user_id The ID of the WordPress User + * + * @return array The list of Inboxes + */ public static function get_inboxes( $user_id ) { - // get all Followers of a WordPress user + // get all Followers of a ID of the WordPress User $terms = new WP_Term_Query( array( 'taxonomy' => self::TAXONOMY, @@ -295,9 +309,9 @@ class Followers { } /** - * Undocumented function + * Migrate Followers * - * @param [type] $user_id + * @param int $user_id The ID of the WordPress User * @return void */ public static function migrate_followers( $user_id ) { diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 169e5f2..84c6b0a 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -8,32 +8,97 @@ use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Follower Class * + * This Object represents a single Follower. + * There is no direct reference to a WordPress User here. + * * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox */ class Follower { - + /** + * The Object ID + * + * @var int + */ private $id; + /** + * The Actor-URL of the Follower + * + * @var string + */ private $actor; + /** + * The Object slug + * + * This is a requirement of the Term-Meta but will not + * be actively used in the ActivityPub context. + * + * @var string + */ private $slug; + /** + * The Object Name + * + * This is the same as the Actor-URL + * + * @var string + */ private $name; + /** + * The Username + * + * @var string + */ private $username; + /** + * The Avatar URL + * + * @var string + */ private $avatar; + /** + * The URL to the Followers Inbox + * + * @var string + */ private $inbox; + /** + * The URL to the Servers Shared-Inbox + * + * If the Server does not support Shared-Inboxes, + * the Inbox will be stored. + * + * @var string + */ private $shared_inbox; + /** + * The date, the Follower was updated + * + * @var string untixtimestamp + */ private $updated_at; + /** + * The complete Remote-Profile of the Follower + * + * @var array + */ private $meta; + /** + * Maps the meta fields to the local db fields + * + * @var array + */ private $map_meta = array( 'name' => 'name', 'preferredUsername' => 'username', From 764a09104679bddf5b9b8e07141f349d3b2a6eec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 09:31:28 +0200 Subject: [PATCH 24/60] fix unit tests --- includes/class-activity-dispatcher.php | 2 +- includes/collection/class-followers.php | 6 ++ includes/peer/class-followers.php | 2 +- ...-class-activitypub-activity-dispatcher.php | 7 +- tests/test-class-db-activitypub-followers.php | 78 ++++++++++++++++--- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 73e49b5..1e31616 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -67,7 +67,7 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = FollowerCollection::get_inboxes( $user_id ); + $inboxes = Followers::get_inboxes( $user_id ); foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 78d0139..9669398 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -240,6 +240,8 @@ class Followers { 'object_ids' => $user_id, 'number' => $number, 'offset' => $offset, + 'orderby' => 'id', + 'order' => 'ASC', ) ); @@ -294,6 +296,10 @@ class Followers { $terms = $terms->get_terms(); + if ( ! $terms ) { + return array(); + } + global $wpdb; $results = $wpdb->get_col( $wpdb->prepare( diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index 8941cc4..e0e6ddb 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -23,7 +23,7 @@ class Followers { public static function add_follower( $actor, $author_id ) { _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' ); - return \Activitypub\Collection\Followers::add_followers( $author_id, $actor ); + return \Activitypub\Collection\Followers::add_follower( $author_id, $actor ); } public static function remove_follower( $actor, $author_id ) { diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 0e503e5..bde52ab 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -5,17 +5,22 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', + 'prefferedUsername' => 'username', ), 'jon@example.com' => array( 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', + 'prefferedUsername' => 'jon', ), ); public function test_dispatch_activity() { $followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' ); - \update_user_meta( 1, 'activitypub_followers', $followers ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } $post = \wp_insert_post( array( diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index c502bf2..0591178 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -1,15 +1,37 @@ 'Person', - 'id' => 'http://sally.example.org', - 'name' => 'Sally Smith', - ); - \update_user_meta( 1, 'activitypub_followers', $followers ); + public static $users = array( + 'username@example.org' => array( + 'url' => 'https://example.org/users/username', + 'inbox' => 'https://example.org/users/username/inbox', + 'name' => 'username', + 'prefferedUsername' => 'username', + ), + 'jon@example.com' => array( + 'url' => 'https://example.com/author/jon', + 'inbox' => 'https://example.com/author/jon/inbox', + 'name' => 'jon', + 'prefferedUsername' => 'jon', + ), + 'sally@example.org' => array( + 'url' => 'http://sally.example.org', + 'inbox' => 'http://sally.example.org/inbox', + 'name' => 'jon', + 'prefferedUsername' => 'jon', + ), + ); - $db_followers = \Activitypub\Peer\Followers::get_followers( 1 ); + public function test_get_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 3, \count( $db_followers ) ); @@ -17,11 +39,43 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } public function test_add_follower() { - $follower = 'https://example.com/author/' . \time(); - \Activitypub\Peer\Followers::add_follower( $follower, 1 ); + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $db_followers = \Activitypub\Peer\Followers::get_followers( 1 ); + $follower = 'https://example.com/author/' . \time(); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertContains( $follower, $db_followers ); } + + public static function http_request_host_is_external( $in, $host ) { + if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { + return true; + } + return $in; + } + public static function http_request_args( $args, $url ) { + if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) { + $args['reject_unsafe_urls'] = false; + } + return $args; + } + + public static function pre_http_request( $preempt, $request, $url ) { + return array( + 'headers' => array( + 'content-type' => 'text/json', + ), + 'body' => '', + 'response' => array( + 'code' => 202, + ), + ); + } + + public static function http_response( $response, $args, $url ) { + return $response; + } } From da63763ddcba83d3df488c62d9693cf4b100e18e Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 10:54:21 +0200 Subject: [PATCH 25/60] Compat: only disable Jetpack's image CDN via filter This follows the discussion in #307. 1. Do not disable Jetpack's image CDN in ActivityPub requests by default. 2. Add a new filter, activitypub_images_use_jetpack_image_cdn, that site owners can use to disable Jetpack's Image CDN if they'd like to. 3. Extract image getting into its own method for improved readability. --- includes/model/class-post.php | 87 ++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index a69e139..254fb8c 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -305,41 +305,16 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { - $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); - - /** - * If you use the Jetpack plugin and its Image CDN, aka Photon, - * the image strings returned will use the Photon URL. - * We don't want that since Fediverse instances already do caching on their end. - * Let the CDN only be used for visitors of the site. - * - * Old versions of Jetpack used the Jetpack_Photon class to do this. - * New versions use the Image_CDN class. - * Let's handle both. - */ - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); - } - - $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); - - // Re-enable Photon now that the image URL has been built. - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); - } - - $mimetype = \get_post_mime_type( $id ); - + $thumbnail = $this->get_image( $id ); if ( $thumbnail ) { - $image = array( - 'type' => 'Image', - 'url' => $thumbnail[0], + $mimetype = \get_post_mime_type( $id ); + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + $image = array( + 'type' => 'Image', + 'url' => $thumbnail[0], 'mediaType' => $mimetype, ); + if ( $alt ) { $image['name'] = $alt; } @@ -352,6 +327,54 @@ class Post { return $images; } + /** + * Return details about an image attachment. + * + * Can return a CDNized URL if Jetpack's image CDN is active. + * This can be disabled with a filter. + * + * @param int $id The attachment ID. + * + * @return array|false Array of image data, or boolean false if no image is available. + */ + public function get_image( $id ) { + /** + * Allow bypassing Jetpack's Image CDN when returning image URLs. + * + * @param bool $should_use_cdn Whether to use the Jetpack Image CDN. True by default. + */ + $should_use_cdn = apply_filters( 'activitypub_images_use_jetpack_image_cdn', true ); + + if ( $should_use_cdn ) { + // Return the full URL, using a CDN URL if Jetpack's image CDN is active. + return \wp_get_attachment_image_src( $id, 'full' ); + } + + /* + * Disable Jetpacks image CDN image processing for this request. + * + * Note: old versions of Jetpack used the Jetpack_Photon class to do this. + * New versions use the Image_CDN class. + * Let's handle both. + */ + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { + \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); + } + + $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); + + // Re-enable Photon now that the image URL has been built. + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { + \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); + } + + return $thumbnail; + } + /** * Returns a list of Tags, used in the Post * From 3fa4a7b58ed3520d567d1d868c8e690db73e271a Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 10:56:17 +0200 Subject: [PATCH 26/60] Add readme entry --- readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.txt b/readme.txt index 9c7661d..b73ec40 100644 --- a/readme.txt +++ b/readme.txt @@ -113,6 +113,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += Next = + +* Compatibility: add filter to allow disabling Jetpack's image CDN when returning images in ActivityPub requests. + = 0.17.0 = * Fix type-selector From 2afe74b29bf49e5d3745a5f21f10db09892c3001 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 11:03:33 +0200 Subject: [PATCH 27/60] Compatibility: the plugin is compatible with WP 6.2. --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 9c7661d..6fe6189 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: pfefferle, mediaformat, akirk, automattic Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 -Tested up to: 6.1 +Tested up to: 6.2 Stable tag: 0.17.0 Requires PHP: 5.6 License: MIT From 6ee59d2a10e5e1d53b3cc93b30850d98a1403145 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 11:04:32 +0200 Subject: [PATCH 28/60] Add changelog --- readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.txt b/readme.txt index 6fe6189..e89c768 100644 --- a/readme.txt +++ b/readme.txt @@ -113,6 +113,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += Next = + +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. + = 0.17.0 = * Fix type-selector From d1f6973d9b02869c9e59e7cc4221265d3e5c22b9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 11:59:08 +0200 Subject: [PATCH 29/60] re-add mention functionality not perfect but works as expected --- includes/class-activity-dispatcher.php | 6 ++- includes/class-mention.php | 54 +++++++++++++++++++++++++- includes/functions.php | 13 +++++-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 1e31616..e71ac19 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -67,7 +67,11 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); diff --git a/includes/class-mention.php b/includes/class-mention.php index e0930cc..7167d14 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -1,6 +1,8 @@ Date: Tue, 25 Apr 2023 20:44:54 +0200 Subject: [PATCH 30/60] ignore `www` subdomain when comparing hosts fix #290 --- includes/rest/class-webfinger.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index d41cf25..10dcfa4 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -54,15 +54,16 @@ class Webfinger { $resource = \str_replace( 'acct:', '', $resource ); $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \substr( \strrchr( $resource, '@' ), 1 ); + $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); - if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) { + if ( $blog_host !== $resource_host ) { return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); } $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); - if ( ! $user || ! user_can( $user, 'publish_posts' ) ) { + if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); } From 98619dc3198181ad44736afdb82cf74ae31a296d Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 26 Apr 2023 10:08:22 +0200 Subject: [PATCH 31/60] Protect img tags from replacing mentions --- includes/class-mention.php | 6 ++++++ tests/test-class-activitypub-mention.php | 1 + 2 files changed, 7 insertions(+) diff --git a/includes/class-mention.php b/includes/class-mention.php index e0930cc..9c93d80 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -46,6 +46,12 @@ class Mention { $the_content ); + $the_content = preg_replace_callback( + '#]+>#i', + $protect, + $the_content + ); + $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content ); $the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); diff --git a/tests/test-class-activitypub-mention.php b/tests/test-class-activitypub-mention.php index 6f6b9ff..ca7395f 100644 --- a/tests/test-class-activitypub-mention.php +++ b/tests/test-class-activitypub-mention.php @@ -31,6 +31,7 @@ ENDPRE; array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), + array( 'hallo https://notiz.blog/@pfefferle/ test', 'hallo https://notiz.blog/@pfefferle/ test' ), array( $code, $code ), array( $pre, $pre ), ); From e16e119e6c5a909763e2b31b86331d23181911fe Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Apr 2023 10:45:35 +0200 Subject: [PATCH 32/60] Switch to general actions and filter As a result, we will not modify the images within the ActivityPub plugin, but the hooks will allow third-parties to do it on their end. See discussion: https://github.com/pfefferle/wordpress-activitypub/pull/309#issuecomment-1521488186 --- includes/model/class-post.php | 58 +++++++++++++++++------------------ readme.txt | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 254fb8c..c4632b6 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -305,7 +305,22 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { - $thumbnail = $this->get_image( $id ); + $image_size = 'full'; + + /** + * Filter the image URL returned for each post. + * + * @param array|false $thumbnail The image URL, or false if no image is available. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. + */ + $thumbnail = apply_filters( + 'activitypub_get_image', + $this->get_image( $id, $image_size ), + $id, + $image_size + ); + if ( $thumbnail ) { $mimetype = \get_post_mime_type( $id ); $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); @@ -333,44 +348,29 @@ class Post { * Can return a CDNized URL if Jetpack's image CDN is active. * This can be disabled with a filter. * - * @param int $id The attachment ID. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. * * @return array|false Array of image data, or boolean false if no image is available. */ - public function get_image( $id ) { + public function get_image( $id, $image_size = 'full' ) { /** - * Allow bypassing Jetpack's Image CDN when returning image URLs. + * Hook into the image retrieval process. Before image retrieval. * - * @param bool $should_use_cdn Whether to use the Jetpack Image CDN. True by default. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. */ - $should_use_cdn = apply_filters( 'activitypub_images_use_jetpack_image_cdn', true ); + do_action( 'activitypub_get_image_pre', $id, $image_size ); - if ( $should_use_cdn ) { - // Return the full URL, using a CDN URL if Jetpack's image CDN is active. - return \wp_get_attachment_image_src( $id, 'full' ); - } + $thumbnail = \wp_get_attachment_image_src( $id, $image_size ); - /* - * Disable Jetpacks image CDN image processing for this request. + /** + * Hook into the image retrieval process. After image retrieval. * - * Note: old versions of Jetpack used the Jetpack_Photon class to do this. - * New versions use the Image_CDN class. - * Let's handle both. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. */ - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); - } - - $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); - - // Re-enable Photon now that the image URL has been built. - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); - } + do_action( 'activitypub_get_image_pre', $id, $image_size ); return $thumbnail; } diff --git a/readme.txt b/readme.txt index 569408f..492942a 100644 --- a/readme.txt +++ b/readme.txt @@ -115,7 +115,7 @@ Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github = Next = -* Compatibility: add filter to allow disabling Jetpack's image CDN when returning images in ActivityPub requests. +* Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. = 0.17.0 = From bd75603fc77ce746ea7670fd7ee564b5d2f0c9b1 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Apr 2023 10:47:49 +0200 Subject: [PATCH 33/60] Remove comment about Jetpack's Photon --- includes/model/class-post.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index c4632b6..4294996 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -345,9 +345,6 @@ class Post { /** * Return details about an image attachment. * - * Can return a CDNized URL if Jetpack's image CDN is active. - * This can be disabled with a filter. - * * @param int $id The attachment ID. * @param string $image_size The image size to retrieve. Set to 'full' by default. * From 4a4a06de378f44dcdd9836506d0aca55ef62febb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:22:44 +0200 Subject: [PATCH 34/60] get_follower requires user_id check --- includes/collection/class-followers.php | 23 +++++++++++++++---- tests/test-class-db-activitypub-followers.php | 17 ++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 9669398..98971b9 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -183,12 +183,27 @@ class Followers { * * @param string $actor The Actor URL * - * @return \Activitypub\Model\Follower The Follower object + * @return \Activitypub\Model\Follower The Follower object */ - public static function get_follower( $actor ) { - $term = get_term_by( 'name', $actor, self::TAXONOMY ); + public static function get_follower( $user_id, $actor ) { + $terms = new WP_Term_Query( + array( + 'name' => $actor, + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => 1, + ) + ); - return new Follower( $term->name ); + $term = $terms->get_terms(); + + if ( is_array( $term ) && ! empty( $term ) ) { + $term = reset( $term ); + return new Follower( $term->name ); + } + + return null; } /** diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 0591178..97318d4 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -50,6 +50,23 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertContains( $follower, $db_followers ); } + public function test_get_follower() { + $followers = array( 'https://example.com/author/jon' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); + $this->assertNull( $follower ); + } + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { return true; From 0ee1266c30cb292fbab25485ac8f82aaa492d8c7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:23:28 +0200 Subject: [PATCH 35/60] add sanitize callbacks --- includes/collection/class-followers.php | 49 ++++++++++++++++++++++--- includes/model/class-follower.php | 8 ++-- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 98971b9..f520702 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -2,6 +2,7 @@ namespace Activitypub\Collection; use WP_Error; +use Exception; use WP_Term_Query; use Activitypub\Webfinger; use Activitypub\Model\Activity; @@ -67,7 +68,9 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_displayname' ), + 'sanitize_callback' => function( $value ) { + return sanitize_user( $value ); + }, ) ); @@ -77,7 +80,9 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_username' ), + 'sanitize_callback' => function( $value ) { + return sanitize_user( $value, true ); + }, ) ); @@ -87,7 +92,13 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_avatar' ), + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return ''; + } + + return esc_url_raw( $value ); + }, ) ); @@ -97,7 +108,29 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_inbox' ), + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + throw new Exception( '"inbox" has to be a valid URL' ); + } + + return esc_url_raw( $value ); + }, + ) + ); + + register_term_meta( + self::TAXONOMY, + 'shared_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return null; + } + + return esc_url_raw( $value ); + }, ) ); @@ -107,7 +140,13 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_updated_at' ), + 'sanitize_callback' => function( $value ) { + if ( ! is_numeric( $value ) && (int) $value !== $value ) { + $value = strtotime( 'now' ); + } + + return $value; + }, ) ); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 84c6b0a..f3f8f7b 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -229,21 +229,21 @@ class Follower { foreach ( $this->map_meta as $remote => $internal ) { if ( ! empty( $meta[ $remote ] ) ) { - update_term_meta( $this->id, $internal, esc_html( $meta[ $remote ] ), true ); + update_term_meta( $this->id, $internal, $meta[ $remote ], true ); $this->$internal = $meta[ $remote ]; } } if ( ! empty( $meta['icon']['url'] ) ) { - update_term_meta( $this->id, 'avatar', esc_url_raw( $meta['icon']['url'] ), true ); + update_term_meta( $this->id, 'avatar', $meta['icon']['url'], true ); $this->avatar = $meta['icon']['url']; } if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['endpoints']['sharedInbox'] ), true ); + update_term_meta( $this->id, 'shared_inbox', $meta['endpoints']['sharedInbox'], true ); $this->shared_inbox = $meta['endpoints']['sharedInbox']; } elseif ( ! empty( $meta['inbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['inbox'] ), true ); + update_term_meta( $this->id, 'shared_inbox', $meta['inbox'], true ); $this->shared_inbox = $meta['inbox']; } From b8c86915b5dc79b39c8b412160067b57171ed99b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:24:27 +0200 Subject: [PATCH 36/60] add missing phpdoc --- includes/collection/class-followers.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index f520702..cd7f5f7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -208,8 +208,8 @@ class Followers { /** * Remove a Follower * - * @param int $user_id The ID of the WordPress User - * @param string $actor The Actor URL + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL * * @return bool|WP_Error True on success, false or WP_Error on failure. */ @@ -220,7 +220,8 @@ class Followers { /** * Remove a Follower * - * @param string $actor The Actor URL + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL * * @return \Activitypub\Model\Follower The Follower object */ From ec822535c9d49e1fc563facd42df0531c6f35d08 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 09:57:50 +0200 Subject: [PATCH 37/60] Follower object should not make any remote calls --- includes/collection/class-followers.php | 10 +- includes/functions.php | 8 +- includes/model/class-follower.php | 94 ++++++++++--------- includes/table/class-followers.php | 8 ++ tests/test-class-db-activitypub-followers.php | 39 +++++++- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index cd7f5f7..3eb09d8 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -10,6 +10,7 @@ use Activitypub\Model\Follower; use function Activitypub\safe_remote_get; use function Activitypub\safe_remote_post; +use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection @@ -193,7 +194,14 @@ class Followers { * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { + $meta = get_remote_metadata_by_actor( $actor ); + + if ( ! $meta || is_wp_error( $meta ) || ! is_array( $meta ) ) { + return $meta; + } + $follower = new Follower( $actor ); + $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -286,8 +294,6 @@ class Followers { * @return array The Term list of Followers, the format depends on $output */ public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { - //self::migrate_followers( $user_id ); - $terms = new WP_Term_Query( array( 'taxonomy' => self::TAXONOMY, diff --git a/includes/functions.php b/includes/functions.php index 09d3911..3a457a9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -102,12 +102,10 @@ function get_webfinger_resource( $user_id ) { * Requests the Meta-Data from the Actors profile * * @param string $actor The Actor URL - * @param bool $cache Enable/Disable caching of the meta. - * This does not effect Error-Caching. * * @return array The Actor profile as array */ -function get_remote_metadata_by_actor( $actor, $cache = false ) { +function get_remote_metadata_by_actor( $actor ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); if ( $pre ) { return $pre; @@ -161,9 +159,7 @@ function get_remote_metadata_by_actor( $actor, $cache = false ) { $metadata = \wp_remote_retrieve_body( $response ); $metadata = \json_decode( $metadata, true ); - if ( true === $cache ) { - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - } + \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); if ( ! $metadata ) { $metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor ); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index f3f8f7b..d4fc70f 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -3,8 +3,6 @@ namespace Activitypub\Model; use Activitypub\Collection\Followers; -use function Activitypub\get_remote_metadata_by_actor; - /** * ActivityPub Follower Class * @@ -111,16 +109,14 @@ class Follower { * @param WP_Post $post */ public function __construct( $actor ) { - $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); - $this->actor = $actor; + $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); + if ( $term ) { $this->id = $term->term_id; $this->slug = $term->slug; $this->meta = json_decode( $term->meta ); - } else { - $this->slug = sanitize_title( $actor ); } } @@ -147,46 +143,72 @@ class Follower { } } + public function from_meta( $meta ) { + $this->meta = $meta; + + foreach ( $this->map_meta as $remote => $internal ) { + if ( ! empty( $meta[ $remote ] ) ) { + $this->$internal = $meta[ $remote ]; + } + } + + if ( ! empty( $meta['icon']['url'] ) ) { + $this->avatar = $meta['icon']['url']; + } + + if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { + $this->shared_inbox = $meta['endpoints']['sharedInbox']; + } elseif ( ! empty( $meta['inbox'] ) ) { + $this->shared_inbox = $meta['inbox']; + } + + $this->updated_at = \strtotime( 'now' ); + } + public function get( $attribute ) { if ( $this->$attribute ) { return $this->$attribute; } - if ( ! $this->id ) { - $this->$attribute = $this->get_meta_by( $attribute ); - return $this->$attribute; + $attribute = get_term_meta( $this->id, $attribute, true ); + if ( $attribute ) { + $this->$attribute = $attribute; + return $attribute; } - $this->$attribute = get_term_meta( $this->id, $attribute, true ); - return $this->$attribute; + $attribute = $this->get_meta_by( $attribute ); + if ( $attribute ) { + $this->$attribute = $attribute; + return $attribute; + } + + return null; } - public function get_meta_by( $attribute, $force = false ) { - $meta = $this->get_meta( $force ); + public function get_meta_by( $attribute ) { + $meta = $this->get_meta(); + // try mapped data ($this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { return $meta[ $remote ]; } } + // try ActivityPub attribtes + if ( ! empty( $this->map_meta[ $attribute ] ) ) { + return $this->map_meta[ $attribute ]; + } + return null; } - public function get_meta( $force = false ) { - if ( $this->meta && false === (bool) $force ) { + public function get_meta() { + if ( $this->meta ) { return $this->meta; } - $remote_data = get_remote_metadata_by_actor( $this->actor ); - - if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { - $remote_data = array(); - } - - $this->meta = $remote_data; - - return $this->meta; + return null; } public function update() { @@ -225,28 +247,12 @@ class Follower { } protected function update_term_meta() { - $meta = $this->get_meta(); + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); - foreach ( $this->map_meta as $remote => $internal ) { - if ( ! empty( $meta[ $remote ] ) ) { - update_term_meta( $this->id, $internal, $meta[ $remote ], true ); - $this->$internal = $meta[ $remote ]; + foreach ( $attributes as $attribute ) { + if ( $this->get( $attribute ) ) { + update_term_meta( $this->id, $attribute, $this->get( $attribute ), true ); } } - - if ( ! empty( $meta['icon']['url'] ) ) { - update_term_meta( $this->id, 'avatar', $meta['icon']['url'], true ); - $this->avatar = $meta['icon']['url']; - } - - if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', $meta['endpoints']['sharedInbox'], true ); - $this->shared_inbox = $meta['endpoints']['sharedInbox']; - } elseif ( ! empty( $meta['inbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', $meta['inbox'], true ); - $this->shared_inbox = $meta['inbox']; - } - - update_term_meta( $this->id, 'updated_at', \strtotime( 'now' ), true ); } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 13d6a91..c39b45f 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -77,6 +77,14 @@ class Followers extends WP_List_Table { ); } + public function column_identifier( $item ) { + return sprintf( + '%s', + $item['identifier'], + $item['identifier'] + ); + } + public function column_cb( $item ) { return sprintf( '', esc_attr( $item['identifier'] ) ); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 97318d4..640c4d1 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -13,14 +13,37 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { 'name' => 'jon', 'prefferedUsername' => 'jon', ), + 'doe@example.org' => array( + 'url' => 'https://example.org/author/doe', + 'inbox' => 'https://example.org/author/doe/inbox', + 'name' => 'doe', + 'prefferedUsername' => 'doe', + ), 'sally@example.org' => array( 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), + '12345@example.com' => array( + 'url' => 'https://12345.example.com', + 'inbox' => 'https://12345.example.com/inbox', + 'name' => '12345', + 'prefferedUsername' => '12345', + ), ); + public function set_up() { + parent::set_up(); + add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + _delete_all_posts(); + } + + public function tear_down() { + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); + parent::tear_down(); + } + public function test_get_followers() { $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); @@ -28,7 +51,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( 1, $follower ); + $response = \Activitypub\Collection\Followers::add_follower( 1, $follower ); } $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); @@ -42,7 +65,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $follower = 'https://example.com/author/' . \time(); + $follower = 'https://12345.example.com'; \Activitypub\Collection\Followers::add_follower( 1, $follower ); $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); @@ -95,4 +118,16 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { public static function http_response( $response, $args, $url ) { return $response; } + + public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { + if ( isset( self::$users[ $actor ] ) ) { + return self::$users[ $actor ]; + } + foreach ( self::$users as $username => $data ) { + if ( $data['url'] === $actor ) { + return $data; + } + } + return $pre; + } } From 230aaa5b244afc25058fd9284e50f5a96b452391 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:34:54 +0200 Subject: [PATCH 38/60] prepare migration --- activitypub.php | 2 + includes/class-http.php | 93 +++++++++++++++++++++++++ includes/class-migration.php | 68 ++++++++++++++++++ includes/collection/class-followers.php | 16 ----- includes/functions.php | 53 +------------- 5 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 includes/class-http.php create mode 100644 includes/class-migration.php diff --git a/activitypub.php b/activitypub.php index 6439ec8..41c01dd 100644 --- a/activitypub.php +++ b/activitypub.php @@ -32,8 +32,10 @@ function init() { \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; + require_once \dirname( __FILE__ ) . '/includes/class-http.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; + require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; diff --git a/includes/class-http.php b/includes/class-http.php new file mode 100644 index 0000000..7b570a2 --- /dev/null +++ b/includes/class-http.php @@ -0,0 +1,93 @@ + 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Digest' => "SHA-256=$digest", + 'Signature' => $signature, + 'Date' => $date, + ), + 'body' => $body, + ); + + $response = \wp_safe_remote_get( $url, $args ); + $code = \wp_remote_retrieve_response_code( $response ); + + if ( 400 >= $code && 500 <= $code ) { + $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); + } + + \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); + + return $response; + } + + /** + * Send a GET Request with the needed HTTP Headers + * + * @param string $url The URL endpoint + * @param int $user_id The WordPress User-ID + * + * @return array|WP_Error The GET Response or an WP_ERROR + */ + public static function get( $url, $user_id ) { + $date = \gmdate( 'D, d M Y H:i:s T' ); + $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); + + $wp_version = \get_bloginfo( 'version' ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $args = array( + 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ), + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Signature' => $signature, + 'Date' => $date, + ), + ); + + $response = \wp_safe_remote_get( $url, $args ); + $code = \wp_remote_retrieve_response_code( $response ); + + if ( 400 >= $code && 500 <= $code ) { + $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); + } + + \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); + + return $response; + } +} diff --git a/includes/class-migration.php b/includes/class-migration.php new file mode 100644 index 0000000..f7e3003 --- /dev/null +++ b/includes/class-migration.php @@ -0,0 +1,68 @@ + 'ID' ) ) as $user_id ) { + $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + + if ( $followes ) { + foreach ( $followes as $follower ) { + Collection\Followers::add_follower( $user_id, $follower ); + } + } + } + } +} diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 3eb09d8..597e0a7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -374,20 +374,4 @@ class Followers { return array_filter( $results ); } - - /** - * Migrate Followers - * - * @param int $user_id The ID of the WordPress User - * @return void - */ - public static function migrate_followers( $user_id ) { - $followes = get_user_meta( $user_id, 'activitypub_followers', true ); - - if ( $followes ) { - foreach ( $followes as $follower ) { - self::add_follower( $user_id, $follower ); - } - } - } } diff --git a/includes/functions.php b/includes/functions.php index 3a457a9..15068d0 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -33,58 +33,11 @@ function get_context() { } function safe_remote_post( $url, $body, $user_id ) { - $date = \gmdate( 'D, d M Y H:i:s T' ); - $digest = \Activitypub\Signature::generate_digest( $body ); - $signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); - - $wp_version = \get_bloginfo( 'version' ); - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - 'timeout' => 100, - 'limit_response_size' => 1048576, - 'redirection' => 3, - 'user-agent' => "$user_agent; ActivityPub", - 'headers' => array( - 'Accept' => 'application/activity+json', - 'Content-Type' => 'application/activity+json', - 'Digest' => "SHA-256=$digest", - 'Signature' => $signature, - 'Date' => $date, - ), - 'body' => $body, - ); - - $response = \wp_safe_remote_post( $url, $args ); - - \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); - - return $response; + return \Activitypub\Http::post( $url, $body, $user_id ); } function safe_remote_get( $url, $user_id ) { - $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); - - $wp_version = \get_bloginfo( 'version' ); - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ), - 'limit_response_size' => 1048576, - 'redirection' => 3, - 'user-agent' => "$user_agent; ActivityPub", - 'headers' => array( - 'Accept' => 'application/activity+json', - 'Content-Type' => 'application/activity+json', - 'Signature' => $signature, - 'Date' => $date, - ), - ); - - $response = \wp_safe_remote_get( $url, $args ); - - \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); - - return $response; + return \Activitypub\Http::get( $url, $user_id ); } /** @@ -149,7 +102,7 @@ function get_remote_metadata_by_actor( $actor ) { return 3; }; add_filter( 'activitypub_remote_get_timeout', $short_timeout ); - $response = \Activitypub\safe_remote_get( $actor, $user_id ); + $response = Http::get( $actor, $user_id ); remove_filter( 'activitypub_remote_get_timeout', $short_timeout ); if ( \is_wp_error( $response ) ) { \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. From 02e3488fd7b7036a8b1faefe7dd618c181cc4440 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:45:38 +0200 Subject: [PATCH 39/60] remove debugging stuff --- includes/class-migration.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index f7e3003..f3aac10 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -35,14 +35,14 @@ class Migration { */ public static function maybe_migrate() { if ( self::is_latest_version() ) { - //return; + return; } $version_from_db = self::get_version(); - //if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { + if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { self::migrate_to_1_0_0(); - //} + } update_option( 'activitypub_db_version', self::$target_version ); } From fb3d6d26349e97cd4e4a056c9b8934ff5b229d07 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:49:39 +0200 Subject: [PATCH 40/60] fix phpcs --- includes/class-http.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 7b570a2..fd8df86 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -43,7 +43,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 >= $code && 500 <= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -52,7 +52,7 @@ class Http { return $response; } - /** + /** * Send a GET Request with the needed HTTP Headers * * @param string $url The URL endpoint @@ -82,7 +82,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 >= $code && 500 <= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } From 5ef41dea027e8199e4de9d7d371628e475255c33 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 09:54:09 +0200 Subject: [PATCH 41/60] schedule migration because it takes quite some time --- activitypub.php | 4 +++- includes/class-activity-dispatcher.php | 3 +++ includes/class-admin.php | 7 +++++++ includes/class-migration.php | 4 ++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 41c01dd..7523bcc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -35,7 +35,6 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-http.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; - require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; @@ -43,6 +42,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; + require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; + Migration::init(); + require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; Activity_Dispatcher::init(); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index e71ac19..422240d 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -61,6 +61,9 @@ class Activity_Dispatcher { * @return void */ public static function send_activity( Post $activitypub_post, $activity_type ) { + // check if a migration is needed before sending new posts + \Activitypub\Migration::maybe_migrate(); + // get latest version of post $user_id = $activitypub_post->get_post_author(); diff --git a/includes/class-admin.php b/includes/class-admin.php index c8c5813..c66c0f0 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,6 +13,7 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); + \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } @@ -144,6 +145,12 @@ class Admin { ); } + public static function schedule_migration() { + if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); + } + } + public static function add_settings_help_tab() { require_once \dirname( __FILE__ ) . '/help.php'; } diff --git a/includes/class-migration.php b/includes/class-migration.php index f3aac10..953ab6f 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -9,6 +9,10 @@ class Migration { */ private static $target_version = '1.0.0'; + public static function init() { + \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); + } + public static function get_target_version() { return self::$target_version; } From f2355cd9607c1d45708461cffcc230d8d2cdf3de Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 11:23:40 +0200 Subject: [PATCH 42/60] fix typo --- includes/class-http.php | 2 +- includes/collection/class-followers.php | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index fd8df86..01c2042 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -40,7 +40,7 @@ class Http { 'body' => $body, ); - $response = \wp_safe_remote_get( $url, $args ); + $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); if ( 400 >= $code && 500 <= $code ) { diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 597e0a7..0bd8d03 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -4,6 +4,7 @@ namespace Activitypub\Collection; use WP_Error; use Exception; use WP_Term_Query; +use Activitypub\Http; use Activitypub\Webfinger; use Activitypub\Model\Activity; use Activitypub\Model\Follower; @@ -269,6 +270,11 @@ class Followers { // @todo send error message //} + if ( isset( $object['user_id'] ) ) { + unset( $object['user_id'] ); + unset( $object['@context'] ); + } + // get inbox $inbox = $follower->get_inbox(); @@ -280,7 +286,7 @@ class Followers { $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity = $activity->to_simple_json(); - $response = safe_remote_post( $inbox, $activity, $user_id ); + $response = Http::post( $inbox, $activity, $user_id ); } /** From 02e0acdf6917d05864b63748aac16dca37ce0aed Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 14:39:33 +0200 Subject: [PATCH 43/60] fix indents --- includes/class-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index f1f7df8..2b7ee2a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -14,7 +14,7 @@ class Admin { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) ); - \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); + \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } From 7c47f9a07c3640d06af0559a0a07ed159e88ab9c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 15:12:30 +0200 Subject: [PATCH 44/60] clean up admin settings --- includes/class-admin.php | 38 +++++++++++++++++-------------------- includes/functions.php | 19 ------------------- templates/user-settings.php | 29 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 templates/user-settings.php diff --git a/includes/class-admin.php b/includes/class-admin.php index 2b7ee2a..66966f9 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,6 +1,8 @@ ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); - ?> -

      - ID ); - ?> -
      - - - - - - - ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); + + \load_template( + ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php', + true, + array( + 'description' => $description, + ) + ); } public static function save_user_description( $user_id ) { - if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) ) { + if ( isset( $_REQUEST['_apnonce'] ) && ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) ) { return false; } if ( ! current_user_can( 'edit_user', $user_id ) ) { diff --git a/includes/functions.php b/includes/functions.php index a436496..4d44853 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -241,25 +241,6 @@ function get_follower_inboxes( $user_id, $cc = array() ) { return $inboxes; } -function get_identifier_settings( $user_id ) { - ?> - - - - - - - -
      - - -

      or

      - -

      -
      - + + + + + + + + + + + + + +
      + + +

      + or + +

      + +

      +
      + + + +

      +
      From 9cd33ad544f1f739a88f2dbaf4776daa1a48c962 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 18:13:16 +0200 Subject: [PATCH 45/60] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 953ab6f..c241c92 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -60,7 +60,7 @@ class Migration { */ public static function migrate_to_1_0_0() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { - $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followes ) { foreach ( $followes as $follower ) { From be73f99b5968dcd2d12105fb95fc7bc4267331ba Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 18:13:59 +0200 Subject: [PATCH 46/60] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index c241c92..f6e6286 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -62,8 +62,8 @@ class Migration { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); - if ( $followes ) { - foreach ( $followes as $follower ) { + if ( $followers ) { + foreach ( $followers as $follower ) { Collection\Followers::add_follower( $user_id, $follower ); } } From 22946ec7798d39c5975b987db7fd9ecfe0cafe12 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:27:35 +0200 Subject: [PATCH 47/60] change migration script to match plugin version /cc @akirk --- activitypub.php | 35 +++++++++++++++++++++ includes/class-admin.php | 2 -- includes/class-migration.php | 57 ++++++++++++++++++++++++++--------- includes/model/class-post.php | 42 -------------------------- 4 files changed, 78 insertions(+), 58 deletions(-) diff --git a/activitypub.php b/activitypub.php index e754e93..6bf1783 100644 --- a/activitypub.php +++ b/activitypub.php @@ -150,3 +150,38 @@ function enable_buddypress_features() { Integration\Buddypress::init(); } add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); + +/** + * `get_plugin_data` wrapper + * + * @return array The plugin metadata array + */ +function get_plugin_meta( $default_headers = array() ) { + if ( ! $default_headers ) { + $default_headers = array( + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + ); + } + + return \get_file_data( __FILE__, $default_headers, 'plugin' ); +} + +/** + * Plugin Version Number used for caching. + */ +function get_plugin_version() { + $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); + + return $meta['Version']; +} diff --git a/includes/class-admin.php b/includes/class-admin.php index 8e19c37..e7e2dc4 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -54,8 +54,6 @@ class Admin { switch ( $tab ) { case 'settings': - Post::upgrade_post_content_template(); - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; case 'welcome': diff --git a/includes/class-migration.php b/includes/class-migration.php index f6e6286..21e315f 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -2,19 +2,12 @@ namespace Activitypub; class Migration { - /** - * Which internal datastructure version we are running on. - * - * @var int - */ - private static $target_version = '1.0.0'; - public static function init() { \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); } public static function get_target_version() { - return self::$target_version; + return plugin_version(); } public static function get_version() { @@ -45,20 +38,19 @@ class Migration { $version_from_db = self::get_version(); if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::migrate_to_1_0_0(); + self::migrate_from_0_17(); + self::migrate_from_0_16(); } - update_option( 'activitypub_db_version', self::$target_version ); + update_option( 'activitypub_db_version', self::get_target_version() ); } /** - * The Migration for Plugin Version 1.0.0 and DB Version 1.0.0 - * - * @since 5.0.0 + * Updates the DB-schema of the followers-list * * @return void */ - public static function migrate_to_1_0_0() { + public static function migrate_from_0_17() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -69,4 +61,41 @@ class Migration { } } } + + /** + * Updates the custom template to use shortcodes instead of the deprecated templates. + * + * @return void + */ + public static function migrate_from_0_16() { + // Get the custom template. + $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); + + // If the old content exists but is a blank string, we're going to need a flag to updated it even + // after setting it to the default contents. + $need_update = false; + + // If the old contents is blank, use the defaults. + if ( '' === $old_content ) { + $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT; + $need_update = true; + } + + // Set the new content to be the old content. + $content = $old_content; + + // Convert old templates to shortcodes. + $content = \str_replace( '%title%', '[ap_title]', $content ); + $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content ); + $content = \str_replace( '%content%', '[ap_content]', $content ); + $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content ); + $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content ); + $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content ); + $content = \str_replace( '%tags%', '[ap_hashtags]', $content ); + + // Store the new template if required. + if ( $content !== $old_content || $need_update ) { + \update_option( 'activitypub_custom_post_content', $content ); + } + } } diff --git a/includes/model/class-post.php b/includes/model/class-post.php index a69e139..db56a45 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -511,48 +511,6 @@ class Post { return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]"; } - // Upgrade from old template codes to shortcodes. - $content = self::upgrade_post_content_template(); - - return $content; - } - - /** - * Updates the custom template to use shortcodes instead of the deprecated templates. - * - * @return string the updated template content - */ - public static function upgrade_post_content_template() { - // Get the custom template. - $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); - - // If the old content exists but is a blank string, we're going to need a flag to updated it even - // after setting it to the default contents. - $need_update = false; - - // If the old contents is blank, use the defaults. - if ( '' === $old_content ) { - $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT; - $need_update = true; - } - - // Set the new content to be the old content. - $content = $old_content; - - // Convert old templates to shortcodes. - $content = \str_replace( '%title%', '[ap_title]', $content ); - $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content ); - $content = \str_replace( '%content%', '[ap_content]', $content ); - $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content ); - $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content ); - $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content ); - $content = \str_replace( '%tags%', '[ap_hashtags]', $content ); - - // Store the new template if required. - if ( $content !== $old_content || $need_update ) { - \update_option( 'activitypub_custom_post_content', $content ); - } - return $content; } } From 725fc0cecd9ad946470c039e84b8ffd806b7ba18 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:29:29 +0200 Subject: [PATCH 48/60] fix function call --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 21e315f..b65ed90 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,7 +7,7 @@ class Migration { } public static function get_target_version() { - return plugin_version(); + return get_plugin_version(); } public static function get_version() { From 654cdd41740238d335ec5599526a24be88b14d22 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:37:11 +0200 Subject: [PATCH 49/60] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index b65ed90..74e6ee2 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -37,10 +37,12 @@ class Migration { $version_from_db = self::get_version(); - if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::migrate_from_0_17(); + if ( version_compare( $version_from_db, '0.16.0', '<' ) ) { self::migrate_from_0_16(); } + if ( version_compare( $version_from_db, '0.17.0', '<' ) ) { + self::migrate_from_0_17(); + } update_option( 'activitypub_db_version', self::get_target_version() ); } From 66942e6c622ec6dc4778ca0f1697f87491df9dcb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 13:54:21 +0200 Subject: [PATCH 50/60] fix error detection --- includes/class-http.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 01c2042..798328b 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -43,7 +43,7 @@ class Http { $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 <= $code && 500 >= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -82,7 +82,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 <= $code && 500 >= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } From 077c43bf95917252648e7c83ec8e068defe0ffa0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 14:35:53 +0200 Subject: [PATCH 51/60] single migration scripts should not be public --- includes/class-migration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 74e6ee2..619fc87 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -52,7 +52,7 @@ class Migration { * * @return void */ - public static function migrate_from_0_17() { + private static function migrate_from_0_17() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -69,7 +69,7 @@ class Migration { * * @return void */ - public static function migrate_from_0_16() { + private static function migrate_from_0_16() { // Get the custom template. $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); From dea5f385613b67fa03498699785806c62c854320 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 14:39:25 +0200 Subject: [PATCH 52/60] better error handling --- includes/collection/class-followers.php | 69 +++++++++++++++++++------ includes/functions.php | 21 ++++++++ includes/model/class-follower.php | 62 +++++++++++++++++++++- includes/table/class-followers.php | 22 ++++---- 4 files changed, 146 insertions(+), 28 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 0bd8d03..e21ce2d 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -9,8 +9,7 @@ use Activitypub\Webfinger; use Activitypub\Model\Activity; use Activitypub\Model\Follower; -use function Activitypub\safe_remote_get; -use function Activitypub\safe_remote_post; +use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; /** @@ -152,6 +151,22 @@ class Followers { ) ); + register_term_meta( + self::TAXONOMY, + 'errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function( $value ) { + if ( ! is_string( $value ) ) { + throw new Exception( 'Error message is no valid string' ); + } + + return esc_sql( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_taxonomy' ); } @@ -197,12 +212,16 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( ! $meta || is_wp_error( $meta ) || ! is_array( $meta ) ) { - return $meta; + $follower = new Follower( $actor ); + + if ( is_tombstone( $meta ) ) { + return; + } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_error( $meta ); + } else { + $follower->from_meta( $meta ); } - $follower = new Follower( $actor ); - $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -299,19 +318,29 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { - $terms = new WP_Term_Query( - array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, - 'number' => $number, - 'offset' => $offset, - 'orderby' => 'id', - 'order' => 'ASC', - ) + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $hide_errors = false, $args = array() ) { + $defaults = array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => $number, + 'offset' => $offset, + 'orderby' => 'id', + 'order' => 'ASC', ); + if ( true === $hide_errors ) { + $defaults['meta_query'] = array( + array( + 'key' => 'errors', + 'compare' => 'NOT EXISTS', + ), + ); + } + + $args = wp_parse_args( $args, $defaults ); + $terms = new WP_Term_Query( $args ); + $items = array(); // change output format @@ -358,6 +387,12 @@ class Followers { 'hide_empty' => false, 'object_ids' => $user_id, 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'inbox', + 'compare' => 'EXISTS', + ), + ), ) ); diff --git a/includes/functions.php b/includes/functions.php index b88f8a8..5fa7ab3 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -211,3 +211,24 @@ function get_author_description( $user_id ) { } return $description; } + +/** + * Check for Tombstone Objects + * + * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox + * + * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request + * + * @return boolean true if HTTP-Code is 410 or 404 + */ +function is_tombstone( $wp_error ) { + if ( ! is_wp_error( $wp_error ) ) { + return false; + } + + if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) { + return true; + } + + return false; +} diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index d4fc70f..694489d 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -92,6 +92,22 @@ class Follower { */ private $meta; + /** + * The latest received error. + * + * This will only temporary and will saved to $this->errors + * + * @var string + */ + private $error; + + /** + * A list of errors + * + * @var array + */ + private $errors; + /** * Maps the meta fields to the local db fields * @@ -185,10 +201,39 @@ class Follower { return null; } + public function get_errors() { + if ( $this->errors ) { + return $this->errors; + } + + $this->errors = get_term_meta( $this->id, 'errors' ); + return $this->errors; + } + + public function count_errors() { + $errors = $this->get_errors(); + + if ( is_array( $errors ) && ! empty( $errors ) ) { + return count( $errors ); + } + + return 0; + } + + public function get_latest_error_message() { + $errors = $this->get_errors(); + + if ( is_array( $errors ) && ! empty( $errors ) ) { + return reset( $errors ); + } + + return ''; + } + public function get_meta_by( $attribute ) { $meta = $this->get_meta(); - // try mapped data ($this->map_meta) + // try mapped data (see $this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { return $meta[ $remote ]; @@ -251,8 +296,21 @@ class Follower { foreach ( $attributes as $attribute ) { if ( $this->get( $attribute ) ) { - update_term_meta( $this->id, $attribute, $this->get( $attribute ), true ); + update_term_meta( $this->id, $attribute, $this->get( $attribute ) ); } } + + if ( $this->error ) { + if ( is_string( $this->error ) ) { + $error = $this->error; + } elseif ( is_wp_error( $this->error ) ) { + $error = $this->error->get_error_message(); + } else { + $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + } + + add_term_meta( $this->id, 'errors', $error ); + } + } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index c39b45f..c9b5948 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -11,11 +11,13 @@ if ( ! \class_exists( '\WP_List_Table' ) ) { class Followers extends WP_List_Table { public function get_columns() { return array( - 'cb' => '', - 'avatar' => \__( 'Avatar', 'activitypub' ), - 'name' => \__( 'Name', 'activitypub' ), - 'username' => \__( 'Username', 'activitypub' ), - 'identifier' => \__( 'Identifier', 'activitypub' ), + 'cb' => '', + 'avatar' => \__( 'Avatar', 'activitypub' ), + 'name' => \__( 'Name', 'activitypub' ), + 'username' => \__( 'Username', 'activitypub' ), + 'identifier' => \__( 'Identifier', 'activitypub' ), + 'errors' => \__( 'Errors', 'activitypub' ), + 'latest-error' => \__( 'Latest Error Message', 'activitypub' ), ); } @@ -47,10 +49,12 @@ class Followers extends WP_List_Table { foreach ( $follower as $follower ) { $item = array( - 'avatar' => esc_attr( $follower->get_avatar() ), - 'name' => esc_attr( $follower->get_name() ), - 'username' => esc_attr( $follower->get_username() ), - 'identifier' => esc_attr( $follower->get_actor() ), + 'avatar' => esc_attr( $follower->get_avatar() ), + 'name' => esc_attr( $follower->get_name() ), + 'username' => esc_attr( $follower->get_username() ), + 'identifier' => esc_attr( $follower->get_actor() ), + 'errors' => $follower->count_errors(), + 'latest-error' => $follower->get_latest_error_message(), ); $this->items[] = $item; From be0f25f3d388d0878339f75a980d554cf67b851b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:50:16 +0200 Subject: [PATCH 53/60] fail if `get_remote_metadata_by_actor` returns error because it is not even possible to send `Accept` or `Reject` response. --- includes/collection/class-followers.php | 30 ++++++++----------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index e21ce2d..7412d52 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -212,16 +212,11 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - $follower = new Follower( $actor ); - - if ( is_tombstone( $meta ) ) { - return; - } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - $follower->set_error( $meta ); - } else { - $follower->from_meta( $meta ); + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + return $meta; } + $follower = new Follower( $actor ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -285,9 +280,11 @@ class Followers { * @return void */ public static function send_follow_response( $actor, $object, $user_id, $follower ) { - //if ( is_wp_error( $follower ) ) { - // @todo send error message - //} + if ( is_wp_error( $follower ) ) { + // it is not even possible to send a "Reject" because + // we can not get the Remote-Inbox + return; + } if ( isset( $object['user_id'] ) ) { unset( $object['user_id'] ); @@ -318,7 +315,7 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $hide_errors = false, $args = array() ) { + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) { $defaults = array( 'taxonomy' => self::TAXONOMY, 'hide_empty' => false, @@ -329,15 +326,6 @@ class Followers { 'order' => 'ASC', ); - if ( true === $hide_errors ) { - $defaults['meta_query'] = array( - array( - 'key' => 'errors', - 'compare' => 'NOT EXISTS', - ), - ); - } - $args = wp_parse_args( $args, $defaults ); $terms = new WP_Term_Query( $args ); From 72f72e79b85a4386bb90337677bf5cdf7342838f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:50:36 +0200 Subject: [PATCH 54/60] use custom (more error tolerant) version for migration --- includes/class-migration.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 619fc87..1e8c44b 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,6 +1,8 @@ set_error( $meta ); + } else { + $follower->from_meta( $meta ); + } + + $follower->upsert(); + + $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); } } } From 7127b0a5688c317de0a89a00dfc4ea46812e4b28 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:54:34 +0200 Subject: [PATCH 55/60] oops --- includes/collection/class-followers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 7412d52..c6c6223 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -217,6 +217,7 @@ class Followers { } $follower = new Follower( $actor ); + $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); From f07869c7d1b864a690dde6cd3d9d480858f04ca8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 15:11:20 +0200 Subject: [PATCH 56/60] be sure to always update date --- includes/model/class-follower.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 694489d..3a15690 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -265,6 +265,7 @@ class Follower { ) ); + $this->updated_at = \strtotime( 'now' ); $this->update_term_meta(); } From 144356bf8aef5c04eeda17ffca2f7c67a5b7f8dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 08:50:44 +0200 Subject: [PATCH 57/60] remove unused second param --- includes/class-mention.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-mention.php b/includes/class-mention.php index 96bb450..49f7860 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -69,7 +69,7 @@ class Mention { * @return string the final string */ public static function replace_with_links( $result ) { - $metadata = get_remote_metadata_by_actor( $result[0], true ); + $metadata = get_remote_metadata_by_actor( $result[0] ); if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) { $username = ltrim( $result[0], '@' ); if ( ! empty( $metadata['name'] ) ) { From 0e193914fa50e127460264f2f5281375c896a65d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 09:01:23 +0200 Subject: [PATCH 58/60] update URLs --- README.md | 8 ++++++-- readme.txt | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1b4461..d7a9662 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Contributors:** [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [akirk](https://profiles.wordpress.org/akirk/), [automattic](https://profiles.wordpress.org/automattic/) **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 -**Tested up to:** 6.1 +**Tested up to:** 6.2 **Stable tag:** 0.17.0 **Requires PHP:** 5.6 **License:** MIT @@ -111,7 +111,11 @@ Where 'blog' is the path to the subdirectory at which your blog resides. ## Changelog ## -Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). + +### Next ### + +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. ### 0.17.0 ### diff --git a/readme.txt b/readme.txt index e89c768..3f9f7f0 100644 --- a/readme.txt +++ b/readme.txt @@ -111,7 +111,7 @@ Where 'blog' is the path to the subdirectory at which your blog resides. == Changelog == -Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). = Next = From e489a048808abd285fdedc048971d6fc673f8985 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 09:32:52 +0200 Subject: [PATCH 59/60] remove unused constants --- activitypub.php | 3 +-- includes/class-admin.php | 4 ++-- includes/functions.php | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index 38886ff..d61af85 100644 --- a/activitypub.php +++ b/activitypub.php @@ -23,9 +23,8 @@ function init() { \defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 ); \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=

      )|(?<=
      )|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); - \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '