PluginProbe
ActivityPub / 5.7.0
ActivityPub v5.7.0
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 +78 -180 9.2.05.7.0 View file →
@@ -6,11 +6,10 @@
6 6 */
7 7
8 8 namespace Activitypub;
9 9
10 -use Activitypub\Activity\Actor;
10 +use WP_Error;
11 11 use Activitypub\Collection\Actors;
12 -use Activitypub\Collection\Remote_Actors;
13 12
14 13 /**
15 14 * ActivityPub WebFinger Class.
16 15 *
@@ -19,32 +18,8 @@
19 18 * @see https://webfinger.net/
20 19 */
21 20 class Webfinger {
22 21 /**
23 - * Check whether a value looks like an `acct` identifier.
24 - *
25 - * Accepts any of:
26 - *
27 - * - `user@host` — bare WebFinger handle.
28 - * - `@user@host` — Mastodon display form with a leading `@`.
29 - * - `acct:user@host` — full RFC 7565 URI form.
30 - *
31 - * The host/local-part pattern follows `ACTIVITYPUB_USERNAME_REGEXP`.
32 - *
33 - * @since 8.3.0
34 - *
35 - * @param mixed $value The candidate value.
36 - * @return bool True if the value matches the acct identifier pattern.
37 - */
38 - public static function is_acct( $value ) {
39 - if ( ! \is_string( $value ) || '' === $value ) {
40 - return false;
41 - }
42 -
43 - return (bool) \preg_match( '/^(?:acct:)?@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $value );
44 - }
45 -
46 - /**
47 22 * Returns a users WebFinger "resource".
48 23 *
49 24 * @param int $user_id The WordPress user id.
50 25 *
@@ -51,9 +26,9 @@
51 26 * @return string The user-resource.
52 27 */
53 28 public static function get_user_resource( $user_id ) {
54 29 $user = Actors::get_by_id( $user_id );
55 - if ( ! $user || \is_wp_error( $user ) ) {
30 + if ( ! $user || is_wp_error( $user ) ) {
56 31 return '';
57 32 }
58 33
59 34 return $user->get_webfinger();
@@ -63,9 +38,9 @@
63 38 * Resolve a WebFinger resource.
64 39 *
65 40 * @param string $uri The WebFinger Resource.
66 41 *
67 - * @return string|\WP_Error The URL or WP_Error.
42 + * @return string|WP_Error The URL or WP_Error.
68 43 */
69 44 public static function resolve( $uri ) {
70 45 $data = self::get_data( $uri );
71 46
@@ -72,12 +47,12 @@
72 47 if ( \is_wp_error( $data ) ) {
73 48 return $data;
74 49 }
75 50
76 - if ( ! \is_array( $data ) || empty( $data['links'] ) ) {
77 - return new \WP_Error(
51 + if ( ! is_array( $data ) || empty( $data['links'] ) ) {
52 + return new WP_Error(
78 53 'webfinger_missing_links',
79 - \__( 'No valid Link elements found.', 'activitypub' ),
54 + __( 'No valid Link elements found.', 'activitypub' ),
80 55 array(
81 56 'status' => 400,
82 57 'data' => $data,
83 58 )
@@ -96,11 +71,11 @@
96 71 return $link['href'];
97 72 }
98 73 }
99 74
100 - return new \WP_Error(
75 + return new WP_Error(
101 76 'webfinger_url_no_activitypub',
102 - \__( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
77 + __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
103 78 array(
104 79 'status' => 400,
105 80 'data' => $data,
106 81 )
@@ -113,14 +88,14 @@
113 88 * @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery
114 89 *
115 90 * @param string $uri The URI (acct:, mailto:, http:, https:).
116 91 *
117 - * @return string|\WP_Error Error or acct URI.
92 + * @return string|WP_Error Error or acct URI.
118 93 */
119 94 public static function uri_to_acct( $uri ) {
120 95 $data = self::get_data( $uri );
121 96
122 - if ( \is_wp_error( $data ) ) {
97 + if ( is_wp_error( $data ) ) {
123 98 return $data;
124 99 }
125 100
126 101 // Check if subject is an acct URI.
@@ -139,11 +114,11 @@
139 114 }
140 115 }
141 116 }
142 117
143 - return new \WP_Error(
118 + return new WP_Error(
144 119 'webfinger_url_no_acct',
145 - \__( 'No acct URI found.', 'activitypub' ),
120 + __( 'No acct URI found.', 'activitypub' ),
146 121 array(
147 122 'status' => 400,
148 123 'data' => $data,
149 124 )
@@ -155,15 +130,15 @@
155 130 * Automatically adds acct: if it's missing.
156 131 *
157 132 * @param string $url The URI (acct:, mailto:, http:, https:).
158 133 *
159 - * @return \WP_Error|array Error reaction or array with identifier and host as values.
134 + * @return WP_Error|array Error reaction or array with identifier and host as values.
160 135 */
161 136 public static function get_identifier_and_host( $url ) {
162 137 if ( ! $url ) {
163 - return new \WP_Error(
138 + return new WP_Error(
164 139 'webfinger_invalid_identifier',
165 - \__( 'Invalid Identifier', 'activitypub' ),
140 + __( 'Invalid Identifier', 'activitypub' ),
166 141 array(
167 142 'status' => 400,
168 143 'data' => $url,
169 144 )
@@ -170,11 +145,11 @@
170 145 );
171 146 }
172 147
173 148 // Remove leading @.
174 - $url = \ltrim( $url, '@' );
149 + $url = ltrim( $url, '@' );
175 150
176 - if ( ! \preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
151 + if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
177 152 $identifier = 'acct:' . $url;
178 153 $scheme = 'acct';
179 154 } else {
180 155 $identifier = $url;
@@ -186,21 +161,21 @@
186 161 switch ( $scheme ) {
187 162 case 'acct':
188 163 case 'mailto':
189 164 case 'xmpp':
190 - if ( \strpos( $identifier, '@' ) !== false ) {
191 - $host = \substr( $identifier, \strpos( $identifier, '@' ) + 1 );
165 + if ( strpos( $identifier, '@' ) !== false ) {
166 + $host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
192 167 }
193 168 break;
194 169 default:
195 - $host = \wp_parse_url( $identifier, PHP_URL_HOST );
170 + $host = wp_parse_url( $identifier, PHP_URL_HOST );
196 171 break;
197 172 }
198 173
199 174 if ( empty( $host ) ) {
200 - return new \WP_Error(
175 + return new WP_Error(
201 176 'webfinger_invalid_identifier',
202 - \__( 'Invalid Identifier', 'activitypub' ),
177 + __( 'Invalid Identifier', 'activitypub' ),
203 178 array(
204 179 'status' => 400,
205 180 'data' => $url,
206 181 )
@@ -214,39 +189,56 @@
214 189 * Get the WebFinger data for a given URI.
215 190 *
216 191 * @param string $uri The Identifier: <identifier>@<host> or URI.
217 192 *
218 - * @return \WP_Error|array Error reaction or array with identifier and host as values.
193 + * @return WP_Error|array Error reaction or array with identifier and host as values.
219 194 */
220 195 public static function get_data( $uri ) {
221 196 $identifier_and_host = self::get_identifier_and_host( $uri );
222 197
223 - if ( \is_wp_error( $identifier_and_host ) ) {
198 + if ( is_wp_error( $identifier_and_host ) ) {
224 199 return $identifier_and_host;
225 200 }
226 201
202 + $transient_key = self::generate_cache_key( $uri );
203 +
227 204 list( $identifier, $host ) = $identifier_and_host;
228 205
229 - $webfinger_url = \sprintf(
206 + $data = \get_transient( $transient_key );
207 + if ( $data ) {
208 + return $data;
209 + }
210 +
211 + $webfinger_url = sprintf(
230 212 'https://%s/.well-known/webfinger?resource=%s',
231 213 $host,
232 - \rawurlencode( $identifier )
214 + rawurlencode( $identifier )
233 215 );
234 216
235 - // Use Http::get() which handles all caching (success and errors).
236 - $response = Http::get(
217 + $response = wp_safe_remote_get(
237 218 $webfinger_url,
238 - array( 'headers' => array( 'Accept' => 'application/jrd+json' ) ),
239 - WEEK_IN_SECONDS
219 + array(
220 + 'headers' => array( 'Accept' => 'application/jrd+json' ),
221 + )
240 222 );
241 223
242 - if ( \is_wp_error( $response ) ) {
243 - return $response;
224 + if ( is_wp_error( $response ) ) {
225 + return new WP_Error(
226 + 'webfinger_url_not_accessible',
227 + __( 'The WebFinger Resource is not accessible.', 'activitypub' ),
228 + array(
229 + 'status' => 400,
230 + 'data' => $webfinger_url,
231 + )
232 + );
244 233 }
245 234
246 - $body = \wp_remote_retrieve_body( $response );
235 + $body = wp_remote_retrieve_body( $response );
236 + $data = json_decode( $body, true );
247 237
248 - return \json_decode( $body, true );
238 + \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
239 +
240 + return $data;
249 241 }
250 242
251 243 /**
252 244 * Get the Remote-Follow endpoint for a given URI.
@@ -252,80 +244,21 @@
252 244 * Get the Remote-Follow endpoint for a given URI.
253 245 *
254 246 * @param string $uri The WebFinger Resource URI.
255 247 *
256 - * @return string|\WP_Error Error or the Remote-Follow endpoint URI.
248 + * @return string|WP_Error Error or the Remote-Follow endpoint URI.
257 249 */
258 250 public static function get_remote_follow_endpoint( $uri ) {
259 - return self::get_intent_endpoint( $uri, 'follow', true );
260 - }
261 -
262 - /**
263 - * Generate a cache key for a given URI.
264 - *
265 - * @param string $uri A WebFinger Resource URI.
266 - *
267 - * @return string The cache key.
268 - */
269 - public static function generate_cache_key( $uri ) {
270 - $uri = \ltrim( $uri, '@' );
271 -
272 - if ( \filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
273 - $uri = 'acct:' . $uri;
274 - }
275 -
276 - return 'webfinger_' . \md5( $uri );
277 - }
278 -
279 - /**
280 - * Infer a shortname from the Actor ID or URL. Used only for fallbacks,
281 - * we will try to use what's supplied.
282 - *
283 - * @param Actor|string $actor_or_uri The Actor or URI.
284 - *
285 - * @return string Hopefully the name of the Follower.
286 - */
287 - public static function guess( $actor_or_uri ) {
288 - if ( ! $actor_or_uri instanceof Actor ) {
289 - $actor = Remote_Actors::fetch_by_uri( $actor_or_uri );
290 - if ( \is_wp_error( $actor ) ) {
291 - return extract_name_from_uri( $actor_or_uri ) . '@' . \wp_parse_url( $actor_or_uri, PHP_URL_HOST );
292 - }
293 -
294 - $actor_or_uri = $actor;
295 - }
296 -
297 - if ( $actor_or_uri->get_preferred_username() ) {
298 - return $actor_or_uri->get_preferred_username() . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
299 - }
300 -
301 - return extract_name_from_uri( $actor_or_uri->get_id() ) . '@' . \wp_parse_url( $actor_or_uri->get_id(), PHP_URL_HOST );
302 - }
303 -
304 - /**
305 - * Get the Intent endpoint for a given URI and intent.
306 - *
307 - * @since 8.0.0
308 - *
309 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
310 - *
311 - * @param string $uri The WebFinger Resource URI.
312 - * @param string $intent The intent to look for.
313 - * @param bool $fallback Whether to fallback to the Remote-Follow endpoint.
314 - *
315 - * @return string|\WP_Error Error or the Intent endpoint URI (may contain `{uri}` placeholder).
316 - */
317 - public static function get_intent_endpoint( $uri, $intent, $fallback = false ) {
318 251 $data = self::get_data( $uri );
319 252
320 - if ( \is_wp_error( $data ) ) {
253 + if ( is_wp_error( $data ) ) {
321 254 return $data;
322 255 }
323 256
324 257 if ( empty( $data['links'] ) ) {
325 - return new \WP_Error(
258 + return new WP_Error(
326 259 'webfinger_missing_links',
327 - \__( 'No valid Link elements found.', 'activitypub' ),
260 + __( 'No valid Link elements found.', 'activitypub' ),
328 261 array(
329 262 'status' => 400,
330 263 'data' => $data,
331 264 )
@@ -331,72 +264,37 @@
331 264 )
332 265 );
333 266 }
334 267
335 - // Normalize the links with $rel as key.
336 - $links = array();
337 -
338 268 foreach ( $data['links'] as $link ) {
339 - if ( isset( $link['rel'] ) && isset( $link['template'] ) ) {
340 - $links[ \strtolower( $link['rel'] ) ] = $link['template'];
269 + if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
270 + return $link['template'];
341 271 }
342 272 }
343 273
344 - $intent = \sanitize_text_field( $intent );
345 - $intent = \strtolower( $intent );
274 + return new WP_Error(
275 + 'webfinger_missing_remote_follow_endpoint',
276 + __( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
277 + array(
278 + 'status' => 400,
279 + 'data' => $data,
280 + )
281 + );
282 + }
346 283
347 - if ( ! \filter_var( $intent, FILTER_VALIDATE_URL ) ) {
348 - $intent = 'https://w3id.org/fep/3b86/' . $intent;
349 - }
284 + /**
285 + * Generate a cache key for a given URI.
286 + *
287 + * @param string $uri A WebFinger Resource URI.
288 + *
289 + * @return string The cache key.
290 + */
291 + public static function generate_cache_key( $uri ) {
292 + $uri = ltrim( $uri, '@' );
350 293
351 - if ( isset( $links[ $intent ] ) ) {
352 - return $links[ $intent ];
294 + if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
295 + $uri = 'acct:' . $uri;
353 296 }
354 297
355 - if ( ! $fallback ) {
356 - return new \WP_Error(
357 - 'webfinger_missing_intent_endpoint',
358 - \__( 'No valid Intent endpoint found.', 'activitypub' ),
359 - array(
360 - 'status' => 400,
361 - 'data' => $data,
362 - )
363 - );
364 - }
365 -
366 - /*
367 - * OStatus subscribe URL (deprecated but still widely supported)
368 - *
369 - * @see https://ostatus.github.io/spec/OStatus%201.0%20Draft%202.html#anchor10
370 - */
371 - if ( isset( $links['http://ostatus.org/schema/1.0/subscribe'] ) ) {
372 - return $links['http://ostatus.org/schema/1.0/subscribe'];
373 - }
374 -
375 - /*
376 - * FEP-3b86 Object Intent — the generic "open this object on my home
377 - * server" link, equivalent to pasting the URL into the home server's
378 - * search box. Useful when no verb-specific intent is advertised.
379 - *
380 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md#5-1-object-intent
381 - */
382 - if ( isset( $links['https://w3id.org/fep/3b86/object'] ) ) {
383 - return $links['https://w3id.org/fep/3b86/object'];
384 - }
385 -
386 - // Last-resort: construct a Mastodon-compatible authorize_interaction URL.
387 - $identifier_and_host = self::get_identifier_and_host( $uri );
388 -
389 - if ( \is_wp_error( $identifier_and_host ) ) {
390 - return new \WP_Error(
391 - 'webfinger_missing_intent_endpoint',
392 - \__( 'No valid Intent endpoint found.', 'activitypub' ),
393 - array(
394 - 'status' => 400,
395 - 'data' => $data,
396 - )
397 - );
398 - }
399 -
400 - return 'https://' . $identifier_and_host[1] . '/authorize_interaction?uri={uri}';
298 + return 'webfinger_' . md5( $uri );
401 299 }
402 300 }