PluginProbe
ActivityPub / 3.2.2
ActivityPub v3.2.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 3.2.2, at includes/class-webfinger.php

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