PluginProbe
ActivityPub / 7.8.2
ActivityPub v7.8.2
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
← All changes | includes/class-webfinger.php +209 -86 1.0.87.8.2 View file →
@@ -1,12 +1,19 @@
1 1 <?php
2 +/**
3 + * WebFinger class file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub;
3 9
4 -use WP_Error;
5 -use Activitypub\Collection\Users;
10 +use Activitypub\Activity\Actor;
11 +use Activitypub\Collection\Actors;
12 +use Activitypub\Collection\Remote_Actors;
6 13
7 14 /**
8 - * ActivityPub WebFinger Class
15 + * ActivityPub WebFinger Class.
9 16 *
10 17 * @author Matthias Pfefferle
11 18 *
12 19 * @see https://webfinger.net/
@@ -12,88 +19,112 @@
12 19 * @see https://webfinger.net/
13 20 */
14 21 class Webfinger {
15 22 /**
16 - * Returns a users WebFinger "resource"
23 + * Returns a users WebFinger "resource".
17 24 *
18 - * @param int $user_id
25 + * @param int $user_id The WordPress user id.
19 26 *
20 - * @return string The user-resource
27 + * @return string The user-resource.
21 28 */
22 29 public static function get_user_resource( $user_id ) {
23 - // use WebFinger plugin if installed
24 - if ( \function_exists( '\get_webfinger_resource' ) ) {
25 - return \get_webfinger_resource( $user_id, false );
26 - }
27 -
28 - $user = Users::get_by_id( $user_id );
30 + $user = Actors::get_by_id( $user_id );
29 31 if ( ! $user || is_wp_error( $user ) ) {
30 32 return '';
31 33 }
32 34
33 - return $user->get_resource();
35 + return $user->get_webfinger();
34 36 }
35 37
36 38 /**
37 - * Resolve a WebFinger resource
39 + * Resolve a WebFinger resource.
38 40 *
39 - * @param string $resource The WebFinger resource
41 + * @param string $uri The WebFinger Resource.
40 42 *
41 - * @return string|WP_Error The URL or WP_Error
43 + * @return string|\WP_Error The URL or WP_Error.
42 44 */
43 - public static function resolve( $resource ) {
44 - if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $resource, $m ) ) {
45 - return null;
45 + public static function resolve( $uri ) {
46 + $data = self::get_data( $uri );
47 +
48 + if ( \is_wp_error( $data ) ) {
49 + return $data;
46 50 }
47 - $transient_key = 'activitypub_resolve_' . ltrim( $resource, '@' );
48 51
49 - $link = \get_transient( $transient_key );
50 - if ( $link ) {
51 - return $link;
52 + if ( ! is_array( $data ) || empty( $data['links'] ) ) {
53 + return new \WP_Error(
54 + 'webfinger_missing_links',
55 + __( 'No valid Link elements found.', 'activitypub' ),
56 + array(
57 + 'status' => 400,
58 + 'data' => $data,
59 + )
60 + );
52 61 }
53 62
54 - $url = \add_query_arg( 'resource', 'acct:' . ltrim( $resource, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
55 - if ( ! \wp_http_validate_url( $url ) ) {
56 - $response = new WP_Error( 'invalid_webfinger_url', null, $url );
57 - \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
58 - return $response;
63 + foreach ( $data['links'] as $link ) {
64 + if (
65 + 'self' === $link['rel'] &&
66 + isset( $link['type'] ) &&
67 + (
68 + 'application/activity+json' === $link['type'] ||
69 + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
70 + )
71 + ) {
72 + return $link['href'];
73 + }
59 74 }
60 75
61 - // try to access author URL
62 - $response = \wp_remote_get(
63 - $url,
76 + return new \WP_Error(
77 + 'webfinger_url_no_activitypub',
78 + __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
64 79 array(
65 - 'headers' => array( 'Accept' => 'application/jrd+json' ),
66 - 'redirection' => 2,
67 - 'timeout' => 2,
80 + 'status' => 400,
81 + 'data' => $data,
68 82 )
69 83 );
84 + }
70 85
71 - if ( \is_wp_error( $response ) ) {
72 - $link = new WP_Error( 'webfinger_url_not_accessible', null, $url );
73 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
74 - return $link;
86 + /**
87 + * Transform a URI to an acct <identifier>@<host>.
88 + *
89 + * @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery
90 + *
91 + * @param string $uri The URI (acct:, mailto:, http:, https:).
92 + *
93 + * @return string|\WP_Error Error or acct URI.
94 + */
95 + public static function uri_to_acct( $uri ) {
96 + $data = self::get_data( $uri );
97 +
98 + if ( is_wp_error( $data ) ) {
99 + return $data;
75 100 }
76 101
77 - $body = \wp_remote_retrieve_body( $response );
78 - $body = \json_decode( $body, true );
79 -
80 - if ( empty( $body['links'] ) ) {
81 - $link = new WP_Error( 'webfinger_url_invalid_response', null, $url );
82 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
83 - return $link;
102 + // Check if subject is an acct URI.
103 + if (
104 + isset( $data['subject'] ) &&
105 + \str_starts_with( $data['subject'], 'acct:' )
106 + ) {
107 + return $data['subject'];
84 108 }
85 109
86 - foreach ( $body['links'] as $link ) {
87 - if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
88 - \set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
89 - return $link['href'];
110 + // Search for an acct URI in the aliases.
111 + if ( isset( $data['aliases'] ) ) {
112 + foreach ( $data['aliases'] as $alias ) {
113 + if ( \str_starts_with( $alias, 'acct:' ) ) {
114 + return $alias;
115 + }
90 116 }
91 117 }
92 118
93 - $link = new WP_Error( 'webfinger_url_no_activitypub', null, $body );
94 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
95 - return $link;
119 + return new \WP_Error(
120 + 'webfinger_url_no_acct',
121 + __( 'No acct URI found.', 'activitypub' ),
122 + array(
123 + 'status' => 400,
124 + 'data' => $data,
125 + )
126 + );
96 127 }
97 128
98 129 /**
99 130 * Convert a URI string to an identifier and its host.
@@ -98,23 +129,33 @@
98 129 /**
99 130 * Convert a URI string to an identifier and its host.
100 131 * Automatically adds acct: if it's missing.
101 132 *
102 - * @param string $url The URI (acct:, mailto:, http:, https:)
133 + * @param string $url The URI (acct:, mailto:, http:, https:).
103 134 *
104 - * @return WP_Error|array Error reaction or array with
105 - * identifier and host as values
135 + * @return \WP_Error|array Error reaction or array with identifier and host as values.
106 136 */
107 137 public static function get_identifier_and_host( $url ) {
108 - // remove leading @
138 + if ( ! $url ) {
139 + return new \WP_Error(
140 + 'webfinger_invalid_identifier',
141 + __( 'Invalid Identifier', 'activitypub' ),
142 + array(
143 + 'status' => 400,
144 + 'data' => $url,
145 + )
146 + );
147 + }
148 +
149 + // Remove leading @.
109 150 $url = ltrim( $url, '@' );
110 151
111 152 if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
112 153 $identifier = 'acct:' . $url;
113 - $scheme = 'acct';
154 + $scheme = 'acct';
114 155 } else {
115 156 $identifier = $url;
116 - $scheme = $match[1];
157 + $scheme = $match[1];
117 158 }
118 159
119 160 $host = null;
120 161
@@ -131,9 +172,16 @@
131 172 break;
132 173 }
133 174
134 175 if ( empty( $host ) ) {
135 - return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
176 + return new \WP_Error(
177 + 'webfinger_invalid_identifier',
178 + __( 'Invalid Identifier', 'activitypub' ),
179 + array(
180 + 'status' => 400,
181 + 'data' => $url,
182 + )
183 + );
136 184 }
137 185
138 186 return array( $identifier, $host );
139 187 }
@@ -138,59 +186,85 @@
138 186 return array( $identifier, $host );
139 187 }
140 188
141 189 /**
142 - * Get the WebFinger data for a given URI
190 + * Get the WebFinger data for a given URI.
143 191 *
144 - * @param string $identifier The Identifier: <identifier>@<host>
145 - * @param string $host The Host: <identifier>@<host>
192 + * @param string $uri The Identifier: <identifier>@<host> or URI.
146 193 *
147 - * @return WP_Error|array Error reaction or array with
148 - * identifier and host as values
194 + * @return \WP_Error|array Error reaction or array with identifier and host as values.
149 195 */
150 - public static function get_data( $identifier, $host ) {
151 - $webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
196 + public static function get_data( $uri ) {
197 + $identifier_and_host = self::get_identifier_and_host( $uri );
152 198
153 - $response = wp_safe_remote_get(
199 + if ( is_wp_error( $identifier_and_host ) ) {
200 + return $identifier_and_host;
201 + }
202 +
203 + $transient_key = self::generate_cache_key( $uri );
204 +
205 + list( $identifier, $host ) = $identifier_and_host;
206 +
207 + $data = \get_transient( $transient_key );
208 + if ( $data ) {
209 + return $data;
210 + }
211 +
212 + $webfinger_url = sprintf(
213 + 'https://%s/.well-known/webfinger?resource=%s',
214 + $host,
215 + \rawurlencode( $identifier )
216 + );
217 +
218 + $response = \wp_safe_remote_get(
154 219 $webfinger_url,
155 220 array(
156 221 'headers' => array( 'Accept' => 'application/jrd+json' ),
157 - 'redirection' => 0,
158 - 'timeout' => 2,
159 222 )
160 223 );
161 224
162 - if ( is_wp_error( $response ) ) {
163 - return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url );
225 + if ( \is_wp_error( $response ) || \wp_remote_retrieve_response_code( $response ) >= 400 ) {
226 + return new \WP_Error(
227 + 'webfinger_url_not_accessible',
228 + __( 'The WebFinger Resource is not accessible.', 'activitypub' ),
229 + array(
230 + 'status' => 400,
231 + 'data' => $webfinger_url,
232 + )
233 + );
164 234 }
165 235
166 - $body = wp_remote_retrieve_body( $response );
236 + $body = \wp_remote_retrieve_body( $response );
237 + $data = \json_decode( $body, true );
167 238
168 - return json_decode( $body, true );
239 + \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
240 +
241 + return $data;
169 242 }
170 243
171 244 /**
172 - * Undocumented function
245 + * Get the Remote-Follow endpoint for a given URI.
173 246 *
174 - * @return void
247 + * @param string $uri The WebFinger Resource URI.
248 + *
249 + * @return string|\WP_Error Error or the Remote-Follow endpoint URI.
175 250 */
176 251 public static function get_remote_follow_endpoint( $uri ) {
177 - $identifier_and_host = self::get_identifier_and_host( $uri );
252 + $data = self::get_data( $uri );
178 253
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 254 if ( is_wp_error( $data ) ) {
188 255 return $data;
189 256 }
190 257
191 258 if ( empty( $data['links'] ) ) {
192 - return new WP_Error( 'webfinger_url_invalid_response', null, $data );
259 + return new \WP_Error(
260 + 'webfinger_missing_links',
261 + __( 'No valid Link elements found.', 'activitypub' ),
262 + array(
263 + 'status' => 400,
264 + 'data' => $data,
265 + )
266 + );
193 267 }
194 268
195 269 foreach ( $data['links'] as $link ) {
196 270 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
@@ -197,7 +271,56 @@
197 271 return $link['template'];
198 272 }
199 273 }
200 274
201 - return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', $data, array( 'status' => 417 ) );
275 + return new \WP_Error(
276 + 'webfinger_missing_remote_follow_endpoint',
277 + __( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
278 + array(
279 + 'status' => 400,
280 + 'data' => $data,
281 + )
282 + );
283 + }
284 +
285 + /**
286 + * Generate a cache key for a given URI.
287 + *
288 + * @param string $uri A WebFinger Resource URI.
289 + *
290 + * @return string The cache key.
291 + */
292 + public static function generate_cache_key( $uri ) {
293 + $uri = ltrim( $uri, '@' );
294 +
295 + if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
296 + $uri = 'acct:' . $uri;
297 + }
298 +
299 + return 'webfinger_' . md5( $uri );
300 + }
301 +
302 + /**
303 + * Infer a shortname from the Actor ID or URL. Used only for fallbacks,
304 + * we will try to use what's supplied.
305 + *
306 + * @param Actor|string $actor_or_uri The Actor or URI.
307 + *
308 + * @return string Hopefully the name of the Follower.
309 + */
310 + public static function guess( $actor_or_uri ) {
311 + if ( ! $actor_or_uri instanceof Actor ) {
312 + $actor = Remote_Actors::fetch_by_uri( $actor_or_uri );
313 + if ( \is_wp_error( $actor ) ) {
314 + return extract_name_from_uri( $actor_or_uri ) . '@' . \wp_parse_url( $actor_or_uri, PHP_URL_HOST );
315 + }
316 +
317 + $actor_or_uri = $actor;
318 + }
319 +
320 + if ( $actor_or_uri->get_preferred_username() ) {
321 + return $actor_or_uri->get_preferred_username() . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
322 + }
323 +
324 + return extract_name_from_uri( $actor_or_uri->get_id() ) . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
202 325 }
203 326 }