PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.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
activitypub / includes / class-webfinger.php

class-webfinger.php in ActivityPub 7.8.0, at includes/class-webfinger.php

327 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WebFinger class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Actor;
11 use Activitypub\Collection\Actors;
12 use Activitypub\Collection\Remote_Actors;
13
14 /**
15 * ActivityPub WebFinger Class.
16 *
17 * @author Matthias Pfefferle
18 *
19 * @see https://webfinger.net/
20 */
21 class Webfinger {
22 /**
23 * Returns a users WebFinger "resource".
24 *
25 * @param int $user_id The WordPress user id.
26 *
27 * @return string The user-resource.
28 */
29 public static function get_user_resource( $user_id ) {
30 $user = Actors::get_by_id( $user_id );
31 if ( ! $user || is_wp_error( $user ) ) {
32 return '';
33 }
34
35 return $user->get_webfinger();
36 }
37
38 /**
39 * Resolve a WebFinger resource.
40 *
41 * @param string $uri The WebFinger Resource.
42 *
43 * @return string|\WP_Error The URL or WP_Error.
44 */
45 public static function resolve( $uri ) {
46 $data = self::get_data( $uri );
47
48 if ( \is_wp_error( $data ) ) {
49 return $data;
50 }
51
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 );
61 }
62
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 }
74 }
75
76 return new \WP_Error(
77 'webfinger_url_no_activitypub',
78 __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
79 array(
80 'status' => 400,
81 'data' => $data,
82 )
83 );
84 }
85
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;
100 }
101
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'];
108 }
109
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 }
116 }
117 }
118
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 );
127 }
128
129 /**
130 * Convert a URI string to an identifier and its host.
131 * Automatically adds acct: if it's missing.
132 *
133 * @param string $url The URI (acct:, mailto:, http:, https:).
134 *
135 * @return \WP_Error|array Error reaction or array with identifier and host as values.
136 */
137 public static function get_identifier_and_host( $url ) {
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 @.
150 $url = ltrim( $url, '@' );
151
152 if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
153 $identifier = 'acct:' . $url;
154 $scheme = 'acct';
155 } else {
156 $identifier = $url;
157 $scheme = $match[1];
158 }
159
160 $host = null;
161
162 switch ( $scheme ) {
163 case 'acct':
164 case 'mailto':
165 case 'xmpp':
166 if ( strpos( $identifier, '@' ) !== false ) {
167 $host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
168 }
169 break;
170 default:
171 $host = wp_parse_url( $identifier, PHP_URL_HOST );
172 break;
173 }
174
175 if ( empty( $host ) ) {
176 return new \WP_Error(
177 'webfinger_invalid_identifier',
178 __( 'Invalid Identifier', 'activitypub' ),
179 array(
180 'status' => 400,
181 'data' => $url,
182 )
183 );
184 }
185
186 return array( $identifier, $host );
187 }
188
189 /**
190 * Get the WebFinger data for a given URI.
191 *
192 * @param string $uri The Identifier: <identifier>@<host> or URI.
193 *
194 * @return \WP_Error|array Error reaction or array with identifier and host as values.
195 */
196 public static function get_data( $uri ) {
197 $identifier_and_host = self::get_identifier_and_host( $uri );
198
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(
219 $webfinger_url,
220 array(
221 'headers' => array( 'Accept' => 'application/jrd+json' ),
222 )
223 );
224
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 );
234 }
235
236 $body = \wp_remote_retrieve_body( $response );
237 $data = \json_decode( $body, true );
238
239 \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
240
241 return $data;
242 }
243
244 /**
245 * Get the Remote-Follow endpoint for a given URI.
246 *
247 * @param string $uri The WebFinger Resource URI.
248 *
249 * @return string|\WP_Error Error or the Remote-Follow endpoint URI.
250 */
251 public static function get_remote_follow_endpoint( $uri ) {
252 $data = self::get_data( $uri );
253
254 if ( is_wp_error( $data ) ) {
255 return $data;
256 }
257
258 if ( empty( $data['links'] ) ) {
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 );
267 }
268
269 foreach ( $data['links'] as $link ) {
270 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
271 return $link['template'];
272 }
273 }
274
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 );
325 }
326 }
327