From 1fd0cca18581cb55e2a6d56c842edc29a69129dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 10 Aug 2023 15:10:07 +0200 Subject: [PATCH 1/9] fix check! --- includes/class-http.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index e243997..35003b8 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -52,7 +52,7 @@ class Http { $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 <= $code && 500 >= $code ) { + if ( $code <= 400 ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -100,7 +100,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 <= $code && 500 >= $code ) { + if ( $code <= 400 ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } From 6e2656311b61bf46630fc2b6946eedcc2fb5ac4a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 10 Aug 2023 15:35:10 +0200 Subject: [PATCH 2/9] oops --- includes/class-http.php | 4 ++-- includes/class-mention.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 35003b8..5b741f4 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -52,7 +52,7 @@ class Http { $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( $code <= 400 ) { + if ( $code >= 400 ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -100,7 +100,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( $code <= 400 ) { + if ( $code >= 400 ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } diff --git a/includes/class-mention.php b/includes/class-mention.php index fda0dd5..d6c4bbb 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -59,8 +59,7 @@ class Mention { ); $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content ); - - $the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); + $the_content = \str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); return $the_content; } @@ -74,6 +73,7 @@ class Mention { */ public static function replace_with_links( $result ) { $metadata = get_remote_metadata_by_actor( $result[0] ); + if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) { $username = ltrim( $result[0], '@' ); if ( ! empty( $metadata['name'] ) ) { From bc7e173fe05958530d2ea2709296ffe64ee0af4b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 09:22:46 +0200 Subject: [PATCH 3/9] also allow JSON --- includes/functions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 6d9013d..73d3499 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -305,8 +305,9 @@ function is_activitypub_request() { * and return true when the header includes at least one of the following: * - application/activity+json * - application/ld+json + * - application/json */ - if ( preg_match( '/(application\/(ld\+json|activity\+json))/', $accept ) ) { + if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { return true; } } From 30eb07ba17f38de37f3d23e0a1dd7131e917573b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 09:23:49 +0200 Subject: [PATCH 4/9] add missing "type" see https://git.joinfirefish.org/firefish/firefish/-/issues/10650#note_1011 --- includes/rest/class-collection.php | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 3849594..3936d78 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -4,6 +4,7 @@ namespace Activitypub\Rest; use WP_REST_Server; use WP_REST_Response; use Activitypub\Transformer\Post; +use Activitypub\Activity\Activity; use function Activitypub\esc_hashtag; use function Activitypub\get_rest_url_by_path; @@ -80,13 +81,9 @@ class Collection { } $response = array( - '@context' => array( - 'https://www.w3.org/ns/activitystreams', - array( - 'Hashtag' => 'as:Hashtag', - ), - ), + '@context' => Activity::CONTEXT, 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user_id ) ), + 'type' => 'Collection', 'totalItems' => count( $tags ), 'items' => array(), ); @@ -124,23 +121,15 @@ class Collection { $posts = \get_posts( $args ); $response = array( - '@context' => 'https://www.w3.org/ns/activitystreams', - array( - 'ostatus' => 'http://ostatus.org#', - 'atomUri' => 'ostatus:atomUri', - 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', - 'conversation' => 'ostatus:conversation', - 'sensitive' => 'as:sensitive', - 'toot' => 'http://joinmastodon.org/ns#', - 'votersCount' => 'toot:votersCount', - ), - 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ), - 'totalItems' => count( $posts ), - 'items' => array(), + '@context' => Activity::CONTEXT, + 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ), + 'type' => 'OrderedCollection', + 'totalItems' => count( $posts ), + 'orderedItems' => array(), ); foreach ( $posts as $post ) { - $response['items'][] = Post::transform( $post )->to_object()->to_array(); + $response['orderedItems'][] = Post::transform( $post )->to_object()->to_array(); } return new WP_REST_Response( $response, 200 ); From 626203002a3f58caf9ab576b895e4662b07b027c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 09:24:45 +0200 Subject: [PATCH 5/9] only include the minimum required fields for Accept call --- includes/collection/class-followers.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index f28a17a..baadf09 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -265,11 +265,18 @@ class Followers { return; } - if ( isset( $object['user_id'] ) ) { - unset( $object['user_id'] ); - } - - unset( $object['@context'] ); + // only send minimal data + $object = array_intersect_key( + $object, + array_flip( + array( + 'id', + 'type', + 'actor', + 'object', + ) + ) + ); $user = Users::get_by_id( $user_id ); @@ -282,7 +289,7 @@ class Followers { $activity->set_object( $object ); $activity->set_actor( $user->get_id() ); $activity->set_to( $actor ); - $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); + $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() ); $activity = $activity->to_json(); From 69ba1c87e127b4c654aaa9f9edb3c4f816672712 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 11:16:06 +0200 Subject: [PATCH 6/9] fix sticky posts endpoint --- includes/rest/class-collection.php | 43 +++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 3936d78..b29a090 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -5,8 +5,10 @@ use WP_REST_Server; use WP_REST_Response; use Activitypub\Transformer\Post; use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\esc_hashtag; +use function Activitypub\is_single_user; use function Activitypub\get_rest_url_by_path; /** @@ -65,7 +67,13 @@ class Collection { */ public static function tags_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $number = 4; + $user = User_Collection::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + $number = 4; $tags = \get_terms( array( @@ -82,7 +90,7 @@ class Collection { $response = array( '@context' => Activity::CONTEXT, - 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user_id ) ), + 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ), 'type' => 'Collection', 'totalItems' => count( $tags ), 'items' => array(), @@ -108,17 +116,32 @@ class Collection { */ public static function featured_get( $request ) { $user_id = $request->get_param( 'user_id' ); + $user = User_Collection::get_by_various( $user_id ); - $args = array( - 'post__in' => \get_option( 'sticky_posts' ), - 'ignore_sticky_posts' => 1, - ); - - if ( $user_id > 0 ) { - $args['author'] = $user_id; + if ( is_wp_error( $user ) ) { + return $user; } - $posts = \get_posts( $args ); + $sticky_posts = \get_option( 'sticky_posts' ); + + if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) { + $posts = array(); + } elseif ( $sticky_posts ) { + $args = array( + 'post__in' => $sticky_posts, + 'ignore_sticky_posts' => 1, + 'orderby' => 'date', + 'order' => 'DESC', + ); + + if ( $user->get__id() > 0 ) { + $args['author'] = $user->get__id(); + } + + $posts = \get_posts( $args ); + } else { + $posts = array(); + } $response = array( '@context' => Activity::CONTEXT, From a9648798a8eb21d6ec2f9364917f145ba63c6e0b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 12:20:56 +0200 Subject: [PATCH 7/9] input fields should be readonly --- templates/welcome.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/welcome.php b/templates/welcome.php index 891bb54..c193a6e 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -30,13 +30,13 @@

- +

- +

@@ -62,13 +62,13 @@

- +

- +

From 6f63e6c6517c300e9eebc7bd61e1c97914665b58 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 11 Aug 2023 20:33:50 +0200 Subject: [PATCH 8/9] update readme --- README.md | 6 ++++-- readme.txt | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 55d2895..cdc6d62 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,10 @@ Implemented: * follow (accept follows) * share posts * receive comments/reactions +* signature verification To implement: -* signature verification -* better WordPress integration * better configuration possibilities * threaded comments support @@ -120,6 +119,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Add: a Followers Block. * Add: Simple caching +* Add: Collection endpoints for Featured Tags and Featured Posts * Update: Complete rewrite of the Follower-System based on Custom Post Types. * Update: Improved linter (PHPCS) * Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. @@ -127,6 +127,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. * Compatibility: Avoid PHP notice on sites using PHP 8.2. * Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests. +* Fixed: Updating posts +* Fixed: Hashtag now support CamelCase and UTF-8 ### 0.17.0 ### diff --git a/readme.txt b/readme.txt index a494cc5..a09b750 100644 --- a/readme.txt +++ b/readme.txt @@ -75,11 +75,10 @@ Implemented: * follow (accept follows) * share posts * receive comments/reactions +* signature verification To implement: -* signature verification -* better WordPress integration * better configuration possibilities * threaded comments support @@ -120,6 +119,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Add: a Followers Block. * Add: Simple caching +* Add: Collection endpoints for Featured Tags and Featured Posts * Update: Complete rewrite of the Follower-System based on Custom Post Types. * Update: Improved linter (PHPCS) * Compatibility: Add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. @@ -127,6 +127,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Compatibility: Indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. * Compatibility: Avoid PHP notice on sites using PHP 8.2. * Fixed: Load the plugin later in the WordPress code lifecycle to avoid errors in some requests. +* Fixed: Updating posts +* Fixed: Hashtag now support CamelCase and UTF-8 = 0.17.0 = From 14b91cf760a5f8b076fe7e34cd92a097ccbf7eb8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 12 Aug 2023 00:41:34 +0200 Subject: [PATCH 9/9] remote-follow endpoint (#392) Adds an endpoint at `users/$user_id/follow-me` to return the follow template for a remote user, to enable following them more easily. --- includes/class-webfinger.php | 108 +++++++++++++++++++++++++++++++++- includes/rest/class-users.php | 53 +++++++++++++++++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 9a53ac4..c077f12 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -62,7 +62,7 @@ class Webfinger { $response = \wp_remote_get( $url, array( - 'headers' => array( 'Accept' => 'application/activity+json' ), + 'headers' => array( 'Accept' => 'application/jrd+json' ), 'redirection' => 0, 'timeout' => 2, ) @@ -94,4 +94,110 @@ class Webfinger { \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. return $link; } + + /** + * Convert a URI string to an identifier and its host. + * Automatically adds acct: if it's missing. + * + * @param string $url The URI (acct:, mailto:, http:, https:) + * + * @return WP_Error|array Error reaction or array with + * identifier and host as values + */ + public static function get_identifier_and_host( $url ) { + // remove leading @ + $url = ltrim( $url, '@' ); + + if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) { + $identifier = 'acct:' . $url; + $scheme = 'acct'; + } else { + $identifier = $url; + $scheme = $match[1]; + } + + $host = null; + + switch ( $scheme ) { + case 'acct': + case 'mailto': + case 'xmpp': + if ( strpos( $identifier, '@' ) !== false ) { + $host = substr( $identifier, strpos( $identifier, '@' ) + 1 ); + } + break; + default: + $host = wp_parse_url( $identifier, PHP_URL_HOST ); + break; + } + + if ( empty( $host ) ) { + return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) ); + } + + return array( $identifier, $host ); + } + + /** + * Get the WebFinger data for a given URI + * + * @param string $identifier The Identifier: @ + * @param string $host The Host: @ + * + * @return WP_Error|array Error reaction or array with + * identifier and host as values + */ + public static function get_data( $identifier, $host ) { + $webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier ); + + $response = wp_safe_remote_get( + $webfinger_url, + array( + 'headers' => array( 'Accept' => 'application/jrd+json' ), + 'redirection' => 0, + 'timeout' => 2, + ) + ); + + if ( is_wp_error( $response ) ) { + return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url ); + } + + $body = wp_remote_retrieve_body( $response ); + + return json_decode( $body, true ); + } + + /** + * Undocumented function + * + * @return void + */ + public static function get_remote_follow_endpoint( $uri ) { + $identifier_and_host = self::get_identifier_and_host( $uri ); + + if ( is_wp_error( $identifier_and_host ) ) { + return $identifier_and_host; + } + + list( $identifier, $host ) = $identifier_and_host; + + $data = self::get_data( $identifier, $host ); + + if ( is_wp_error( $data ) ) { + return $data; + } + + if ( empty( $data['links'] ) ) { + return new WP_Error( 'webfinger_url_invalid_response', null, $data ); + } + + foreach ( $data['links'] as $link ) { + if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) { + return $link['template']; + } + } + + return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', null, $data ); + } } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index b678043..820f653 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -3,7 +3,9 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; +use WP_REST_Request; use WP_REST_Response; +use Activitypub\Webfinger; use Activitypub\Activity\Activity; use Activitypub\Collection\Users as User_Collection; @@ -40,6 +42,25 @@ class Users { ), ) ); + + \register_rest_route( + ACTIVITYPUB_REST_NAMESPACE, + '/users/(?P[\w\-\.]+)/remote-follow', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( self::class, 'remote_follow_get' ), + + 'args' => array( + 'resource' => array( + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'permission_callback' => '__return_true', + ), + ) + ); } /** @@ -80,6 +101,38 @@ class Users { return $response; } + + /** + * Endpoint for remote follow UI/Block + * + * @param WP_REST_Request $request The request object. + * + * @return void|string The URL to the remote follow page + */ + public static function remote_follow_get( WP_REST_Request $request ) { + $resource = $request->get_param( 'resource' ); + $user_id = $request->get_param( 'user_id' ); + $user = User_Collection::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + $template = Webfinger::get_remote_follow_endpoint( $resource ); + + if ( is_wp_error( $template ) ) { + return $template; + } + + $resource = $user->get_resource(); + $url = str_replace( '{uri}', $resource, $template ); + + return new WP_REST_Response( + array( 'url' => $url ), + 200 + ); + } + /** * The supported parameters *