Skip to content

Commit 14b91cf

Browse files
authored
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.
1 parent 6f63e6c commit 14b91cf

File tree

2 files changed

+160
-1
lines changed

2 files changed

+160
-1
lines changed

includes/class-webfinger.php

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static function resolve( $resource ) {
6262
$response = \wp_remote_get(
6363
$url,
6464
array(
65-
'headers' => array( 'Accept' => 'application/activity+json' ),
65+
'headers' => array( 'Accept' => 'application/jrd+json' ),
6666
'redirection' => 0,
6767
'timeout' => 2,
6868
)
@@ -94,4 +94,110 @@ public static function resolve( $resource ) {
9494
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
9595
return $link;
9696
}
97+
98+
/**
99+
* Convert a URI string to an identifier and its host.
100+
* Automatically adds acct: if it's missing.
101+
*
102+
* @param string $url The URI (acct:, mailto:, http:, https:)
103+
*
104+
* @return WP_Error|array Error reaction or array with
105+
* identifier and host as values
106+
*/
107+
public static function get_identifier_and_host( $url ) {
108+
// remove leading @
109+
$url = ltrim( $url, '@' );
110+
111+
if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
112+
$identifier = 'acct:' . $url;
113+
$scheme = 'acct';
114+
} else {
115+
$identifier = $url;
116+
$scheme = $match[1];
117+
}
118+
119+
$host = null;
120+
121+
switch ( $scheme ) {
122+
case 'acct':
123+
case 'mailto':
124+
case 'xmpp':
125+
if ( strpos( $identifier, '@' ) !== false ) {
126+
$host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
127+
}
128+
break;
129+
default:
130+
$host = wp_parse_url( $identifier, PHP_URL_HOST );
131+
break;
132+
}
133+
134+
if ( empty( $host ) ) {
135+
return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
136+
}
137+
138+
return array( $identifier, $host );
139+
}
140+
141+
/**
142+
* Get the WebFinger data for a given URI
143+
*
144+
* @param string $identifier The Identifier: <identifier>@<host>
145+
* @param string $host The Host: <identifier>@<host>
146+
*
147+
* @return WP_Error|array Error reaction or array with
148+
* identifier and host as values
149+
*/
150+
public static function get_data( $identifier, $host ) {
151+
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
152+
153+
$response = wp_safe_remote_get(
154+
$webfinger_url,
155+
array(
156+
'headers' => array( 'Accept' => 'application/jrd+json' ),
157+
'redirection' => 0,
158+
'timeout' => 2,
159+
)
160+
);
161+
162+
if ( is_wp_error( $response ) ) {
163+
return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url );
164+
}
165+
166+
$body = wp_remote_retrieve_body( $response );
167+
168+
return json_decode( $body, true );
169+
}
170+
171+
/**
172+
* Undocumented function
173+
*
174+
* @return void
175+
*/
176+
public static function get_remote_follow_endpoint( $uri ) {
177+
$identifier_and_host = self::get_identifier_and_host( $uri );
178+
179+
if ( is_wp_error( $identifier_and_host ) ) {
180+
return $identifier_and_host;
181+
}
182+
183+
list( $identifier, $host ) = $identifier_and_host;
184+
185+
$data = self::get_data( $identifier, $host );
186+
187+
if ( is_wp_error( $data ) ) {
188+
return $data;
189+
}
190+
191+
if ( empty( $data['links'] ) ) {
192+
return new WP_Error( 'webfinger_url_invalid_response', null, $data );
193+
}
194+
195+
foreach ( $data['links'] as $link ) {
196+
if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
197+
return $link['template'];
198+
}
199+
}
200+
201+
return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', null, $data );
202+
}
97203
}

includes/rest/class-users.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
use WP_Error;
55
use WP_REST_Server;
6+
use WP_REST_Request;
67
use WP_REST_Response;
8+
use Activitypub\Webfinger;
79
use Activitypub\Activity\Activity;
810
use Activitypub\Collection\Users as User_Collection;
911

@@ -40,6 +42,25 @@ public static function register_routes() {
4042
),
4143
)
4244
);
45+
46+
\register_rest_route(
47+
ACTIVITYPUB_REST_NAMESPACE,
48+
'/users/(?P<user_id>[\w\-\.]+)/remote-follow',
49+
array(
50+
array(
51+
'methods' => WP_REST_Server::READABLE,
52+
'callback' => array( self::class, 'remote_follow_get' ),
53+
54+
'args' => array(
55+
'resource' => array(
56+
'required' => true,
57+
'sanitize_callback' => 'sanitize_text_field',
58+
),
59+
),
60+
'permission_callback' => '__return_true',
61+
),
62+
)
63+
);
4364
}
4465

4566
/**
@@ -80,6 +101,38 @@ public static function get( $request ) {
80101
return $response;
81102
}
82103

104+
105+
/**
106+
* Endpoint for remote follow UI/Block
107+
*
108+
* @param WP_REST_Request $request The request object.
109+
*
110+
* @return void|string The URL to the remote follow page
111+
*/
112+
public static function remote_follow_get( WP_REST_Request $request ) {
113+
$resource = $request->get_param( 'resource' );
114+
$user_id = $request->get_param( 'user_id' );
115+
$user = User_Collection::get_by_various( $user_id );
116+
117+
if ( is_wp_error( $user ) ) {
118+
return $user;
119+
}
120+
121+
$template = Webfinger::get_remote_follow_endpoint( $resource );
122+
123+
if ( is_wp_error( $template ) ) {
124+
return $template;
125+
}
126+
127+
$resource = $user->get_resource();
128+
$url = str_replace( '{uri}', $resource, $template );
129+
130+
return new WP_REST_Response(
131+
array( 'url' => $url ),
132+
200
133+
);
134+
}
135+
83136
/**
84137
* The supported parameters
85138
*

0 commit comments

Comments
 (0)