PluginProbe
ActivityPub / 5.1.0
ActivityPub v5.1.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-query.php +57 -296 9.2.25.1.0 View file →
@@ -6,13 +6,9 @@
6 6 */
7 7
8 8 namespace Activitypub;
9 9
10 -use Activitypub\Activity\Extended_Object\Feature_Authorization;
11 -use Activitypub\Activity\Extended_Object\Quote_Authorization;
12 10 use Activitypub\Collection\Actors;
13 -use Activitypub\Collection\Outbox;
14 -use Activitypub\Handler\Feature_Request;
15 11 use Activitypub\Transformer\Factory;
16 12
17 13 /**
18 14 * Singleton class to handle and store the ActivityPub query.
@@ -51,15 +47,8 @@
51 47 */
52 48 private $is_activitypub_request;
53 49
54 50 /**
55 - * Whether the current request is from the old host.
56 - *
57 - * @var bool
58 - */
59 - private $is_old_host_request;
60 -
61 - /**
62 51 * The constructor.
63 52 */
64 53 private function __construct() {
65 54 // Do nothing.
@@ -94,16 +83,24 @@
94 83 if ( $this->activitypub_object ) {
95 84 return $this->activitypub_object;
96 85 }
97 86
98 - if ( $this->prepare_activitypub_data() ) {
99 - return $this->activitypub_object;
87 + $queried_object = $this->get_queried_object();
88 +
89 + if ( ! $queried_object ) {
90 + // If the object is not a valid ActivityPub object, try to get a virtual object.
91 + $activitypub_object = $this->maybe_get_virtual_object();
92 +
93 + if ( $activitypub_object ) {
94 + $this->activitypub_object = $activitypub_object;
95 +
96 + return $this->activitypub_object;
97 + }
100 98 }
101 99
102 - $queried_object = $this->get_queried_object();
103 - $transformer = Factory::get_transformer( $queried_object );
100 + $transformer = Factory::get_transformer( $queried_object );
104 101
105 - if ( $transformer && ! \is_wp_error( $transformer ) ) {
102 + if ( $transformer && ! is_wp_error( $transformer ) ) {
106 103 $this->activitypub_object = $transformer->to_object();
107 104 }
108 105
109 106 return $this->activitypub_object;
@@ -118,68 +115,29 @@
118 115 if ( $this->activitypub_object_id ) {
119 116 return $this->activitypub_object_id;
120 117 }
121 118
122 - if ( $this->prepare_activitypub_data() ) {
123 - return $this->activitypub_object_id;
124 - }
119 + $queried_object = $this->get_queried_object();
120 + $this->activitypub_object_id = null;
125 121
126 - $queried_object = $this->get_queried_object();
127 - $transformer = Factory::get_transformer( $queried_object );
122 + if ( ! $queried_object ) {
123 + // If the object is not a valid ActivityPub object, try to get a virtual object.
124 + $virtual_object = $this->maybe_get_virtual_object();
128 125
129 - if ( $transformer && ! \is_wp_error( $transformer ) ) {
130 - $this->activitypub_object_id = $transformer->to_id();
131 - }
126 + if ( $virtual_object ) {
127 + $this->activitypub_object_id = $virtual_object->get_id();
132 128
133 - return $this->activitypub_object_id;
134 - }
135 -
136 - /**
137 - * Prepare and set both ActivityPub object and ID for Outbox activities and virtual objects.
138 - *
139 - * @return bool True if an object was found and set, false otherwise.
140 - */
141 - private function prepare_activitypub_data() {
142 - $queried_object = $this->get_queried_object();
143 -
144 - if ( \get_query_var( 'stamp' ) ) {
145 - if ( $queried_object instanceof \WP_Post ) {
146 - return $this->maybe_get_stamp();
129 + return $this->activitypub_object_id;
147 130 }
148 -
149 - // Note: the blog actor's `actor` query var is '0', which is falsy but valid.
150 - if ( $queried_object instanceof \WP_User || '' !== \get_query_var( 'actor' ) ) {
151 - return $this->maybe_get_actor_stamp();
152 - }
153 131 }
154 132
155 - // Check for Outbox Activity.
156 - if (
157 - $queried_object instanceof \WP_Post &&
158 - Outbox::POST_TYPE === $queried_object->post_type
159 - ) {
160 - $activitypub_object = Outbox::maybe_get_activity( $queried_object );
133 + $transformer = Factory::get_transformer( $queried_object );
161 134
162 - // Check if the Outbox Activity is public.
163 - if ( ! \is_wp_error( $activitypub_object ) ) {
164 - $this->activitypub_object = $activitypub_object;
165 - $this->activitypub_object_id = $this->activitypub_object->get_id();
166 - return true;
167 - }
135 + if ( $transformer && ! is_wp_error( $transformer ) ) {
136 + $this->activitypub_object_id = $transformer->to_id();
168 137 }
169 138
170 - if ( ! $queried_object ) {
171 - // If the object is not a valid ActivityPub object, try to get a virtual object.
172 - $activitypub_object = $this->maybe_get_virtual_object();
173 -
174 - if ( $activitypub_object ) {
175 - $this->activitypub_object = $activitypub_object;
176 - $this->activitypub_object_id = $this->activitypub_object->get_id();
177 - return true;
178 - }
179 - }
180 -
181 - return false;
139 + return $this->activitypub_object_id;
182 140 }
183 141
184 142 /**
185 143 * Get the queried object.
@@ -206,16 +164,8 @@
206 164 $queried_object = \get_post( $post_id );
207 165 }
208 166 }
209 167
210 - // Check Term by ID.
211 - if ( ! $queried_object ) {
212 - $term_id = \get_query_var( 'term_id' );
213 - if ( $term_id ) {
214 - $queried_object = \get_term( $term_id );
215 - }
216 - }
217 -
218 168 // Try to get Author by ID.
219 169 if ( ! $queried_object ) {
220 170 $url = $this->get_request_url();
221 171 $author_id = url_to_authorid( $url );
@@ -228,9 +178,9 @@
228 178 * Filters the queried object.
229 179 *
230 180 * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
231 181 */
232 - return \apply_filters( 'activitypub_queried_object', $queried_object );
182 + return apply_filters( 'activitypub_queried_object', $queried_object );
233 183 }
234 184
235 185 /**
236 186 * Get the virtual object.
@@ -235,11 +185,12 @@
235 185 /**
236 186 * Get the virtual object.
237 187 *
238 188 * Virtual objects are objects that are not stored in the database, but are created on the fly.
239 - * The plugin currently supports one virtual object: The Blog-Actor.
189 + * The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
240 190 *
241 - * @see \Activitypub\Model\Blog
191 + * @see \Activitypub\Blog
192 + * @see \Activitypub\Application
242 193 *
243 194 * @return object|null The virtual object.
244 195 */
245 196 protected function maybe_get_virtual_object() {
@@ -250,13 +201,13 @@
250 201 }
251 202
252 203 $author_id = url_to_authorid( $url );
253 204
254 - if ( ! \is_numeric( $author_id ) ) {
255 - $author_id = $url;
205 + if ( ! is_numeric( $author_id ) ) {
206 + return null;
256 207 }
257 208
258 - $user = Actors::get_by_various( $author_id );
209 + $user = Actors::get_by_id( $author_id );
259 210
260 211 if ( \is_wp_error( $user ) || ! $user ) {
261 212 return null;
262 213 }
@@ -268,9 +219,9 @@
268 219 * Get the request URL.
269 220 *
270 221 * @return string|null The request URL.
271 222 */
272 - public function get_request_url() {
223 + protected function get_request_url() {
273 224 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
274 225 return null;
275 226 }
276 227
@@ -287,234 +238,44 @@
287 238 *
288 239 * @return bool True if the request is an ActivityPub request, false otherwise.
289 240 */
290 241 public function is_activitypub_request() {
291 - if ( ! isset( $this->is_activitypub_request ) ) {
292 - global $wp_query;
293 -
294 - $this->is_activitypub_request = false;
295 -
296 - // One can trigger an ActivityPub request by adding `?activitypub` to the URL.
297 - if ( isset( $wp_query->query_vars['activitypub'] ) || isset( $_GET['activitypub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
298 - \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
299 - $this->is_activitypub_request = true;
300 -
301 - // The other (more common) option to make an ActivityPub request is to send an Accept header.
302 - } elseif ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
303 - /*
304 - * The Accept-header decision is delegated to accept_prefers_activitypub() so the plugin and the
305 - * Surge cache drop-in classify byte-for-byte identically. Both must hand it the same raw
306 - * header, and they reach that raw form differently on purpose: this runs after
307 - * wp_magic_quotes() has addslashed $_SERVER, so it wp_unslash()es to recover the original
308 - * bytes; the drop-in runs before wp_magic_quotes() and passes its already-raw value
309 - * untouched. Do NOT sanitize it (the drop-in can't, its sanitizers aren't loaded yet) and
310 - * the helper must not stripslashes() either (that would corrupt the drop-in's genuine
311 - * bytes). It is only used to pick a content type, never stored or echoed.
312 - *
313 - * The request is ActivityPub when the highest-priority (by `q`, then order) media type is
314 - * an ActivityPub type (`application/activity+json`, or `application/ld+json` with the AS2
315 - * profile). A browser (`text/html` at q=1) gets the normal page; a client that prefers
316 - * ActivityPub but also accepts HTML as a low-`q` fallback (Mastodon) gets ActivityPub.
317 - */
318 - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Classified only; wp_unslash() recovers the raw bytes the pre-plugin cache path sees, and it must not be sanitized.
319 - if ( accept_prefers_activitypub( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ) ) {
320 - \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
321 - $this->is_activitypub_request = true;
322 - }
323 - }
242 + if ( isset( $this->is_activitypub_request ) ) {
243 + return $this->is_activitypub_request;
324 244 }
325 245
326 - /**
327 - * Filters whether the current request is an ActivityPub request.
328 - *
329 - * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise.
330 - */
331 - return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request );
332 - }
246 + global $wp_query;
333 247
334 - /**
335 - * Check if content negotiation is allowed for a request.
336 - *
337 - * @return bool True if content negotiation is allowed, false otherwise.
338 - */
339 - public function should_negotiate_content() {
340 - $return = false;
341 - $always_negotiate = array( 'p', 'c', 'author', 'actor', 'stamp', 'preview', 'activitypub' );
342 - $url = \wp_parse_url( $this->get_request_url(), PHP_URL_QUERY );
343 - $query = array();
344 - \wp_parse_str( $url, $query );
248 + // One can trigger an ActivityPub request by adding ?activitypub to the URL.
249 + if ( isset( $wp_query->query_vars['activitypub'] ) ) {
250 + $this->is_activitypub_request = true;
345 251
346 - // Check if any of the query params are in the `$always_negotiate` array.
347 - if ( \array_intersect( \array_keys( $query ), $always_negotiate ) ) {
348 - $return = true;
252 + return true;
349 253 }
350 254
351 - if ( \get_option( 'activitypub_content_negotiation', '1' ) ) {
352 - $return = true;
353 - }
354 -
355 - if ( \is_author() && \get_user_option( 'activitypub_use_permalink_as_id', \get_queried_object_id() ) ) {
356 - $return = true;
357 - }
358 -
359 - /**
360 - * Filters whether content negotiation should be forced.
361 - *
362 - * @param bool $return Whether content negotiation should be forced.
363 - */
364 - return \apply_filters( 'activitypub_should_negotiate_content', $return );
365 - }
366 -
367 - /**
368 - * Check if the current request is from the old host.
369 - *
370 - * @return bool True if the request is from the old host, false otherwise.
371 - */
372 - public function is_old_host_request() {
373 - if ( isset( $this->is_old_host_request ) ) {
374 - return $this->is_old_host_request;
375 - }
376 -
377 - $old_host = \get_option( 'activitypub_old_host' );
378 -
379 - if ( ! $old_host ) {
380 - $this->is_old_host_request = false;
381 - return false;
382 - }
383 -
384 - $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
385 - $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
386 -
387 - // Check if the domain matches either the request domain or referer.
388 - $check = $old_host === $request_host || $old_host === $referer_host;
389 - $this->is_old_host_request = $check;
390 -
391 - return $check;
392 - }
393 -
394 - /**
395 - * Fake an old host request.
396 - *
397 - * @param bool $state Optional. The state to set. Default true.
398 - */
399 - public function set_old_host_request( $state = true ) {
400 - $this->is_old_host_request = $state;
401 - }
402 -
403 - /**
404 - * Maybe get a QuoteAuthorization object from a stamp.
405 - *
406 - * @return bool True if the object was prepared, false otherwise.
407 - */
408 - private function maybe_get_stamp() {
409 - require_once ABSPATH . 'wp-admin/includes/post.php';
410 -
411 - $stamp = \get_query_var( 'stamp' );
412 - $meta = \get_post_meta_by_id( (int) $stamp );
413 -
414 - if ( ! $meta ) {
415 - return false;
416 - }
417 -
418 - $post = $this->get_queried_object();
419 -
420 255 /*
421 - * Only quote-authorization meta may be reflected as a stamp, and only for the queried
422 - * post. Checking the post id alone would still let an unauthenticated request read any
423 - * of that post's meta rows (e.g. _edit_lock or private custom fields) by guessing a
424 - * meta_id, so the meta key is verified too.
256 + * The other (more common) option to make an ActivityPub request
257 + * is to send an Accept header.
425 258 */
426 - if ( '_activitypub_quoted_by' !== $meta->meta_key || (int) $meta->post_id !== $post->ID ) {
427 - return false;
428 - }
259 + if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
260 + $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
429 261
430 - $user_uri = get_user_id( $post->post_author );
262 + /*
263 + * $accept can be a single value, or a comma separated list of values.
264 + * We want to support both scenarios,
265 + * and return true when the header includes at least one of the following:
266 + * - application/activity+json
267 + * - application/ld+json
268 + * - application/json
269 + */
270 + if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
271 + $this->is_activitypub_request = true;
431 272
432 - if ( ! $user_uri ) {
433 - return false;
434 - }
435 -
436 - $stamp_uri = \add_query_arg(
437 - array(
438 - 'p' => $post->ID,
439 - 'stamp' => $meta->meta_id,
440 - ),
441 - \home_url( '/' )
442 - );
443 -
444 - $activitypub_object = new Quote_Authorization();
445 - $activitypub_object->set_id( $stamp_uri );
446 - $activitypub_object->set_attributed_to( $user_uri );
447 - $activitypub_object->set_interacting_object( $meta->meta_value );
448 - $activitypub_object->set_interaction_target( get_post_id( $post->ID ) );
449 -
450 - $this->activitypub_object = $activitypub_object;
451 - $this->activitypub_object_id = $activitypub_object->get_id();
452 -
453 - return true;
454 - }
455 -
456 - /**
457 - * Maybe get a FeatureAuthorization object from an actor-scoped stamp.
458 - *
459 - * Resolves URLs of the form `?actor=USER_ID&stamp=STAMP_ID` against the
460 - * actor's stamp store, see {@see Feature_Request::get_stamp()}. Ownership
461 - * is enforced by resolving the stamp scoped to the queried actor, which
462 - * includes the blog actor (`actor=0`).
463 - *
464 - * @return bool True if a FeatureAuthorization was prepared, false otherwise.
465 - */
466 - private function maybe_get_actor_stamp() {
467 - $stamp_id = (int) \get_query_var( 'stamp' );
468 - $actor_var = \get_query_var( 'actor' );
469 -
470 - if ( ! $stamp_id ) {
471 - return false;
472 - }
473 -
474 - if ( '' === $actor_var ) {
475 - $queried = $this->get_queried_object();
476 - if ( ! $queried instanceof \WP_User ) {
477 - return false;
273 + return true;
478 274 }
479 -
480 - $actor_id = (int) $queried->ID;
481 - } else {
482 - // Values like '0e1' or '1.5' pass is_numeric() but cast to 0/1 and alias
483 - // an actor, so require a plain decimal integer before casting.
484 - if ( ! \ctype_digit( (string) $actor_var ) ) {
485 - return false;
486 - }
487 -
488 - $actor_id = (int) $actor_var;
489 275 }
490 276
491 - $instrument = Feature_Request::get_stamp( $actor_id, $stamp_id );
492 - if ( null === $instrument ) {
493 - return false;
494 - }
277 + $this->is_activitypub_request = false;
495 278
496 - $actor = Actors::get_by_id( $actor_id );
497 - if ( \is_wp_error( $actor ) ) {
498 - return false;
499 - }
500 -
501 - $stamp_url = \add_query_arg(
502 - array(
503 - 'actor' => $actor_id,
504 - 'stamp' => $stamp_id,
505 - ),
506 - \home_url( '/' )
507 - );
508 -
509 - $authorization = new Feature_Authorization();
510 - $authorization->set_id( $stamp_url );
511 - $authorization->set_attributed_to( $actor->get_id() );
512 - $authorization->set_interacting_object( $instrument );
513 - $authorization->set_interaction_target( $actor->get_id() );
514 -
515 - $this->activitypub_object = $authorization;
516 - $this->activitypub_object_id = $authorization->get_id();
517 -
518 - return true;
279 + return false;
519 280 }
520 281 }