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
← All changes | includes/class-webfinger.php +179 -82 1.2.0 → 5.7.0 View file →
@@ -1,12 +1,18 @@
1 1 <?php
2 +/**
3 + * WebFinger class file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub;
3 9
4 10 use WP_Error;
5 -use Activitypub\Collection\Users;
11 +use Activitypub\Collection\Actors;
6 12
7 13 /**
8 - * ActivityPub WebFinger Class
14 + * ActivityPub WebFinger Class.
9 15 *
10 16 * @author Matthias Pfefferle
11 17 *
12 18 * @see https://webfinger.net/
@@ -12,88 +18,112 @@
12 18 * @see https://webfinger.net/
13 19 */
14 20 class Webfinger {
15 21 /**
16 - * Returns a users WebFinger "resource"
22 + * Returns a users WebFinger "resource".
17 23 *
18 - * @param int $user_id
24 + * @param int $user_id The WordPress user id.
19 25 *
20 - * @return string The user-resource
26 + * @return string The user-resource.
21 27 */
22 28 public static function get_user_resource( $user_id ) {
23 - // use WebFinger plugin if installed
24 - if ( \function_exists( '\get_webfinger_resource' ) ) {
25 - return \get_webfinger_resource( $user_id, false );
26 - }
27 -
28 - $user = Users::get_by_id( $user_id );
29 + $user = Actors::get_by_id( $user_id );
29 30 if ( ! $user || is_wp_error( $user ) ) {
30 31 return '';
31 32 }
32 33
33 - return $user->get_resource();
34 + return $user->get_webfinger();
34 35 }
35 36
36 37 /**
37 - * Resolve a WebFinger resource
38 + * Resolve a WebFinger resource.
38 39 *
39 - * @param string $resource The WebFinger resource
40 + * @param string $uri The WebFinger Resource.
40 41 *
41 - * @return string|WP_Error The URL or WP_Error
42 + * @return string|WP_Error The URL or WP_Error.
42 43 */
43 - public static function resolve( $resource ) {
44 - if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $resource, $m ) ) {
45 - return null;
44 + public static function resolve( $uri ) {
45 + $data = self::get_data( $uri );
46 +
47 + if ( \is_wp_error( $data ) ) {
48 + return $data;
46 49 }
47 - $transient_key = 'activitypub_resolve_' . ltrim( $resource, '@' );
48 50
49 - $link = \get_transient( $transient_key );
50 - if ( $link ) {
51 - return $link;
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 + );
52 60 }
53 61
54 - $url = \add_query_arg( 'resource', 'acct:' . ltrim( $resource, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
55 - if ( ! \wp_http_validate_url( $url ) ) {
56 - $response = new WP_Error( 'invalid_webfinger_url', null, $url );
57 - \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
58 - return $response;
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 + }
59 73 }
60 74
61 - // try to access author URL
62 - $response = \wp_remote_get(
63 - $url,
75 + return new WP_Error(
76 + 'webfinger_url_no_activitypub',
77 + __( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
64 78 array(
65 - 'headers' => array( 'Accept' => 'application/jrd+json' ),
66 - 'redirection' => 2,
67 - 'timeout' => 2,
79 + 'status' => 400,
80 + 'data' => $data,
68 81 )
69 82 );
83 + }
70 84
71 - if ( \is_wp_error( $response ) ) {
72 - $link = new WP_Error( 'webfinger_url_not_accessible', null, $url );
73 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
74 - return $link;
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;
75 99 }
76 100
77 - $body = \wp_remote_retrieve_body( $response );
78 - $body = \json_decode( $body, true );
79 -
80 - if ( empty( $body['links'] ) ) {
81 - $link = new WP_Error( 'webfinger_url_invalid_response', null, $url );
82 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
83 - return $link;
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'];
84 107 }
85 108
86 - foreach ( $body['links'] as $link ) {
87 - if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
88 - \set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
89 - return $link['href'];
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 + }
90 115 }
91 116 }
92 117
93 - $link = new WP_Error( 'webfinger_url_no_activitypub', null, $body );
94 - \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
95 - return $link;
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 + );
96 126 }
97 127
98 128 /**
99 129 * Convert a URI string to an identifier and its host.
@@ -98,23 +128,33 @@
98 128 /**
99 129 * Convert a URI string to an identifier and its host.
100 130 * Automatically adds acct: if it's missing.
101 131 *
102 - * @param string $url The URI (acct:, mailto:, http:, https:)
132 + * @param string $url The URI (acct:, mailto:, http:, https:).
103 133 *
104 - * @return WP_Error|array Error reaction or array with
105 - * identifier and host as values
134 + * @return WP_Error|array Error reaction or array with identifier and host as values.
106 135 */
107 136 public static function get_identifier_and_host( $url ) {
108 - // remove leading @
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 @.
109 149 $url = ltrim( $url, '@' );
110 150
111 151 if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
112 152 $identifier = 'acct:' . $url;
113 - $scheme = 'acct';
153 + $scheme = 'acct';
114 154 } else {
115 155 $identifier = $url;
116 - $scheme = $match[1];
156 + $scheme = $match[1];
117 157 }
118 158
119 159 $host = null;
120 160
@@ -131,9 +171,16 @@
131 171 break;
132 172 }
133 173
134 174 if ( empty( $host ) ) {
135 - return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
175 + return new WP_Error(
176 + 'webfinger_invalid_identifier',
177 + __( 'Invalid Identifier', 'activitypub' ),
178 + array(
179 + 'status' => 400,
180 + 'data' => $url,
181 + )
182 + );
136 183 }
137 184
138 185 return array( $identifier, $host );
139 186 }
@@ -138,59 +185,85 @@
138 185 return array( $identifier, $host );
139 186 }
140 187
141 188 /**
142 - * Get the WebFinger data for a given URI
189 + * Get the WebFinger data for a given URI.
143 190 *
144 - * @param string $identifier The Identifier: <identifier>@<host>
145 - * @param string $host The Host: <identifier>@<host>
191 + * @param string $uri The Identifier: <identifier>@<host> or URI.
146 192 *
147 - * @return WP_Error|array Error reaction or array with
148 - * identifier and host as values
193 + * @return WP_Error|array Error reaction or array with identifier and host as values.
149 194 */
150 - public static function get_data( $identifier, $host ) {
151 - $webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
195 + public static function get_data( $uri ) {
196 + $identifier_and_host = self::get_identifier_and_host( $uri );
152 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 +
153 217 $response = wp_safe_remote_get(
154 218 $webfinger_url,
155 219 array(
156 220 'headers' => array( 'Accept' => 'application/jrd+json' ),
157 - 'redirection' => 0,
158 - 'timeout' => 2,
159 221 )
160 222 );
161 223
162 224 if ( is_wp_error( $response ) ) {
163 - return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url );
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 + );
164 233 }
165 234
166 235 $body = wp_remote_retrieve_body( $response );
236 + $data = json_decode( $body, true );
167 237
168 - return json_decode( $body, true );
238 + \set_transient( $transient_key, $data, WEEK_IN_SECONDS );
239 +
240 + return $data;
169 241 }
170 242
171 243 /**
172 - * Undocumented function
244 + * Get the Remote-Follow endpoint for a given URI.
173 245 *
174 - * @return void
246 + * @param string $uri The WebFinger Resource URI.
247 + *
248 + * @return string|WP_Error Error or the Remote-Follow endpoint URI.
175 249 */
176 250 public static function get_remote_follow_endpoint( $uri ) {
177 - $identifier_and_host = self::get_identifier_and_host( $uri );
251 + $data = self::get_data( $uri );
178 252
179 - if ( is_wp_error( $identifier_and_host ) ) {
180 - return $identifier_and_host;
181 - }
182 -
183 - list( $identifier, $host ) = $identifier_and_host;
184 -
185 - $data = self::get_data( $identifier, $host );
186 -
187 253 if ( is_wp_error( $data ) ) {
188 254 return $data;
189 255 }
190 256
191 257 if ( empty( $data['links'] ) ) {
192 - return new WP_Error( 'webfinger_url_invalid_response', null, $data );
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 + );
193 266 }
194 267
195 268 foreach ( $data['links'] as $link ) {
196 269 if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
@@ -197,7 +270,31 @@
197 270 return $link['template'];
198 271 }
199 272 }
200 273
201 - return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', $data, array( 'status' => 417 ) );
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 );
202 299 }
203 300 }