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 *