From 7769d76849d6aa1a72d5ca51868679d0c1f6a2f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 14:56:22 +0200 Subject: [PATCH 01/33] 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 02/33] 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 03/33] 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 04/33] 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 05/33] 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 06/33] 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 07/33] 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 84a82c2ac41a44bb4278d46072b193193c18a1c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Apr 2023 20:46:51 +0200 Subject: [PATCH 08/33] 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 09/33] 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 10/33] 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 d1f6973d9b02869c9e59e7cc4221265d3e5c22b9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 11:59:08 +0200 Subject: [PATCH 11/33] 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: Wed, 26 Apr 2023 17:22:44 +0200 Subject: [PATCH 12/33] 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 13/33] 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 14/33] 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 15/33] 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 16/33] 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 17/33] 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 18/33] 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 19/33] 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 20/33] 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 9cd33ad544f1f739a88f2dbaf4776daa1a48c962 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 18:13:16 +0200 Subject: [PATCH 21/33] 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 22/33] 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 23/33] 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 24/33] 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 25/33] 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 26/33] 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 27/33] 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 28/33] 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 29/33] 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 30/33] 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 31/33] 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 32/33] 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 33/33] 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'] ) ) {