PluginProbe
ActivityPub / 2.6.0
ActivityPub v2.6.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 2.6.0, at includes/class-webfinger.php

288 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( 'https://%s/.well-known/webfinger?resource=%s', $host, rawurlencode( $identifier ) );
205
206 $response = wp_safe_remote_get(
207 $webfinger_url,
208 array(
209 'headers' => array( 'Accept' => 'application/jrd+json' ),
210 )
211 );
212
213 if ( is_wp_error( $response ) ) {
214 return new WP_Error(
215 'webfinger_url_not_accessible',
216 __( 'The WebFinger Resource is not accessible.', 'activitypub' ),
217 array(
218 'status' => 400,
219 'data' => $webfinger_url,
220 )
221 );
222 }
223
224 $body = wp_remote_retrieve_body( $response );
225 $data = json_decode( $body, true );
226
227 \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
228
229 return $data;
230 }
231
232 /**
233 * Get the Remote-Follow endpoint for a given URI
234 *
235 * @return string|WP_Error Error or the Remote-Follow endpoint URI.
236 */
237 public static function get_remote_follow_endpoint( $uri ) {
238 $data = self::get_data( $uri );
239
240 if ( is_wp_error( $data ) ) {
241 return $data;
242 }
243
244 if ( empty( $data['links'] ) ) {
245 return new WP_Error(
246 'webfinger_missing_links',
247 __( 'No valid Link elements found.', 'activitypub' ),
248 array(
249 'status' => 400,
250 'data' => $data,
251 )
252 );
253 }
254
255 foreach ( $data['links'] as $link ) {
256 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
257 return $link['template'];
258 }
259 }
260
261 return new WP_Error(
262 'webfinger_missing_remote_follow_endpoint',
263 __( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
264 array(
265 'status' => 400,
266 'data' => $data,
267 )
268 );
269 }
270
271 /**
272 * Generate a cache key for a given URI
273 *
274 * @param string $uri A WebFinger Resource URI
275 *
276 * @return string The cache key
277 */
278 public static function generate_cache_key( $uri ) {
279 $uri = ltrim( $uri, '@' );
280
281 if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
282 $uri = 'acct:' . $uri;
283 }
284
285 return 'webfinger_' . md5( $uri );
286 }
287 }
288