From 09518ea66b96c4955c557603b6bda18a19898611 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 16:32:00 +0200 Subject: [PATCH] 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', + ) + ); +} +?> +