PluginProbe
ActivityPub / 4.0.2
ActivityPub v4.0.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
activitypub / includes / class-webfinger.php

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

298 lines 6.2 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\Users;
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 = Users::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 (
66 'application/activity+json' === $link['type'] ||
67 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
68 )
69 ) {
70 return $link['href'];
71 }
72 }
73
74 return new WP_Error(
75 'webfinger_url_no_activitypub',
76 __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
77 array(
78 'status' => 400,
79 'data' => $data,
80 )
81 );
82 }
83
84 /**
85 * Transform a URI to an acct <identifier>@<host>.
86 *
87 * @param string $uri The URI (acct:, mailto:, http:, https:).
88 *
89 * @return string|WP_Error Error or acct URI.
90 */
91 public static function uri_to_acct( $uri ) {
92 $data = self::get_data( $uri );
93
94 if ( is_wp_error( $data ) ) {
95 return $data;
96 }
97
98 // Check if subject is an acct URI.
99 if (
100 isset( $data['subject'] ) &&
101 \str_starts_with( $data['subject'], 'acct:' )
102 ) {
103 return $data['subject'];
104 }
105
106 // Search for an acct URI in the aliases.
107 if ( isset( $data['aliases'] ) ) {
108 foreach ( $data['aliases'] as $alias ) {
109 if ( \str_starts_with( $alias, 'acct:' ) ) {
110 return $alias;
111 }
112 }
113 }
114
115 return new WP_Error(
116 'webfinger_url_no_acct',
117 __( 'No acct URI found.', 'activitypub' ),
118 array(
119 'status' => 400,
120 'data' => $data,
121 )
122 );
123 }
124
125 /**
126 * Convert a URI string to an identifier and its host.
127 * Automatically adds acct: if it's missing.
128 *
129 * @param string $url The URI (acct:, mailto:, http:, https:).
130 *
131 * @return WP_Error|array Error reaction or array with identifier and host as values.
132 */
133 public static function get_identifier_and_host( $url ) {
134 if ( ! $url ) {
135 return new WP_Error(
136 'webfinger_invalid_identifier',
137 __( 'Invalid Identifier', 'activitypub' ),
138 array(
139 'status' => 400,
140 'data' => $url,
141 )
142 );
143 }
144
145 // Remove leading @.
146 $url = ltrim( $url, '@' );
147
148 if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
149 $identifier = 'acct:' . $url;
150 $scheme = 'acct';
151 } else {
152 $identifier = $url;
153 $scheme = $match[1];
154 }
155
156 $host = null;
157
158 switch ( $scheme ) {
159 case 'acct':
160 case 'mailto':
161 case 'xmpp':
162 if ( strpos( $identifier, '@' ) !== false ) {
163 $host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
164 }
165 break;
166 default:
167 $host = wp_parse_url( $identifier, PHP_URL_HOST );
168 break;
169 }
170
171 if ( empty( $host ) ) {
172 return new WP_Error(
173 'webfinger_invalid_identifier',
174 __( 'Invalid Identifier', 'activitypub' ),
175 array(
176 'status' => 400,
177 'data' => $url,
178 )
179 );
180 }
181
182 return array( $identifier, $host );
183 }
184
185 /**
186 * Get the WebFinger data for a given URI.
187 *
188 * @param string $uri The Identifier: <identifier>@<host> or URI.
189 *
190 * @return WP_Error|array Error reaction or array with identifier and host as values.
191 */
192 public static function get_data( $uri ) {
193 $identifier_and_host = self::get_identifier_and_host( $uri );
194
195 if ( is_wp_error( $identifier_and_host ) ) {
196 return $identifier_and_host;
197 }
198
199 $transient_key = self::generate_cache_key( $uri );
200
201 list( $identifier, $host ) = $identifier_and_host;
202
203 $data = \get_transient( $transient_key );
204 if ( $data ) {
205 return $data;
206 }
207
208 $webfinger_url = sprintf(
209 'https://%s/.well-known/webfinger?resource=%s',
210 $host,
211 rawurlencode( $identifier )
212 );
213
214 $response = wp_safe_remote_get(
215 $webfinger_url,
216 array(
217 'headers' => array( 'Accept' => 'application/jrd+json' ),
218 )
219 );
220
221 if ( is_wp_error( $response ) ) {
222 return new WP_Error(
223 'webfinger_url_not_accessible',
224 __( 'The WebFinger Resource is not accessible.', 'activitypub' ),
225 array(
226 'status' => 400,
227 'data' => $webfinger_url,
228 )
229 );
230 }
231
232 $body = wp_remote_retrieve_body( $response );
233 $data = json_decode( $body, true );
234
235 \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
236
237 return $data;
238 }
239
240 /**
241 * Get the Remote-Follow endpoint for a given URI.
242 *
243 * @param string $uri The WebFinger Resource URI.
244 *
245 * @return string|WP_Error Error or the Remote-Follow endpoint URI.
246 */
247 public static function get_remote_follow_endpoint( $uri ) {
248 $data = self::get_data( $uri );
249
250 if ( is_wp_error( $data ) ) {
251 return $data;
252 }
253
254 if ( empty( $data['links'] ) ) {
255 return new WP_Error(
256 'webfinger_missing_links',
257 __( 'No valid Link elements found.', 'activitypub' ),
258 array(
259 'status' => 400,
260 'data' => $data,
261 )
262 );
263 }
264
265 foreach ( $data['links'] as $link ) {
266 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
267 return $link['template'];
268 }
269 }
270
271 return new WP_Error(
272 'webfinger_missing_remote_follow_endpoint',
273 __( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
274 array(
275 'status' => 400,
276 'data' => $data,
277 )
278 );
279 }
280
281 /**
282 * Generate a cache key for a given URI.
283 *
284 * @param string $uri A WebFinger Resource URI.
285 *
286 * @return string The cache key.
287 */
288 public static function generate_cache_key( $uri ) {
289 $uri = ltrim( $uri, '@' );
290
291 if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
292 $uri = 'acct:' . $uri;
293 }
294
295 return 'webfinger_' . md5( $uri );
296 }
297 }
298