From 2d84fcc6003322612a13c2cf9d9feb1c571e4ee5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 8 Dec 2018 00:02:18 +0100 Subject: [PATCH] implemented "follow" --- .gitignore | 1 + README.md | 6 +- activitypub.php | 23 ++++- includes/class-activitypub-activities.php | 64 +++++++++++++ includes/class-activitypub-signature.php | 37 ++++---- includes/class-activitypub.php | 17 ---- includes/class-db-activitypub-actor.php | 93 +++++++++++++++++++ includes/class-rest-activitypub-followers.php | 76 +++++++++++++++ ...x.php => class-rest-activitypub-inbox.php} | 6 +- ....php => class-rest-activitypub-outbox.php} | 4 +- includes/class-rest-activitypub-webfinger.php | 20 ++++ includes/functions.php | 48 ++++++++++ languages/activitypub.pot | 39 ++++++-- readme.txt | 6 +- templates/json-author.php | 24 ++++- 15 files changed, 403 insertions(+), 61 deletions(-) create mode 100644 includes/class-activitypub-activities.php create mode 100644 includes/class-db-activitypub-actor.php create mode 100644 includes/class-rest-activitypub-followers.php rename includes/{class-activitypub-inbox.php => class-rest-activitypub-inbox.php} (91%) rename includes/{class-activitypub-outbox.php => class-rest-activitypub-outbox.php} (96%) create mode 100644 includes/class-rest-activitypub-webfinger.php diff --git a/.gitignore b/.gitignore index e6fe5be..566a865 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /vendor/ package-lock.json composer.lock +.DS_Store diff --git a/README.md b/README.md index 5f312a9..9002225 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ Implemented: * profile pages (JSON representation) * custom links * functional inbox/outbox +* follow (accept follows) To implement: -* follow (accept follows) * share posts * share comments @@ -55,6 +55,10 @@ To implement: Project maintained on github at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +### 0.1.0 ### + +* fully functional "follow" activity + ### 0.0.2 ### * refactorins diff --git a/activitypub.php b/activitypub.php index 1d855b2..6bfc32c 100644 --- a/activitypub.php +++ b/activitypub.php @@ -18,18 +18,31 @@ function activitypub_init() { require_once dirname( __FILE__ ) . '/includes/class-activitypub-signature.php'; require_once dirname( __FILE__ ) . '/includes/class-activitypub-post.php'; + require_once dirname( __FILE__ ) . '/includes/class-db-activitypub-actor.php'; require_once dirname( __FILE__ ) . '/includes/functions.php'; require_once dirname( __FILE__ ) . '/includes/class-activitypub.php'; add_filter( 'template_include', array( 'Activitypub', 'render_json_template' ), 99 ); - add_action( 'webfinger_user_data', array( 'Activitypub', 'add_webfinger_discovery' ), 10, 3 ); add_filter( 'query_vars', array( 'Activitypub', 'add_query_vars' ) ); add_action( 'init', array( 'Activitypub', 'add_rewrite_endpoint' ) ); - require_once dirname( __FILE__ ) . '/includes/class-activitypub-outbox.php'; - require_once dirname( __FILE__ ) . '/includes/class-activitypub-inbox.php'; // Configure the REST API route - add_action( 'rest_api_init', array( 'Activitypub_Outbox', 'register_routes' ) ); - add_action( 'rest_api_init', array( 'Activitypub_Inbox', 'register_routes' ) ); + require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-outbox.php'; + add_action( 'rest_api_init', array( 'Rest_Activitypub_Outbox', 'register_routes' ) ); + + require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-inbox.php'; + add_action( 'rest_api_init', array( 'Rest_Activitypub_Inbox', 'register_routes' ) ); + + require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-followers.php'; + add_action( 'rest_api_init', array( 'Rest_Activitypub_Followers', 'register_routes' ) ); + + require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-webfinger.php'; + add_action( 'webfinger_user_data', array( 'Rest_Activitypub_Webfinger', 'add_webfinger_discovery' ), 10, 3 ); + + // Configure activities + require_once dirname( __FILE__ ) . '/includes/class-activitypub-activities.php'; + add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'accept' ), 10, 2 ); + add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'follow' ), 10, 2 ); + add_action( 'activitypub_inbox_unfollow', array( 'Activitypub_Activities', 'unfollow' ), 10, 2 ); } add_action( 'plugins_loaded', 'activitypub_init' ); diff --git a/includes/class-activitypub-activities.php b/includes/class-activitypub-activities.php new file mode 100644 index 0000000..0e900c5 --- /dev/null +++ b/includes/class-activitypub-activities.php @@ -0,0 +1,64 @@ + array( 'https://www.w3.org/ns/activitystreams' ), + 'type' => 'Accept', + 'actor' => get_author_posts_url( $author_id ), + 'object' => $data, + 'to' => $data['actor'], + ) + ); + + return activitypub_safe_remote_post( $inbox, $activity, $author_id ); + } + + /** + * [follow description] + * @param [type] $data [description] + * @param [type] $author_id [description] + * @return [type] [description] + */ + public static function follow( $data, $author_id ) { + if ( ! array_key_exists( 'actor', $data ) ) { + return new WP_Error( 'activitypub_no_actor', __( 'No "Actor" found', 'activitypub' ), $metadata ); + } + + Db_Activitypub_Actor::add_follower( $data['actor'], $author_id ); + } + + /** + * [unfollow description] + * @param [type] $data [description] + * @param [type] $author_id [description] + * @return [type] [description] + */ + public static function unfollow( $data, $author_id ) { + + } + + /** + * [create description] + * @param [type] $data [description] + * @param [type] $author_id [description] + * @return [type] [description] + */ + public static function create( $data, $author_id ) { + + } +} diff --git a/includes/class-activitypub-signature.php b/includes/class-activitypub-signature.php index 4470286..85cba19 100644 --- a/includes/class-activitypub-signature.php +++ b/includes/class-activitypub-signature.php @@ -1,38 +1,39 @@ 'sha512', 'private_key_bits' => 2048, @@ -55,16 +56,16 @@ class Activitypub_Signature { openssl_pkey_export( $key, $priv_key ); // private key - update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); + update_user_meta( $author_id, 'magic_sig_private_key', $priv_key ); $detail = openssl_pkey_get_details( $key ); // public key - update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); + update_user_meta( $author_id, 'magic_sig_public_key', $detail['key'] ); } - public static function generate_signature( $user_id, $inbox ) { - $key = self::get_private_key( $user_id ); + public static function generate_signature( $author_id, $inbox, $date ) { + $key = self::get_private_key( $author_id ); $url_parts = wp_parse_url( $inbox ); @@ -73,7 +74,7 @@ class Activitypub_Signature { // add path if ( ! empty( $url_parts['path'] ) ) { - $path .= $url_parts['path']; + $path = $url_parts['path']; } // add query @@ -81,11 +82,11 @@ class Activitypub_Signature { $path .= '?' . $url_parts['query']; } - $date = gmdate( 'D, d M Y H:i:s T' ); $signed_string = "(request-target): post $path\nhost: $host\ndate: $date"; $signature = null; openssl_sign( $signed_string, $signature, $key, OPENSSL_ALGO_SHA256 ); + $signature = base64_encode( $signature ); $key_id = get_author_posts_url( $author_id ) . '#main-key'; diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 94db1d5..82fc2e4 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -54,23 +54,6 @@ class Activitypub { return $json_template; } - /** - * 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_webfinger_discovery( $array, $resource, $user ) { - $array['links'][] = array( - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => get_author_posts_url( $user->ID ), - ); - - return $array; - } - /** * Add the 'photos' query variable so WordPress * won't mangle it. diff --git a/includes/class-db-activitypub-actor.php b/includes/class-db-activitypub-actor.php new file mode 100644 index 0000000..fe17900 --- /dev/null +++ b/includes/class-db-activitypub-actor.php @@ -0,0 +1,93 @@ + 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( 'accept' => 'application/activity+json' ), + ); + + $response = wp_safe_remote_get( $actor, $args ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $metadata = wp_remote_retrieve_body( $response ); + $metadata = json_decode( $metadata, true ); + + if ( ! $metadata ) { + return new WP_Error( 'activitypub_invalid_json', __( 'No valid JSON data', 'activitypub' ), $actor ); + } + + set_transient( 'activitypub_' . $actor, $metadata, WEEK_IN_SECONDS ); + + return $metadata; + } + + public static function add_follower( $actor, $author_id ) { + $followers = get_user_option( 'activitypub_followers', $author_id ); + + if ( ! is_array( $followers ) ) { + $followers = array( $actor ); + } else { + $followers[] = $actor; + } + + $followers = array_unique( $followers ); + + update_user_meta( $author_id, 'activitypub_followers', $followers ); + } + + public static function remove_follower( $actor, $author_id ) { + $followers = get_user_option( 'activitypub_followers', $author_id ); + + foreach ( $followers as $key => $value ) { + if ( $value === $actor) { + unset( $followers[$key] ); + } + } + + update_user_meta( $author_id, 'activitypub_followers', $followers ); + } +} diff --git a/includes/class-rest-activitypub-followers.php b/includes/class-rest-activitypub-followers.php new file mode 100644 index 0000000..9aa5f7a --- /dev/null +++ b/includes/class-rest-activitypub-followers.php @@ -0,0 +1,76 @@ +\d+)/followers', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( 'Rest_Activitypub_Followers', 'get' ), + 'args' => self::request_parameters(), + ), + ) + ); + } + + public static function get( $request ) { + $author_id = $request->get_param( 'id' ); + $author = get_user_by( 'ID', $author_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' ) + ) + ) ); + } + + $page = $request->get_param( 'page', 0 ); + + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + do_action( 'activitypub_outbox_pre' ); + + $json = new stdClass(); + + $json->{'@context'} = get_activitypub_context(); + + $followers = get_user_option( 'activitypub_followers', $author_id ); + + if ( ! is_array( $followers ) ) { + $followers = array(); + } + + $json->totlaItems = count( $followers ); + $json->orderedItems = $followers; + + $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' => 'integer', + ); + + $params['id'] = array( + 'required' => true, + 'type' => 'integer', + ); + + return $params; + } +} diff --git a/includes/class-activitypub-inbox.php b/includes/class-rest-activitypub-inbox.php similarity index 91% rename from includes/class-activitypub-inbox.php rename to includes/class-rest-activitypub-inbox.php index 08e802f..48e7580 100644 --- a/includes/class-activitypub-inbox.php +++ b/includes/class-rest-activitypub-inbox.php @@ -4,7 +4,7 @@ * * @author Matthias Pfefferle */ -class Activitypub_Inbox { +class Rest_Activitypub_Inbox { /** * Register routes */ @@ -13,7 +13,7 @@ class Activitypub_Inbox { 'activitypub/1.0', '/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( 'Activitypub_Inbox', 'global_inbox' ), + 'callback' => array( 'Rest_Activitypub_Inbox', 'global_inbox' ), ), ) ); @@ -22,7 +22,7 @@ class Activitypub_Inbox { 'activitypub/1.0', '/users/(?P\d+)/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( 'Activitypub_Inbox', 'user_inbox' ), + 'callback' => array( 'Rest_Activitypub_Inbox', 'user_inbox' ), 'args' => self::request_parameters(), ), ) diff --git a/includes/class-activitypub-outbox.php b/includes/class-rest-activitypub-outbox.php similarity index 96% rename from includes/class-activitypub-outbox.php rename to includes/class-rest-activitypub-outbox.php index df596af..6e7872d 100644 --- a/includes/class-activitypub-outbox.php +++ b/includes/class-rest-activitypub-outbox.php @@ -4,7 +4,7 @@ * * @author Matthias Pfefferle */ -class Activitypub_Outbox { +class Rest_Activitypub_Outbox { /** * Register routes */ @@ -13,7 +13,7 @@ class Activitypub_Outbox { 'activitypub/1.0', '/users/(?P\d+)/outbox', array( array( 'methods' => WP_REST_Server::READABLE, - 'callback' => array( 'Activitypub_Outbox', 'get' ), + 'callback' => array( 'Rest_Activitypub_Outbox', 'get' ), 'args' => self::request_parameters(), ), ) diff --git a/includes/class-rest-activitypub-webfinger.php b/includes/class-rest-activitypub-webfinger.php new file mode 100644 index 0000000..d240b0e --- /dev/null +++ b/includes/class-rest-activitypub-webfinger.php @@ -0,0 +1,20 @@ + 'self', + 'type' => 'application/activity+json', + 'href' => get_author_posts_url( $user->ID ), + ); + + return $array; + } +} diff --git a/includes/functions.php b/includes/functions.php index 2f3269e..6f1b3fa 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -38,3 +38,51 @@ function get_activitypub_context() { return apply_filters( 'activitypub_json_context', $context ); } + +if ( ! function_exists( 'base64_url_encode' ) ) { + /** + * Encode data + * + * @param string $input input text + * + * @return string the encoded text + */ + function base64_url_encode( $input ) { + return strtr( base64_encode( $input ), '+/', '-_' ); + } +} +if ( ! function_exists( 'base64_url_decode' ) ) { + /** + * Dencode data + * + * @param string $input input text + * + * @return string the decoded text + */ + function base64_url_decode( $input ) { + return base64_decode( strtr( $input, '-_', '+/' ) ); + } +} + +function activitypub_safe_remote_post( $url, $body, $author_id ) { + $date = gmdate( 'D, d M Y H:i:s T' ); + $signature = Activitypub_Signature::generate_signature( $author_id, $url, $date ); + + $wp_version = get_bloginfo( 'version' ); + $user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' ) ); + $args = array( + 'timeout' => 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Signature' => $signature, + 'Date' => $date, + ), + 'body' => $body, + ); + + $response = wp_safe_remote_post( $url, $args ); +} diff --git a/languages/activitypub.pot b/languages/activitypub.pot index 69921da..3415b83 100644 --- a/languages/activitypub.pot +++ b/languages/activitypub.pot @@ -4,7 +4,7 @@ msgid "" msgstr "" "Project-Id-Version: ActivityPub 0.0.2\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/activitypub\n" -"POT-Creation-Date: 2018-12-01 20:22:43+00:00\n" +"POT-Creation-Date: 2018-12-07 23:01:55+00:00\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -13,28 +13,47 @@ msgstr "" "Language-Team: LANGUAGE \n" "X-Generator: grunt-wp-i18n1.0.2\n" -#: includes/class-activitypub-inbox.php:47 -msgid "Invalid payload" +#: includes/class-activitypub-activities.php:13 +#: includes/class-activitypub-activities.php:39 +msgid "No \"Actor\" found" msgstr "" -#: includes/class-activitypub-inbox.php:62 -msgid "This method is not yet implemented" +#: includes/class-db-activitypub-actor.php:20 +msgid "No \"Inbox\" found" msgstr "" -#: includes/class-activitypub-outbox.php:28 -#: includes/class-activitypub-outbox.php:30 +#: includes/class-db-activitypub-actor.php:36 +msgid "The \"actor\" is no valid URL" +msgstr "" + +#: includes/class-db-activitypub-actor.php:60 +msgid "No valid JSON data" +msgstr "" + +#: includes/class-rest-activitypub-followers.php:24 +#: includes/class-rest-activitypub-followers.php:26 +#: includes/class-rest-activitypub-outbox.php:28 +#: includes/class-rest-activitypub-outbox.php:30 msgid "User not found" msgstr "" -#: templates/json-author.php:44 +#: includes/class-rest-activitypub-inbox.php:47 +msgid "Invalid payload" +msgstr "" + +#: includes/class-rest-activitypub-inbox.php:62 +msgid "This method is not yet implemented" +msgstr "" + +#: templates/json-author.php:48 msgid "Blog" msgstr "" -#: templates/json-author.php:50 +#: templates/json-author.php:58 msgid "Profile" msgstr "" -#: templates/json-author.php:57 +#: templates/json-author.php:69 msgid "Website" msgstr "" diff --git a/readme.txt b/readme.txt index 22f21ac..e51fb80 100644 --- a/readme.txt +++ b/readme.txt @@ -26,10 +26,10 @@ Implemented: * profile pages (JSON representation) * custom links * functional inbox/outbox +* follow (accept follows) To implement: -* follow (accept follows) * share posts * share comments @@ -55,6 +55,10 @@ To implement: Project maintained on github at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += 0.1.0 = + +* fully functional "follow" activity + = 0.0.2 = * refactorins diff --git a/templates/json-author.php b/templates/json-author.php index dce986b..8401a0a 100644 --- a/templates/json-author.php +++ b/templates/json-author.php @@ -7,7 +7,11 @@ $json->{'@context'} = get_activitypub_context(); $json->id = get_author_posts_url( $author_id ); $json->type = 'Person'; $json->name = get_the_author_meta( 'display_name', $author_id ); -$json->summary = esc_html( get_the_author_meta( 'description', $author_id ) ); +$json->summary = html_entity_decode( + get_the_author_meta( '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( @@ -42,20 +46,32 @@ $json->attachment = array(); $json->attachment[] = array( 'type' => 'PropertyValue', 'name' => __( 'Blog', 'activitypub' ), - 'value' => esc_html( '' . wp_parse_url( home_url( '/' ), PHP_URL_HOST ) . '' ), + 'value' => html_entity_decode( + '' . wp_parse_url( home_url( '/' ), PHP_URL_HOST ) . '', + ENT_QUOTES, + 'UTF-8' + ), ); $json->attachment[] = array( 'type' => 'PropertyValue', 'name' => __( 'Profile', 'activitypub' ), - 'value' => esc_html( '' . wp_parse_url( get_author_posts_url( $author_id ), PHP_URL_HOST ) . '' ), + '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[] = array( 'type' => 'PropertyValue', 'name' => __( 'Website', 'activitypub' ), - 'value' => esc_html( '' . wp_parse_url( get_the_author_meta( 'user_url', $author_id ), PHP_URL_HOST ) . '' ), + 'value' => html_entity_decode( + '' . wp_parse_url( get_the_author_meta( 'user_url', $author_id ), PHP_URL_HOST ) . '', + ENT_QUOTES, + 'UTF-8' + ), ); }