From 09518ea66b96c4955c557603b6bda18a19898611 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 16:32:00 +0200 Subject: [PATCH 01/97] prepare pseudo users like a blog wide user. this allows also other constructs like tag oder category users fix #1 --- activitypub.php | 1 + assets/css/activitypub-admin.css | 6 +- includes/class-activitypub.php | 5 + includes/class-admin.php | 15 ++ includes/class-user-factory.php | 157 ++++++++++++++ includes/functions.php | 37 +--- includes/model/class-activity.php | 15 +- includes/model/class-application-user.php | 39 ++++ includes/model/class-blog-user.php | 91 ++++++++ includes/model/class-user.php | 239 +++++++++++++++++++++- includes/rest/class-followers.php | 31 +-- includes/rest/class-following.php | 28 +-- includes/rest/class-inbox.php | 31 +-- includes/rest/class-outbox.php | 32 +-- includes/rest/class-user.php | 89 ++++++++ includes/rest/class-webfinger.php | 27 +-- includes/table/class-followers.php | 28 ++- templates/admin-header.php | 4 + templates/author-json.php | 92 +-------- templates/followers-list.php | 14 ++ 20 files changed, 744 insertions(+), 237 deletions(-) create mode 100644 includes/class-user-factory.php create mode 100644 includes/model/class-application-user.php create mode 100644 includes/model/class-blog-user.php create mode 100644 includes/rest/class-user.php diff --git a/activitypub.php b/activitypub.php index 8f665fc..0f9e8d3 100644 --- a/activitypub.php +++ b/activitypub.php @@ -39,6 +39,7 @@ function init() { Collection\Followers::init(); // Configure the REST API route + Rest\User::init(); Rest\Outbox::init(); Rest\Inbox::init(); Rest\Followers::init(); diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index cd1808c..8925bfd 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -4,6 +4,10 @@ margin-top: 10px; } +.settings_page_activitypub .wrap { + padding-left: 22px; +} + .activitypub-settings-header { text-align: center; margin: 0 0 1rem; @@ -28,7 +32,7 @@ -ms-grid-columns: 1fr 1fr; vertical-align: top; display: inline-grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; } .activitypub-settings-tab.active { diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 55329b3..6ebfcbf 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -224,6 +224,11 @@ class Activitypub { 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2', 'top' ); + \add_rewrite_rule( + '^@([\w]+)', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', + 'top' + ); } \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); diff --git a/includes/class-admin.php b/includes/class-admin.php index 7b62a08..b1b5bc7 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,6 +13,10 @@ class Admin { * Initialize the class, registering WordPress hooks */ public static function init() { + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } + \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_profile' ) ); @@ -24,6 +28,11 @@ class Admin { * Add admin menu entry */ public static function admin_menu() { + // user has to be able to publish posts + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } + $settings_page = \add_options_page( 'Welcome', 'ActivityPub', @@ -55,6 +64,9 @@ class Admin { case 'settings': \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; + case 'followers': + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + break; case 'welcome': default: wp_enqueue_script( 'plugin-install' ); @@ -70,6 +82,9 @@ class Admin { * Load user settings page */ public static function followers_list_page() { + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); } diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php new file mode 100644 index 0000000..3ae3237 --- /dev/null +++ b/includes/class-user-factory.php @@ -0,0 +1,157 @@ + 404 ) + ); + } + + return new User( $user->ID ); + } + } + + /** + * Get the User by username. + * + * @param string $username The User-Name. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_username( $username ) { + // check for blog user. + if ( get_option( 'activitypub_blog_identifier', null ) === $username ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + + // check for 'activitypub_username' meta + $user = new WP_User_Query( + array( + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'activitypub_identifier', + 'value' => $username, + 'compare' => 'LIKE', + ), + ), + ) + ); + + if ( $user->results ) { + return self::get_by_id( $user->results[0] ); + } + + // check for login or nicename. + $user = new WP_User_Query( + array( + 'search' => $username, + 'search_columns' => array( 'user_login', 'user_nicename' ), + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + ) + ); + + if ( $user->results ) { + return self::get_by_id( $user->results[0] ); + } + + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + /** + * Get the User by resource. + * + * @param string $resource The User-Resource. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_resource( $resource ) { + if ( \strpos( $resource, '@' ) === false ) { + return new WP_Error( + 'activitypub_unsupported_resource', + \__( 'Resource is invalid', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $resource = \str_replace( 'acct:', '', $resource ); + + $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); + $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + + if ( $blog_host !== $resource_host ) { + return new WP_Error( + 'activitypub_wrong_host', + \__( 'Resource host does not match blog host', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + return self::get_by_username( $resource_identifier ); + } + + /** + * Get the User by resource. + * + * @param string $resource The User-Resource. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_various( $id ) { + if ( is_numeric( $id ) ) { + return self::get_by_id( $id ); + } elseif ( filter_var( $id, FILTER_VALIDATE_URL ) ) { + return self::get_by_resource( $id ); + } else { + return self::get_by_username( $id ); + } + } +} diff --git a/includes/functions.php b/includes/functions.php index 2a17bfa..8811d9b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -7,27 +7,7 @@ namespace Activitypub; * @return array the activitypub context */ function get_context() { - $context = array( - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - array( - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'PropertyValue' => 'schema:PropertyValue', - 'schema' => 'http://schema.org#', - 'pt' => 'https://joinpeertube.org/ns#', - 'toot' => 'http://joinmastodon.org/ns#', - 'value' => 'schema:value', - 'Hashtag' => 'as:Hashtag', - 'featured' => array( - '@id' => 'toot:featured', - '@type' => '@id', - ), - 'featuredTags' => array( - '@id' => 'toot:featuredTags', - '@type' => '@id', - ), - ), - ); + $context = Model\Activity::CONTEXT; return \apply_filters( 'activitypub_json_context', $context ); } @@ -187,21 +167,6 @@ function url_to_authorid( $url ) { return 0; } -/** - * Return the custom Activity Pub description, if set, or default author description. - * - * @param int $user_id The user ID. - * - * @return string The author description. - */ -function get_author_description( $user_id ) { - $description = get_user_meta( $user_id, 'activitypub_user_description', true ); - if ( empty( $description ) ) { - $description = get_user_meta( $user_id, 'description', true ); - } - return \wpautop( \wp_kses( $description, 'default' ) ); -} - /** * Check for Tombstone Objects * diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index bf06bc1..bef7f1e 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -11,12 +11,8 @@ use function Activitypub\get_rest_url_by_path; * @see https://www.w3.org/TR/activitypub/ */ class Activity { - /** - * The JSON-LD context. - * - * @var array - */ - private $context = array( + + const CONTEXT = array( 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', array( @@ -38,6 +34,13 @@ class Activity { ), ); + /** + * The JSON-LD context. + * + * @var array + */ + private $context = self::CONTEXT; + /** * The published date. * diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php new file mode 100644 index 0000000..c800531 --- /dev/null +++ b/includes/model/class-application-user.php @@ -0,0 +1,39 @@ + 'date', + 'order' => 'ASC', + 'number' => 1, + ) + ); + + if ( ! empty( $first_post->posts[0] ) ) { + $time = \strtotime( $first_post->posts[0]->post_date_gmt ); + } else { + $time = \time(); + } + + return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + } +} diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 5c62473..ba24a66 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -1,23 +1,242 @@ user_id = $user_id; + + add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + add_filter( 'activitypub_json_author_array', array( $this, 'add_attachments' ), 10, 2 ); + } + + /** + * 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 ) { + return $this->$var; + } + + if ( \strncasecmp( $method, 'has', 3 ) === 0 ) { + return (bool) call_user_func( 'get_' . $var, $this ); + } + } + + /** + * Get the User-ID. + * + * @return string The User-ID. + */ + public function get_id() { + return $this->get_url(); + } + + /** + * Get the User-Name. + * + * @return string The User-Name. + */ + public function get_name() { + return \esc_attr( \get_the_author_meta( 'display_name', $this->user_id ) ); + } + + /** + * Get the User-Description. + * + * @return string The User-Description. + */ + public function get_summary() { + $description = get_user_meta( $this->user_id, 'activitypub_user_description', true ); + if ( empty( $description ) ) { + $description = get_user_meta( $this->user_id, 'description', true ); + } + return \wpautop( \wp_kses( $description, 'default' ) ); + } + + /** + * Get the User-Url. + * + * @return string The User-Url. + */ + public function get_url() { + return \esc_url( \get_author_posts_url( $this->user_id ) ); + } + + public function get_username() { + return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); + } + + public function get_avatar() { + return \esc_url( + \get_avatar_url( + $this->user_id, + array( 'size' => 120 ) + ) + ); + } + + public function get_header_image() { + if ( \has_header_image() ) { + return \esc_url( \get_header_image() ); + } + + return null; + } + + public function get_published() { + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->user_id ) ) ); + } + + public function get_public_key() { + //return Signature::get_public_key( $this->user_id ); + return null; + } + + /** + * Array representation of the User. + * + * @param bool $context Whether to include the @context. + * + * @return array The array representation of the User. + */ + public function to_array( $context = true ) { + $output = array(); + + if ( $context ) { + $output['@context'] = Activity::CONTEXT; + } + + $output['id'] = $this->get_url(); + $output['type'] = $this->get_type(); + $output['name'] = $this->get_name(); + $output['summary'] = \html_entity_decode( + $this->get_summary(), + \ENT_QUOTES, + 'UTF-8' + ); + $output['preferredUsername'] = $this->get_username(); // phpcs:ignore + $output['url'] = $this->get_url(); + $output['icon'] = array( + 'type' => 'Image', + 'url' => $this->get_avatar(), + ); + + if ( $this->has_header_image() ) { + $output['image'] = array( + 'type' => 'Image', + 'url' => $this->get_header_image(), + ); + } + + $output['published'] = $this->get_published(); + + $output['publicKey'] = array( + 'id' => $this->get_url() . '#main-key', + 'owner' => $this->get_url(), + 'publicKeyPem' => \trim( $this->get_public_key() ), + ); + + $output['manuallyApprovesFollowers'] = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore + + // filter output + $output = \apply_filters( 'activitypub_json_author_array', $output, $this->user_id, $this ); + + return $output; + } + + /** + * Extend the User-Output with API-Endpoints. + * + * @param array $array The User-Output. + * @param numeric $user_id The User-ID. + * + * @return array The extended User-Output. + */ + public function add_api_endpoints( $array, $user_id ) { + $array['inbox'] = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); + $array['outbox'] = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); + $array['followers'] = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); + $array['following'] = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); + + return $array; + } + + /** + * Extend the User-Output with Attachments. + * + * @param array $array The User-Output. + * @param numeric $user_id The User-ID. + * + * @return array The extended User-Output. + */ + public function add_attachments( $array, $user_id ) { + $array['attachment'] = array(); + + $array['attachment']['blog_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Blog', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + $array['attachment']['profile_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Profile', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \get_author_posts_url( $user_id ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + if ( \get_the_author_meta( 'user_url', $user_id ) ) { + $array['attachment']['user_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Website', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \get_the_author_meta( 'user_url', $user_id ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + } + + return $array; + } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 30509be..233c4ed 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,6 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; +use Activitypub\User_Factory; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -30,7 +31,7 @@ class Followers { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/followers', + '/users/(?P\w+)/followers', array( array( 'methods' => WP_REST_Server::READABLE, @@ -51,19 +52,10 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = \get_user_by( 'ID', $user_id ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $user ) { - return new WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } /* @@ -77,18 +69,18 @@ class Followers { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get_user_id() ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore - $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user->get_user_id() ); // phpcs:ignore // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user_id ) + FollowerCollection::get_followers( $user->get_user_id() ) ); $response = new WP_REST_Response( $json, 200 ); @@ -111,10 +103,7 @@ class Followers { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 93ad6a7..416d3a4 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,6 +1,8 @@ \d+)/following', + '/users/(?P\w+)/following', array( array( 'methods' => \WP_REST_Server::READABLE, @@ -45,19 +47,10 @@ class Following { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = \get_user_by( 'ID', $user_id ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $user ) { - return new \WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } /* @@ -71,10 +64,10 @@ class Following { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore @@ -100,10 +93,7 @@ class Following { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 76ce57c..8e77edb 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Signature; +use Activitypub\User_Factory; use Activitypub\Model\Activity; use function Activitypub\get_context; @@ -48,7 +48,7 @@ class Inbox { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/inbox', + '/users/(?P\w+)/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, @@ -74,6 +74,12 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + $page = $request->get_param( 'page', 0 ); /* @@ -87,7 +93,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore @@ -120,13 +126,18 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } $data = $request->get_params(); $type = $request->get_param( 'type' ); $type = \strtolower( $type ); - \do_action( 'activitypub_inbox', $data, $user_id, $type ); - \do_action( "activitypub_inbox_{$type}", $data, $user_id ); + \do_action( 'activitypub_inbox', $data, $user->get_user_id(), $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user->get_user_id() ); return new WP_REST_Response( array(), 202 ); } @@ -185,10 +196,7 @@ class Inbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; @@ -208,10 +216,7 @@ class Inbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); $params['id'] = array( diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 2138f71..d7e730f 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,6 +5,7 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; +use Activitypub\User_Factory; use Activitypub\Model\Post; use Activitypub\Model\Activity; @@ -32,7 +33,7 @@ class Outbox { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/outbox', + '/users/(?P\w+)/outbox', array( array( 'methods' => WP_REST_Server::READABLE, @@ -52,22 +53,14 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $author = \get_user_by( 'ID', $user_id ); - $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $author ) { - return new WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } + $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); + $page = $request->get_param( 'page', 0 ); /* @@ -80,9 +73,9 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -104,7 +97,7 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user_id, + 'author' => $user->get_user_id(), 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) @@ -148,10 +141,7 @@ class Outbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-user.php b/includes/rest/class-user.php new file mode 100644 index 0000000..4bb3783 --- /dev/null +++ b/includes/rest/class-user.php @@ -0,0 +1,89 @@ +\w+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( self::class, 'get' ), + 'args' => self::request_parameters(), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Handle GET request + * + * @param WP_REST_Request $request + * + * @return WP_REST_Response + */ + public static function get( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_outbox_pre' ); + + $json = $user->to_array(); + + $response = new WP_REST_Response( $json, 200 ); + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; + } + + /** + * The supported parameters + * + * @return array list of parameters + */ + public static function request_parameters() { + $params = array(); + + $params['page'] = array( + 'type' => 'string', + ); + + $params['user_id'] = array( + 'required' => true, + 'type' => 'string', + ); + + return $params; + } +} diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f75a3f7..4f1d5cf 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,6 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; +use Activitypub\User_Factory; /** * ActivityPub WebFinger REST-Class @@ -47,41 +48,27 @@ class Webfinger { public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - if ( \strpos( $resource, '@' ) === false ) { - return new WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); - } + $user = User_Factory::get_by_resource( $resource ); - $resource = \str_replace( 'acct:', '', $resource ); - - $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); - $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); - - if ( $blog_host !== $resource_host ) { - return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); - } - - $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); - - if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { - return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); + if ( is_wp_error( $user ) ) { + return $user; } $json = array( 'subject' => $resource, 'aliases' => array( - \get_author_posts_url( $user->ID ), + $user->get_url(), ), 'links' => array( array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ), array( 'rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ), ), ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 246cc0b..5f1c199 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -9,6 +9,24 @@ if ( ! \class_exists( '\WP_List_Table' ) ) { } class Followers extends WP_List_Table { + private $user_id; + + public function __construct() { + if ( get_current_screen()->id === 'settings_page_activitypub' ) { + $this->user_id = -1; + } else { + $this->user_id = \get_current_user_id(); + } + + parent::__construct( + array( + 'singular' => \__( 'Follower', 'activitypub' ), + 'plural' => \__( 'Followers', 'activitypub' ), + 'ajax' => false, + ) + ); + } + public function get_columns() { return array( 'cb' => '', @@ -36,8 +54,8 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $followers = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); - $counter = FollowerCollection::count_followers( \get_current_user_id() ); + $follower = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); $this->set_pagination_args( @@ -104,7 +122,7 @@ class Followers extends WP_List_Table { return false; } - if ( ! current_user_can( 'edit_user', \get_current_user_id() ) ) { + if ( ! current_user_can( 'edit_user', $this->user_id ) ) { return false; } @@ -121,4 +139,8 @@ class Followers extends WP_List_Table { break; } } + + public function get_user_count() { + return FollowerCollection::count_followers( $this->user_id ); + } } diff --git a/templates/admin-header.php b/templates/admin-header.php index 73a830b..974b224 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -11,6 +11,10 @@ + + + +
diff --git a/templates/author-json.php b/templates/author-json.php index 7b112ac..aa38b97 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,92 +1,10 @@ {'@context'} = \Activitypub\get_context(); -$json->id = \get_author_posts_url( $author_id ); -$json->type = 'Person'; -$json->name = \get_the_author_meta( 'display_name', $author_id ); -$json->summary = \html_entity_decode( - \Activitypub\get_author_description( $author_id ), - \ENT_QUOTES, - 'UTF-8' -); -$json->preferredUsername = \get_the_author_meta( 'login', $author_id ); // phpcs:ignore -$json->url = \get_author_posts_url( $author_id ); -$json->icon = array( - 'type' => 'Image', - 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), -); - -$json->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $author_id ) ) ); - -if ( \has_header_image() ) { - $json->image = array( - 'type' => 'Image', - 'url' => \get_header_image(), - ); -} - -$json->inbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) ); -$json->outbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) ); -$json->followers = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) ); -$json->following = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/following', $author_id ) ); - -$json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - -// phpcs:ignore -$json->publicKey = array( - 'id' => \get_author_posts_url( $author_id ) . '#main-key', - 'owner' => \get_author_posts_url( $author_id ), - 'publicKeyPem' => \trim( \Activitypub\Signature::get_public_key( $author_id ) ), -); - -$json->tag = array(); -$json->attachment = array(); - -$json->attachment['blog_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -$json->attachment['profile_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Profile', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_author_posts_url( $author_id ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -if ( \get_the_author_meta( 'user_url', $author_id ) ) { - $json->attachment['user_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Website', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_the_author_meta( 'user_url', $author_id ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), - ); -} - -// filter output -$json = \apply_filters( 'activitypub_json_author_array', $json, $author_id ); - -// migrate to ActivityPub standard -$json->attachment = array_values( $json->attachment ); +$user = \Activitypub\User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $author_id ); +\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -101,12 +19,12 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $author_id ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); \header( 'Content-Type: application/activity+json' ); -echo \wp_json_encode( $json, $options ); +echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $author_id ); +\do_action( 'activitypub_json_author_post', $user->get_user_id() ); diff --git a/templates/followers-list.php b/templates/followers-list.php index 7476192..733753e 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,3 +1,17 @@ +id === 'settings_page_activitypub' ) { + \load_template( + \dirname( __FILE__ ) . '/admin-header.php', + true, + array( + 'settings' => '', + 'welcome' => '', + 'followers' => 'active', + ) + ); +} +?> +

From 03f2c2489249c133bf54bc77f94eae9428250914 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 17:27:46 +0200 Subject: [PATCH 02/97] small improvements --- activitypub.php | 2 +- includes/class-activitypub.php | 1 + includes/model/class-blog-user.php | 4 ++++ includes/model/class-user.php | 4 ++-- includes/rest/{class-user.php => class-users.php} | 2 +- includes/rest/class-webfinger.php | 3 +-- 6 files changed, 10 insertions(+), 6 deletions(-) rename includes/rest/{class-user.php => class-users.php} (99%) diff --git a/activitypub.php b/activitypub.php index 0f9e8d3..81639b8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -39,7 +39,7 @@ function init() { Collection\Followers::init(); // Configure the REST API route - Rest\User::init(); + Rest\Users::init(); Rest\Outbox::init(); Rest\Inbox::init(); Rest\Followers::init(); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6ebfcbf..ae178d3 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -97,6 +97,7 @@ class Activitypub { return $template; } } + return $json_template; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index e237fcc..c8df74d 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -88,4 +88,8 @@ class Blog_User extends User { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } + + public function get_public_key() { + return ''; + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index ba24a66..29b65bf 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -3,6 +3,7 @@ namespace Activitypub\Model; use WP_Query; use WP_Error; +use Activitypub\Signature; use Activitypub\Model\User; use Activitypub\User_Factory; @@ -121,8 +122,7 @@ class User { } public function get_public_key() { - //return Signature::get_public_key( $this->user_id ); - return null; + return Signature::get_public_key( $this->user_id ); } /** diff --git a/includes/rest/class-user.php b/includes/rest/class-users.php similarity index 99% rename from includes/rest/class-user.php rename to includes/rest/class-users.php index 4bb3783..56a10bc 100644 --- a/includes/rest/class-user.php +++ b/includes/rest/class-users.php @@ -13,7 +13,7 @@ use Activitypub\User_Factory; * * @see https://www.w3.org/TR/activitypub/#followers */ -class User { +class Users { /** * Initialize the class, registering WordPress hooks */ diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 4f1d5cf..9b07a95 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -47,8 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - - $user = User_Factory::get_by_resource( $resource ); + $user = User_Factory::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; From a1791b963c4b77bbacc3e1ae5ec578859acff61e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 17:40:48 +0200 Subject: [PATCH 03/97] try new id urls --- includes/model/class-application-user.php | 4 ++++ includes/model/class-blog-user.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index c800531..b5623ed 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -36,4 +36,8 @@ class Application_User extends Blog_User { public function get_url() { return ''; } + + public function get_name() { + return \esc_html( \get_bloginfo( 'activitypub_application_identifier', 'application' ) ); + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c8df74d..435fde8 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -52,11 +52,11 @@ class Blog_User extends User { * @return string The User-Url. */ public function get_url() { - return ''; + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } public function get_username() { - return ''; + return \esc_html( \get_bloginfo( 'activitypub_blog_identifier', 'feed' ) ); } public function get_avatar() { From f8b93760df767038f528c52a31bd18e8470f7bde Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 10:30:51 +0200 Subject: [PATCH 04/97] fix copy&paste issue thanks @mattwiebe --- includes/model/class-application-user.php | 2 +- includes/model/class-blog-user.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index b5623ed..77ba7fb 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -38,6 +38,6 @@ class Application_User extends Blog_User { } public function get_name() { - return \esc_html( \get_bloginfo( 'activitypub_application_identifier', 'application' ) ); + return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 435fde8..944fe6d 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -56,7 +56,7 @@ class Blog_User extends User { } public function get_username() { - return \esc_html( \get_bloginfo( 'activitypub_blog_identifier', 'feed' ) ); + return \esc_html( \get_option( 'activitypub_blog_identifier', 'feed' ) ); } public function get_avatar() { From 3feef1e8cf89181d8273a1a53ee6c342cb0fe740 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 13:55:18 +0200 Subject: [PATCH 05/97] send user and blog activities and set the blog to "single-mode" --- activitypub.php | 1 + includes/class-activity-dispatcher.php | 25 +++++++++++++++++++++++-- includes/functions.php | 9 +++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 81639b8..7bc4306 100644 --- a/activitypub.php +++ b/activitypub.php @@ -28,6 +28,7 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); + \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) || \define( 'ACTIVITYPUB_SINGLE_USER_MODE', false ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index ee435a7..811f923 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\User_Factory; use Activitypub\Collection\Followers; use function Activitypub\safe_remote_post; @@ -66,12 +67,32 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + if ( ! is_single_user_mode() ) { + // send User-Activity + self::send_user_activity( $activitypub_post, $activity_type ); + } + + // send Blog-User-Activity + self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); + } + + public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { + if ( $user_id ) { + $user = User_Factory::get_by_id( $user_id ); + $actor = $user->get_url(); + } else { + // get latest version of post + $user_id = $activitypub_post->get_post_author(); + $actor = null; + } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); + if ( $actor ) { + $activitypub_activity->set_actor( $actor ); + } + $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/functions.php b/includes/functions.php index 8811d9b..1f25e04 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -267,3 +267,12 @@ function is_activitypub_request() { return false; } + +/** + * Check if the current site is in single-user mode. + * + * @return boolean + */ +function is_single_user_mode() { + return ACTIVITYPUB_SINGLE_USER_MODE; +} From 503353bcd0f31f6bca6078e89063c8a75b2043c2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 26 May 2023 16:08:08 +0200 Subject: [PATCH 06/97] Added settings for blog-wide user --- assets/css/activitypub-admin.css | 16 +++++- includes/class-admin.php | 33 ++++++++++- includes/class-user-factory.php | 9 ++- includes/class-webfinger.php | 17 ++++-- includes/model/class-blog-user.php | 2 +- includes/rest/class-followers.php | 2 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 2 +- includes/rest/class-outbox.php | 2 +- includes/rest/class-users.php | 2 +- templates/settings.php | 90 +++++++++++++++++++++++++++--- 11 files changed, 152 insertions(+), 25 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 8925bfd..db4546c 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -1,3 +1,8 @@ +.activitypub-settings-body { + max-width: 800px; + margin: 0 auto; +} + .settings_page_activitypub .notice { max-width: 800px; margin: auto; @@ -115,7 +120,8 @@ summary { flex-grow: 1; } -.activitypub-settings-accordion-trigger .icon, .activitypub-settings-accordion-viewed .icon { +.activitypub-settings-accordion-trigger .icon, +.activitypub-settings-accordion-viewed .icon { border: solid #50575e medium; border-width: 0 2px 2px 0; height: .5rem; @@ -131,7 +137,8 @@ summary { transform: translateY(-30%) rotate(-135deg); } -.activitypub-settings-accordion-trigger:active, .activitypub-settings-accordion-trigger:hover { +.activitypub-settings-accordion-trigger:active, +.activitypub-settings-accordion-trigger:hover { background: #f6f7f7; } @@ -143,3 +150,8 @@ summary { outline: 2px solid #2271b1; background-color: #f6f7f7; } + +.activitypub-settings-body +input.blog-user-identifier { + text-align: right; +} diff --git a/includes/class-admin.php b/includes/class-admin.php index b1b5bc7..b88a8de 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -100,7 +100,11 @@ class Admin { 'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ), 'show_in_rest' => array( 'schema' => array( - 'enum' => array( 'title', 'excerpt', 'content' ), + 'enum' => array( + 'title', + 'excerpt', + 'content', + ), ), ), 'default' => 'content', @@ -133,7 +137,11 @@ class Admin { 'description' => \__( 'The Activity-Object-Type', 'activitypub' ), 'show_in_rest' => array( 'schema' => array( - 'enum' => array( 'note', 'article', 'wordpress-post-format' ), + 'enum' => array( + 'note', + 'article', + 'wordpress-post-format', + ), ), ), 'default' => 'note', @@ -158,6 +166,27 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); + \register_setting( + 'activitypub', + 'activitypub_blog_user_identifier', + array( + 'type' => 'string', + 'description' => \esc_html__( 'The Identifier of th Blog-User', 'activitypub' ), + 'show_in_rest' => true, + 'default' => 'feed', + 'sanitize_callback' => function( $value ) { + // hack to allow dots in the username + $parts = explode( '.', $value ); + $sanitized = array(); + + foreach ( $parts as $part ) { + $sanitized[] = \sanitize_title( $part ); + } + + return implode( '.', $sanitized ); + }, + ) + ); } public static function add_settings_help_tab() { diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 3ae3237..faa170e 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -58,10 +58,15 @@ class User_Factory { */ public static function get_by_username( $username ) { // check for blog user. - if ( get_option( 'activitypub_blog_identifier', null ) === $username ) { + if ( get_option( 'activitypub_blog_user_identifier', null ) === $username ) { return self::get_by_id( self::BLOG_USER_ID ); } + // check for application user. + if ( get_option( 'activitypub_application_user_identifier', null ) === $username ) { + return self::get_by_id( self::APPLICATION_USER_ID ); + } + // check for 'activitypub_username' meta $user = new WP_User_Query( array( @@ -71,7 +76,7 @@ class User_Factory { 'meta_query' => array( 'relation' => 'OR', array( - 'key' => 'activitypub_identifier', + 'key' => 'activitypub_user_identifier', 'value' => $username, 'compare' => 'LIKE', ), diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 1581853..44a4a27 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -32,18 +32,25 @@ class Webfinger { return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } - public static function resolve( $account ) { - if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $account, $m ) ) { + /** + * Resolve a WebFinger resource + * + * @param string $resource The WebFinger resource + * + * @return string|WP_Error The URL or WP_Error + */ + public static function resolve( $resource ) { + if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $resource, $m ) ) { return null; } - $transient_key = 'activitypub_resolve_' . ltrim( $account, '@' ); + $transient_key = 'activitypub_resolve_' . ltrim( $resource, '@' ); $link = \get_transient( $transient_key ); if ( $link ) { return $link; } - $url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' ); + $url = \add_query_arg( 'resource', 'acct:' . ltrim( $resource, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' ); if ( ! \wp_http_validate_url( $url ) ) { $response = new WP_Error( 'invalid_webfinger_url', null, $url ); \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. @@ -82,7 +89,7 @@ class Webfinger { } } - $link = new WP_Error( 'webfinger_url_no_activity_pub', null, $body ); + $link = new WP_Error( 'webfinger_url_no_activitypub', null, $body ); \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. return $link; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 944fe6d..2183efb 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -56,7 +56,7 @@ class Blog_User extends User { } public function get_username() { - return \esc_html( \get_option( 'activitypub_blog_identifier', 'feed' ) ); + return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); } public function get_avatar() { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 233c4ed..792cdfb 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -31,7 +31,7 @@ class Followers { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/followers', + '/users/(?P[\w\-\.]+)/followers', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 416d3a4..0d1de61 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -26,7 +26,7 @@ class Following { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/following', + '/users/(?P[\w\-\.]+)/following', array( array( 'methods' => \WP_REST_Server::READABLE, diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 8e77edb..54e2894 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -48,7 +48,7 @@ class Inbox { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/inbox', + '/users/(?P[\w\-\.]+)/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index d7e730f..cb09cfd 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -33,7 +33,7 @@ class Outbox { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/outbox', + '/users/(?P[\w\-\.]+)/outbox', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 56a10bc..c6d92ee 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -27,7 +27,7 @@ class Users { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)', + '/users/(?P[\w\-\.]+)', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/templates/settings.php b/templates/settings.php index 0daca17..819dbb0 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -9,7 +9,7 @@ ); ?> -
+

+

+ +

+ + + + + + + + +
+ + + +

+ +

+
+ + +

@@ -42,16 +67,44 @@

- - +

- - +

- - +

- - +

@@ -98,13 +151,34 @@

- - +

- - +

- - +

From a617553ddfe4e6e4cc3e15a3e53677b527cd75aa Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:00:48 +0200 Subject: [PATCH 07/97] fix profile pages --- includes/class-activitypub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index ae178d3..72cea52 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -226,8 +226,8 @@ class Activitypub { 'top' ); \add_rewrite_rule( - '^@([\w]+)', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', + '^@([\w\-\.]+)', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]&redirect=true', 'top' ); } From c95e501f98a5708f09258f9592c80c686d33742a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:22:01 +0200 Subject: [PATCH 08/97] redirect to canonical URL if it is not an ActivityPub request --- includes/class-activitypub.php | 2 +- includes/functions.php | 2 +- includes/model/class-blog-user.php | 4 ++++ includes/model/class-user.php | 4 ++++ includes/rest/class-users.php | 10 +++++++++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 72cea52..2da2dce 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -227,7 +227,7 @@ class Activitypub { ); \add_rewrite_rule( '^@([\w\-\.]+)', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]&redirect=true', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', 'top' ); } diff --git a/includes/functions.php b/includes/functions.php index 1f25e04..dfbdf37 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -236,7 +236,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( 'REST_REQUEST' ) && ! REST_REQUEST ) { return false; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 2183efb..fd01aa7 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -55,6 +55,10 @@ class Blog_User extends User { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } + public function get_canonical_url() { + return \get_home_url(); + } + public function get_username() { return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 29b65bf..5bb1e77 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -96,6 +96,10 @@ class User { return \esc_url( \get_author_posts_url( $this->user_id ) ); } + public function get_canonical_url() { + return $this->get_url(); + } + public function get_username() { return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index c6d92ee..8c0452d 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -6,6 +6,8 @@ use WP_REST_Server; use WP_REST_Response; use Activitypub\User_Factory; +use function Activitypub\is_activitypub_request; + /** * ActivityPub Followers REST-Class * @@ -54,6 +56,12 @@ class Users { return $user; } + // redirect to canonical URL if it is not an ActivityPub request + if ( ! is_activitypub_request() ) { + header( 'Location: ' . $user->get_canonical_url() ); + exit; + } + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -81,7 +89,7 @@ class Users { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; From daf228fd44af2906837c4c343aae47f55685a6a1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:29:23 +0200 Subject: [PATCH 09/97] move permanently --- includes/rest/class-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 8c0452d..66637b7 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -58,7 +58,7 @@ class Users { // redirect to canonical URL if it is not an ActivityPub request if ( ! is_activitypub_request() ) { - header( 'Location: ' . $user->get_canonical_url() ); + header( 'Location: ' . $user->get_canonical_url(), true, 301 ); exit; } From 2feca1388ad007e6131ab660803ec81cf55e7edf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 11:22:20 +0200 Subject: [PATCH 10/97] generate default username --- includes/class-user-factory.php | 2 +- includes/model/class-blog-user.php | 49 +++++++++++++++++++++- templates/blog-json.php | 66 +++--------------------------- templates/settings.php | 2 +- 4 files changed, 55 insertions(+), 64 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index faa170e..8ba4521 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -58,7 +58,7 @@ class User_Factory { */ public static function get_by_username( $username ) { // check for blog user. - if ( get_option( 'activitypub_blog_user_identifier', null ) === $username ) { + if ( Blog_User::get_default_username() === $username ) { return self::get_by_id( self::BLOG_USER_ID ); } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index fd01aa7..492bf76 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -59,8 +59,55 @@ class Blog_User extends User { return \get_home_url(); } + /** + * Generate and save a default Username. + * + * @return string The auto-generated Username. + */ + public static function get_default_username() { + $username = \get_option( 'activitypub_blog_user_identifier' ); + + if ( $username ) { + return $username; + } + + // check if domain host has a subdomain + $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); + $host = \str_replace( 'www.', '', $host ); + $host_parts = \explode( '.', $host ); + + if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { + \update_option( 'activitypub_blog_user_identifier', $host ); + return $host; + } + + // check blog title + $blog_title = \get_bloginfo( 'name' ); + $blog_title = \sanitize_title( $blog_title ); + + if ( strlen( $blog_title ) <= 15 ) { + \update_option( 'activitypub_blog_user_identifier', $blog_title ); + return $blog_title; + } + + $default_identifier = array( + 'feed', + 'all', + 'everyone', + 'authors', + 'follow', + 'posts', + ); + + // get random item of $default_identifier + $default = $default_identifier[ \array_rand( $default_identifier ) ]; + \update_option( 'activitypub_blog_user_identifier', $default ); + + return $default; + } + public function get_username() { - return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); + return self::get_default_username(); } public function get_avatar() { diff --git a/templates/blog-json.php b/templates/blog-json.php index b87bc94..cfa759c 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,66 +1,10 @@ {'@context'} = \Activitypub\get_context(); -$json->id = \get_home_url( '/' ); -$json->type = 'Organization'; -$json->name = \get_bloginfo( 'name' ); -$json->summary = \html_entity_decode( - \get_bloginfo( 'description' ), - \ENT_QUOTES, - 'UTF-8' -); -$json->preferredUsername = \get_bloginfo( 'name' ); // phpcs:ignore -$json->url = \get_home_url( '/' ); - -if ( \has_site_icon() ) { - $json->icon = array( - 'type' => 'Image', - 'url' => \get_site_icon_url( 120 ), - ); -} - -if ( \has_header_image() ) { - $json->image = array( - 'type' => 'Image', - 'url' => \get_header_image(), - ); -} - -$json->inbox = \Activitypub\get_rest_url_by_path( 'blog/inbox' ); -$json->outbox = \Activitypub\get_rest_url_by_path( 'blog/outbox' ); -$json->followers = \Activitypub\get_rest_url_by_path( 'blog/followers' ); -$json->following = \Activitypub\get_rest_url_by_path( 'blog/following' ); - -$json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - -// phpcs:ignore -$json->publicKey = array( - 'id' => \get_home_url( '/' ) . '#main-key', - 'owner' => \get_home_url( '/' ), - 'publicKeyPem' => '', -); - -$json->tag = array(); -$json->attachment = array(); - -$json->attachment[] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -// filter output -$json = \apply_filters( 'activitypub_json_blog_array', $json ); +$user = \Activitypub\User_Factory::get_by_id( 0 ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_blog_pre' ); +\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -75,12 +19,12 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_blog_options', $options ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); \header( 'Content-Type: application/activity+json' ); -echo \wp_json_encode( $json, $options ); +echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_blog_post' ); +\do_action( 'activitypub_json_author_post', $user->get_user_id() ); diff --git a/templates/settings.php b/templates/settings.php index 819dbb0..08a65a1 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -42,7 +42,7 @@

From 7b9b3dbc3771039f5387b87a9524117f48d6db2d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 11:37:21 +0200 Subject: [PATCH 11/97] add @-urls to webfinger aliases --- includes/model/class-user.php | 4 ++++ includes/rest/class-webfinger.php | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 5bb1e77..2cee714 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -100,6 +100,10 @@ class User { return $this->get_url(); } + public function get_at_url() { + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); + } + public function get_username() { return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); } diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 9b07a95..ec76686 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -53,12 +53,16 @@ class Webfinger { return $user; } + $aliases = array( + $user->get_url(), + $user->get_canonical_url(), + $user->get_at_url(), + ); + $json = array( 'subject' => $resource, - 'aliases' => array( - $user->get_url(), - ), - 'links' => array( + 'aliases' => array_values( array_unique( $aliases ) ), + 'links' => array( array( 'rel' => 'self', 'type' => 'application/activity+json', From 4d8170413bec2270d2c0eb3a6dbe29af6c70decf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 15:19:05 +0200 Subject: [PATCH 12/97] avatar and header-image settings --- assets/css/activitypub-admin.css | 21 +++++++++++++++++++++ includes/class-activitypub.php | 29 +++++++++++++++++++++++++++++ templates/settings.php | 20 +++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index db4546c..805aaa5 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -155,3 +155,24 @@ summary { input.blog-user-identifier { text-align: right; } + +.activitypub-settings-body +.header-image { + width: 100%; + height: 80px; + position: relative; + display: block; + margin-bottom: 40px; + background-image: rgb(168,165,175); + background-image: linear-gradient(180deg, red, yellow); + background-size: cover; +} + +.activitypub-settings-body +.logo { + height: 80px; + width: 80px; + position: relative; + top: 40px; + left: 40px; +} diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 2da2dce..7e5ea2f 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -28,6 +28,8 @@ class Activitypub { \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); \add_action( 'init', array( self::class, 'add_rewrite_rules' ) ); + + \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 ); } /** @@ -242,4 +244,31 @@ class Activitypub { self::add_rewrite_rules(); \flush_rewrite_rules(); } + + public static function theme_compat() { + $site_icon = get_theme_support( 'custom-logo' ); + + if ( ! $site_icon ) { + // custom logo support + add_theme_support( + 'custom-logo', + array( + 'height' => 80, + 'width' => 80, + ) + ); + } + + $custom_header = get_theme_support( 'custom-header' ); + + if ( ! $custom_header ) { + // This theme supports a custom header + $custom_header_args = array( + 'width' => 1250, + 'height' => 600, + 'header-text' => true, + ); + add_theme_support( 'custom-header', $custom_header_args ); + } + } } diff --git a/templates/settings.php b/templates/settings.php index 08a65a1..5bc9c90 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -32,7 +32,25 @@

-

+

+ + + + + + + + +
+ + +
+ +
+

+ +

+
From 913b60c7c7af445c65a4c033a9c42e29f0895f26 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 08:45:14 +0200 Subject: [PATCH 13/97] Fix WebFinger resources for Blog-User and updated settings. --- includes/class-activitypub.php | 5 +++++ includes/class-webfinger.php | 4 ++-- includes/model/class-blog-user.php | 2 +- includes/model/class-user.php | 4 ++++ templates/settings.php | 13 ++++++++++-- templates/welcome.php | 33 ++++++++++++++++++++++++++---- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 7e5ea2f..cd95fc7 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -245,6 +245,11 @@ class Activitypub { \flush_rewrite_rules(); } + /** + * Theme compatibility stuff + * + * @return void + */ public static function theme_compat() { $site_icon = get_theme_support( 'custom-logo' ); diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 44a4a27..958f544 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -24,12 +24,12 @@ class Webfinger { return \get_webfinger_resource( $user_id, false ); } - $user = \get_user_by( 'id', $user_id ); + $user = User_Factory::get_by_id( $user_id ); if ( ! $user ) { return ''; } - return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $user->get_resource(); } /** diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 492bf76..d703275 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -24,7 +24,7 @@ class Blog_User extends User { * * @param int $user_id The User-ID. */ - public function __construct( $user_id ) { + public function __construct( $user_id = null ) { add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 2cee714..8791a50 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -247,4 +247,8 @@ class User { return $array; } + + public function get_resource() { + return $this->get_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + } } diff --git a/templates/settings.php b/templates/settings.php index 5bc9c90..6d8a0cb 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -42,10 +42,19 @@ diff --git a/templates/welcome.php b/templates/welcome.php index cb4d05f..acf986d 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -13,29 +13,54 @@

+

%1$s or the URL %2$s. Users who can not access this settings page will find their username on the Edit Profile page.', + 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings Page.', 'activitypub' ), - \esc_attr( \Activitypub\get_webfinger_resource( wp_get_current_user()->ID ) ), - \esc_url_raw( \get_author_posts_url( wp_get_current_user()->ID ) ), + \esc_attr( $blog_user->get_resource() ), + \esc_url_raw( $blog_user->get_url() ), + \esc_url_raw( \admin_url( '/options-general.php?page=activitypub&tab=settings' ) ) + ), + 'default' + ); + ?> +

+

+

+ ID ); + echo wp_kses( + \sprintf( + // translators: + \__( + 'People can also follow you by using your Username %1$s or your Author-URL %2$s. Users who can not access this settings page will find their username on the Edit Profile page.', + 'activitypub' + ), + \esc_attr( $user->get_resource() ), + \esc_url_raw( $user->get_url() ), \esc_url_raw( \admin_url( 'profile.php#activitypub' ) ) ), 'default' ); ?>

+

Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), + \__( + 'If you have problems using this plugin, please check the Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', + 'activitypub' + ), \esc_url_raw( admin_url( 'site-health.php' ) ) ), 'default' From 6e237fe76cab78c5938efb118fb124b7d4d8c376 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 08:49:33 +0200 Subject: [PATCH 14/97] text changes --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index acf986d..a908d82 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -21,7 +21,7 @@ \sprintf( // translators: \__( - 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings Page.', + 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings page.', 'activitypub' ), \esc_attr( $blog_user->get_resource() ), From 0f72f944063e2b9690f5d315242d8bbc6ee41792 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 09:39:35 +0200 Subject: [PATCH 15/97] small updates --- includes/class-activity-dispatcher.php | 22 ++++++++++++++-------- templates/blog-json.php | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 811f923..bea6b4e 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -76,22 +76,28 @@ class Activity_Dispatcher { self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); } + /** + * Send Activities to followers and mentioned users. + * + * @param Post $activitypub_post The ActivityPub Post. + * @param string $activity_type The Activity-Type. + * @param int $user_id The User-ID. + * + * @return void + */ public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { if ( $user_id ) { - $user = User_Factory::get_by_id( $user_id ); - $actor = $user->get_url(); + $user = User_Factory::get_by_id( $user_id ); + $user_id = $user->get_id(); + $actor = $user->get_url(); } else { - // get latest version of post $user_id = $activitypub_post->get_post_author(); - $actor = null; + $actor = $activitypub_activity->get_actor(); } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - - if ( $actor ) { - $activitypub_activity->set_actor( $actor ); - } + $activitypub_activity->set_actor( $actor ); $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/templates/blog-json.php b/templates/blog-json.php index cfa759c..b9c5f96 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,5 +1,5 @@ Date: Wed, 31 May 2023 10:31:14 +0200 Subject: [PATCH 16/97] use correct blog-user-id --- includes/table/class-followers.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 5f1c199..3a107c7 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -2,6 +2,7 @@ namespace Activitypub\Table; use WP_List_Table; +use Activitypub\User_Factory; use Activitypub\Collection\Followers as FollowerCollection; if ( ! \class_exists( '\WP_List_Table' ) ) { @@ -13,7 +14,7 @@ class Followers extends WP_List_Table { public function __construct() { if ( get_current_screen()->id === 'settings_page_activitypub' ) { - $this->user_id = -1; + $this->user_id = User_Factory::BLOG_USER_ID; } else { $this->user_id = \get_current_user_id(); } From e1fd0e1c3977a95cabb9660d509ec972629db382 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 10:31:49 +0200 Subject: [PATCH 17/97] move signature to user object --- includes/class-signature.php | 25 +++++--------- includes/model/class-application-user.php | 40 +++++++++++++++++++++++ includes/model/class-blog-user.php | 39 +++++++++++++++++++++- includes/model/class-user.php | 36 +++++++++++++++++++- 4 files changed, 122 insertions(+), 18 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a3293d6..66b16ba 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -5,6 +5,7 @@ use WP_Error; use DateTime; use DateTimeZone; use Activitypub\Model\User; +use Activitypub\User_Factory; /** * ActivityPub Signature Class @@ -73,7 +74,7 @@ class Signature { * * @return void */ - public static function generate_key_pair( $user_id ) { + public static function generate_key_pair() { $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 2048, @@ -84,22 +85,13 @@ class Signature { $priv_key = null; \openssl_pkey_export( $key, $priv_key ); + $detail = \openssl_pkey_get_details( $key ); - if ( User::APPLICATION_USER_ID === $user_id ) { - // private key - \update_option( 'activitypub_magic_sig_private_key', $priv_key ); - - // public key - \update_option( 'activitypub_magic_sig_public_key', $detail['key'] ); - - } else { - // private key - \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); - - // public key - \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); - } + return array( + 'private_key' => $priv_key, + 'public_key' => $detail['key'], + ); } /** @@ -114,7 +106,8 @@ class Signature { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { - $key = self::get_private_key( $user_id ); + $user = User_Factory::get_by_id( $user_id ); + $key = $user->get_private_key(); $url_parts = \wp_parse_url( $url ); diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 77ba7fb..1e562b1 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -40,4 +40,44 @@ class Application_User extends Blog_User { public function get_name() { return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); } + + public function get_public_key() { + $key = \get_option( 'activitypub_application_user_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + $key = \get_option( 'activitypub_application_user_public_key', true ); + + return $key; + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_option( 'activitypub_application_user_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_option( 'activitypub_application_user_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'], true ); + \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'], true ); + } + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index d703275..32b2dc7 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -2,6 +2,7 @@ namespace Activitypub\Model; use WP_Query; +use Activitypub\Signature; use Activitypub\User_Factory; class Blog_User extends User { @@ -141,6 +142,42 @@ class Blog_User extends User { } public function get_public_key() { - return ''; + $key = \get_option( 'activitypub_blog_user_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + $key = \get_option( 'activitypub_blog_user_public_key', true ); + + return $key; + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_option( 'activitypub_blog_user_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_option( 'activitypub_blog_user_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'], true ); + \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'], true ); + } } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 8791a50..302b784 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -130,7 +130,41 @@ class User { } public function get_public_key() { - return Signature::get_public_key( $this->user_id ); + $key = \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_user_meta( $this->get_user_id(), 'magic_sig_public_key', $key_pair['public_key'], true ); + \update_user_meta( $this->get_user_id(), 'magic_sig_private_key', $key_pair['private_key'], true ); + } } /** From 4f2a162f6cfdf81a5e7b6c93043f3fd124e37ac3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 11:04:29 +0200 Subject: [PATCH 18/97] Fix follower-list actions --- includes/class-admin.php | 4 +-- templates/blog-user-followers-list.php | 30 +++++++++++++++++++ ...owers-list.php => user-followers-list.php} | 14 --------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 templates/blog-user-followers-list.php rename templates/{followers-list.php => user-followers-list.php} (73%) diff --git a/includes/class-admin.php b/includes/class-admin.php index b88a8de..1f8b640 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -65,7 +65,7 @@ class Admin { \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; case 'followers': - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' ); break; case 'welcome': default: @@ -85,7 +85,7 @@ class Admin { if ( ! current_user_can( 'publish_posts' ) ) { return; } - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' ); } /** diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php new file mode 100644 index 0000000..db77e1e --- /dev/null +++ b/templates/blog-user-followers-list.php @@ -0,0 +1,30 @@ + '', + 'welcome' => '', + 'followers' => 'active', + ) +); +?> + +

+

+ + + + +

get_user_count() ) ); ?>

+ +
+ + + prepare_items(); + $followers_table->display(); + ?> + + +
diff --git a/templates/followers-list.php b/templates/user-followers-list.php similarity index 73% rename from templates/followers-list.php rename to templates/user-followers-list.php index 733753e..7476192 100644 --- a/templates/followers-list.php +++ b/templates/user-followers-list.php @@ -1,17 +1,3 @@ -id === 'settings_page_activitypub' ) { - \load_template( - \dirname( __FILE__ ) . '/admin-header.php', - true, - array( - 'settings' => '', - 'welcome' => '', - 'followers' => 'active', - ) - ); -} -?> -

From 73c767df399f4542d95e77023e7cec5619515137 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 13:47:37 +0200 Subject: [PATCH 19/97] fix admin-header issue --- templates/settings.php | 5 +++-- templates/welcome.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 6d8a0cb..477d3e6 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -3,8 +3,9 @@ \dirname( __FILE__ ) . '/admin-header.php', true, array( - 'settings' => 'active', - 'welcome' => '', + 'settings' => 'active', + 'welcome' => '', + 'followers' => '', ) ); ?> diff --git a/templates/welcome.php b/templates/welcome.php index a908d82..e3c47ee 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -3,8 +3,9 @@ \dirname( __FILE__ ) . '/admin-header.php', true, array( - 'settings' => '', - 'welcome' => 'active', + 'settings' => '', + 'welcome' => 'active', + 'followers' => '', ) ); ?> From e924019a7307f06f6aea3cf7a444c9236f5f2be7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 13:51:23 +0200 Subject: [PATCH 20/97] added translators hint --- templates/blog-user-followers-list.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php index db77e1e..9ce3bd1 100644 --- a/templates/blog-user-followers-list.php +++ b/templates/blog-user-followers-list.php @@ -15,7 +15,7 @@ - +

get_user_count() ) ); ?>

From 112eb51af180c4d26a9a6fb86be1d7822924ab08 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:45:07 +0200 Subject: [PATCH 21/97] updated signature feature to new structure --- includes/class-signature.php | 2 +- includes/class-user-factory.php | 3 ++- includes/model/class-application-user.php | 13 +++++++--- includes/rest/class-server.php | 24 +++---------------- ...typub-rest-post-signature-verification.php | 8 +++++-- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 66b16ba..a91ea0e 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -245,7 +245,7 @@ class Signature { * @return string The public key. */ public static function get_remote_key( $key_id ) { // phpcs:ignore - $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore + $actor = get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; } diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 8ba4521..db3be67 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -5,6 +5,7 @@ use WP_Error; use WP_User_Query; use Activitypub\Model\User; use Activitypub\Model\Blog_User; +use Activitypub\Model\Application_User; class User_Factory { /** @@ -63,7 +64,7 @@ class User_Factory { } // check for application user. - if ( get_option( 'activitypub_application_user_identifier', null ) === $username ) { + if ( 'application' === $username ) { return self::get_by_id( self::APPLICATION_USER_ID ); } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 1e562b1..fe5fcc8 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -2,8 +2,11 @@ namespace Activitypub\Model; use WP_Query; +use Activitypub\Signature; use Activitypub\User_Factory; +use function Activitypub\get_rest_url_by_path; + class Application_User extends Blog_User { /** * The User-ID @@ -24,7 +27,7 @@ class Application_User extends Blog_User { * * @param int $user_id The User-ID. */ - public function __construct( $user_id ) { + public function __construct( $user_id = null ) { // do nothing } @@ -34,11 +37,15 @@ class Application_User extends Blog_User { * @return string The User-Url. */ public function get_url() { - return ''; + return get_rest_url_by_path( 'application' ); } public function get_name() { - return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); + return 'application'; + } + + public function get_username() { + return $this::get_name(); } public function get_public_key() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 351284d..3b78af2 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -4,11 +4,7 @@ namespace Activitypub\Rest; use stdClass; use WP_REST_Response; use Activitypub\Signature; -use Activitypub\Model\User; - -use function Activitypub\get_context; -use function Activitypub\get_rest_url_by_path; - +use Activitypub\Model\Application_User; /** * ActivityPub Server REST-Class @@ -18,7 +14,6 @@ use function Activitypub\get_rest_url_by_path; * @see https://www.w3.org/TR/activitypub/#security-verification */ class Server { - /** * Initialize the class, registering WordPress hooks */ @@ -50,21 +45,8 @@ class Server { * @return WP_REST_Response The JSON profile of the Application Actor. */ public static function application_actor() { - $json = new stdClass(); - - $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( 'application' ); - $json->type = 'Application'; - $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions - $json->name = get_bloginfo( 'name' ); - $json->summary = __( 'WordPress-ActivityPub application actor', 'activitypub' ); - $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions - $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax - $json->publicKey = array( // phpcs:ignore WordPress.NamingConventions - 'id' => get_rest_url_by_path( 'application#main-key' ), - 'owner' => get_rest_url_by_path( 'application' ), - 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions - ); + $user = new Application_User(); + $json = $user->to_array(); $response = new WP_REST_Response( $json, 200 ); diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f0acd34..2b7fc29 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -42,7 +42,9 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); - $public_key = Activitypub\Signature::get_public_key( 1 ); + $user = Activitypub\User_Factory::get_by_id( 1 ); + + $public_key = $user->get_public_key(); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; @@ -53,6 +55,8 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { add_filter( 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { + $user = Activitypub\User_Factory::get_by_id( 1 ); + $public_key = $user->get_public_key(); // return ActivityPub Profile with signature return array( 'id' => $actor, @@ -60,7 +64,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { 'publicKey' => array( 'id' => $actor . '#main-key', 'owner' => $actor, - 'publicKeyPem' => \Activitypub\Signature::get_public_key( 1 ), + 'publicKeyPem' => $public_key, ), ); }, From d251060624c466703185b9f901aa3e87f357ea4e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:50:01 +0200 Subject: [PATCH 22/97] migrated missing parts --- includes/class-http.php | 4 ++-- includes/class-signature.php | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 58551a4..240e5ea 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,7 +2,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\Model\User; +use Activitypub\User_Factory; /** * ActivityPub HTTP Class @@ -63,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( User::APPLICATION_USER_ID, 'get', $url, $date ); + $signature = Signature::generate_signature( User_Factory::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index a91ea0e..c1ccbdf 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -136,11 +136,8 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - if ( User::APPLICATION_USER_ID === $user_id ) { - $key_id = \get_rest_url( null, 'activitypub/1.0/application#main-key' ); - } else { - $key_id = \get_author_posts_url( $user_id ) . '#main-key'; - } + $user = User_Factory::get_by_id( $user_id ); + $key_id = $user->get_url() . '#main-key'; if ( ! empty( $digest ) ) { return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature ); From 723a3e336324ee3cdc7b5d8f4692365e64fea144 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 12:47:08 +0200 Subject: [PATCH 23/97] fix signature issue --- includes/model/class-application-user.php | 12 ++++++------ includes/model/class-blog-user.php | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index fe5fcc8..bbeedd0 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -49,7 +49,7 @@ class Application_User extends Blog_User { } public function get_public_key() { - $key = \get_option( 'activitypub_application_user_public_key', true ); + $key = \get_option( 'activitypub_application_user_public_key' ); if ( $key ) { return $key; @@ -57,7 +57,7 @@ class Application_User extends Blog_User { $this->generate_key_pair(); - $key = \get_option( 'activitypub_application_user_public_key', true ); + $key = \get_option( 'activitypub_application_user_public_key' ); return $key; } @@ -68,7 +68,7 @@ class Application_User extends Blog_User { * @return mixed */ public function get_private_key() { - $key = \get_option( 'activitypub_application_user_private_key', true ); + $key = \get_option( 'activitypub_application_user_private_key' ); if ( $key ) { return $key; @@ -76,15 +76,15 @@ class Application_User extends Blog_User { $this->generate_key_pair(); - return \get_option( 'activitypub_application_user_private_key', true ); + return \get_option( 'activitypub_application_user_private_key' ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'], true ); - \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'], true ); + \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'] ); + \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'] ); } } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 32b2dc7..c92fdd3 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -142,7 +142,7 @@ class Blog_User extends User { } public function get_public_key() { - $key = \get_option( 'activitypub_blog_user_public_key', true ); + $key = \get_option( 'activitypub_blog_user_public_key' ); if ( $key ) { return $key; @@ -150,7 +150,7 @@ class Blog_User extends User { $this->generate_key_pair(); - $key = \get_option( 'activitypub_blog_user_public_key', true ); + $key = \get_option( 'activitypub_blog_user_public_key' ); return $key; } @@ -161,7 +161,7 @@ class Blog_User extends User { * @return mixed */ public function get_private_key() { - $key = \get_option( 'activitypub_blog_user_private_key', true ); + $key = \get_option( 'activitypub_blog_user_private_key' ); if ( $key ) { return $key; @@ -169,15 +169,15 @@ class Blog_User extends User { $this->generate_key_pair(); - return \get_option( 'activitypub_blog_user_private_key', true ); + return \get_option( 'activitypub_blog_user_private_key' ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'], true ); - \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'], true ); + \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'] ); + \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); } } } From a8fe587f913c52fba41154bb92640daf2fbc67eb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jun 2023 15:02:45 +0200 Subject: [PATCH 24/97] prepare federation method --- includes/class-activity-dispatcher.php | 16 +++++++------- includes/class-scheduler.php | 29 ++++++++++++++++---------- includes/functions.php | 3 ++- includes/model/class-blog-user.php | 7 ++++++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index bea6b4e..cde9785 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -26,6 +26,8 @@ class Activity_Dispatcher { \add_action( 'activitypub_send_create_activity', array( self::class, 'send_create_activity' ) ); \add_action( 'activitypub_send_update_activity', array( self::class, 'send_update_activity' ) ); \add_action( 'activitypub_send_delete_activity', array( self::class, 'send_delete_activity' ) ); + + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); } /** @@ -67,13 +69,8 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - if ( ! is_single_user_mode() ) { - // send User-Activity - self::send_user_activity( $activitypub_post, $activity_type ); - } - // send Blog-User-Activity - self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); + self::send_user_activity( $activitypub_post, $activity_type ); } /** @@ -92,12 +89,15 @@ class Activity_Dispatcher { $actor = $user->get_url(); } else { $user_id = $activitypub_post->get_post_author(); - $actor = $activitypub_activity->get_actor(); + $actor = null; } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $activitypub_activity->set_actor( $actor ); + + if ( $actor ) { + $activitypub_activity->set_actor( $actor ); + } $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index d55fb35..a5cfbc8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -70,22 +70,29 @@ class Scheduler { $activitypub_post = new Post( $post ); + $activity_type = false; + if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_create_activity', - array( $activitypub_post ) - ); + $activity_type = 'Create'; } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_activity', - array( $activitypub_post ) - ); + $activity_type = 'Update'; } elseif ( 'trash' === $new_status ) { + $activity_type = 'Delete'; + } + + if ( $activity_type ) { \wp_schedule_single_event( \time(), - 'activitypub_send_delete_activity', + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), array( $activitypub_post ) ); } diff --git a/includes/functions.php b/includes/functions.php index dfbdf37..595e85b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -56,9 +56,10 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { return $actor; } + $transient_key = 'activitypub_' . $actor; + // only check the cache if needed. if ( $cached ) { - $transient_key = 'activitypub_' . $actor; $metadata = \get_transient( $transient_key ); if ( $metadata ) { diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c92fdd3..8a04018 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -44,7 +44,12 @@ class Blog_User extends User { * @return string The User-Description. */ public function get_summary() { - return \wpautop( \wp_kses( \get_bloginfo( 'description' ), 'default' ) ); + return \wpautop( + \wp_kses( + \get_bloginfo( 'description' ), + 'default' + ) + ); } /** From 255ace3ae6ac8c0915ae652eb2de1d1494141c11 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:44:46 +0200 Subject: [PATCH 25/97] revert latest changes to simplify dispatching for now --- includes/class-activity-dispatcher.php | 30 ++------------------------ 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index cde9785..00ffd48 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -69,37 +69,11 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - // send Blog-User-Activity - self::send_user_activity( $activitypub_post, $activity_type ); - } - - /** - * Send Activities to followers and mentioned users. - * - * @param Post $activitypub_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. - * @param int $user_id The User-ID. - * - * @return void - */ - public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { - if ( $user_id ) { - $user = User_Factory::get_by_id( $user_id ); - $user_id = $user->get_id(); - $actor = $user->get_url(); - } else { - $user_id = $activitypub_post->get_post_author(); - $actor = null; - } - $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - if ( $actor ) { - $activitypub_activity->set_actor( $actor ); - } - - $follower_inboxes = Followers::get_inboxes( $user_id ); + $user_id = $activitypub_post->get_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 ); From 5f1abd246159f5404d62f683c3b7364d3de020a6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:45:03 +0200 Subject: [PATCH 26/97] fail early --- includes/class-scheduler.php | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index a5cfbc8..293b610 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -80,22 +80,24 @@ class Scheduler { $activity_type = 'Delete'; } - if ( $activity_type ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $activity_type ) - ), - array( $activitypub_post ) - ); + if ( ! $activity_type ) { + return; } + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); } /** From e88ee5911376dc61a6dfb7c47d4a09819f05d228 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:45:35 +0200 Subject: [PATCH 27/97] make user filterable, to change author to blog wide user --- includes/model/class-post.php | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 8e58a07..5ebeb34 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -1,6 +1,7 @@ post_author, $this->post ); + } + /** * Converts this Object into an Array. * - * @return array + * @return array the array representation of a Post. */ public function to_array() { $post = $this->post; @@ -203,7 +213,7 @@ class Post { 'url' => $this->get_url(), 'type' => $this->get_object_type(), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), - 'attributedTo' => \get_author_posts_url( $post->post_author ), + 'attributedTo' => $this->get_actor(), 'summary' => $this->get_summary(), 'inReplyTo' => null, 'content' => $this->get_content(), @@ -219,6 +229,17 @@ class Post { return \apply_filters( 'activitypub_post', $array, $this->post ); } + /** + * Returns the Actor of this Object. + * + * @return string The URL of the Actor. + */ + public function get_actor() { + $user = User_Factory::get_by_id( $this->get_user_id() ); + + return $user->get_url(); + } + /** * Converts this Object into a JSON String * From 6ddbe25852400535c0e9e3ff80d95c0e0b3383b6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:46:25 +0200 Subject: [PATCH 28/97] overwrite activity-object-user on single_user_mode --- includes/class-activitypub.php | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cd95fc7..733ae00 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -30,6 +30,15 @@ class Activitypub { \add_action( 'init', array( self::class, 'add_rewrite_rules' ) ); \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 ); + + if ( is_single_user_mode() ) { + add_filter( + 'activitypub_post_user_id', + function( $actor ) { + return User_Factory::BLOG_USER_ID; + } + ); + } } /** @@ -71,12 +80,15 @@ class Activitypub { * @return string The new path to the JSON template. */ public static function render_json_template( $template ) { - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + if ( ! is_activitypub_request() ) { return $template; } - // Ensure that edge caches know that this page can deliver both HTML and JSON. - header( 'Vary: Accept' ); + $json_template = false; + + if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + return $template; + } // check if user can publish posts if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { @@ -91,15 +103,12 @@ class Activitypub { $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php'; } - if ( is_activitypub_request() ) { - if ( ACTIVITYPUB_SECURE_MODE ) { - $verification = Signature::verify_http_signature( $_SERVER ); - if ( \is_wp_error( $verification ) ) { - // fallback as template_loader can't return http headers - return $template; - } + if ( ACTIVITYPUB_SECURE_MODE ) { + $verification = Signature::verify_http_signature( $_SERVER ); + if ( \is_wp_error( $verification ) ) { + // fallback as template_loader can't return http headers + return $template; } - return $json_template; } From 359cd5708161cffcf52f397419e21b75d27f3caf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:46:34 +0200 Subject: [PATCH 29/97] normalizing --- includes/class-user-factory.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index db3be67..ce70ccb 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -130,8 +130,8 @@ class User_Factory { $resource = \str_replace( 'acct:', '', $resource ); $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); - $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + $resource_host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); if ( $blog_host !== $resource_host ) { return new WP_Error( @@ -160,4 +160,15 @@ class User_Factory { return self::get_by_username( $id ); } } + + /** + * Normalize the host. + * + * @param string $host The host. + * + * @return string The normalized host. + */ + public static function normalize_host( $host ) { + return \str_replace( 'www.', '', $host ); + } } From 58c04856c9a9efbc7574249b144142bf67ed7ee9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 17:10:52 +0200 Subject: [PATCH 30/97] check if a user is enabled or not --- includes/class-user-factory.php | 42 ++++++++++++++++++------------ includes/functions.php | 46 +++++++++++++++++++++++++++++++++ includes/model/class-post.php | 2 +- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index ce70ccb..4a0ce5a 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -30,24 +30,32 @@ class User_Factory { * @return \Acitvitypub\Model\User The User. */ public static function get_by_id( $user_id ) { - $user_id = (int) $user_id; - - if ( self::BLOG_USER_ID === $user_id ) { - return new Blog_User( $user_id ); - } elseif ( self::APPLICATION_USER_ID === $user_id ) { - return new Application_User( $user_id ); - } else { - $user = get_user_by( 'ID', $user_id ); - if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - return new User( $user->ID ); + if ( is_string( $user_id ) || is_numeric( $user_id ) ) { + $user_id = (int) $user_id; } + + if ( + self::BLOG_USER_ID === $user_id && + is_user_enabled( $user_id ) + ) { + return new Blog_User( $user_id ); + } elseif ( + self::APPLICATION_USER_ID === $user_id && + is_user_enabled( $user_id ) + ) { + return new Application_User( $user_id ); + } elseif ( + $user_id > 0 && + is_user_enabled( $user_id ) + ) { + return new User( $user_id ); + } + + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); } /** diff --git a/includes/functions.php b/includes/functions.php index 595e85b..7fc62d8 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -277,3 +277,49 @@ function is_activitypub_request() { function is_single_user_mode() { return ACTIVITYPUB_SINGLE_USER_MODE; } + +/** + * This function checks if a user is enabled for ActivityPub. + * + * @param int $user_id The User-ID. + * + * @return boolean True if the user is enabled, false otherwise. + */ +function is_user_enabled( $user_id ) { + switch ( $user_id ) { + // if the user is the application user, it's always enabled. + case \Activitypub\User_Factory::APPLICATION_USER_ID: + return true; + // if the user is the blog user, it's only enabled in single-user mode. + case \Activitypub\User_Factory::BLOG_USER_ID: + if ( is_single_user_mode() ) { + return true; + } + + return false; + // if the user is any other user, it's enabled if it can publish posts. + default: + if ( + ! is_single_user_mode() && + \user_can( $user_id, 'publish_posts' ) + ) { + return true; + } + + return false; + } +} + +if ( ! function_exists( 'get_self_link' ) ) { + /** + * Get the correct self URL + * + * @return boolean + */ + function get_self_link() { + $host = wp_parse_url( home_url() ); + + return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); + } +} + diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 5ebeb34..5004162 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -197,7 +197,7 @@ class Post { * @return int the User ID. */ public function get_user_id() { - return apply_filters( 'activitypub_post_user_id', $this->post_author, $this->post ); + return apply_filters( 'activitypub_post_user_id', $this->get_post_author(), $this->post ); } /** From 36540c0f78c40904fc7004817ec893e032477622 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 09:56:18 +0200 Subject: [PATCH 31/97] fix delete --- includes/table/class-followers.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 3a107c7..f16a479 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -34,7 +34,7 @@ class Followers extends WP_List_Table { 'avatar' => \__( 'Avatar', 'activitypub' ), 'name' => \__( 'Name', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), - 'identifier' => \__( 'Identifier', 'activitypub' ), + 'url' => \__( 'URL', 'activitypub' ), 'updated' => \__( 'Last updated', 'activitypub' ), //'errors' => \__( 'Errors', 'activitypub' ), //'latest-error' => \__( 'Latest Error Message', 'activitypub' ), @@ -55,8 +55,8 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); - $counter = FollowerCollection::count_followers( $this->user_id ); + $followers = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); $this->set_pagination_args( @@ -72,7 +72,8 @@ class Followers extends WP_List_Table { 'icon' => esc_attr( $follower->get_icon_url() ), 'name' => esc_attr( $follower->get_name() ), 'username' => esc_attr( $follower->get_preferred_username() ), - 'identifier' => esc_attr( $follower->get_url() ), + 'url' => esc_attr( $follower->get_url() ), + 'identifier' => esc_attr( $follower->get_id() ), 'updated' => esc_attr( $follower->get_updated() ), 'errors' => $follower->count_errors(), 'latest-error' => $follower->get_latest_error_message(), @@ -102,11 +103,11 @@ class Followers extends WP_List_Table { ); } - public function column_identifier( $item ) { + public function column_url( $item ) { return sprintf( '%s', - $item['identifier'], - $item['identifier'] + $item['url'], + $item['url'] ); } @@ -135,7 +136,7 @@ class Followers extends WP_List_Table { $followers = array( $followers ); } foreach ( $followers as $follower ) { - FollowerCollection::remove_follower( \get_current_user_id(), $follower ); + FollowerCollection::remove_follower( $this->user_id, $follower ); } break; } From 83ddca8f28b3b37972175c5c601c1f2f9d4868b6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 10:14:13 +0200 Subject: [PATCH 32/97] fix templating --- includes/class-activitypub.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 733ae00..a4b29d0 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -109,10 +109,9 @@ class Activitypub { // fallback as template_loader can't return http headers return $template; } - return $json_template; } - return $template; + return $json_template; } /** From c266c927da203167de8eee21f688e80fdff75636 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 14:22:27 +0200 Subject: [PATCH 33/97] transform users to actors --- activitypub.php | 4 +- includes/activity/class-actor.php | 14 +++ includes/activity/class-base-object.php | 10 +- includes/activity/class-person.php | 20 ---- includes/class-activitypub.php | 11 +- includes/class-admin.php | 16 +-- includes/class-user-factory.php | 21 +--- includes/functions.php | 30 ++--- includes/model/class-application-user.php | 13 +-- includes/model/class-blog-user.php | 12 +- includes/model/class-user.php | 128 ++++++++-------------- includes/rest/class-followers.php | 6 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 6 +- includes/rest/class-outbox.php | 4 +- templates/admin-header.php | 4 + templates/author-json.php | 6 +- templates/blog-json.php | 6 +- templates/settings.php | 31 +----- templates/welcome.php | 11 ++ 20 files changed, 134 insertions(+), 221 deletions(-) delete mode 100644 includes/activity/class-person.php diff --git a/activitypub.php b/activitypub.php index 7bc4306..75d52b1 100644 --- a/activitypub.php +++ b/activitypub.php @@ -28,7 +28,9 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); - \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) || \define( 'ACTIVITYPUB_SINGLE_USER_MODE', false ); + + \defined( 'ACTIVITYPUB_DISABLE_USER' ) || \define( 'ACTIVITYPUB_DISABLE_USER', false ); + \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) || \define( 'ACTIVITYPUB_DISABLE_BLOG_USER', false ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 604261f..202783f 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -114,4 +114,18 @@ class Actor extends Base_Object { * @var string|array|null */ protected $public_key; + + /** + * It's not part of the ActivityPub protocol but it's a quite common + * practice to lock an account. If anabled, new followers will not be + * automatically accepted, but will instead require you to manually + * approve them. + * + * WordPress does only support 'false' at the moment. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#as + * + * @var boolean + */ + protected $manually_approves_followers = false; } diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 009cab3..18382a5 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -577,16 +577,22 @@ class Base_Object { foreach ( $vars as $key => $value ) { // if value is empty, try to get it from a getter. - if ( ! $value ) { + if ( ! isset( $value ) ) { $value = call_user_func( array( $this, 'get_' . $key ) ); } // if value is still empty, ignore it for the array and continue. - if ( $value ) { + if ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; } } + $class = new \ReflectionClass( $this ); + $class = strtolower( $class->getShortName() ); + + $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); + $array = \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); + return $array; } } diff --git a/includes/activity/class-person.php b/includes/activity/class-person.php deleted file mode 100644 index f2ed9b9..0000000 --- a/includes/activity/class-person.php +++ /dev/null @@ -1,20 +0,0 @@ - 0 && - is_user_enabled( $user_id ) - ) { - return new User( $user_id ); + if ( self::BLOG_USER_ID === $user_id ) { + return Blog_User::from_wp_user( $user_id ); + } elseif ( self::APPLICATION_USER_ID === $user_id ) { + return Application_User::from_wp_user( $user_id ); + } elseif ( $user_id > 0 ) { + return User::from_wp_user( $user_id ); } return new WP_Error( diff --git a/includes/functions.php b/includes/functions.php index 7fc62d8..a66d44e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -270,42 +270,34 @@ function is_activitypub_request() { } /** - * Check if the current site is in single-user mode. - * - * @return boolean - */ -function is_single_user_mode() { - return ACTIVITYPUB_SINGLE_USER_MODE; -} - -/** - * This function checks if a user is enabled for ActivityPub. + * This function checks if a user is disabled for ActivityPub. * * @param int $user_id The User-ID. * - * @return boolean True if the user is enabled, false otherwise. + * @return boolean True if the user is disabled, false otherwise. */ -function is_user_enabled( $user_id ) { +function is_user_disabled( $user_id ) { switch ( $user_id ) { // if the user is the application user, it's always enabled. case \Activitypub\User_Factory::APPLICATION_USER_ID: - return true; + return false; // if the user is the blog user, it's only enabled in single-user mode. case \Activitypub\User_Factory::BLOG_USER_ID: - if ( is_single_user_mode() ) { - return true; + if ( defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) { + return ACTIVITYPUB_DISABLE_BLOG_USER; } return false; // if the user is any other user, it's enabled if it can publish posts. default: - if ( - ! is_single_user_mode() && - \user_can( $user_id, 'publish_posts' ) - ) { + if ( ! \user_can( $user_id, 'publish_posts' ) ) { return true; } + if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { + return ACTIVITYPUB_DISABLE_USER; + } + return false; } } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index bbeedd0..d9bee08 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -13,23 +13,14 @@ class Application_User extends Blog_User { * * @var int */ - public $user_id = User_Factory::APPLICATION_USER_ID; + protected $_id = User_Factory::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Application'; - - /** - * The User constructor. - * - * @param int $user_id The User-ID. - */ - public function __construct( $user_id = null ) { - // do nothing - } + protected $type = 'Application'; /** * Get the User-Url. diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 8a04018..fe15bd9 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -11,22 +11,22 @@ class Blog_User extends User { * * @var int */ - public $user_id = User_Factory::BLOG_USER_ID; + protected $_id = User_Factory::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Person'; + protected $type = 'Person'; /** * The User constructor. * * @param int $user_id The User-ID. */ - public function __construct( $user_id = null ) { - add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + public function __construct() { + add_filter( 'activitypub_activity_blog_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); } /** @@ -61,10 +61,6 @@ class Blog_User extends User { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } - public function get_canonical_url() { - return \get_home_url(); - } - /** * Generate and save a default Username. * diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 302b784..320a713 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -6,34 +6,45 @@ use WP_Error; use Activitypub\Signature; use Activitypub\Model\User; use Activitypub\User_Factory; +use Activitypub\Activity\Actor; +use function Activitypub\is_user_disabled; use function Activitypub\get_rest_url_by_path; -class User { +class User extends Actor { /** * The User-ID * * @var int */ - public $user_id; + protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Person'; + protected $type = 'Person'; /** * The User constructor. * * @param numeric $user_id The User-ID. */ - public function __construct( $user_id ) { - $this->user_id = $user_id; + public function __construct() { + add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_attachments' ), 10, 2 ); + } - add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); - add_filter( 'activitypub_json_author_array', array( $this, 'add_attachments' ), 10, 2 ); + public static function from_wp_user( $user_id ) { + if ( is_user_disabled( $user_id ) ) { + return null; + } + + $object = new static(); + $object->_id = $user_id; + + return $object; } /** @@ -71,7 +82,7 @@ class User { * @return string The User-Name. */ public function get_name() { - return \esc_attr( \get_the_author_meta( 'display_name', $this->user_id ) ); + return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); } /** @@ -80,9 +91,9 @@ class User { * @return string The User-Description. */ public function get_summary() { - $description = get_user_meta( $this->user_id, 'activitypub_user_description', true ); + $description = get_user_meta( $this->_id, 'activitypub_user_description', true ); if ( empty( $description ) ) { - $description = get_user_meta( $this->user_id, 'description', true ); + $description = get_user_meta( $this->_id, 'description', true ); } return \wpautop( \wp_kses( $description, 'default' ) ); } @@ -93,44 +104,49 @@ class User { * @return string The User-Url. */ public function get_url() { - return \esc_url( \get_author_posts_url( $this->user_id ) ); - } - - public function get_canonical_url() { - return $this->get_url(); + return \esc_url( \get_author_posts_url( $this->_id ) ); } public function get_at_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } - public function get_username() { - return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); + public function get_preferred_username() { + return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); } - public function get_avatar() { - return \esc_url( + public function get_icon() { + $icon = \esc_url( \get_avatar_url( - $this->user_id, + $this->_id, array( 'size' => 120 ) ) ); + + return array( + 'type' => 'Image', + 'url' => $icon, + ); } - public function get_header_image() { + public function get_image() { if ( \has_header_image() ) { - return \esc_url( \get_header_image() ); + $image = \esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => $image, + ); } return null; } public function get_published() { - return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->user_id ) ) ); + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } public function get_public_key() { - $key = \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + $key = \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); if ( $key ) { return $key; @@ -138,7 +154,7 @@ class User { $this->generate_key_pair(); - return \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + return \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); } /** @@ -147,7 +163,7 @@ class User { * @return mixed */ public function get_private_key() { - $key = \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); if ( $key ) { return $key; @@ -155,70 +171,18 @@ class User { $this->generate_key_pair(); - return \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + return \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_user_meta( $this->get_user_id(), 'magic_sig_public_key', $key_pair['public_key'], true ); - \update_user_meta( $this->get_user_id(), 'magic_sig_private_key', $key_pair['private_key'], true ); + \update_user_meta( $this->get__id(), 'magic_sig_public_key', $key_pair['public_key'], true ); + \update_user_meta( $this->get__id(), 'magic_sig_private_key', $key_pair['private_key'], true ); } } - /** - * Array representation of the User. - * - * @param bool $context Whether to include the @context. - * - * @return array The array representation of the User. - */ - public function to_array( $context = true ) { - $output = array(); - - if ( $context ) { - $output['@context'] = Activity::CONTEXT; - } - - $output['id'] = $this->get_url(); - $output['type'] = $this->get_type(); - $output['name'] = $this->get_name(); - $output['summary'] = \html_entity_decode( - $this->get_summary(), - \ENT_QUOTES, - 'UTF-8' - ); - $output['preferredUsername'] = $this->get_username(); // phpcs:ignore - $output['url'] = $this->get_url(); - $output['icon'] = array( - 'type' => 'Image', - 'url' => $this->get_avatar(), - ); - - if ( $this->has_header_image() ) { - $output['image'] = array( - 'type' => 'Image', - 'url' => $this->get_header_image(), - ); - } - - $output['published'] = $this->get_published(); - - $output['publicKey'] = array( - 'id' => $this->get_url() . '#main-key', - 'owner' => $this->get_url(), - 'publicKeyPem' => \trim( $this->get_public_key() ), - ); - - $output['manuallyApprovesFollowers'] = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - - // filter output - $output = \apply_filters( 'activitypub_json_author_array', $output, $this->user_id, $this ); - - return $output; - } - /** * Extend the User-Output with API-Endpoints. * @@ -283,6 +247,6 @@ class User { } public function get_resource() { - return $this->get_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 792cdfb..477393e 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -72,15 +72,15 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore - $json->totalItems = FollowerCollection::count_followers( $user->get_user_id() ); // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get_user_id() ) + FollowerCollection::get_followers( $user->get__id() ) ); $response = new WP_REST_Response( $json, 200 ); diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 0d1de61..6f13482 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -67,7 +67,7 @@ class Following { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 54e2894..b8d75c9 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -93,7 +93,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore @@ -136,8 +136,8 @@ class Inbox { $type = $request->get_param( 'type' ); $type = \strtolower( $type ); - \do_action( 'activitypub_inbox', $data, $user->get_user_id(), $type ); - \do_action( "activitypub_inbox_{$type}", $data, $user->get_user_id() ); + \do_action( 'activitypub_inbox', $data, $user->get__id(), $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() ); return new WP_REST_Response( array(), 202 ); } diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index cb09cfd..1dc05c7 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -75,7 +75,7 @@ class Outbox { $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -97,7 +97,7 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user->get_user_id(), + 'author' => $user->get__id(), 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) diff --git a/templates/admin-header.php b/templates/admin-header.php index 974b224..f2e7ed5 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -12,9 +12,13 @@ + + + +

diff --git a/templates/author-json.php b/templates/author-json.php index aa38b97..fe569db 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -4,7 +4,7 @@ $user = \Activitypub\User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); +\do_action( 'activitypub_json_author_pre', $user->get__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -19,7 +19,7 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get__id() ); \header( 'Content-Type: application/activity+json' ); echo \wp_json_encode( $user->to_array(), $options ); @@ -27,4 +27,4 @@ echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $user->get_user_id() ); +\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/templates/blog-json.php b/templates/blog-json.php index b9c5f96..677c574 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -4,7 +4,7 @@ $user = new \Activitypub\Model\Blog_User(); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); +\do_action( 'activitypub_json_author_pre', $user->get__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -19,7 +19,7 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get__id() ); \header( 'Content-Type: application/activity+json' ); echo \wp_json_encode( $user->to_array(), $options ); @@ -27,4 +27,4 @@ echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $user->get_user_id() ); +\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/templates/settings.php b/templates/settings.php index 477d3e6..162ea02 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,37 +31,12 @@ + +

-
- +

- + Customizer.', 'activitypub' ), + \esc_url_raw( \admin_url( 'customize.php' ) ) + ), + 'default' + ); + ?>

- - - - - - -
- - -
- -
-

- Customizer.', 'activitypub' ), - \esc_url_raw( \admin_url( 'customize.php' ) ) - ), - 'default' - ); - ?> -

-
- @@ -83,6 +58,8 @@ + +

diff --git a/templates/welcome.php b/templates/welcome.php index e3c47ee..0bce61c 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -14,6 +14,9 @@

+ + +

+ + + + +

+ + +

Date: Wed, 28 Jun 2023 16:42:20 +0200 Subject: [PATCH 34/97] put @context at the top of the JSON output --- includes/activity/class-base-object.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 18382a5..0ad4457 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -587,6 +587,13 @@ class Base_Object { } } + // replace 'context' key with '@context' and move it to the top. + if ( array_key_exists( 'context', $array ) ) { + $context = $array['context']; + unset( $array['context'] ); + $array = array_merge( array( '@context' => $context ), $array ); + } + $class = new \ReflectionClass( $this ); $class = strtolower( $class->getShortName() ); From a706bef1306c6af78d748f5a4c814a2ac350e32a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:42:33 +0200 Subject: [PATCH 35/97] check for option field --- includes/class-user-factory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 8011445..8cc9f26 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -62,6 +62,10 @@ class User_Factory { return self::get_by_id( self::BLOG_USER_ID ); } + if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + // check for application user. if ( 'application' === $username ) { return self::get_by_id( self::APPLICATION_USER_ID ); From 43db2f27078feca8f421b4c0ca144283e2121050 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:43:05 +0200 Subject: [PATCH 36/97] set context for output --- templates/author-json.php | 4 ++++ templates/blog-json.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/templates/author-json.php b/templates/author-json.php index fe569db..3defd98 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,6 +1,10 @@ set_context( + \Activitypub\Model\Activity::CONTEXT +); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ diff --git a/templates/blog-json.php b/templates/blog-json.php index 677c574..635e7d5 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,6 +1,10 @@ set_context( + \Activitypub\Model\Activity::CONTEXT +); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ From c02702f773bf7d445c75c5a35691e1074dd94f40 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:43:41 +0200 Subject: [PATCH 37/97] replace filters --- includes/model/class-application-user.php | 20 +++++ includes/model/class-blog-user.php | 26 ++++-- includes/model/class-user.php | 103 ++++++++++------------ 3 files changed, 87 insertions(+), 62 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index d9bee08..9c53f9d 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -78,4 +78,24 @@ class Application_User extends Blog_User { \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'] ); } } + + public function get_inbox() { + return null; + } + + public function get_outbox() { + return null; + } + + public function get_followers() { + return null; + } + + public function get_following() { + return null; + } + + public function get_attachment() { + return array(); + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index fe15bd9..a4d3374 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -5,6 +5,8 @@ use WP_Query; use Activitypub\Signature; use Activitypub\User_Factory; +use function Activitypub\is_user_disabled; + class Blog_User extends User { /** * The User-ID @@ -20,13 +22,19 @@ class Blog_User extends User { */ protected $type = 'Person'; - /** - * The User constructor. - * - * @param int $user_id The User-ID. - */ - public function __construct() { - add_filter( 'activitypub_activity_blog_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + public static function from_wp_user( $user_id ) { + if ( is_user_disabled( $user_id ) ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $object = new static(); + $object->_id = $user_id; + + return $object; } /** @@ -181,4 +189,8 @@ class Blog_User extends User { \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); } } + + public function get_attachment() { + return array(); + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 320a713..2ee2258 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -26,19 +26,16 @@ class User extends Actor { */ protected $type = 'Person'; - /** - * The User constructor. - * - * @param numeric $user_id The User-ID. - */ - public function __construct() { - add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); - add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_attachments' ), 10, 2 ); - } - public static function from_wp_user( $user_id ) { - if ( is_user_disabled( $user_id ) ) { - return null; + if ( + is_user_disabled( $user_id ) || + ! get_user_by( 'id', $user_id ) + ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); } $object = new static(); @@ -47,26 +44,6 @@ class User extends Actor { return $object; } - /** - * 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 ) { - return $this->$var; - } - - if ( \strncasecmp( $method, 'has', 3 ) === 0 ) { - return (bool) call_user_func( 'get_' . $var, $this ); - } - } - /** * Get the User-ID. * @@ -158,7 +135,7 @@ class User extends Actor { } /** - * @param int $user_id + * @param int $this->get__id() * * @return mixed */ @@ -184,34 +161,50 @@ class User extends Actor { } /** - * Extend the User-Output with API-Endpoints. + * Returns the Inbox-API-Endpoint. * - * @param array $array The User-Output. - * @param numeric $user_id The User-ID. - * - * @return array The extended User-Output. + * @return string The Inbox-Endpoint. */ - public function add_api_endpoints( $array, $user_id ) { - $array['inbox'] = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); - $array['outbox'] = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); - $array['followers'] = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); - $array['following'] = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); + public function get_inbox() { + return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) ); + } - return $array; + /** + * Returns the Outbox-API-Endpoint. + * + * @return string The Outbox-Endpoint. + */ + public function get_outbox() { + return get_rest_url_by_path( sprintf( 'users/%d/outbox', $this->get__id() ) ); + } + + /** + * Returns the Followers-API-Endpoint. + * + * @return string The Followers-Endpoint. + */ + public function get_followers() { + return get_rest_url_by_path( sprintf( 'users/%d/followers', $this->get__id() ) ); + } + + /** + * Returns the Following-API-Endpoint. + * + * @return string The Following-Endpoint. + */ + public function get_following() { + return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) ); } /** * Extend the User-Output with Attachments. * - * @param array $array The User-Output. - * @param numeric $user_id The User-ID. - * * @return array The extended User-Output. */ - public function add_attachments( $array, $user_id ) { - $array['attachment'] = array(); + public function get_attachment() { + $array = array(); - $array['attachment']['blog_url'] = array( + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Blog', 'activitypub' ), 'value' => \html_entity_decode( @@ -221,22 +214,22 @@ class User extends Actor { ), ); - $array['attachment']['profile_url'] = array( + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Profile', 'activitypub' ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_author_posts_url( $user_id ), \PHP_URL_HOST ) . '', + '' . \wp_parse_url( \get_author_posts_url( $this->get__id() ), \PHP_URL_HOST ) . '', \ENT_QUOTES, 'UTF-8' ), ); - if ( \get_the_author_meta( 'user_url', $user_id ) ) { - $array['attachment']['user_url'] = array( + if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) { + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Website', 'activitypub' ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_the_author_meta( 'user_url', $user_id ), \PHP_URL_HOST ) . '', + '' . \wp_parse_url( \get_the_author_meta( 'user_url', $this->get__id() ), \PHP_URL_HOST ) . '', \ENT_QUOTES, 'UTF-8' ), From 75a77b3f5c5e0cdecddcc2e44bfeb5443b0e946b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 18:02:14 +0200 Subject: [PATCH 38/97] finalize account handling still missing: publishing --- includes/activity/class-base-object.php | 1 - includes/class-activitypub.php | 8 ++++---- includes/class-signature.php | 2 +- includes/functions.php | 2 +- includes/model/class-application-user.php | 4 ++-- includes/model/class-blog-user.php | 8 ++++++-- includes/model/class-user.php | 19 ++++++++++++++++++- includes/rest/class-users.php | 4 ++++ includes/rest/class-webfinger.php | 1 - ...-class-activitypub-activity-dispatcher.php | 1 + ...typub-rest-post-signature-verification.php | 4 ++-- 11 files changed, 39 insertions(+), 15 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 0ad4457..c8520ba 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -472,7 +472,6 @@ class Base_Object { public function get( $key ) { if ( ! $this->has( $key ) ) { return new WP_Error( 'invalid_key', 'Invalid key' ); - } return $this->$key; diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 8e72caa..cd84dc1 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -71,16 +71,16 @@ class Activitypub { * @return string The new path to the JSON template. */ public static function render_json_template( $template ) { + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return $template; + } + if ( ! is_activitypub_request() ) { return $template; } $json_template = false; - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { - return $template; - } - // check if user can publish posts if ( \is_author() && ! User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ) ) { return $template; diff --git a/includes/class-signature.php b/includes/class-signature.php index c1ccbdf..b9666cc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -107,7 +107,7 @@ class Signature { */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $user = User_Factory::get_by_id( $user_id ); - $key = $user->get_private_key(); + $key = $user->get__private_key(); $url_parts = \wp_parse_url( $url ); diff --git a/includes/functions.php b/includes/functions.php index a66d44e..88b0e2b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -237,7 +237,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( 'REST_REQUEST' ) && ! REST_REQUEST ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) && ! \REST_REQUEST ) { return false; } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 9c53f9d..c4ffaca 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -39,7 +39,7 @@ class Application_User extends Blog_User { return $this::get_name(); } - public function get_public_key() { + public function get__public_key() { $key = \get_option( 'activitypub_application_user_public_key' ); if ( $key ) { @@ -58,7 +58,7 @@ class Application_User extends Blog_User { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_option( 'activitypub_application_user_private_key' ); if ( $key ) { diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a4d3374..f3fd095 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -150,7 +150,7 @@ class Blog_User extends User { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } - public function get_public_key() { + public function get__public_key() { $key = \get_option( 'activitypub_blog_user_public_key' ); if ( $key ) { @@ -169,7 +169,7 @@ class Blog_User extends User { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_option( 'activitypub_blog_user_private_key' ); if ( $key ) { @@ -193,4 +193,8 @@ class Blog_User extends User { public function get_attachment() { return array(); } + + public function get_canonical_url() { + return \home_url(); + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 2ee2258..10d10d6 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -123,6 +123,19 @@ class User extends Actor { } public function get_public_key() { + return array( + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), + 'publicKeyPem' => $this->get__public_key(), + ); + } + + /** + * @param int $this->get__id() + * + * @return mixed + */ + public function get__public_key() { $key = \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); if ( $key ) { @@ -139,7 +152,7 @@ class User extends Actor { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); if ( $key ) { @@ -242,4 +255,8 @@ class User extends Actor { public function get_resource() { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + + public function get_canonical_url() { + return $this->get_url(); + } } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 66637b7..1dd08f8 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -67,6 +67,10 @@ class Users { */ \do_action( 'activitypub_outbox_pre' ); + $user->set_context( + \Activitypub\Model\Activity::CONTEXT + ); + $json = $user->to_array(); $response = new WP_REST_Response( $json, 200 ); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index ec76686..f124889 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -55,7 +55,6 @@ class Webfinger { $aliases = array( $user->get_url(), - $user->get_canonical_url(), $user->get_at_url(), ); diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 70ed304..61f8c2b 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -49,6 +49,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_mentions() { $post = \wp_insert_post( array( diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index 2b7fc29..3ee895e 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -44,7 +44,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $user = Activitypub\User_Factory::get_by_id( 1 ); - $public_key = $user->get_public_key(); + $public_key = $user->get__public_key(); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; @@ -56,7 +56,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { $user = Activitypub\User_Factory::get_by_id( 1 ); - $public_key = $user->get_public_key(); + $public_key = $user->get__public_key(); // return ActivityPub Profile with signature return array( 'id' => $actor, From 1fe8c26b1d43bd8908507aa4c48be486eced3c2e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 19:38:19 +0200 Subject: [PATCH 39/97] ignore prefixed attributes --- includes/activity/class-base-object.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index c8520ba..ee98411 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -462,6 +462,24 @@ class Base_Object { } } + /** + * Magic function, to transform the object to string. + * + * @return string The object id. + */ + public function __toString() { + return $this->to_string(); + } + + /** + * Function to transform the object to string. + * + * @return string The object id. + */ + public function to_string() { + return $this->get_id(); + } + /** * Generic getter. * @@ -575,6 +593,11 @@ class Base_Object { $vars = get_object_vars( $this ); foreach ( $vars as $key => $value ) { + // ignotre all _prefixed keys. + if ( '_' === substr( $key, 0, 1 ) ) { + continue; + } + // if value is empty, try to get it from a getter. if ( ! isset( $value ) ) { $value = call_user_func( array( $this, 'get_' . $key ) ); From 68e9bfdc7996921c3e3ed00154e385cb07079318 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 19:38:50 +0200 Subject: [PATCH 40/97] this is now part of the Base_Object --- includes/model/class-follower.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index e95aff8..297bb36 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -54,15 +54,6 @@ class Follower extends Actor { */ protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - /** - * Magic function to return the Actor-URL when the Object is used as a string - * - * @return string - */ - public function __toString() { - return $this->get_url(); - } - /** * Set new Error * From 1543c49c19eb05d52889cf8382cf2af2471e5649 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 14:54:45 +0200 Subject: [PATCH 41/97] some doc changes --- includes/activity/class-base-object.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index ee98411..4343782 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -11,15 +11,16 @@ use WP_Error; use function Activitypub\camel_to_snake_case; use function Activitypub\snake_to_camel_case; + /** - * ObjectType is an implementation of one of the + * Base_Object is an implementation of one of the * Activity Streams Core Types. * * The Object is the primary base type for the Activity Streams * vocabulary. * * Note: Object is a reserved keyword in PHP. It has been suffixed with - * 'Type' for this reason. + * 'Base_' for this reason. * * @see https://www.w3.org/TR/activitystreams-core/#object */ From 3e969c859ac9a815b97638ea620091cd3fa12b60 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 18:44:25 +0200 Subject: [PATCH 42/97] send blog-wide activities if enabled --- includes/class-scheduler.php | 49 ++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 293b610..0349e44 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -84,20 +84,43 @@ class Scheduler { return; } - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) - ); + // send User activities + if ( ! is_user_disabled( $activitypub_post->get_user_id() ) ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $activity_type ) - ), - array( $activitypub_post ) - ); + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); + } + + // send Blog-User activities + if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + $activitypub_post->set_post_author( User_Factory::BLOG_USER_ID ); + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); + } } /** From ced8cd0e29c7e730777afd67c94e46ef07dfded0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 19:10:49 +0200 Subject: [PATCH 43/97] send activities for blog-wide user --- includes/class-scheduler.php | 26 +++++++++++++------------- includes/model/class-post.php | 10 +++++++++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 0349e44..94a74a8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -68,35 +68,35 @@ class Scheduler { return; } - $activitypub_post = new Post( $post ); - - $activity_type = false; + $type = false; if ( 'publish' === $new_status && 'publish' !== $old_status ) { - $activity_type = 'Create'; + $type = 'Create'; } elseif ( 'publish' === $new_status ) { - $activity_type = 'Update'; + $type = 'Update'; } elseif ( 'trash' === $new_status ) { - $activity_type = 'Delete'; + $type = 'Delete'; } - if ( ! $activity_type ) { + if ( ! $type ) { return; } // send User activities - if ( ! is_user_disabled( $activitypub_post->get_user_id() ) ) { + if ( ! is_user_disabled( $post->post_author ) ) { + $activitypub_post = new Post( $post ); + \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) + array( $activitypub_post, $type ) ); \wp_schedule_single_event( \time(), sprintf( 'activitypub_send_%s_activity', - \strtolower( $activity_type ) + \strtolower( $type ) ), array( $activitypub_post ) ); @@ -104,19 +104,19 @@ class Scheduler { // send Blog-User activities if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { - $activitypub_post->set_post_author( User_Factory::BLOG_USER_ID ); + $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) + array( $activitypub_post, $type ) ); \wp_schedule_single_event( \time(), sprintf( 'activitypub_send_%s_activity', - \strtolower( $activity_type ) + \strtolower( $type ) ), array( $activitypub_post ) ); diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 5004162..03199ff 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -147,9 +147,17 @@ class Post { * Constructor * * @param WP_Post $post + * @param int $post_author */ - public function __construct( $post ) { + public function __construct( $post, $post_author = null ) { $this->post = \get_post( $post ); + + if ( $post_author ) { + $this->post_author = $post_author; + } else { + $this->post_author = $this->post->post_author; + } + $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) ); $this->add_to( get_rest_url_by_path( $path ) ); } From f207089269a5070be395785996269a9371cbbb7c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 30 Jun 2023 16:08:28 +0200 Subject: [PATCH 44/97] revert scheduler/dispatcher changes --- includes/class-scheduler.php | 51 ++++++++++-------------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 94a74a8..ddb68e1 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -82,45 +82,22 @@ class Scheduler { return; } - // send User activities - if ( ! is_user_disabled( $post->post_author ) ) { - $activitypub_post = new Post( $post ); + $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $type ) - ); + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $type ) + ); - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $activitypub_post ) - ); - } - - // send Blog-User activities - if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { - $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); - - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $activitypub_post ) - ); - } + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $type ) + ), + array( $activitypub_post ) + ); } /** From dd67f76db199fcdc069dbbfeac574c5de56bae03 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 30 Jun 2023 16:12:04 +0200 Subject: [PATCH 45/97] fix class names --- includes/class-scheduler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ddb68e1..db2d28c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; +use Activitypub\User_Factory; use Activitypub\Collection\Followers; /** @@ -82,7 +83,7 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); + $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); \wp_schedule_single_event( \time(), From 359eabf67149448eee8f2086e9687078142b5537 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 11:20:44 +0200 Subject: [PATCH 46/97] use collection instead of factory --- includes/activity/class-activity.php | 165 ++++++++++++++++++ includes/activity/class-actor.php | 8 + includes/class-activity-dispatcher.php | 2 +- includes/class-activitypub.php | 2 +- includes/class-http.php | 4 +- includes/class-scheduler.php | 3 +- includes/class-signature.php | 6 +- includes/class-webfinger.php | 2 +- .../class-users.php} | 4 +- includes/functions.php | 4 +- includes/model/class-application-user.php | 4 +- includes/model/class-blog-user.php | 4 +- includes/model/class-post.php | 4 +- includes/model/class-user.php | 4 +- includes/peer/class-users.php | 67 ------- includes/rest/class-followers.php | 4 +- includes/rest/class-following.php | 4 +- includes/rest/class-inbox.php | 6 +- includes/rest/class-outbox.php | 4 +- includes/rest/class-users.php | 7 +- includes/rest/class-webfinger.php | 4 +- includes/table/class-followers.php | 4 +- includes/transformer/class-wp-user.php | 76 ++++++++ templates/admin-header.php | 2 +- templates/author-json.php | 2 +- templates/settings.php | 2 +- templates/welcome.php | 4 +- ...typub-rest-post-signature-verification.php | 4 +- 28 files changed, 295 insertions(+), 111 deletions(-) create mode 100644 includes/activity/class-activity.php rename includes/{class-user-factory.php => collection/class-users.php} (98%) delete mode 100644 includes/peer/class-users.php create mode 100644 includes/transformer/class-wp-user.php diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php new file mode 100644 index 0000000..592bbc1 --- /dev/null +++ b/includes/activity/class-activity.php @@ -0,0 +1,165 @@ + 'as:manuallyApprovesFollowers', + 'PropertyValue' => 'schema:PropertyValue', + 'schema' => 'http://schema.org#', + 'pt' => 'https://joinpeertube.org/ns#', + 'toot' => 'http://joinmastodon.org/ns#', + 'value' => 'schema:value', + 'Hashtag' => 'as:Hashtag', + 'featured' => array( + '@id' => 'toot:featured', + '@type' => '@id', + ), + 'featuredTags' => array( + '@id' => 'toot:featuredTags', + '@type' => '@id', + ), + ), + ); + + /** + * The object's unique global identifier + * + * @see https://www.w3.org/TR/activitypub/#obj-id + * + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $type = 'Activity'; + + /** + * The context within which the object exists or an activity was + * performed. + * The notion of "context" used is intentionally vague. + * The intended function is to serve as a means of grouping objects + * and activities that share a common originating context or + * purpose. An example could be all activities relating to a common + * project or event. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $context = self::CONTEXT; + + /** + * Describes the direct object of the activity. + * For instance, in the activity "John added a movie to his + * wishlist", the object of the activity is the movie added. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $object; + + /** + * Describes one or more entities that either performed or are + * expected to perform the activity. + * Any single activity can have multiple actors. + * The actor MAY be specified using an indirect Link. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor + * + * @var string + * | \ActivityPhp\Type\Extended\AbstractActor + * | array + * | array + * | Link + */ + protected $actor; + + /** + * The indirect object, or target, of the activity. + * The precise meaning of the target is largely dependent on the + * type of action being described but will often be the object of + * the English preposition "to". + * For instance, in the activity "John added a movie to his + * wishlist", the target of the activity is John's wishlist. + * An activity can have more than one target. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target + * + * @var string + * | ObjectType + * | array + * | Link + * | array + */ + protected $target; + + /** + * Describes the result of the activity. + * For instance, if a particular action results in the creation of + * a new resource, the result property can be used to describe + * that new resource. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $result; + + /** + * An indirect object of the activity from which the + * activity is directed. + * The precise meaning of the origin is the object of the English + * preposition "from". + * For instance, in the activity "John moved an item to List B + * from List A", the origin of the activity is "List A". + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $origin; + + /** + * One or more objects used (or to be used) in the completion of an + * Activity. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $instrument; +} diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 202783f..fabd653 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -7,6 +7,14 @@ namespace Activitypub\Activity; +/** + * \Activitypub\Activity\Actor is an implementation of + * one an Activity Streams Actor. + * + * Represents an individual actor. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + */ class Actor extends Base_Object { /** * @var string diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 00ffd48..c7862c0 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,7 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use function Activitypub\safe_remote_post; diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cd84dc1..3b4d68a 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -82,7 +82,7 @@ class Activitypub { $json_template = false; // check if user can publish posts - if ( \is_author() && ! User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ) ) { + if ( \is_author() && ! Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) { return $template; } diff --git a/includes/class-http.php b/includes/class-http.php index 240e5ea..dd19f9b 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,7 +2,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub HTTP Class @@ -63,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( User_Factory::APPLICATION_USER_ID, 'get', $url, $date ); + $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ddb68e1..ed0dbda 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers; /** @@ -82,7 +83,7 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); + $activitypub_post = new Post( $post ); \wp_schedule_single_event( \time(), diff --git a/includes/class-signature.php b/includes/class-signature.php index b9666cc..b7c98fc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -5,7 +5,7 @@ use WP_Error; use DateTime; use DateTimeZone; use Activitypub\Model\User; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub Signature Class @@ -106,7 +106,7 @@ class Signature { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); $key = $user->get__private_key(); $url_parts = \wp_parse_url( $url ); @@ -136,7 +136,7 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); $key_id = $user->get_url() . '#main-key'; if ( ! empty( $digest ) ) { diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 958f544..4a3a1a6 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -24,7 +24,7 @@ class Webfinger { return \get_webfinger_resource( $user_id, false ); } - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); if ( ! $user ) { return ''; } diff --git a/includes/class-user-factory.php b/includes/collection/class-users.php similarity index 98% rename from includes/class-user-factory.php rename to includes/collection/class-users.php index 8cc9f26..62b4264 100644 --- a/includes/class-user-factory.php +++ b/includes/collection/class-users.php @@ -1,5 +1,5 @@ get_user_id() ); + $user = Users::get_by_id( $this->get_user_id() ); return $user->get_url(); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 10d10d6..c66e3d8 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -5,7 +5,7 @@ use WP_Query; use WP_Error; use Activitypub\Signature; use Activitypub\Model\User; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Activity\Actor; use function Activitypub\is_user_disabled; @@ -13,7 +13,7 @@ use function Activitypub\get_rest_url_by_path; class User extends Actor { /** - * The User-ID + * The local User-ID (WP_User). * * @var int */ diff --git a/includes/peer/class-users.php b/includes/peer/class-users.php deleted file mode 100644 index fd9c7b8..0000000 --- a/includes/peer/class-users.php +++ /dev/null @@ -1,67 +0,0 @@ -wp_rewrite_rules(); - - // not using rewrite rules, and 'author=N' method failed, so we're out of options - if ( empty( $rewrite ) ) { - return 0; - } - - // generate rewrite rule for the author url - $author_rewrite = $wp_rewrite->get_author_permastruct(); - $author_regexp = \str_replace( '%author%', '', $author_rewrite ); - - // match the rewrite rule with the passed url - if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) { - $user = \get_user_by( 'slug', $match[2] ); - if ( $user ) { - return $user->ID; - } - } - - return 0; - } -} diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 477393e..c359391 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,7 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -52,7 +52,7 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 6f13482..10f4bed 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,7 +1,7 @@ get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b8d75c9..eee740c 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Model\Activity; use function Activitypub\get_context; @@ -74,7 +74,7 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -126,7 +126,7 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 1dc05c7..3df982f 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,7 +5,7 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Model\Post; use Activitypub\Model\Activity; @@ -53,7 +53,7 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 1dd08f8..dac7792 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,7 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use \Activitypub\Model\Activity; +use Activitypub\Collection\User_Collection; use function Activitypub\is_activitypub_request; @@ -50,7 +51,7 @@ class Users { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -68,7 +69,7 @@ class Users { \do_action( 'activitypub_outbox_pre' ); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + Activity::CONTEXT ); $json = $user->to_array(); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f124889..1dc79c9 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,7 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub WebFinger REST-Class @@ -47,7 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = User_Factory::get_by_resource( $resource ); + $user = Users::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index f16a479..93d9456 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -2,7 +2,7 @@ namespace Activitypub\Table; use WP_List_Table; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers as FollowerCollection; if ( ! \class_exists( '\WP_List_Table' ) ) { @@ -14,7 +14,7 @@ class Followers extends WP_List_Table { public function __construct() { if ( get_current_screen()->id === 'settings_page_activitypub' ) { - $this->user_id = User_Factory::BLOG_USER_ID; + $this->user_id = Users::BLOG_USER_ID; } else { $this->user_id = \get_current_user_id(); } diff --git a/includes/transformer/class-wp-user.php b/includes/transformer/class-wp-user.php new file mode 100644 index 0000000..083b119 --- /dev/null +++ b/includes/transformer/class-wp-user.php @@ -0,0 +1,76 @@ +wp_user = $wp_user; + } + + public function to_user() { + $wp_user = $this->wp_user; + if ( + is_user_disabled( $user->ID ) || + ! get_user_by( 'id', $user->ID ) + ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $user = new User(); + + $user->setwp_user->ID( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); + $user->set_url( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); + $user->set_summary( $this->get_summary() ); + $user->set_name( \esc_attr( $wp_user->display_name ) ); + $user->set_preferred_username( \esc_attr( $wp_user->login ) ); + + $user->set_icon( $this->get_icon() ); + $user->set_image( $this->get_image() ); + + return $user; + } + + public function get_summary() { + $description = get_user_meta( $this->wp_user->ID, 'activitypub_user_description', true ); + if ( empty( $description ) ) { + $description = $this->wp_user->description; + } + return \wpautop( \wp_kses( $description, 'default' ) ); + } + + public function get_icon() { + $icon = \esc_url( + \get_avatar_url( + $this->wp_user->ID, + array( 'size' => 120 ) + ) + ); + + return array( + 'type' => 'Image', + 'url' => $icon, + ); + } + + public function get_image() { + if ( \has_header_image() ) { + $image = \esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => $image, + ); + } + + return null; + } +} diff --git a/templates/admin-header.php b/templates/admin-header.php index f2e7ed5..3b40468 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -12,7 +12,7 @@ - + diff --git a/templates/author-json.php b/templates/author-json.php index 3defd98..6c5f8ad 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,5 +1,5 @@ set_context( \Activitypub\Model\Activity::CONTEXT diff --git a/templates/settings.php b/templates/settings.php index 162ea02..b587b9e 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,7 +31,7 @@ - +

diff --git a/templates/welcome.php b/templates/welcome.php index 0bce61c..02e8be3 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -15,7 +15,7 @@

- +

@@ -44,7 +44,7 @@

ID ); + $user = \Activitypub\Collection\Users::get_by_id( wp_get_current_user()->ID ); echo wp_kses( \sprintf( // translators: diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index 3ee895e..f39927d 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -42,7 +42,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); - $user = Activitypub\User_Factory::get_by_id( 1 ); + $user = Activitypub\Collection\Users::get_by_id( 1 ); $public_key = $user->get__public_key(); @@ -55,7 +55,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { add_filter( 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { - $user = Activitypub\User_Factory::get_by_id( 1 ); + $user = Activitypub\Collection\Users::get_by_id( 1 ); $public_key = $user->get__public_key(); // return ActivityPub Profile with signature return array( From 1685ec7cc8725bf67d246f82a5a12b8c9ef333d4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 11:56:25 +0200 Subject: [PATCH 47/97] allow sending blog-wide activities --- includes/class-activity-dispatcher.php | 95 +++++++++++-------- includes/class-scheduler.php | 6 +- ...-class-activitypub-activity-dispatcher.php | 10 +- 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 00ffd48..618d668 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -1,11 +1,14 @@ from_post( $activitypub_post ); + if ( is_user_disabled( $wp_post->post_author ) ) { + return; + } - $user_id = $activitypub_post->get_user_id(); + $post = new Post( $wp_post ); + + $activitypub_activity = new Activity( $activity_type ); + $activitypub_activity->from_post( $post ); + + $user_id = $wp_post->post_author; + $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(); + + safe_remote_post( $inbox, $activity, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $activity_type The Activity-Type. + * + * @return void + */ + public static function send_blog_activity( WP_Post $wp_post, $activity_type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + if ( is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + return; + } + + $post = new Post( $wp_post, User_Factory::BLOG_USER_ID ); + + $activitypub_activity = new Activity( $activity_type ); + $activitypub_activity->from_post( $post ); + + $user_id = User_Factory::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index db2d28c..fa8ccc2 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -83,12 +83,10 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); - \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $type ) + array( $post, $type ) ); \wp_schedule_single_event( @@ -97,7 +95,7 @@ class Scheduler { 'activitypub_send_%s_activity', \strtolower( $type ) ), - array( $activitypub_post ) + array( $post ) ); } diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 61f8c2b..cb76c17 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,10 +34,9 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); + $post = get_post( $post ); - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( $post, 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -77,10 +76,9 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); + $post = get_post( $post ); - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( $post, 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); From 493b8ffad5925539511718c616e7ab0a1ddcbdba Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 17:59:42 +0200 Subject: [PATCH 48/97] use transformer instead of post-model --- includes/activity/class-activity.php | 38 ++ includes/activity/class-base-object.php | 15 + includes/class-activity-dispatcher.php | 96 ++-- includes/class-activitypub.php | 1 + includes/class-admin.php | 2 - includes/class-scheduler.php | 8 +- includes/collection/class-followers.php | 2 +- includes/model/class-activity.php | 244 ---------- includes/model/class-user.php | 1 - includes/rest/class-inbox.php | 2 +- includes/rest/class-outbox.php | 20 +- includes/rest/class-users.php | 4 +- .../{model => transformer}/class-post.php | 420 ++++++------------ includes/transformer/class-wp-user.php | 76 ---- templates/author-json.php | 2 +- templates/blog-json.php | 2 +- templates/post-json.php | 4 +- ...-class-activitypub-activity-dispatcher.php | 10 +- tests/test-class-activitypub-activity.php | 7 +- tests/test-class-activitypub-post.php | 4 +- ...typub-rest-post-signature-verification.php | 14 +- 21 files changed, 282 insertions(+), 690 deletions(-) delete mode 100644 includes/model/class-activity.php rename includes/{model => transformer}/class-post.php (55%) delete mode 100644 includes/transformer/class-wp-user.php diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index 592bbc1..a1bd3c5 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -7,6 +7,8 @@ namespace Activitypub\Activity; +use Activitypub\Activity\Base_Object; + /** * \Activitypub\Activity\Activity implements the common * attributes of an Activity. @@ -162,4 +164,40 @@ class Activity extends Base_Object { * | null */ protected $instrument; + + /** + * Set the object and copy Object properties to the Activity. + * + * Any to, bto, cc, bcc, and audience properties specified on the object + * MUST be copied over to the new Create activity by the server. + * + * @see https://www.w3.org/TR/activitypub/#object-without-create + * + * @param \Activitypub\Activity\Base_Object $object + * + * @return void + */ + public function set_object( Base_Object $object ) { + parent::set_object( $object ); + + foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { + $this->set( $i, $object->get( $i ) ); + } + + if ( $object->get_published() && ! $this->get_published() ) { + $this->set( 'published', $object->get_published() ); + } + + if ( $object->get_updated() && ! $this->get_updated() ) { + $this->set( 'updated', $object->get_updated() ); + } + + if ( $object->attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->attributed_to() ); + } + + if ( $object->get_id() && ! $this->get_id() ) { + $this->set( 'id', $object->get_id() . '#activity' ); + } + } } diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 4343782..151ebf1 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -604,6 +604,10 @@ class Base_Object { $value = call_user_func( array( $this, 'get_' . $key ) ); } + if ( is_object( $value ) ) { + $value = $value->to_array(); + } + // if value is still empty, ignore it for the array and continue. if ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; @@ -625,4 +629,15 @@ class Base_Object { return $array; } + + /** + * Convert Object to JSON. + * + * @return string The JSON string. + */ + public function to_json() { + $array = $this->to_array(); + + return \wp_json_encode( $array ); + } } diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index c7862c0..2a5444a 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -1,10 +1,11 @@ from_post( $activitypub_post ); + $object = Post::transform( $post )->to_object(); - $user_id = $activitypub_post->get_user_id(); + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); + + $user_id = $post->post_author; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - foreach ( $inboxes as $inbox ) { - $activity = $activitypub_activity->to_json(); + $array = $activity->to_json(); - safe_remote_post( $inbox, $activity, $user_id ); + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $array, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_blog_activity( WP_Post $post, $type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + $user = Users::get_by_id( Users::BLOG_USER_ID ); + + $object = Post::transform( $post )->to_object(); + $object->set_attributed_to( $user->get_url() ); + + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); + + $follower_inboxes = Followers::get_inboxes( $user->get__id() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); + + $array = $activity->to_array(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $array, $user_id ); } } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 3b4d68a..507da58 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -2,6 +2,7 @@ namespace Activitypub; use Activitypub\Signature; +use Activitypub\Collection\Users; /** * ActivityPub Class diff --git a/includes/class-admin.php b/includes/class-admin.php index 1a94aa3..647805a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,8 +1,6 @@ 'as:manuallyApprovesFollowers', - 'PropertyValue' => 'schema:PropertyValue', - 'schema' => 'http://schema.org#', - 'pt' => 'https://joinpeertube.org/ns#', - 'toot' => 'http://joinmastodon.org/ns#', - 'value' => 'schema:value', - 'Hashtag' => 'as:Hashtag', - 'featured' => array( - '@id' => 'toot:featured', - '@type' => '@id', - ), - 'featuredTags' => array( - '@id' => 'toot:featuredTags', - '@type' => '@id', - ), - ), - ); - - /** - * The JSON-LD context. - * - * @var array - */ - private $context = self::CONTEXT; - - /** - * The published date. - * - * @var string - */ - private $published = ''; - - /** - * The Activity-ID. - * - * @var string - */ - private $id = ''; - - /** - * The Activity-Type. - * - * @var string - */ - private $type = 'Create'; - - /** - * The Activity-Actor. - * - * @var string - */ - private $actor = ''; - - /** - * The Audience. - * - * @var array - */ - private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - - /** - * The CC. - * - * @var array - */ - private $cc = array(); - - /** - * The Activity-Object. - * - * @var array - */ - private $object = null; - - /** - * The Class-Constructor. - * - * @param string $type The Activity-Type. - * @param boolean $context The JSON-LD context. - */ - public function __construct( $type = 'Create', $context = true ) { - if ( true !== $context ) { - $this->context = null; - } - - $this->type = \ucfirst( $type ); - $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \time() ); - } - - /** - * Magic Getter/Setter - * - * @param string $method The method name. - * @param string $params The method params. - * - * @return mixed The value. - */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - - if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { - if ( ! is_array( $this->$var ) ) { - $this->$var = $params[0]; - } - - if ( is_array( $params[0] ) ) { - $this->$var = array_merge( $this->$var, $params[0] ); - } else { - array_push( $this->$var, $params[0] ); - } - - $this->$var = array_unique( $this->$var ); - } - } - - /** - * Convert from a Post-Object. - * - * @param Post $post The Post-Object. - * - * @return void - */ - public function from_post( Post $post ) { - $this->object = $post->to_array(); - - if ( isset( $object['published'] ) ) { - $this->published = $object['published']; - } - - $path = sprintf( 'users/%d/followers', intval( $post->get_post_author() ) ); - $this->add_to( get_rest_url_by_path( $path ) ); - - if ( isset( $this->object['attributedTo'] ) ) { - $this->actor = $this->object['attributedTo']; - } - - foreach ( $post->get_tags() as $tag ) { - if ( 'Mention' === $tag['type'] ) { - $this->add_cc( $tag['href'] ); - } - } - - $type = \strtolower( $this->type ); - - if ( isset( $this->object['id'] ) ) { - $this->id = add_query_arg( 'activity', $type, $this->object['id'] ); - } - } - - public function from_comment( $object ) { - - } - - public function to_comment() { - - } - - public function from_remote_array( $array ) { - - } - - /** - * Convert to an Array. - * - * @return array The Array. - */ - public function to_array() { - $array = array_filter( \get_object_vars( $this ) ); - - if ( $this->context ) { - $array = array( '@context' => $this->context ) + $array; - } - - unset( $array['context'] ); - - return $array; - } - - /** - * Convert to JSON - * - * @return string The JSON. - */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } - - /** - * Convert to a Simple Array. - * - * @return string The array. - */ - public function to_simple_array() { - $activity = array( - '@context' => $this->context, - 'type' => $this->type, - 'actor' => $this->actor, - 'object' => $this->object, - 'to' => $this->to, - 'cc' => $this->cc, - ); - - if ( $this->id ) { - $activity['id'] = $this->id; - } - - return $activity; - } - - /** - * Convert to a Simple JSON. - * - * @return string The JSON. - */ - public function to_simple_json() { - return \wp_json_encode( $this->to_simple_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } -} diff --git a/includes/model/class-user.php b/includes/model/class-user.php index c66e3d8..d641497 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -4,7 +4,6 @@ namespace Activitypub\Model; use WP_Query; use WP_Error; use Activitypub\Signature; -use Activitypub\Model\User; use Activitypub\Collection\Users; use Activitypub\Activity\Actor; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index eee740c..4f0c3c3 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -5,7 +5,7 @@ use WP_Error; use WP_REST_Server; use WP_REST_Response; use Activitypub\Collection\Users; -use Activitypub\Model\Activity; +use Activitypub\Activity\Activity; use function Activitypub\get_context; use function Activitypub\url_to_authorid; diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 3df982f..91af46d 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; +use Activitypub\Activity\Activity; use Activitypub\Collection\Users; -use Activitypub\Model\Post; -use Activitypub\Model\Activity; +use Activitypub\Transformer\Post; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; @@ -73,9 +73,9 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = $user->get_id(); + //$json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get__id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -97,18 +97,20 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user->get__id(), + 'author' => $user_id, 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) ); foreach ( $posts as $post ) { - $activitypub_post = new Post( $post ); - $activitypub_activity = new Activity( 'Create', false ); + $post = Post::transform( $post )->to_object(); + $activity = new Activity(); + $activity->set_type( 'Create' ); + $activity->set_context( null ); + $activity->set_object( $post ); - $activitypub_activity->from_post( $activitypub_post ); - $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore + $json->orderedItems[] = $activity->to_array(); // phpcs:ignore } } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index dac7792..2017036 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,8 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use \Activitypub\Model\Activity; -use Activitypub\Collection\User_Collection; +use \Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\is_activitypub_request; diff --git a/includes/model/class-post.php b/includes/transformer/class-post.php similarity index 55% rename from includes/model/class-post.php rename to includes/transformer/class-post.php index 1aa862a..ca35949 100644 --- a/includes/model/class-post.php +++ b/includes/transformer/class-post.php @@ -1,84 +1,37 @@ array( 'href' => array(), 'title' => array(), @@ -126,185 +79,70 @@ class Post { ); /** - * List of audience + * Static function to Transform a WP_Post Object. * - * Also used for visibility + * This helps to chain the output of the Transformer. * - * @var array - */ - private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - - /** - * List of audience - * - * Also used for visibility - * - * @var array - */ - private $cc = array(); - - /** - * Constructor - * - * @param WP_Post $post - * @param int $post_author - */ - public function __construct( $post, $post_author = null ) { - $this->post = \get_post( $post ); - - if ( $post_author ) { - $this->post_author = $post_author; - } else { - $this->post_author = $this->post->post_author; - } - - $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) ); - $this->add_to( get_rest_url_by_path( $path ) ); - } - - /** - * Magic function to implement getter and setter - * - * @param string $method - * @param string $params + * @param WP_Post $wp_post The WP_Post object * * @return void */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - if ( empty( $this->$var ) && ! empty( $this->post->$var ) ) { - return $this->post->$var; - } - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - - if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { - if ( ! is_array( $this->$var ) ) { - $this->$var = $params[0]; - } - - if ( is_array( $params[0] ) ) { - $this->$var = array_merge( $this->$var, $params[0] ); - } else { - array_push( $this->$var, $params[0] ); - } - - $this->$var = array_unique( $this->$var ); - } + public static function transform( WP_Post $wp_post ) { + return new self( $wp_post ); } /** - * Returns the User ID. * - * @return int the User ID. + * + * @param WP_Post $wp_post */ - public function get_user_id() { - return apply_filters( 'activitypub_post_user_id', $this->get_post_author(), $this->post ); + public function __construct( WP_Post $wp_post ) { + $this->wp_post = $wp_post; } /** - * Converts this Object into an Array. + * Transforms the WP_Post object to an ActivityPub Object * - * @return array the array representation of a Post. + * @see \Activitypub\Activity\Base_Object + * + * @return \Activitypub\Activity\Base_Object The ActivityPub Object */ - public function to_array() { - $post = $this->post; + public function to_object() { + $wp_post = $this->wp_post; + $object = new Base_Object(); - $array = array( - 'id' => $this->get_id(), - 'url' => $this->get_url(), - 'type' => $this->get_object_type(), - 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), - 'attributedTo' => $this->get_actor(), - 'summary' => $this->get_summary(), - 'inReplyTo' => null, - 'content' => $this->get_content(), - 'contentMap' => array( + $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); + $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); + $object->set_type( $this->get_object_type() ); + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + $object->attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_content( $this->get_content() ); + $object->set_content_map( + array( \strstr( \get_locale(), '_', true ) => $this->get_content(), - ), - 'to' => $this->get_to(), - 'cc' => $this->get_cc(), - 'attachment' => $this->get_attachments(), - 'tag' => $this->get_tags(), + ) ); + $path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) ); - return \apply_filters( 'activitypub_post', $array, $this->post ); + $object->set_to( + array( + 'https://www.w3.org/ns/activitystreams#Public', + get_rest_url_by_path( $path ), + ) + ); + $object->set_cc( $this->get_cc() ); + $object->set_attachment( $this->get_attachments() ); + $object->set_tag( $this->get_tags() ); + + return $object; } /** - * Returns the Actor of this Object. + * Generates all Image Attachments for a Post. * - * @return string The URL of the Actor. + * @return array The Image Attachments. */ - public function get_actor() { - $user = Users::get_by_id( $this->get_user_id() ); - - return $user->get_url(); - } - - /** - * Converts this Object into a JSON String - * - * @return string - */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } - - /** - * Returns the URL of an Activity Object - * - * @return string - */ - public function get_url() { - if ( $this->url ) { - return $this->url; - } - - $post = $this->post; - - if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); - } else { - $permalink = \get_permalink( $post ); - } - - $this->url = $permalink; - - return $permalink; - } - - /** - * Returns the ID of an Activity Object - * - * @return string - */ - public function get_id() { - if ( $this->id ) { - return $this->id; - } - - $this->id = $this->get_url(); - - return $this->id; - } - - /** - * Returns a list of Image Attachments - * - * @return array - */ - public function get_attachments() { - if ( $this->attachments ) { - return $this->attachments; - } - + protected function get_attachments() { $max_images = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) ); $images = array(); @@ -314,7 +152,7 @@ class Post { return $images; } - $id = $this->post->ID; + $id = $this->wp_post->ID; $image_ids = array(); @@ -393,7 +231,7 @@ class Post { * * @return array|false Array of image data, or boolean false if no image is available. */ - public function get_image( $id, $image_size = 'full' ) { + protected function get_image( $id, $image_size = 'full' ) { /** * Hook into the image retrieval process. Before image retrieval. * @@ -416,64 +254,22 @@ class Post { } /** - * Returns a list of Tags, used in the Post + * Returns the ActivityStreams 2.0 Object-Type for a Post based on the + * settings and the Post-Type. * - * @return array - */ - public function get_tags() { - if ( $this->tags ) { - return $this->tags; - } - - $tags = array(); - - $post_tags = \get_the_tags( $this->post->ID ); - if ( $post_tags ) { - foreach ( $post_tags as $post_tag ) { - $tag = array( - 'type' => 'Hashtag', - 'href' => \get_tag_link( $post_tag->term_id ), - 'name' => '#' . $post_tag->slug, - ); - $tags[] = $tag; - } - } - - $mentions = apply_filters( 'activitypub_extract_mentions', array(), $this->post->post_content, $this ); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => $url, - 'name' => $mention, - ); - $tags[] = $tag; - } - } - - $this->tags = $tags; - - return $tags; - } - - /** - * Returns the as2 object-type for a given post + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types * - * @return string the object-type + * @return string The Object-Type. */ - public function get_object_type() { - if ( $this->object_type ) { - return $this->object_type; - } - + protected function get_object_type() { if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) { return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); } - $post_type = \get_post_type( $this->post ); + $post_type = \get_post_type( $this->wp_post ); switch ( $post_type ) { case 'post': - $post_format = \get_post_format( $this->post ); + $post_format = \get_post_format( $this->wp_post ); switch ( $post_format ) { case 'aside': case 'status': @@ -519,25 +315,78 @@ class Post { break; } - $this->object_type = $object_type; - return $object_type; } + /** + * Returns a list of Mentions, used in the Post. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#Mention + * + * @return array The list of Mentions. + */ + protected function get_cc() { + $cc = array(); + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $cc[] = $url; + } + } + + return $cc; + } + + /** + * Returns a list of Tags, used in the Post. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tags() { + $tags = array(); + + $post_tags = \get_the_tags( $this->wp_post->ID ); + if ( $post_tags ) { + foreach ( $post_tags as $post_tag ) { + $tag = array( + 'type' => 'Hashtag', + 'href' => esc_url( \get_tag_link( $post_tag->term_id ) ), + 'name' => '#' . \esc_attr( $post_tag->slug ), + ); + $tags[] = $tag; + } + } + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $tag = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + $tags[] = $tag; + } + } + + return $tags; + } + /** * Returns the content for the ActivityPub Item. * - * @return string the content + * The content will be generated based on the user settings. + * + * @return string The content. */ - public function get_content() { + protected function get_content() { global $post; - if ( $this->content ) { - return $this->content; - } - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->post; + $post = $this->wp_post; $content = $this->get_post_content_template(); // Fill in the shortcodes. @@ -553,17 +402,15 @@ class Post { $content = \apply_filters( 'activitypub_the_content', $content, $post ); $content = \html_entity_decode( $content, \ENT_QUOTES, 'UTF-8' ); - $this->content = $content; - return $content; } /** * Gets the template to use to generate the content of the activitypub item. * - * @return string the template + * @return string The Template. */ - public function get_post_content_template() { + protected function get_post_content_template() { if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) { return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]"; } @@ -578,4 +425,13 @@ class Post { return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); } + + /** + * Helper function to get the @-Mentions from the post content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post ); + } } diff --git a/includes/transformer/class-wp-user.php b/includes/transformer/class-wp-user.php deleted file mode 100644 index 083b119..0000000 --- a/includes/transformer/class-wp-user.php +++ /dev/null @@ -1,76 +0,0 @@ -wp_user = $wp_user; - } - - public function to_user() { - $wp_user = $this->wp_user; - if ( - is_user_disabled( $user->ID ) || - ! get_user_by( 'id', $user->ID ) - ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - $user = new User(); - - $user->setwp_user->ID( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); - $user->set_url( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); - $user->set_summary( $this->get_summary() ); - $user->set_name( \esc_attr( $wp_user->display_name ) ); - $user->set_preferred_username( \esc_attr( $wp_user->login ) ); - - $user->set_icon( $this->get_icon() ); - $user->set_image( $this->get_image() ); - - return $user; - } - - public function get_summary() { - $description = get_user_meta( $this->wp_user->ID, 'activitypub_user_description', true ); - if ( empty( $description ) ) { - $description = $this->wp_user->description; - } - return \wpautop( \wp_kses( $description, 'default' ) ); - } - - public function get_icon() { - $icon = \esc_url( - \get_avatar_url( - $this->wp_user->ID, - array( 'size' => 120 ) - ) - ); - - return array( - 'type' => 'Image', - 'url' => $icon, - ); - } - - public function get_image() { - if ( \has_header_image() ) { - $image = \esc_url( \get_header_image() ); - return array( - 'type' => 'Image', - 'url' => $image, - ); - } - - return null; - } -} diff --git a/templates/author-json.php b/templates/author-json.php index 6c5f8ad..70fb43b 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -2,7 +2,7 @@ $user = \Activitypub\Collection\Users::get_by_id( \get_the_author_meta( 'ID' ) ); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + \Activitypub\Activity\Activity::CONTEXT ); /* diff --git a/templates/blog-json.php b/templates/blog-json.php index 635e7d5..7ce6a27 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -2,7 +2,7 @@ $user = new \Activitypub\Model\Blog_User(); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + \Activitypub\Activity\Activity::CONTEXT ); /* diff --git a/templates/post-json.php b/templates/post-json.php index 4c597d6..89467c4 100644 --- a/templates/post-json.php +++ b/templates/post-json.php @@ -2,8 +2,8 @@ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post = \get_post(); -$activitypub_post = new \Activitypub\Model\Post( $post ); -$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $activitypub_post->to_array() ); +$object = new \Activitypub\Transformer\Post( $post ); +$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() ); // filter output $json = \apply_filters( 'activitypub_json_post_array', $json ); diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 61f8c2b..f028798 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,10 +34,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); - - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -77,10 +74,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); - - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 8262f6c..b25545c 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -17,10 +17,11 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { 10 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity = new \Activitypub\Activity\Activity(); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); diff --git a/tests/test-class-activitypub-post.php b/tests/test-class-activitypub-post.php index 4f1c74e..e995afa 100644 --- a/tests/test-class-activitypub-post.php +++ b/tests/test-class-activitypub-post.php @@ -10,13 +10,13 @@ class Test_Activitypub_Post extends WP_UnitTestCase { $permalink = \get_permalink( $post ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); $this->assertEquals( $permalink, $activitypub_post->get_id() ); \wp_trash_post( $post ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); $this->assertEquals( $permalink, $activitypub_post->get_id() ); } diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f39927d..97f78f9 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -10,9 +10,10 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ) ); $remote_actor = \get_author_posts_url( 2 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); + $activitypub_activity = new Activitypub\Activity\Activity( 'Create' ); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->add_cc( $remote_actor ); $activity = $activitypub_activity->to_json(); @@ -81,9 +82,10 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ); $remote_actor = \get_author_posts_url( 2 ); $remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( \get_post( $post ) )->to_object(); + $activitypub_activity = new Activitypub\Activity\Activity(); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->add_cc( $remote_actor_inbox ); $activity = $activitypub_activity->to_json(); From 7f3059427db0f48c6f49b278aec6333acd73d1ac Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 18:18:03 +0200 Subject: [PATCH 49/97] fix tests --- includes/class-activity-dispatcher.php | 37 +++++++++++++++----------- includes/functions.php | 14 ++++++---- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 692c487..e3ce52d 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -30,11 +30,11 @@ class Activity_Dispatcher { * Send Activities to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. + * @param string $type The Activity-Type. * * @return void */ - public static function send_user_activity( WP_Post $wp_post, $activity_type ) { + public static function send_user_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -42,22 +42,23 @@ class Activity_Dispatcher { return; } - $post = new Post( $wp_post ); + $object = Post::transform( $wp_post )->to_object(); - $activitypub_activity = new Activity( $activity_type ); - $activitypub_activity->from_post( $post ); + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); $user_id = $wp_post->post_author; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - $array = $activity->to_json(); + $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $array, $user_id ); + safe_remote_post( $inbox, $json, $user_id ); } } @@ -65,11 +66,11 @@ class Activity_Dispatcher { * Send Activities to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. + * @param string $type The Activity-Type. * * @return void */ - public static function send_blog_activity( WP_Post $wp_post, $activity_type ) { + public static function send_blog_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -77,22 +78,26 @@ class Activity_Dispatcher { return; } - $post = new Post( $wp_post, User_Factory::BLOG_USER_ID ); + $user = User_Factory::get_user( User_Factory::BLOG_USER_ID ); - $activitypub_activity = new Activity( $activity_type ); - $activitypub_activity->from_post( $post ); + $object = Post::transform( $wp_post )->to_object(); + $object->set_attributed_to( $user->get_id() ); + + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); $user_id = User_Factory::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - $array = $activity->to_json(); + $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $array, $user_id ); + safe_remote_post( $inbox, $json, $user_id ); } } } diff --git a/includes/functions.php b/includes/functions.php index 85cf94a..65279cb 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,23 +1,27 @@ Date: Mon, 3 Jul 2023 19:25:49 +0200 Subject: [PATCH 50/97] consistent use of namespaces --- includes/rest/class-outbox.php | 2 +- includes/transformer/class-post.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 91af46d..a8708e5 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Activity\Activity; use Activitypub\Collection\Users; use Activitypub\Transformer\Post; +use Activitypub\Activity\Activity; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index ca35949..88295c8 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -2,8 +2,8 @@ namespace Activitypub\Transformer; use WP_Post; -use \Activitypub\Activity\Base_Object; -use \Activitypub\Collection\Users; +use Activitypub\Collection\Users; +use Activitypub\Activity\Base_Object; use function Activitypub\get_rest_url_by_path; From 47957c2a6a31030072f68af84b0ce6fa397e9bc4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 19:52:54 +0200 Subject: [PATCH 51/97] fix code --- includes/class-scheduler.php | 2 +- includes/rest/class-outbox.php | 6 +++--- includes/rest/class-users.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 7603441..271c4b6 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -4,7 +4,7 @@ namespace Activitypub; use Activitypub\Collection\Users; use Activitypub\Collection\Followers; -use \Activitypub\Transformer\Post; +use Activitypub\Transformer\Post; /** * ActivityPub Scheduler Class diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index a8708e5..8485647 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; use Activitypub\Transformer\Post; use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; @@ -53,7 +53,7 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -73,7 +73,7 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - //$json->actor = $user->get_id(); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 2017036..b4f85fe 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use \Activitypub\Activity\Activity; +use Activitypub\Activity\Activity; use Activitypub\Collection\Users as User_Collection; use function Activitypub\is_activitypub_request; From be07574cfea7e6c320654074363d05f6a9401ae3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 19:56:06 +0200 Subject: [PATCH 52/97] fix code --- includes/rest/class-followers.php | 4 ++-- includes/rest/class-following.php | 4 ++-- includes/rest/class-inbox.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index c359391..38b4746 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,7 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; +use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -52,7 +52,7 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 10f4bed..29e7e07 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,7 +1,7 @@ get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4f0c3c3..7cdb0b7 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,8 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\get_context; use function Activitypub\url_to_authorid; @@ -74,7 +74,7 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; From 52e644631a57d4e20ec2d754d01abef75224c13d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 20:00:47 +0200 Subject: [PATCH 53/97] add missing `attributed_to` --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 88295c8..f9f36f3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -115,7 +115,7 @@ class Post { $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); - $object->attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); $object->set_content( $this->get_content() ); $object->set_content_map( array( From 07b0ae6e2d766cb016ef3803316c27deb9697763 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 20:02:00 +0200 Subject: [PATCH 54/97] fix namespaces --- includes/rest/class-webfinger.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 1dc79c9..5e74308 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,7 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; -use Activitypub\Collection\Users; +use Activitypub\Collection\Users as User_Collection; /** * ActivityPub WebFinger REST-Class @@ -47,7 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = Users::get_by_resource( $resource ); + $user = User_Collection::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; From e65b70763db0a3743e7cbda8cdce26629f592cf3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 12:18:48 +0200 Subject: [PATCH 55/97] use URL as post-name --- 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 297bb36..5dd7e2a 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -148,6 +148,7 @@ class Follower extends Actor { 'post_title' => $this->get_name(), 'post_author' => 0, 'post_type' => Followers::POST_TYPE, + 'post_name' => $this->get_id(), 'post_content' => $this->get_summary(), 'post_status' => 'publish', 'meta_input' => $this->get_post_meta_input(), From 7a360dbf6f5b11b23fef3902318092da21261648 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:31:06 +0200 Subject: [PATCH 56/97] fix object handling --- includes/activity/class-activity.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index a1bd3c5..37a469f 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -79,7 +79,7 @@ class Activity extends Base_Object { * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term * * @var string - * | ObjectType + * | Base_Objectr * | Link * | null */ @@ -173,12 +173,16 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitypub/#object-without-create * - * @param \Activitypub\Activity\Base_Object $object + * @param string|Base_Objectr|Link|null $object * * @return void */ - public function set_object( Base_Object $object ) { - parent::set_object( $object ); + public function set_object( $object ) { + $this->set( 'object', $object ); + + if ( ! is_object( $object ) ) { + return; + } foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { $this->set( $i, $object->get( $i ) ); From 1380025d4ad2ef9332e0c13bcb1a3683c971fa1a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:31:45 +0200 Subject: [PATCH 57/97] always use Followers::add_follower to not ran into inconsistencies --- includes/class-migration.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index d3c0fcb..2e5f19c 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,7 +1,6 @@ set_error( $meta ); - } - - $follower->upsert(); - - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + Followers::add_follower( $user_id, $actor ); } } } From 52038c9f43f1ea3356b937869e492317888fb4db Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:32:26 +0200 Subject: [PATCH 58/97] fix image and username handling --- includes/model/class-blog-user.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c3b6cb1..2549fc4 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -66,7 +66,7 @@ class Blog_User extends User { * @return string The User-Url. */ public function get_url() { - return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); } /** @@ -116,17 +116,29 @@ class Blog_User extends User { return $default; } - public function get_username() { + public function get_preferred_username() { return self::get_default_username(); } - public function get_avatar() { - return \esc_url( \get_site_icon_url( 120 ) ); + public function get_icon() { + $image = wp_get_attachment_image_src( get_theme_mod( 'custom_logo' ) ); + + if ( $image ) { + return array( + 'type' => 'Image', + 'url' => esc_url( $image[0] ), + ); + } + + return null; } public function get_header_image() { if ( \has_header_image() ) { - return esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => esc_url( \get_header_image() ), + ); } return null; From 862de71cd21d40a75c36db2d6c1ae5b9c253f2d1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:32:49 +0200 Subject: [PATCH 59/97] fix WebFinger for pseudo-users --- includes/rest/class-webfinger.php | 83 ++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 5e74308..846ac50 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -18,7 +18,8 @@ class Webfinger { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_action( 'webfinger_user_data', array( self::class, 'add_webfinger_discovery' ), 10, 3 ); + \add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 10, 3 ); + \add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 99, 2 ); } /** @@ -47,35 +48,9 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = User_Collection::get_by_resource( $resource ); + $response = self::get_profile( $resource ); - if ( is_wp_error( $user ) ) { - return $user; - } - - $aliases = array( - $user->get_url(), - $user->get_at_url(), - ); - - $json = array( - 'subject' => $resource, - 'aliases' => array_values( array_unique( $aliases ) ), - 'links' => array( - array( - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $user->get_url(), - ), - array( - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => $user->get_url(), - ), - ), - ); - - return new WP_REST_Response( $json, 200 ); + return new WP_REST_Response( $response, 200 ); } /** @@ -102,7 +77,7 @@ class Webfinger { * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user */ - public static function add_webfinger_discovery( $array, $resource, $user ) { + public static function add_user_discovery( $array, $resource, $user ) { $array['links'][] = array( 'rel' => 'self', 'type' => 'application/activity+json', @@ -111,4 +86,52 @@ class Webfinger { return $array; } + + /** + * Add WebFinger discovery links + * + * @param array $array the jrd array + * @param string $resource the WebFinger resource + * @param WP_User $user the WordPress user + */ + public static function add_pseudo_user_discovery( $array, $resource ) { + if ( ! $array ) { + $array = array(); + } + + $profile = self::get_profile( $resource ); + + return array_merge( $array, $profile ); + } + + public static function get_profile( $resource ) { + $user = User_Collection::get_by_resource( $resource ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + $aliases = array( + $user->get_url(), + ); + + $profile = array( + 'subject' => $resource, + 'aliases' => array_values( array_unique( $aliases ) ), + 'links' => array( + array( + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $user->get_url(), + ), + array( + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $user->get_url(), + ), + ), + ); + + return $profile; + } } From eed43355b3b2ab82f32866cadb15c227ca4fac04 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:33:07 +0200 Subject: [PATCH 60/97] fix inbox --- includes/rest/class-inbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 7cdb0b7..b530a37 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -126,7 +126,7 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; From 1269cc6248b91911047c456bc133e5f6d27c8fb0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:33:16 +0200 Subject: [PATCH 61/97] better instancing --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index f9f36f3..b337f68 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -88,7 +88,7 @@ class Post { * @return void */ public static function transform( WP_Post $wp_post ) { - return new self( $wp_post ); + return new static( $wp_post ); } /** From 5c59834a0c622bcb841450afa425acf28b01ebcb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:34:22 +0200 Subject: [PATCH 62/97] various fixes mainly regarding `send_follow_response` --- includes/collection/class-followers.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 5b67a7f..c63a28c 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -7,7 +7,9 @@ use WP_Query; use Activitypub\Http; use Activitypub\Webfinger; use Activitypub\Model\Follower; +use Activitypub\Collection\Users; use Activitypub\Activity\Activity; +use Activitypub\Activity\Base_Object; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -131,7 +133,7 @@ class Followers { '_actor', array( 'type' => 'string', - 'single' => false, + 'single' => true, 'sanitize_callback' => function( $value ) { return esc_sql( $value ); }, @@ -191,7 +193,7 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + if ( is_tombstone( $meta ) ) { return $meta; } @@ -241,7 +243,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -280,17 +282,21 @@ class Followers { unset( $object['@context'] ); } + $user = Users::get_by_id( $user_id ); + // get inbox $inbox = $follower->get_inbox(); // send "Accept" activity - $activity = new Activity( 'Accept' ); - $activity->set_activity_object( $object ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity = new Activity(); + $activity->set_type( 'Accept' ); + $activity->set_object( $object ); + $activity->set_actor( $user->get_id() ); $activity->set_to( $actor ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); + $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); + + $activity = $activity->to_json(); - $activity = $activity->to_simple_json(); $response = Http::post( $inbox, $activity, $user_id ); } From 19d60d8fec9ee0bbb92aaec010e9b21d4f573f42 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 16:16:31 +0200 Subject: [PATCH 63/97] fix sending activities --- includes/class-activity-dispatcher.php | 48 ++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index e3ce52d..5c91b04 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -74,20 +74,62 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - if ( is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } - $user = User_Factory::get_user( User_Factory::BLOG_USER_ID ); + $user = Users::get_by_id( Users::BLOG_USER_ID ); $object = Post::transform( $wp_post )->to_object(); $object->set_attributed_to( $user->get_id() ); $activity = new Activity(); $activity->set_type( $type ); + $activity->set_actor( $user->get_id() ); $activity->set_object( $object ); - $user_id = User_Factory::BLOG_USER_ID; + $user_id = Users::BLOG_USER_ID; + $follower_inboxes = Followers::get_inboxes( $user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); + + $json = $activity->to_json(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_blog_announce_activity( WP_Post $wp_post, $type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + return; + } + + $user = Users::get_by_id( Users::BLOG_USER_ID ); + + $object = Post::transform( $wp_post )->to_object(); + + $activity = new Activity(); + $activity->set_type( 'Announce' ); + $activity->set_actor( $user->get_id() ); + $activity->set_object( $object ); + + $activity->set_object( $object->get_id() ); + + $user_id = Users::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); From c1da689d6696c1865e3fdc542b79a2ce415312b4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 18:13:46 +0200 Subject: [PATCH 64/97] fix `is_activitypub_request` function --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 65279cb..c41304d 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -241,7 +241,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) && ! \REST_REQUEST ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) { return false; } From 96c1e921516feb0791222b48afa949a1992e60f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 6 Jul 2023 14:42:18 +0200 Subject: [PATCH 65/97] optimize and simplify followers --- includes/activity/class-base-object.php | 35 +++- includes/class-scheduler.php | 10 +- includes/collection/class-followers.php | 120 ++++++------ includes/model/class-follower.php | 175 +++++------------- includes/table/class-followers.php | 2 +- templates/user-followers-list.php | 1 - tests/test-class-activitypub-activity.php | 2 +- tests/test-class-db-activitypub-followers.php | 14 +- 8 files changed, 146 insertions(+), 213 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 151ebf1..510ed23 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -555,12 +555,12 @@ class Base_Object { * * @return string The JSON string. * - * @return array An Object built from the JSON string. + * @return \Activitypub\Activity\Base_Object An Object built from the JSON string. */ - public static function from_json( $json ) { - $array = wp_json_decode( $json, true ); + public static function init_from_json( $json ) { + $array = \json_decode( $json, true ); - return self::from_array( $array ); + return self::init_from_array( $array ); } /** @@ -568,9 +568,9 @@ class Base_Object { * * @return string The object array. * - * @return array An Object built from the JSON string. + * @return \Activitypub\Activity\Base_Object An Object built from the JSON string. */ - public static function from_array( $array ) { + public static function init_from_array( $array ) { $object = new static(); foreach ( $array as $key => $value ) { @@ -581,6 +581,29 @@ class Base_Object { return $object; } + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param string $json The JSON string. + */ + public function from_json( $json ) { + $array = \json_decode( $json, true ); + + $this->from_array( $array ); + } + + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param array $array The array. + */ + public function from_array( $array ) { + foreach ( $array as $key => $value ) { + $key = camel_to_snake_case( $key ); + $this->set( $key, $value ); + } + } + /** * Convert Object to an array. * diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 271c4b6..d77a132 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -111,12 +111,11 @@ class Scheduler { $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - $follower->set_error( $meta ); + Followers::add_error( $follower->get__id(), $meta ); } else { - $follower->from_meta( $meta ); + $follower->from_array( $meta ); + $follower->update(); } - - $follower->update(); } } @@ -137,8 +136,7 @@ class Scheduler { if ( 5 <= $follower->count_errors() ) { $follower->delete(); } else { - $follower->set_error( $meta ); - $follower->update(); + Followers::add_error( $follower->get__id(), $meta ); } } else { $follower->reset_errors(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index c63a28c..bc35a18 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -64,37 +64,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'preferred_username', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function( $value ) { - return sanitize_user( $value, true ); - }, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'icon', - array( - 'single' => true, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'url', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), - ) - ); - - register_post_meta( - self::POST_TYPE, - 'inbox', + 'activitypub_inbox', array( 'type' => 'string', 'single' => true, @@ -104,17 +74,7 @@ class Followers { register_post_meta( self::POST_TYPE, - '_shared_inbox', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), - ) - ); - - register_post_meta( - self::POST_TYPE, - '_errors', + 'activitypub_errors', array( 'type' => 'string', 'single' => false, @@ -130,10 +90,10 @@ class Followers { register_post_meta( self::POST_TYPE, - '_actor', + 'activitypub_user_id', array( 'type' => 'string', - 'single' => true, + 'single' => false, 'sanitize_callback' => function( $value ) { return esc_sql( $value ); }, @@ -197,13 +157,28 @@ class Followers { return $meta; } - $follower = Follower::from_array( $meta ); + $error = null; + + $follower = new Follower(); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_id( $actor ); + $follower->set_url( $actor ); + $error = $meta; + } else { + $follower->from_array( $meta ); + } + $follower->upsert(); - $meta = get_post_meta( $follower->get__id(), '_user_id' ); + $meta = get_post_meta( $follower->get__id(), 'activitypub_user_id' ); + + if ( $error ) { + self::add_error( $follower->get__id(), $error ); + } if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + add_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -227,7 +202,7 @@ class Followers { return false; } - return delete_post_meta( $follower->get__id(), '_user_id', $user_id ); + return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); } /** @@ -243,7 +218,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -254,7 +229,7 @@ class Followers { if ( $post_id ) { $post = get_post( $post_id ); - return Follower::from_custom_post_type( $post ); + return Follower::init_from_cpt( $post ); } return null; @@ -310,16 +285,16 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { + public static function get_followers( $user_id, $number = null, $page = null, $args = array() ) { $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, - 'offset' => $offset, + 'paged' => $page, 'orderby' => 'ID', 'order' => 'DESC', 'meta_query' => array( array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -330,7 +305,7 @@ class Followers { $items = array(); foreach ( $query->get_posts() as $post ) { - $items[] = Follower::from_custom_post_type( $post ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore } return $items; @@ -343,11 +318,11 @@ class Followers { * * @return array The Term list of Followers. */ - public static function get_all_followers( $user_id = null ) { + public static function get_all_followers() { $args = array( 'meta_query' => array(), ); - return self::get_followers( $user_id, null, null, $args ); + return self::get_followers( null, null, null, $args ); } /** @@ -364,7 +339,7 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -396,11 +371,11 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => '_shared_inbox', + 'key' => 'activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -418,7 +393,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = '_shared_inbox' + AND meta_key = 'activitypub_inbox' AND meta_value IS NOT NULL", $posts ) @@ -458,7 +433,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore } return $items; @@ -478,7 +453,7 @@ class Followers { 'posts_per_page' => $number, 'meta_query' => array( array( - 'key' => 'errors', + 'key' => 'activitypub_errors', 'compare' => 'EXISTS', ), ), @@ -488,9 +463,28 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore } return $items; } + + /** + * Undocumented function + * + * @param [type] $post_id + * @param [type] $error + * @return void + */ + public static function add_error( $post_id, $error ) { + if ( is_string( $error ) ) { + $error_message = $error; + } elseif ( is_wp_error( $error ) ) { + $error_message = $error->get_error_message(); + } else { + $error_message = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + } + + return add_post_meta( $post_id, 'activitypub_errors', $error_message ); + } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 5dd7e2a..573696b 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -24,60 +24,13 @@ class Follower extends Actor { */ protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - /** - * The complete Remote-Profile of the Follower - * - * @var array - */ - protected $_shared_inbox; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * The complete Remote-Profile of the Follower - * - * @var array - */ - protected $_actor; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * The latest received error. - * - * This will only temporary and will saved to $this->errors - * - * @var string - */ - protected $_error; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * A list of errors - * - * @var array - */ - protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * Set new Error - * - * @param mixed $error The latest HTTP-Error. - * - * @return void - */ - public function set_error( $error ) { - $this->_errors = array(); - $this->_error = $error; - } - /** * Get the errors. * * @return mixed */ public function get_errors() { - if ( $this->_errors ) { - return $this->_errors; - } - - $this->_errors = get_post_meta( $this->_id, 'errors' ); - return $this->_errors; + return get_post_meta( $this->_id, 'activitypub_errors' ); } /** @@ -86,7 +39,7 @@ class Follower extends Actor { * @return void */ public function reset_errors() { - delete_post_meta( $this->_id, 'errors' ); + delete_post_meta( $this->_id, 'activitypub_errors' ); } /** @@ -95,7 +48,7 @@ class Follower extends Actor { * @return int The number of errors. */ public function count_errors() { - $errors = $this->get__errors(); + $errors = $this->get_errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return count( $errors ); @@ -104,21 +57,13 @@ class Follower extends Actor { return 0; } - public function get_url() { - if ( ! $this->url ) { - return $this->id; - } - - return $this->url; - } - /** * Return the latest error message. * * @return string The error message. */ public function get_latest_error_message() { - $errors = $this->get__errors(); + $errors = $this->get_errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return reset( $errors ); @@ -142,16 +87,33 @@ class Follower extends Actor { * @return void */ public function save() { + if ( ! $this->get__id() ) { + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $this->get_id() ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + $this->set__id( $post->ID ); + } + } + $args = array( - 'ID' => $this->get__id(), - 'guid' => $this->get_id(), - 'post_title' => $this->get_name(), - 'post_author' => 0, - 'post_type' => Followers::POST_TYPE, - 'post_name' => $this->get_id(), - 'post_content' => $this->get_summary(), - 'post_status' => 'publish', - 'meta_input' => $this->get_post_meta_input(), + 'ID' => $this->get__id(), + 'guid' => esc_url_raw( $this->get_id() ), + 'post_title' => esc_html( $this->get_name() ), + 'post_author' => 0, + 'post_type' => Followers::POST_TYPE, + 'post_name' => esc_url_raw( $this->get_id() ), + 'post_excerpt' => esc_html( $this->get_summary() ) ? $this->get_summary() : '', + 'post_content' => esc_sql( $this->to_json() ), + 'post_status' => 'publish', + 'meta_input' => $this->get_post_meta_input(), ); $post_id = wp_insert_post( $args ); @@ -164,16 +126,17 @@ class Follower extends Actor { * @return void */ public function upsert() { - if ( $this->_id ) { - $this->update(); - } else { - $this->save(); - } + $this->save(); } /** * Delete the current Follower-Object. * + * Beware that this os deleting a Follower for ALL users!!! + * + * To delete only the User connection (unfollow) + * @see \Activitypub\Rest\Followers::remove_follower() + * * @return void */ public function delete() { @@ -186,27 +149,8 @@ class Follower extends Actor { * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', '_shared_inbox', 'icon', 'preferred_username', '_actor', 'url' ); - $meta_input = array(); - - foreach ( $attributes as $attribute ) { - if ( $this->get( $attribute ) ) { - $meta_input[ $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' ); - } - - $meta_input['_errors'] = $_error; - } + $meta_input['activitypub_inbox'] = esc_url_raw( $this->get_shared_inbox() ); return $meta_input; } @@ -231,37 +175,18 @@ class Follower extends Actor { } /** - * Converts an ActivityPub Array to an Follower Object. + * Get the shared inbox, with a fallback to the inbox. * - * @param array $array The ActivityPub Array. - * - * @return Activitypub\Model\Follower The Follower Object. + * @return string|null The URL to the shared inbox, the inbox or null. */ - public static function from_array( $array ) { - $object = parent::from_array( $array ); - $object->set__actor( $array ); - - global $wpdb; - - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $object->get_id() ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - $object->set__id( $post->ID ); + public function get_shared_inbox() { + if ( ! empty( $this->get_endpoints()['sharedInbox'] ) ) { + return $this->get_endpoints()['sharedInbox']; + } elseif ( ! empty( $this->get_inbox() ) ) { + return $this->get_inbox(); } - if ( ! empty( $object->get_endpoints()['sharedInbox'] ) ) { - $object->_shared_inbox = $object->get_endpoints()['sharedInbox']; - } elseif ( ! empty( $object->get_inbox() ) ) { - $object->_shared_inbox = $object->get_inbox(); - } - - return $object; + return null; } /** @@ -271,16 +196,10 @@ class Follower extends Actor { * * @return array Activitypub\Model\Follower */ - public static function from_custom_post_type( $post ) { - $object = new static(); - + public static function init_from_cpt( $post ) { + $object = self::init_from_json( $post->post_content ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); - $object->set_name( $post->post_title ); - $object->set_summary( $post->post_content ); - $object->set_url( get_post_meta( $post->ID, 'url', true ) ); - $object->set_icon( get_post_meta( $post->ID, 'icon', true ) ); - $object->set_preferred_username( get_post_meta( $post->ID, 'preferred_username', true ) ); $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 93d9456..b815df6 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -55,7 +55,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $followers = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $followers = FollowerCollection::get_followers( $this->user_id, $per_page, $page_num ); $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); diff --git a/templates/user-followers-list.php b/templates/user-followers-list.php index 7476192..576b1cf 100644 --- a/templates/user-followers-list.php +++ b/templates/user-followers-list.php @@ -1,6 +1,5 @@

-

diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index b25545c..ba9f5a2 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -37,7 +37,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { 'content' => 'Hello world!', ); - $object = \Activitypub\Activity\Base_Object::from_array( $test_array ); + $object = \Activitypub\Activity\Base_Object::init_from_array( $test_array ); $this->assertEquals( 'Hello world!', $object->get_content() ); $this->assertEquals( $test_array, $object->to_array() ); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 3634713..559bd06 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -6,42 +6,42 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', - 'prefferedUsername' => 'username', + 'preferredUsername' => 'username', ), 'jon@example.com' => array( 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', - 'prefferedUsername' => 'jon', + 'preferredUsername' => 'jon', ), 'doe@example.org' => array( 'id' => 'https://example.org/author/doe', 'url' => 'https://example.org/author/doe', 'inbox' => 'https://example.org/author/doe/inbox', 'name' => 'doe', - 'prefferedUsername' => 'doe', + 'preferredUsername' => 'doe', ), 'sally@example.org' => array( 'id' => 'http://sally.example.org', 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', - 'prefferedUsername' => 'jon', + 'preferredUsername' => 'jon', ), '12345@example.com' => array( 'id' => 'https://12345.example.com', 'url' => 'https://12345.example.com', 'inbox' => 'https://12345.example.com/inbox', 'name' => '12345', - 'prefferedUsername' => '12345', + 'preferredUsername' => '12345', ), 'user2@example.com' => array( 'id' => 'https://user2.example.com', 'url' => 'https://user2.example.com', 'inbox' => 'https://user2.example.com/inbox', 'name' => 'user2', - 'prefferedUsername' => 'user2', + 'preferredUsername' => 'user2', ), ); @@ -223,7 +223,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); for ( $i = 1; $i <= 15; $i++ ) { - add_post_meta( $follower->get__id(), 'errors', 'error ' . $i ); + add_post_meta( $follower->get__id(), 'activitypub_errors', 'error ' . $i ); } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); From d4f5ad8ec19ba2307ec8cbd3decfcaeff5daf13b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 6 Jul 2023 16:10:48 +0200 Subject: [PATCH 66/97] use post_meta instead of post_content --- includes/collection/class-followers.php | 17 +++++++++++++++-- includes/model/class-follower.php | 7 ++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index bc35a18..65d8cdb 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -100,6 +100,18 @@ class Followers { ) ); + register_post_meta( + self::POST_TYPE, + 'activitypub_actor_json', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function( $value ) { + return sanitize_text_field( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_post_type' ); } @@ -254,13 +266,14 @@ class Followers { if ( isset( $object['user_id'] ) ) { unset( $object['user_id'] ); - unset( $object['@context'] ); } + unset( $object['@context'] ); + $user = Users::get_by_id( $user_id ); // get inbox - $inbox = $follower->get_inbox(); + $inbox = $follower->get_shared_inbox(); // send "Accept" activity $activity = new Activity(); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 573696b..0036f8f 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -111,7 +111,6 @@ class Follower extends Actor { 'post_type' => Followers::POST_TYPE, 'post_name' => esc_url_raw( $this->get_id() ), 'post_excerpt' => esc_html( $this->get_summary() ) ? $this->get_summary() : '', - 'post_content' => esc_sql( $this->to_json() ), 'post_status' => 'publish', 'meta_input' => $this->get_post_meta_input(), ); @@ -150,7 +149,8 @@ class Follower extends Actor { */ protected function get_post_meta_input() { $meta_input = array(); - $meta_input['activitypub_inbox'] = esc_url_raw( $this->get_shared_inbox() ); + $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; } @@ -197,7 +197,8 @@ class Follower extends Actor { * @return array Activitypub\Model\Follower */ public static function init_from_cpt( $post ) { - $object = self::init_from_json( $post->post_content ); + $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); + $object = self::init_from_json( $actor_json ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); From 5b712fb7cd7cdedfb7eb0f5bb53d234a4d51e343 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 13:43:12 +0200 Subject: [PATCH 67/97] fix some last "follower" issues --- includes/activity/class-base-object.php | 2 +- includes/class-activity-dispatcher.php | 12 ++++++++++-- includes/collection/class-followers.php | 2 +- includes/table/class-followers.php | 2 +- templates/blog-user-followers-list.php | 8 ++++---- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 510ed23..9c5f52a 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -661,6 +661,6 @@ class Base_Object { public function to_json() { $array = $this->to_array(); - return \wp_json_encode( $array ); + return \wp_json_encode( $array, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } } diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 5c91b04..51d69b2 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -23,7 +23,7 @@ class Activity_Dispatcher { */ public static function init() { \add_action( 'activitypub_send_activity', array( self::class, 'send_user_activity' ), 10, 2 ); - \add_action( 'activitypub_send_activity', array( self::class, 'send_blog_activity' ), 10, 2 ); + \add_action( 'activitypub_send_activity', array( self::class, 'send_announce_activity' ), 10, 2 ); } /** @@ -74,6 +74,10 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); + if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { + return; + } + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } @@ -110,10 +114,14 @@ class Activity_Dispatcher { * * @return void */ - public static function send_blog_announce_activity( WP_Post $wp_post, $type ) { + public static function send_announce_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); + if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { + return; + } + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 65d8cdb..ce62619 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -298,7 +298,7 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $page = null, $args = array() ) { + public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index b815df6..b27f311 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -62,7 +62,7 @@ class Followers extends WP_List_Table { $this->set_pagination_args( array( 'total_items' => $counter, - 'total_pages' => round( $counter / $per_page ), + 'total_pages' => ceil( $counter / $per_page ), 'per_page' => $per_page, ) ); diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php index 9ce3bd1..f73826f 100644 --- a/templates/blog-user-followers-list.php +++ b/templates/blog-user-followers-list.php @@ -13,17 +13,17 @@

- + -

get_user_count() ) ); ?>

+

get_user_count() ) ); ?>

prepare_items(); - $followers_table->display(); + $table->prepare_items(); + $table->display(); ?> From d00b7b54f21ae824fab0d1a6669a043609e46727 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 14:54:28 +0200 Subject: [PATCH 68/97] use esc_sql --- includes/collection/class-followers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index ce62619..44a1ec8 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -107,7 +107,7 @@ class Followers { 'type' => 'string', 'single' => true, 'sanitize_callback' => function( $value ) { - return sanitize_text_field( $value ); + return esc_sql( $value ); }, ) ); From 4b8ffc874ad25ef5a101aa18cf2464d7f814e466 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:02:34 +0200 Subject: [PATCH 69/97] add pager to followers endpoint --- includes/rest/class-followers.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 38b4746..edd1445 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -58,6 +58,8 @@ class Followers { return $user; } + $page = $request->get_param( 'page', 1 ); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -72,15 +74,22 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore + + $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore + $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 20 ), $json->partOf ); // phpcs:ignore + + if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) >= $page ) ) { // phpcs:ignore + $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore + } + // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get__id() ) + FollowerCollection::get_followers( $user->get__id(), 20, $page ) ); $response = new WP_REST_Response( $json, 200 ); @@ -99,6 +108,7 @@ class Followers { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $params['user_id'] = array( From 7f3d31c59ea179e796618be4c20489d4c0bdee29 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:09:22 +0200 Subject: [PATCH 70/97] add prev --- includes/rest/class-followers.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index edd1445..9b9ed89 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -80,10 +80,14 @@ class Followers { $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 20 ), $json->partOf ); // phpcs:ignore - if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) >= $page ) ) { // phpcs:ignore + if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) > $page ) ) { // phpcs:ignore $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore } + if ( $page && ( $page > 1 ) ) { // phpcs:ignore + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + } + // phpcs:ignore $json->orderedItems = array_map( function( $item ) { From f3d2243afbb884fd408bf73a74389b3551df6fd0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:10:22 +0200 Subject: [PATCH 71/97] use `paged` instead of `offset` --- includes/rest/class-outbox.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 8485647..047ae22 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -61,7 +61,7 @@ class Outbox { $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); - $page = $request->get_param( 'page', 0 ); + $page = $request->get_param( 'page', 1 ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client @@ -78,9 +78,6 @@ class Outbox { $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore - // phpcs:ignore - $json->totalItems = 0; - foreach ( $post_types as $post_type ) { $count_posts = \wp_count_posts( $post_type ); $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore @@ -93,13 +90,17 @@ class Outbox { $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore } + if ( $page && ( $page > 1 ) ) { // phpcs:ignore + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + } + if ( $page ) { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user_id, - 'offset' => ( $page - 1 ) * 10, - 'post_type' => $post_types, + 'author' => $user_id, + 'paged' => $page, + 'post_type' => $post_types, ) ); @@ -139,6 +140,7 @@ class Outbox { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $params['user_id'] = array( From 9559a089bed80a8163246fddc71b408deb299d01 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 16:45:38 +0200 Subject: [PATCH 72/97] fix sanitization --- includes/collection/class-followers.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 44a1ec8..449dd78 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -107,7 +107,7 @@ class Followers { 'type' => 'string', 'single' => true, 'sanitize_callback' => function( $value ) { - return esc_sql( $value ); + return sanitize_text_field( $value ); }, ) ); @@ -315,9 +315,10 @@ class Followers { $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); + $posts = $query->get_posts(); $items = array(); - foreach ( $query->get_posts() as $post ) { + foreach ( $posts as $post ) { $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore } From a0a1e33dc8cf59cfd02a9c8167b0cdf3aa946295 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:28:45 +0200 Subject: [PATCH 73/97] Fall back to ID id URL is empty --- includes/model/class-follower.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 0036f8f..39018e2 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -33,6 +33,22 @@ class Follower extends Actor { return get_post_meta( $this->_id, 'activitypub_errors' ); } + /** + * Getter for URL attribute. + * + * Falls back to ID, if no URL is set. This is relevant for + * Plattforms like Lemmy, where the ID is the URL. + * + * @return string The URL. + */ + public function get_url() { + if ( $this->url ) { + return $this->url; + } + + return $this->id; + } + /** * Reset (delete) all errors. * From 799f4be1d8d8159ed660f7b7e48f023594341d3c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:29:02 +0200 Subject: [PATCH 74/97] check for "single user mode" --- includes/functions.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index c41304d..725d9f6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -306,11 +306,27 @@ function is_user_disabled( $user_id ) { } } +/** + * Check if the blog is in single-user mode. + * + * @return boolean True if the blog is in single-user mode, false otherwise. + */ +function is_single_user() { + if ( + false === ACTIVITYPUB_DISABLE_BLOG_USER && + true === ACTIVITYPUB_DISABLE_USER + ) { + return true; + } + + return false; +} + if ( ! function_exists( 'get_self_link' ) ) { /** - * Get the correct self URL + * Returns the link for the currently displayed feed. * - * @return boolean + * @return string Correct link for the atom:self element. */ function get_self_link() { $host = wp_parse_url( home_url() ); From fe99fffab69a0bf1b510f4d4881c7570d924b77f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:29:15 +0200 Subject: [PATCH 75/97] use Group type for blog-user --- includes/model/class-blog-user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 2549fc4..a1f1be3 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -20,7 +20,7 @@ class Blog_User extends User { * * @var string */ - protected $type = 'Person'; + protected $type = 'Group'; public static function from_wp_user( $user_id ) { if ( is_user_disabled( $user_id ) ) { From 69326d027c6b23efc9cb6f371de7189c20a2e252 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:57:06 +0200 Subject: [PATCH 76/97] return blog-user when in single mode --- includes/transformer/class-post.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index b337f68..03fdff4 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -3,8 +3,10 @@ namespace Activitypub\Transformer; use WP_Post; use Activitypub\Collection\Users; +use Activitypub\Collection\Blog_Users; use Activitypub\Activity\Base_Object; +use function Activitypub\is_single_user; use function Activitypub\get_rest_url_by_path; /** @@ -115,7 +117,7 @@ class Post { $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); - $object->set_attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( array( @@ -137,6 +139,22 @@ class Post { return $object; } + /** + * Returns the User-URL of the Author of the Post. + * + * If `single_user` mode is enabled, the URL of the Blog-User is returned. + * + * @return string The User-URL. + */ + protected function get_attributed_to() { + if ( is_single_user() ) { + $user = new Blog_User(); + return $user->get_url(); + } + + return Users::get_by_id( $wp_post->post_author )->get_url(); + } + /** * Generates all Image Attachments for a Post. * From 2252b87b1bb4ac32f37ee70ab233ea0bbdd2b671 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:58:34 +0200 Subject: [PATCH 77/97] check what activity should be send --- includes/class-activity-dispatcher.php | 63 ++++++++------------------ 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 51d69b2..4e203c8 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -7,6 +7,7 @@ use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use Activitypub\Transformer\Post; +use function Activitypub\is_single_user; use function Activitypub\is_user_disabled; use function Activitypub\safe_remote_post; @@ -22,8 +23,11 @@ class Activity_Dispatcher { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_send_activity', array( self::class, 'send_user_activity' ), 10, 2 ); - \add_action( 'activitypub_send_activity', array( self::class, 'send_announce_activity' ), 10, 2 ); + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); } /** @@ -34,10 +38,7 @@ class Activity_Dispatcher { * * @return void */ - public static function send_user_activity( WP_Post $wp_post, $type ) { - // check if a migration is needed before sending new posts - Migration::maybe_migrate(); - + public static function send_activity( WP_Post $wp_post, $type ) { if ( is_user_disabled( $wp_post->post_author ) ) { return; } @@ -48,8 +49,7 @@ class Activity_Dispatcher { $activity->set_type( $type ); $activity->set_object( $object ); - $user_id = $wp_post->post_author; - $follower_inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $wp_post->post_author ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); @@ -63,46 +63,24 @@ class Activity_Dispatcher { } /** - * Send Activities to followers and mentioned users. + * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. * * @param WP_Post $wp_post The ActivityPub Post. * @param string $type The Activity-Type. * * @return void */ - public static function send_blog_activity( WP_Post $wp_post, $type ) { - // check if a migration is needed before sending new posts - Migration::maybe_migrate(); - - if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { - return; - } - + public static function send_activity_or_announce( WP_Post $wp_post, $type ) { if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } - $user = Users::get_by_id( Users::BLOG_USER_ID ); + $wp_post->post_author = Users::BLOG_USER_ID; - $object = Post::transform( $wp_post )->to_object(); - $object->set_attributed_to( $user->get_id() ); - - $activity = new Activity(); - $activity->set_type( $type ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $object ); - - $user_id = Users::BLOG_USER_ID; - $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); - - $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); - $inboxes = array_unique( $inboxes ); - - $json = $activity->to_json(); - - foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + if ( is_single_user() ) { + self::send_user_activity( $wp_post, $type ); + } else { + self::send_announce_activity( $wp_post, $type ); } } @@ -114,7 +92,7 @@ class Activity_Dispatcher { * * @return void */ - public static function send_announce_activity( WP_Post $wp_post, $type ) { + public static function send_announce( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -126,19 +104,16 @@ class Activity_Dispatcher { return; } - $user = Users::get_by_id( Users::BLOG_USER_ID ); - $object = Post::transform( $wp_post )->to_object(); $activity = new Activity(); $activity->set_type( 'Announce' ); - $activity->set_actor( $user->get_id() ); + // to pre-fill attributes like "published" and "id" $activity->set_object( $object ); - + // send only the id $activity->set_object( $object->get_id() ); - $user_id = Users::BLOG_USER_ID; - $follower_inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $wp_post->post_author ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); From b4fb214e705f78742ac136ce536816bcc558e970 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:45:41 +0200 Subject: [PATCH 78/97] make CSS more flexible for various content items --- assets/css/activitypub-admin.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 805aaa5..a7744ce 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -34,10 +34,10 @@ .activitypub-settings-tabs-wrapper { display: -ms-inline-grid; - -ms-grid-columns: 1fr 1fr; + -ms-grid-columns: auto auto auto; vertical-align: top; display: inline-grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: auto auto auto; } .activitypub-settings-tab.active { From 64d2d2995ba8412eba57825e2f964d9061331b92 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:49:43 +0200 Subject: [PATCH 79/97] oops --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 03fdff4..b408484 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -152,7 +152,7 @@ class Post { return $user->get_url(); } - return Users::get_by_id( $wp_post->post_author )->get_url(); + return Users::get_by_id( $this->wp_post->post_author )->get_url(); } /** From 81d0e09f6e5a013231eb2bba9a37c7a8816da7b1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:56:46 +0200 Subject: [PATCH 80/97] fix wrong function names --- includes/class-activity-dispatcher.php | 4 ++-- tests/test-class-activitypub-activity-dispatcher.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 4e203c8..9aed87a 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -78,9 +78,9 @@ class Activity_Dispatcher { $wp_post->post_author = Users::BLOG_USER_ID; if ( is_single_user() ) { - self::send_user_activity( $wp_post, $type ); + self::send_activity( $wp_post, $type ); } else { - self::send_announce_activity( $wp_post, $type ); + self::send_announce( $wp_post, $type ); } } diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index f028798..3e17306 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,7 +34,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); + \Activitypub\Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -74,7 +74,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); + \Activitypub\Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); From 2f5a3214747a52985567a944b25d359e3c5cf36a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 12:12:12 +0200 Subject: [PATCH 81/97] fix missing user_id issue --- includes/class-activity-dispatcher.php | 50 +++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 9aed87a..98904bc 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -30,6 +30,28 @@ class Activity_Dispatcher { \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); } + /** + * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_activity_or_announce( WP_Post $wp_post, $type ) { + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + return; + } + + $wp_post->post_author = Users::BLOG_USER_ID; + + if ( is_single_user() ) { + self::send_activity( $wp_post, $type ); + } else { + self::send_announce( $wp_post, $type ); + } + } + /** * Send Activities to followers and mentioned users. * @@ -58,34 +80,12 @@ class Activity_Dispatcher { $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $wp_post->post_author ); } } /** - * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. - * - * @param WP_Post $wp_post The ActivityPub Post. - * @param string $type The Activity-Type. - * - * @return void - */ - public static function send_activity_or_announce( WP_Post $wp_post, $type ) { - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { - return; - } - - $wp_post->post_author = Users::BLOG_USER_ID; - - if ( is_single_user() ) { - self::send_activity( $wp_post, $type ); - } else { - self::send_announce( $wp_post, $type ); - } - } - - /** - * Send Activities to followers and mentioned users. + * Send Announces to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. * @param string $type The Activity-Type. @@ -122,7 +122,7 @@ class Activity_Dispatcher { $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $wp_post->post_author ); } } } From 465a912a706bed79e045dc0c555f477bf73d0c3b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:08:51 +0200 Subject: [PATCH 82/97] fix user settings --- includes/class-webfinger.php | 1 + templates/user-settings.php | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 4a3a1a6..9a53ac4 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -2,6 +2,7 @@ namespace Activitypub; use WP_Error; +use Activitypub\Collection\Users; /** * ActivityPub WebFinger Class diff --git a/templates/user-settings.php b/templates/user-settings.php index 24a6484..a98b6ce 100644 --- a/templates/user-settings.php +++ b/templates/user-settings.php @@ -1,3 +1,4 @@ +

@@ -8,11 +9,11 @@ From be6d8a1792af14a1deec1b977b49450e6403e756 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:59:12 +0200 Subject: [PATCH 83/97] fix activity --- includes/activity/class-activity.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index 37a469f..bd13cf8 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -196,8 +196,8 @@ class Activity extends Base_Object { $this->set( 'updated', $object->get_updated() ); } - if ( $object->attributed_to() && ! $this->get_actor() ) { - $this->set( 'actor', $object->attributed_to() ); + if ( $object->get_attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->get_attributed_to() ); } if ( $object->get_id() && ! $this->get_id() ) { From 0fab95bfffb8d552a10d876b1339021aa67d9755 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:59:35 +0200 Subject: [PATCH 84/97] enhance tests to also test announce and blog wide activities --- ...-class-activitypub-activity-dispatcher.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 3e17306..c7abf02 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -39,11 +39,15 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); $first_call_args = array_shift( $all_args ); + $this->assertEquals( 'https://example.com/author/jon/inbox', $first_call_args[2] ); $second_call_args = array_shift( $all_args ); $this->assertEquals( 'https://example.org/users/username/inbox', $second_call_args[2] ); + $json = json_decode( $second_call_args[1]['body'] ); + $this->assertEquals( 'Create', $json->type ); + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } @@ -88,6 +92,39 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_announce() { + $followers = array( 'https://example.com/author/jon' ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Users::BLOG_USER_ID, $follower ); + } + + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello', + ) + ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + \Activitypub\Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); + + $all_args = $pre_http_request->get_args(); + $first_call_args = $all_args[0]; + + $this->assertSame( 1, $pre_http_request->get_call_count() ); + + $user = new \Activitypub\Model\Blog_User(); + + $json = json_decode( $first_call_args[1]['body'] ); + $this->assertEquals( 'Announce', $json->type ); + $this->assertEquals( $user->get_url(), $json->actor ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + 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 ); From 8920c60c61f32e6a412e2d5afd58fd16efa6aec4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 15:14:37 +0200 Subject: [PATCH 85/97] final fixes and more tests --- includes/functions.php | 6 ++- includes/transformer/class-post.php | 2 +- ...-class-activitypub-activity-dispatcher.php | 43 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 725d9f6..0c3dc70 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -312,14 +312,16 @@ function is_user_disabled( $user_id ) { * @return boolean True if the blog is in single-user mode, false otherwise. */ function is_single_user() { + $return = false; + if ( false === ACTIVITYPUB_DISABLE_BLOG_USER && true === ACTIVITYPUB_DISABLE_USER ) { - return true; + $return = true; } - return false; + return apply_filters( 'activitypub_is_single_user', $return ); } if ( ! function_exists( 'get_self_link' ) ) { diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index b408484..7c7d990 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -3,7 +3,7 @@ namespace Activitypub\Transformer; use WP_Post; use Activitypub\Collection\Users; -use Activitypub\Collection\Blog_Users; +use Activitypub\Model\Blog_User; use Activitypub\Activity\Base_Object; use function Activitypub\is_single_user; diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index c7abf02..54ebda0 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -47,6 +47,8 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $json = json_decode( $second_call_args[1]['body'] ); $this->assertEquals( 'Create', $json->type ); + $this->assertEquals( 'http://example.org/?author=1', $json->actor ); + $this->assertEquals( 'http://example.org/?author=1', $json->object->attributedTo ); remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } @@ -125,6 +127,47 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_blog_activity() { + $followers = array( 'https://example.com/author/jon' ); + + add_filter( + 'activitypub_is_single_user', + function( $return ) { + return true; + } + ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Users::BLOG_USER_ID, $follower ); + } + + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello', + ) + ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + \Activitypub\Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); + + $all_args = $pre_http_request->get_args(); + $first_call_args = $all_args[0]; + + $this->assertSame( 1, $pre_http_request->get_call_count() ); + + $user = new \Activitypub\Model\Blog_User(); + + $json = json_decode( $first_call_args[1]['body'] ); + $this->assertEquals( 'Create', $json->type ); + $this->assertEquals( $user->get_url(), $json->actor ); + $this->assertEquals( $user->get_url(), $json->object->attributedTo ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + 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 ); From d5a389420dd2a1dee5726b13cbc4e1f338aeed94 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 08:53:18 +0200 Subject: [PATCH 86/97] some fixes based on the feedback of @mattwiebe --- includes/collection/class-followers.php | 24 ++++++++++++++++++------ includes/functions.php | 8 ++++---- includes/model/class-blog-user.php | 2 +- includes/rest/class-followers.php | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 449dd78..af0156e 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -484,11 +484,16 @@ class Followers { } /** - * Undocumented function + * This function is used to store errors that occur when + * sending an ActivityPub message to a Follower. * - * @param [type] $post_id - * @param [type] $error - * @return void + * The error will be stored in the + * post meta. + * + * @param int $post_id The ID of the WordPress Custom-Post-Type. + * @param mixed $error The error message. Can be a string or a WP_Error. + * + * @return int|false The meta ID on success, false on failure. */ public static function add_error( $post_id, $error ) { if ( is_string( $error ) ) { @@ -496,9 +501,16 @@ class Followers { } elseif ( is_wp_error( $error ) ) { $error_message = $error->get_error_message(); } else { - $error_message = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + $error_message = __( + 'Unknown Error or misconfigured Error-Message', + 'activitypub' + ); } - return add_post_meta( $post_id, 'activitypub_errors', $error_message ); + return add_post_meta( + $post_id, + 'activitypub_errors', + $error_message + ); } } diff --git a/includes/functions.php b/includes/functions.php index 0c3dc70..05e14b9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -294,14 +294,14 @@ function is_user_disabled( $user_id ) { return false; // if the user is any other user, it's enabled if it can publish posts. default: - if ( ! \user_can( $user_id, 'publish_posts' ) ) { - return true; - } - if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { return ACTIVITYPUB_DISABLE_USER; } + if ( ! \user_can( $user_id, 'publish_posts' ) ) { + return true; + } + return false; } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a1f1be3..d1413dc 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -83,7 +83,7 @@ class Blog_User extends User { // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); - $host = \str_replace( 'www.', '', $host ); + $host = \preg_replace( '/^www\./i', '', $host ); $host_parts = \explode( '.', $host ); if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 9b9ed89..cdff551 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -6,7 +6,7 @@ use stdClass; use WP_REST_Server; use WP_REST_Response; use Activitypub\Collection\Users as User_Collection; -use Activitypub\Collection\Followers as FollowerCollection; +use Activitypub\Collection\Followers as Follower_Collection; use function Activitypub\get_rest_url_by_path; @@ -74,7 +74,7 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore + $json->totalItems = Follower_Collection::count_followers( $user->get__id() ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore @@ -93,7 +93,7 @@ class Followers { function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get__id(), 20, $page ) + Follower_Collection::get_followers( $user->get__id(), 20, $page ) ); $response = new WP_REST_Response( $json, 200 ); From 0ab61b644151ca3458e875acecf3dc1fe8174715 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 08:58:50 +0200 Subject: [PATCH 87/97] make `is_user_disabled` filterable --- includes/functions.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 05e14b9..15a769e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -281,29 +281,39 @@ function is_activitypub_request() { * @return boolean True if the user is disabled, false otherwise. */ function is_user_disabled( $user_id ) { + $return = false; + switch ( $user_id ) { // if the user is the application user, it's always enabled. case \Activitypub\Collection\Users::APPLICATION_USER_ID: - return false; + $return = false; + break; // if the user is the blog user, it's only enabled in single-user mode. case \Activitypub\Collection\Users::BLOG_USER_ID: if ( defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) { - return ACTIVITYPUB_DISABLE_BLOG_USER; + $return = ACTIVITYPUB_DISABLE_BLOG_USER; + break; } - return false; + $return = false; + break; // if the user is any other user, it's enabled if it can publish posts. default: if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { - return ACTIVITYPUB_DISABLE_USER; + $return = ACTIVITYPUB_DISABLE_USER; + break; } if ( ! \user_can( $user_id, 'publish_posts' ) ) { - return true; + $return = true; + break; } - return false; + $return = false; + break; } + + return apply_filters( 'activitypub_is_user_disabled', $return, $user_id ); } /** From a461ea3b1f9ef72acd41cdd1a066a08671decb60 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:09:37 +0200 Subject: [PATCH 88/97] some refactorings --- includes/collection/class-users.php | 10 ++++++++++ includes/functions.php | 5 +++++ includes/model/class-user.php | 5 +---- includes/rest/class-inbox.php | 2 -- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php index 62b4264..506722b 100644 --- a/includes/collection/class-users.php +++ b/includes/collection/class-users.php @@ -7,6 +7,8 @@ use Activitypub\Model\User; use Activitypub\Model\Blog_User; use Activitypub\Model\Application_User; +use function Activitypub\is_user_disabled; + class Users { /** * The ID of the Blog User @@ -34,6 +36,14 @@ class Users { $user_id = (int) $user_id; } + if ( is_user_disabled( $user_id ) ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + if ( self::BLOG_USER_ID === $user_id ) { return Blog_User::from_wp_user( $user_id ); } elseif ( self::APPLICATION_USER_ID === $user_id ) { diff --git a/includes/functions.php b/includes/functions.php index 15a769e..9d2a03e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -299,6 +299,11 @@ function is_user_disabled( $user_id ) { break; // if the user is any other user, it's enabled if it can publish posts. default: + if ( ! \get_user_by( 'id', $user_id ) ) { + $return = true; + break; + } + if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { $return = ACTIVITYPUB_DISABLE_USER; break; diff --git a/includes/model/class-user.php b/includes/model/class-user.php index d641497..23cd013 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -26,10 +26,7 @@ class User extends Actor { protected $type = 'Person'; public static function from_wp_user( $user_id ) { - if ( - is_user_disabled( $user_id ) || - ! get_user_by( 'id', $user_id ) - ) { + if ( is_user_disabled( $user_id ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b530a37..98b4e55 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -124,7 +124,6 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { - $user_id = $request->get_param( 'user_id' ); $user = User_Collection::get_by_various( $user_id ); @@ -150,7 +149,6 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { - $data = $request->get_params(); $type = $request->get_param( 'type' ); $users = self::extract_recipients( $data ); From befd0d4f1e26c64b37a4502909be310f5db98e0c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:21:16 +0200 Subject: [PATCH 89/97] do not persist data in a getter! --- includes/model/class-blog-user.php | 15 ++++++--------- templates/settings.php | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index d1413dc..4cf41f9 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -75,19 +75,12 @@ class Blog_User extends User { * @return string The auto-generated Username. */ public static function get_default_username() { - $username = \get_option( 'activitypub_blog_user_identifier' ); - - if ( $username ) { - return $username; - } - // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); $host_parts = \explode( '.', $host ); if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { - \update_option( 'activitypub_blog_user_identifier', $host ); return $host; } @@ -96,7 +89,6 @@ class Blog_User extends User { $blog_title = \sanitize_title( $blog_title ); if ( strlen( $blog_title ) <= 15 ) { - \update_option( 'activitypub_blog_user_identifier', $blog_title ); return $blog_title; } @@ -111,12 +103,17 @@ class Blog_User extends User { // get random item of $default_identifier $default = $default_identifier[ \array_rand( $default_identifier ) ]; - \update_option( 'activitypub_blog_user_identifier', $default ); return $default; } public function get_preferred_username() { + $username = \get_option( 'activitypub_blog_user_identifier' ); + + if ( $username ) { + return $username; + } + return self::get_default_username(); } diff --git a/templates/settings.php b/templates/settings.php index b587b9e..3da7eaf 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,7 +31,7 @@
- +

@@ -58,7 +58,7 @@ - +

From 57bc4214b70912b61bf9dcaee4ce8a74d5f40d1e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:28:10 +0200 Subject: [PATCH 90/97] If the Blog is in "single user" mode, return "Person" insted of "Group". --- includes/model/class-blog-user.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 4cf41f9..a0efcb6 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -5,6 +5,7 @@ use WP_Query; use Activitypub\Signature; use Activitypub\Collection\Users; +use function Activitypub\is_single_user; use function Activitypub\is_user_disabled; class Blog_User extends User { @@ -206,4 +207,19 @@ class Blog_User extends User { public function get_canonical_url() { return \home_url(); } + + /** + * Get the type of the object. + * + * If the Blog is in "single user" mode, return "Person" insted of "Group". + * + * @return string The type of the object. + */ + public function get_type() { + if ( is_single_user() ) { + return 'Person'; + } else { + return $this->type; + } + } } From e0d767ed98adf1ef641e4f2e291ae388646f830c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:26:07 +0200 Subject: [PATCH 91/97] Fix WebFinger endpoint --- includes/rest/class-webfinger.php | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 846ac50..8e651d0 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -14,7 +14,9 @@ use Activitypub\Collection\Users as User_Collection; */ class Webfinger { /** - * Initialize the class, registering WordPress hooks + * Initialize the class, registering WordPress hooks. + * + * @return void */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); @@ -23,7 +25,9 @@ class Webfinger { } /** - * Register routes + * Register routes. + * + * @return void */ public static function register_routes() { \register_rest_route( @@ -41,10 +45,11 @@ class Webfinger { } /** - * Render JRD file + * WebFinger endpoint. * - * @param WP_REST_Request $request - * @return WP_REST_Response + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response The response object. */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); @@ -76,12 +81,16 @@ class Webfinger { * @param array $array the jrd array * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user + * + * @return array the jrd array */ public static function add_user_discovery( $array, $resource, $user ) { + $user = User_Collection::get_by_id( $user->ID ); + $array['links'][] = array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ); return $array; @@ -93,17 +102,24 @@ class Webfinger { * @param array $array the jrd array * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user + * + * @return array the jrd array */ public static function add_pseudo_user_discovery( $array, $resource ) { - if ( ! $array ) { - $array = array(); + if ( $array ) { + return $array; } - $profile = self::get_profile( $resource ); - - return array_merge( $array, $profile ); + return self::get_profile( $resource ); } + /** + * Get the WebFinger profile. + * + * @param string $resource the WebFinger resource. + * + * @return array the WebFinger profile. + */ public static function get_profile( $resource ) { $user = User_Collection::get_by_resource( $resource ); From 002d4e7981d00e7dd12be082dae1e3abc0c4b426 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:34:11 +0200 Subject: [PATCH 92/97] refactoring --- includes/class-http.php | 13 +++++++++++++ includes/class-migration.php | 22 +++++++++++++++++++++- includes/model/class-blog-user.php | 26 ++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index dd19f9b..1157137 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -25,6 +25,12 @@ class Http { $signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); $wp_version = \get_bloginfo( 'version' ); + + /** + * Filter the HTTP headers user agent. + * + * @param string $user_agent The user agent string. + */ $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); $args = array( 'timeout' => 100, @@ -66,7 +72,14 @@ class Http { $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); + + /** + * Filter the HTTP headers user agent. + * + * @param string $user_agent The user agent string. + */ $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, diff --git a/includes/class-migration.php b/includes/class-migration.php index 2e5f19c..1ff8b66 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,6 +1,7 @@ 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -72,6 +87,11 @@ class Migration { } } } + + // set the default username for the Blog User + if ( ! \get_option( 'activitypub_blog_user_identifier' ) ) { + \update_option( 'activitypub_blog_user_identifier', Blog_User::get_default_username() ); + } } /** diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a0efcb6..feee097 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -71,7 +71,7 @@ class Blog_User extends User { } /** - * Generate and save a default Username. + * Generate a default Username. * * @return string The auto-generated Username. */ @@ -105,9 +105,19 @@ class Blog_User extends User { // get random item of $default_identifier $default = $default_identifier[ \array_rand( $default_identifier ) ]; - return $default; + /** + * Filter the default blog username. + * + * @param string $default The default username. + */ + return apply_filters( 'activitypub_default_blog_username', $default ); } + /** + * Get the preferred User-Name. + * + * @return string The User-Name. + */ public function get_preferred_username() { $username = \get_option( 'activitypub_blog_user_identifier' ); @@ -118,6 +128,11 @@ class Blog_User extends User { return self::get_default_username(); } + /** + * Get the User-Icon. + * + * @return array|null The User-Icon. + */ public function get_icon() { $image = wp_get_attachment_image_src( get_theme_mod( 'custom_logo' ) ); @@ -131,6 +146,11 @@ class Blog_User extends User { return null; } + /** + * Get the User-Header-Image. + * + * @return array|null The User-Header-Image. + */ public function get_header_image() { if ( \has_header_image() ) { return array( @@ -175,6 +195,8 @@ class Blog_User extends User { } /** + * Get the User-Private-Key. + * * @param int $user_id * * @return mixed From ad18edbceab9bdd8a5964774b5115a9aa597b090 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:40:31 +0200 Subject: [PATCH 93/97] fix #358 --- includes/transformer/class-post.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7c7d990..568a0e3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,7 +116,17 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + + $published = \strtotime( $wp_post->post_date_gmt ); + + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + + $updated = \strtotime( $wp_post->post_modified_gmt ); + + if ( $updated > $published ) { + $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + } + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 4a82edcd22106cb73d5dafd839fb9fe6cceab1bf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:48:04 +0200 Subject: [PATCH 94/97] Revert "fix #358" This reverts commit ad18edbceab9bdd8a5964774b5115a9aa597b090. --- includes/transformer/class-post.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 568a0e3..7c7d990 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,17 +116,7 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - - $published = \strtotime( $wp_post->post_date_gmt ); - - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); - - $updated = \strtotime( $wp_post->post_modified_gmt ); - - if ( $updated > $published ) { - $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); - } - + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 00fbc296b36b246e068603c35f3cbd75539a0400 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:48:49 +0200 Subject: [PATCH 95/97] fix #343 --- includes/transformer/class-post.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7c7d990..568a0e3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,7 +116,17 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + + $published = \strtotime( $wp_post->post_date_gmt ); + + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + + $updated = \strtotime( $wp_post->post_modified_gmt ); + + if ( $updated > $published ) { + $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + } + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 5ae978a8bcaded05f249a8b3e12eef9be11dde6f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 13 Jul 2023 10:35:15 +0200 Subject: [PATCH 96/97] `user_id` could be an int and meta always returns strings remove strict comparison in this case and add tests to verify the correct behaviour --- includes/collection/class-followers.php | 3 ++- tests/test-class-db-activitypub-followers.php | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index af0156e..f5527f1 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -189,7 +189,8 @@ class Followers { self::add_error( $follower->get__id(), $error ); } - if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { + // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict + if ( is_array( $meta ) && ! in_array( $user_id, $meta ) ) { add_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 559bd06..2f21767 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -244,6 +244,29 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertEquals( 0, count( $followers ) ); } + public function test_add_duplicate_follower() { + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + $follower = 'https://12345.example.com'; + + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); + + $this->assertContains( $follower, $db_followers ); + + $follower = current( $db_followers ); + $meta = get_post_meta( $follower->get__id(), 'activitypub_user_id' ); + + $this->assertCount( 1, $meta ); + } + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { From 626616a747371b2590182be6d4b81a6af9de9776 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 14 Jul 2023 11:29:03 +0200 Subject: [PATCH 97/97] always use host as default username --- includes/model/class-blog-user.php | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index feee097..32d167a 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -79,38 +79,13 @@ class Blog_User extends User { // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); - $host_parts = \explode( '.', $host ); - - if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { - return $host; - } - - // check blog title - $blog_title = \get_bloginfo( 'name' ); - $blog_title = \sanitize_title( $blog_title ); - - if ( strlen( $blog_title ) <= 15 ) { - return $blog_title; - } - - $default_identifier = array( - 'feed', - 'all', - 'everyone', - 'authors', - 'follow', - 'posts', - ); - - // get random item of $default_identifier - $default = $default_identifier[ \array_rand( $default_identifier ) ]; /** * Filter the default blog username. * - * @param string $default The default username. + * @param string $host The default username. */ - return apply_filters( 'activitypub_default_blog_username', $default ); + return apply_filters( 'activitypub_default_blog_username', $host ); } /**

- or - + get_resource() ); ?> or + get_url() ); ?>

-

+

get_resource() ) ); ?>