PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.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
← All changes | includes/class-http.php +35 -169 9.3.08.0.2 View file →
@@ -17,11 +17,11 @@
17 17 class Http {
18 18 /**
19 19 * Send a POST Request with the needed HTTP Headers
20 20 *
21 - * @param string $url The URL endpoint.
22 - * @param string $body The Post Body.
23 - * @param int|null $user_id The WordPress User-ID, or null to sign with the Application key.
21 + * @param string $url The URL endpoint.
22 + * @param string $body The Post Body.
23 + * @param int $user_id The WordPress User-ID.
24 24 *
25 25 * @return array|\WP_Error The POST Response or an WP_Error.
26 26 */
27 27 public static function post( $url, $body, $user_id ) {
@@ -27,11 +27,11 @@
27 27 public static function post( $url, $body, $user_id ) {
28 28 /**
29 29 * Fires before an HTTP POST request is made.
30 30 *
31 - * @param string $url The URL endpoint.
32 - * @param string $body The POST body.
33 - * @param int|null $user_id The WordPress User ID, or null when signing with the Application key.
31 + * @param string $url The URL endpoint.
32 + * @param string $body The POST body.
33 + * @param int $user_id The WordPress User ID.
34 34 */
35 35 \do_action( 'activitypub_pre_http_post', $url, $body, $user_id );
36 36
37 37 /**
@@ -37,11 +37,10 @@
37 37 /**
38 38 * Filters the HTTP headers user agent string.
39 39 *
40 40 * @param string $user_agent The user agent string.
41 - * @param string $url The request URL.
42 41 */
43 - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ), $url );
42 + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ) );
44 43
45 44 /**
46 45 * Filters the timeout duration for remote POST requests in ActivityPub.
47 46 *
@@ -48,14 +47,8 @@
48 47 * @param int $timeout The timeout value in seconds. Default 10 seconds.
49 48 */
50 49 $timeout = \apply_filters( 'activitypub_remote_post_timeout', 10 );
51 50
52 - /*
53 - * Get the private key for signing the request. If a user ID is provided,
54 - * get the user's private key; otherwise, use the application's private key.
55 - */
56 - $private_key = null === $user_id ? Application::get_private_key() : Actors::get_private_key( $user_id );
57 -
58 51 $args = array(
59 52 'timeout' => $timeout,
60 53 'limit_response_size' => 1048576,
61 54 'redirection' => 3,
@@ -66,9 +59,9 @@
66 59 'Date' => \gmdate( 'D, d M Y H:i:s T' ),
67 60 ),
68 61 'body' => $body,
69 62 'key_id' => \json_decode( $body )->actor . '#main-key',
70 - 'private_key' => $private_key,
63 + 'private_key' => Actors::get_private_key( $user_id ),
71 64 'user_id' => $user_id,
72 65 );
73 66
74 67 $response = \wp_safe_remote_post( $url, $args );
@@ -76,9 +69,9 @@
76 69
77 70 if ( $code >= 400 ) {
78 71 $response = new \WP_Error(
79 72 $code,
80 - \__( 'Failed HTTP Request', 'activitypub' ),
73 + __( 'Failed HTTP Request', 'activitypub' ),
81 74 array(
82 75 'status' => $code,
83 76 'response' => $response,
84 77 )
@@ -90,9 +83,9 @@
90 83 *
91 84 * @param array|\WP_Error $response The response of the remote POST request.
92 85 * @param string $url The URL endpoint.
93 86 * @param string $body The Post Body.
94 - * @param int|null $user_id The WordPress User-ID, or null when signing with the Application key.
87 + * @param int $user_id The WordPress User-ID.
95 88 */
96 89 \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
97 90
98 91 return $response;
@@ -103,10 +96,9 @@
103 96 *
104 97 * @param string $url The URL endpoint.
105 98 * @param array $args Optional. Additional arguments to customize the request.
106 99 * - 'headers': Array of headers to override defaults.
107 - * @param bool|int $cached Optional. Whether to cache the response, or the cache duration in seconds for
108 - * successful responses. Failed responses use a fixed short backoff duration. Default false.
100 + * @param bool|int $cached Optional. Whether to return cached results, or cache duration. Default false.
109 101 *
110 102 * @return array|\WP_Error The GET Response or a WP_Error.
111 103 */
112 104 public static function get( $url, $args = array(), $cached = false ) {
@@ -149,12 +141,14 @@
149 141
150 142 /**
151 143 * Filters the HTTP headers user agent string.
152 144 *
145 + * This filter allows developers to modify the user agent string that is
146 + * sent with HTTP requests.
147 + *
153 148 * @param string $user_agent The user agent string.
154 - * @param string $url The request URL.
155 149 */
156 - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ), $url );
150 + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ) );
157 151
158 152 /**
159 153 * Filters the timeout duration for remote GET requests in ActivityPub.
160 154 *
@@ -171,10 +165,10 @@
171 165 'Accept' => 'application/activity+json',
172 166 'Content-Type' => 'application/activity+json',
173 167 'Date' => \gmdate( 'D, d M Y H:i:s T' ),
174 168 ),
175 - 'key_id' => Application::get_key_id(),
176 - 'private_key' => Application::get_private_key(),
169 + 'key_id' => Actors::get_by_id( Actors::APPLICATION_USER_ID )->get_id() . '#main-key',
170 + 'private_key' => Actors::get_private_key( Actors::APPLICATION_USER_ID ),
177 171 );
178 172
179 173 $args = \wp_parse_args( $args, $defaults );
180 174 $args['headers'] = \wp_parse_args( $args['headers'], $defaults['headers'] );
@@ -182,35 +176,22 @@
182 176 $response = \wp_safe_remote_get( $url, $args );
183 177 $code = \wp_remote_retrieve_response_code( $response );
184 178
185 179 if ( \is_wp_error( $response ) || $code >= 400 ) {
186 - // Capture the served-from URL before $response is replaced by the error below.
187 - $effective_url = \is_wp_error( $response ) ? '' : self::effective_url( $response );
180 + $response = new \WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
188 181
189 - if ( ! $code ) {
190 - $code = 0;
191 - }
192 - $response = new \WP_Error( $code, \__( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
193 -
194 182 /*
195 - * Cache errors to prevent repeated timeout waits, but never one reached via a
196 - * cross-host redirect: cached under the requested URL's key, a one-off open
197 - * redirect on the requested host would let a transient 4xx be replayed as a
198 - * repeatable federation outage (the same caching concern as the success path
199 - * below, just with a WP_Error instead of a document).
200 - *
183 + * Always cache errors to prevent repeated timeout waits.
201 184 * - Retriable errors (timeouts, 5xx): 1 minute (server may recover quickly).
202 185 * - Other errors (4xx): 15 minutes (client errors are more permanent).
203 186 */
204 - if ( $cached && ( ! $effective_url || is_same_host( $url, $effective_url ) ) ) {
205 - if ( \in_array( $code, ACTIVITYPUB_RETRY_ERROR_CODES, true ) || 0 === $code ) {
206 - $cache_duration = MINUTE_IN_SECONDS;
207 - } else {
208 - $cache_duration = 15 * MINUTE_IN_SECONDS;
209 - }
187 + if ( \in_array( $code, ACTIVITYPUB_RETRY_ERROR_CODES, true ) || 0 === $code ) {
188 + $cache_duration = MINUTE_IN_SECONDS;
189 + } else {
190 + $cache_duration = 15 * MINUTE_IN_SECONDS;
191 + }
210 192
211 - \set_transient( $transient_key, $response, $cache_duration );
212 - }
193 + \set_transient( $transient_key, $response, $cache_duration );
213 194
214 195 return $response;
215 196 }
216 197
@@ -221,29 +202,15 @@
221 202 * @param string $url The URL endpoint.
222 203 */
223 204 \do_action( 'activitypub_safe_remote_get_response', $response, $url );
224 205
225 - /*
226 - * Never persist a response that was redirected to a different host. The cache
227 - * is keyed on the requested URL, so caching cross-origin content under that key
228 - * would let a one-off open redirect on the requested host durably associate the
229 - * other host's document with that URL — a later lookup (e.g. a public-key fetch)
230 - * would then return it. Same-host redirects (e.g. http→https) stay cacheable.
231 - */
232 - $effective_url = self::effective_url( $response );
233 - if ( $effective_url && ! is_same_host( $url, $effective_url ) ) {
234 - return $response;
206 + // Always cache successful responses.
207 + $cache_duration = $cached;
208 + if ( ! is_int( $cache_duration ) ) {
209 + $cache_duration = HOUR_IN_SECONDS;
235 210 }
211 + \set_transient( $transient_key, $response, $cache_duration );
236 212
237 - // Cache successful responses when caching is requested.
238 - if ( $cached ) {
239 - $cache_duration = $cached;
240 - if ( ! \is_int( $cache_duration ) ) {
241 - $cache_duration = HOUR_IN_SECONDS;
242 - }
243 - \set_transient( $transient_key, $response, $cache_duration );
244 - }
245 -
246 213 return $response;
247 214 }
248 215
249 216 /**
@@ -253,9 +220,9 @@
253 220 *
254 221 * @return bool True if the URL is a tombstone.
255 222 */
256 223 public static function is_tombstone( $url ) {
257 - \_deprecated_function( __METHOD__, '7.3.0', 'Activitypub\Tombstone::exists_remote' );
224 + _deprecated_function( __METHOD__, '7.3.0', 'Activitypub\Tombstone::exists_remote' );
258 225
259 226 return Tombstone::exists_remote( $url );
260 227 }
261 228
@@ -272,17 +239,8 @@
272 239
273 240 /**
274 241 * Requests the Data from the Object-URL or Object-Array.
275 242 *
276 - * Fetched objects are self-confirmed before they are returned, the same way
277 - * Mastodon's `JsonLdHelper#fetch_resource` works: an object is trusted only when
278 - * its own `id` is the URL it was actually served from (after any redirects). If
279 - * the document served at the requested URL declares a different `id`, that id is
280 - * dereferenced from its own host and accepted only when it self-confirms. This
281 - * makes every caller safe to cache the result under its `id` — one host can never
282 - * serve an object (and its public key) under another host's id — without each
283 - * caller having to re-check the origin itself.
284 - *
285 243 * @param array|string $url_or_object The Object or the Object URL.
286 244 * @param bool $cached Optional. Whether the result should be cached. Default true.
287 245 *
288 246 * @return array|\WP_Error The Object data as array or WP_Error on failure.
@@ -290,15 +248,12 @@
290 248 public static function get_remote_object( $url_or_object, $cached = true ) {
291 249 /**
292 250 * Filters the preemptive return value of a remote object request.
293 251 *
294 - * This is an explicit in-process override (used for caching and tests), not
295 - * untrusted network data, so it is returned as-is without self-confirmation.
296 - *
297 252 * @param array|string|null $response The response.
298 253 * @param array|string|null $url_or_object The Object or the Object URL.
299 254 */
300 - $response = \apply_filters( 'activitypub_pre_http_get_remote_object', null, $url_or_object );
255 + $response = apply_filters( 'activitypub_pre_http_get_remote_object', null, $url_or_object );
301 256 if ( null !== $response ) {
302 257 return $response;
303 258 }
304 259
@@ -303,9 +258,9 @@
303 258 }
304 259
305 260 $url = object_to_uri( $url_or_object );
306 261
307 - if ( Webfinger::is_acct( $url ) ) {
262 + if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
308 263 $url = Webfinger::resolve( $url );
309 264 }
310 265
311 266 if ( ! $url ) {
@@ -322,61 +277,8 @@
322 277 if ( \is_wp_error( $url ) ) {
323 278 return $url;
324 279 }
325 280
326 - $final_url = '';
327 - $object = self::fetch_object( $url, $cached, $final_url );
328 -
329 - if ( \is_wp_error( $object ) ) {
330 - return $object;
331 - }
332 -
333 - // Trust the document when it is served under its own id (after redirects).
334 - if ( id_matches_url( $object, $final_url ) ) {
335 - return $object;
336 - }
337 -
338 - $declared_id = isset( $object['id'] ) && \is_string( $object['id'] ) ? $object['id'] : '';
339 -
340 - /*
341 - * An id-less object cannot be cached under an id, so it cannot be written
342 - * under another id in an id-keyed cache. Return the document as served.
343 - */
344 - if ( '' === $declared_id ) {
345 - return $object;
346 - }
347 -
348 - // Re-fetch the declared id from its own host and require it to self-confirm. One hop only.
349 - $object = self::fetch_object( $declared_id, $cached, $final_url );
350 -
351 - if ( \is_wp_error( $object ) ) {
352 - return $object;
353 - }
354 -
355 - if ( ! id_matches_url( $object, $final_url ) ) {
356 - return new \WP_Error(
357 - 'activitypub_object_id_mismatch',
358 - \__( 'The object id does not match the URL it was served from', 'activitypub' ),
359 - array( 'status' => 400 )
360 - );
361 - }
362 -
363 - return $object;
364 - }
365 -
366 - /**
367 - * Fetch and JSON-decode a single remote document.
368 - *
369 - * @param string $url The URL to fetch. Must already be resolved (not a WebFinger acct).
370 - * @param bool $cached Whether the result may be served from and written to cache.
371 - * @param string $final_url Filled by reference with the URL the document was served from,
372 - * after following any redirects.
373 - *
374 - * @return array|\WP_Error The decoded document, or WP_Error on failure.
375 - */
376 - private static function fetch_object( $url, $cached, &$final_url ) {
377 - $final_url = $url;
378 -
379 281 if ( ! \wp_http_validate_url( $url ) ) {
380 282 return new \WP_Error(
381 283 'activitypub_no_valid_object_url',
382 284 \__( 'The "object" is/has no valid URL', 'activitypub' ),
@@ -392,15 +294,11 @@
392 294 if ( \is_wp_error( $response ) ) {
393 295 return $response;
394 296 }
395 297
396 - $effective_url = self::effective_url( $response );
397 - if ( $effective_url ) {
398 - $final_url = $effective_url;
399 - }
298 + $data = \wp_remote_retrieve_body( $response );
299 + $data = \json_decode( $data, true );
400 300
401 - $data = \json_decode( \wp_remote_retrieve_body( $response ), true );
402 -
403 301 if ( ! $data ) {
404 302 return new \WP_Error(
405 303 'activitypub_invalid_json',
406 304 \__( 'No valid JSON data', 'activitypub' ),
@@ -411,38 +309,6 @@
411 309 );
412 310 }
413 311
414 312 return $data;
415 - }
416 -
417 - /**
418 - * Extract the effective URL a response was served from, after redirects.
419 - *
420 - * WordPress follows redirects transparently and exposes the final URL only on
421 - * the underlying Requests response object. Returns an empty string when the URL
422 - * cannot be determined, so callers fall back to the URL they requested.
423 - *
424 - * SECURITY: the redirect protections that build on this (the self-confirmation in
425 - * get_remote_object() and the cross-host cache skip in get()) fail OPEN when this
426 - * returns an empty string — self-confirmation then compares against the requested
427 - * URL, which a redirect could have bounced away from. This relies on the internal
428 - * `http_response` → Requests response `url` shape; if a future WordPress release
429 - * changes it, re-verify that this still returns the post-redirect URL.
430 - *
431 - * @param array $response A `wp_remote_get()` response array.
432 - *
433 - * @return string The final URL, or an empty string when unavailable.
434 - */
435 - private static function effective_url( $response ) {
436 - if ( empty( $response['http_response'] ) || ! \is_object( $response['http_response'] ) ) {
437 - return '';
438 - }
439 -
440 - $requests_response = $response['http_response']->get_response_object();
441 -
442 - if ( ! \is_object( $requests_response ) || empty( $requests_response->url ) ) {
443 - return '';
444 - }
445 -
446 - return (string) $requests_response->url;
447 313 }
448 314 }