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
activitypub / includes / class-webfinger.php

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

301 lines 6.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 WP_Error;
11 use Activitypub\Collection\Actors;
12
13 /**
14 * ActivityPub WebFinger Class.
15 *
16 * @author Matthias Pfefferle
17 *
18 * @see https://webfinger.net/
19 */
20 class Webfinger {
21 /**
22 * Returns a users WebFinger "resource".
23 *
24 * @param int $user_id The WordPress user id.
25 *
26 * @return string The user-resource.
27 */
28 public static function get_user_resource( $user_id ) {
29 $user = Actors::get_by_id( $user_id );
30 if ( ! $user || is_wp_error( $user ) ) {
31 return '';
32 }
33
34 return $user->get_webfinger();
35 }
36
37 /**
38 * Resolve a WebFinger resource.
39 *
40 * @param string $uri The WebFinger Resource.
41 *
42 * @return string|WP_Error The URL or WP_Error.
43 */
44 public static function resolve( $uri ) {
45 $data = self::get_data( $uri );
46
47 if ( \is_wp_error( $data ) ) {
48 return $data;
49 }
50
51 if ( ! is_array( $data ) || empty( $data['links'] ) ) {
52 return new WP_Error(
53 'webfinger_missing_links',
54 __( 'No valid Link elements found.', 'activitypub' ),
55 array(
56 'status' => 400,
57 'data' => $data,
58 )
59 );
60 }
61
62 foreach ( $data['links'] as $link ) {
63 if (
64 'self' === $link['rel'] &&
65 isset( $link['type'] ) &&
66 (
67 'application/activity+json' === $link['type'] ||
68 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
69 )
70 ) {
71 return $link['href'];
72 }
73 }
74
75 return new WP_Error(
76 'webfinger_url_no_activitypub',
77 __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
78 array(
79 'status' => 400,
80 'data' => $data,
81 )
82 );
83 }
84
85 /**
86 * Transform a URI to an acct <identifier>@<host>.
87 *
88 * @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery
89 *
90 * @param string $uri The URI (acct:, mailto:, http:, https:).
91 *
92 * @return string|WP_Error Error or acct URI.
93 */
94 public static function uri_to_acct( $uri ) {
95 $data = self::get_data( $uri );
96
97 if ( is_wp_error( $data ) ) {
98 return $data;
99 }
100
101 // Check if subject is an acct URI.
102 if (
103 isset( $data['subject'] ) &&
104 \str_starts_with( $data['subject'], 'acct:' )
105 ) {
106 return $data['subject'];
107 }
108
109 // Search for an acct URI in the aliases.
110 if ( isset( $data['aliases'] ) ) {
111 foreach ( $data['aliases'] as $alias ) {
112 if ( \str_starts_with( $alias, 'acct:' ) ) {
113 return $alias;
114 }
115 }
116 }
117
118 return new WP_Error(
119 'webfinger_url_no_acct',
120 __( 'No acct URI found.', 'activitypub' ),
121 array(
122 'status' => 400,
123 'data' => $data,
124 )
125 );
126 }
127
128 /**
129 * Convert a URI string to an identifier and its host.
130 * Automatically adds acct: if it's missing.
131 *
132 * @param string $url The URI (acct:, mailto:, http:, https:).
133 *
134 * @return WP_Error|array Error reaction or array with identifier and host as values.
135 */
136 public static function get_identifier_and_host( $url ) {
137 if ( ! $url ) {
138 return new WP_Error(
139 'webfinger_invalid_identifier',
140 __( 'Invalid Identifier', 'activitypub' ),
141 array(
142 'status' => 400,
143 'data' => $url,
144 )
145 );
146 }
147
148 // Remove leading @.
149 $url = ltrim( $url, '@' );
150
151 if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
152 $identifier = 'acct:' . $url;
153 $scheme = 'acct';
154 } else {
155 $identifier = $url;
156 $scheme = $match[1];
157 }
158
159 $host = null;
160
161 switch ( $scheme ) {
162 case 'acct':
163 case 'mailto':
164 case 'xmpp':
165 if ( strpos( $identifier, '@' ) !== false ) {
166 $host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
167 }
168 break;
169 default:
170 $host = wp_parse_url( $identifier, PHP_URL_HOST );
171 break;
172 }
173
174 if ( empty( $host ) ) {
175 return new WP_Error(
176 'webfinger_invalid_identifier',
177 __( 'Invalid Identifier', 'activitypub' ),
178 array(
179 'status' => 400,
180 'data' => $url,
181 )
182 );
183 }
184
185 return array( $identifier, $host );
186 }
187
188 /**
189 * Get the WebFinger data for a given URI.
190 *
191 * @param string $uri The Identifier: <identifier>@<host> or URI.
192 *
193 * @return WP_Error|array Error reaction or array with identifier and host as values.
194 */
195 public static function get_data( $uri ) {
196 $identifier_and_host = self::get_identifier_and_host( $uri );
197
198 if ( is_wp_error( $identifier_and_host ) ) {
199 return $identifier_and_host;
200 }
201
202 $transient_key = self::generate_cache_key( $uri );
203
204 list( $identifier, $host ) = $identifier_and_host;
205
206 $data = \get_transient( $transient_key );
207 if ( $data ) {
208 return $data;
209 }
210
211 $webfinger_url = sprintf(
212 'https://%s/.well-known/webfinger?resource=%s',
213 $host,
214 rawurlencode( $identifier )
215 );
216
217 $response = wp_safe_remote_get(
218 $webfinger_url,
219 array(
220 'headers' => array( 'Accept' => 'application/jrd+json' ),
221 )
222 );
223
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 );
233 }
234
235 $body = wp_remote_retrieve_body( $response );
236 $data = json_decode( $body, true );
237
238 \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
239
240 return $data;
241 }
242
243 /**
244 * Get the Remote-Follow endpoint for a given URI.
245 *
246 * @param string $uri The WebFinger Resource URI.
247 *
248 * @return string|WP_Error Error or the Remote-Follow endpoint URI.
249 */
250 public static function get_remote_follow_endpoint( $uri ) {
251 $data = self::get_data( $uri );
252
253 if ( is_wp_error( $data ) ) {
254 return $data;
255 }
256
257 if ( empty( $data['links'] ) ) {
258 return new WP_Error(
259 'webfinger_missing_links',
260 __( 'No valid Link elements found.', 'activitypub' ),
261 array(
262 'status' => 400,
263 'data' => $data,
264 )
265 );
266 }
267
268 foreach ( $data['links'] as $link ) {
269 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
270 return $link['template'];
271 }
272 }
273
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 }
283
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, '@' );
293
294 if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
295 $uri = 'acct:' . $uri;
296 }
297
298 return 'webfinger_' . md5( $uri );
299 }
300 }
301