PluginProbe
ActivityPub / 9.0.1
ActivityPub v9.0.1
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-query.php

class-query.php in ActivityPub 9.0.1, at includes/class-query.php

516 lines 13.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Query class.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Extended_Object\Feature_Authorization;
11 use Activitypub\Activity\Extended_Object\Quote_Authorization;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Collection\Outbox;
14 use Activitypub\Handler\Feature_Request;
15 use Activitypub\Transformer\Factory;
16
17 /**
18 * Singleton class to handle and store the ActivityPub query.
19 */
20 class Query {
21
22 /**
23 * The singleton instance.
24 *
25 * @var Query
26 */
27 private static $instance;
28
29 /**
30 * The ActivityPub object.
31 *
32 * @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
33 *
34 * @var object
35 */
36 private $activitypub_object;
37
38 /**
39 * The ActivityPub object ID.
40 *
41 * @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-id
42 *
43 * @var string
44 */
45 private $activitypub_object_id;
46
47 /**
48 * Whether the current request is an ActivityPub request.
49 *
50 * @var bool
51 */
52 private $is_activitypub_request;
53
54 /**
55 * Whether the current request is from the old host.
56 *
57 * @var bool
58 */
59 private $is_old_host_request;
60
61 /**
62 * The constructor.
63 */
64 private function __construct() {
65 // Do nothing.
66 }
67
68 /**
69 * The destructor.
70 */
71 public function __destruct() {
72 self::$instance = null;
73 }
74
75 /**
76 * Get the singleton instance.
77 *
78 * @return Query The singleton instance.
79 */
80 public static function get_instance() {
81 if ( ! isset( self::$instance ) ) {
82 self::$instance = new self();
83 }
84
85 return self::$instance;
86 }
87
88 /**
89 * Get the ActivityPub object.
90 *
91 * @return object The ActivityPub object.
92 */
93 public function get_activitypub_object() {
94 if ( $this->activitypub_object ) {
95 return $this->activitypub_object;
96 }
97
98 if ( $this->prepare_activitypub_data() ) {
99 return $this->activitypub_object;
100 }
101
102 $queried_object = $this->get_queried_object();
103 $transformer = Factory::get_transformer( $queried_object );
104
105 if ( $transformer && ! \is_wp_error( $transformer ) ) {
106 $this->activitypub_object = $transformer->to_object();
107 }
108
109 return $this->activitypub_object;
110 }
111
112 /**
113 * Get the ActivityPub object ID.
114 *
115 * @return string The ActivityPub object ID.
116 */
117 public function get_activitypub_object_id() {
118 if ( $this->activitypub_object_id ) {
119 return $this->activitypub_object_id;
120 }
121
122 if ( $this->prepare_activitypub_data() ) {
123 return $this->activitypub_object_id;
124 }
125
126 $queried_object = $this->get_queried_object();
127 $transformer = Factory::get_transformer( $queried_object );
128
129 if ( $transformer && ! \is_wp_error( $transformer ) ) {
130 $this->activitypub_object_id = $transformer->to_id();
131 }
132
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();
147 }
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 }
154
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 );
161
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 }
168 }
169
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;
182 }
183
184 /**
185 * Get the queried object.
186 *
187 * This adds support for Comments by `?c=123` IDs and Users by `?author=123` and `@username` IDs.
188 *
189 * @return \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null The queried object.
190 */
191 public function get_queried_object() {
192 $queried_object = \get_queried_object();
193
194 // Check Comment by ID.
195 if ( ! $queried_object ) {
196 $comment_id = \get_query_var( 'c' );
197 if ( $comment_id ) {
198 $queried_object = \get_comment( $comment_id );
199 }
200 }
201
202 // Check Post by ID (works for custom post types).
203 if ( ! $queried_object ) {
204 $post_id = \get_query_var( 'p' );
205 if ( $post_id ) {
206 $queried_object = \get_post( $post_id );
207 }
208 }
209
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 // Try to get Author by ID.
219 if ( ! $queried_object ) {
220 $url = $this->get_request_url();
221 $author_id = url_to_authorid( $url );
222 if ( $author_id ) {
223 $queried_object = \get_user_by( 'id', $author_id );
224 }
225 }
226
227 /**
228 * Filters the queried object.
229 *
230 * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
231 */
232 return apply_filters( 'activitypub_queried_object', $queried_object );
233 }
234
235 /**
236 * Get the virtual object.
237 *
238 * Virtual objects are objects that are not stored in the database, but are created on the fly.
239 * The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
240 *
241 * @see \Activitypub\Model\Blog
242 * @see \Activitypub\Model\Application
243 *
244 * @return object|null The virtual object.
245 */
246 protected function maybe_get_virtual_object() {
247 $url = $this->get_request_url();
248
249 if ( ! $url ) {
250 return null;
251 }
252
253 $author_id = url_to_authorid( $url );
254
255 if ( ! is_numeric( $author_id ) ) {
256 $author_id = $url;
257 }
258
259 $user = Actors::get_by_various( $author_id );
260
261 if ( \is_wp_error( $user ) || ! $user ) {
262 return null;
263 }
264
265 return $user;
266 }
267
268 /**
269 * Get the request URL.
270 *
271 * @return string|null The request URL.
272 */
273 public function get_request_url() {
274 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
275 return null;
276 }
277
278 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
279 $url = \wp_unslash( $_SERVER['REQUEST_URI'] );
280 $url = \WP_Http::make_absolute_url( $url, \home_url() );
281 $url = \sanitize_url( $url );
282
283 return $url;
284 }
285
286 /**
287 * Check if the current request is an ActivityPub request.
288 *
289 * @return bool True if the request is an ActivityPub request, false otherwise.
290 */
291 public function is_activitypub_request() {
292 if ( ! isset( $this->is_activitypub_request ) ) {
293 global $wp_query;
294
295 $this->is_activitypub_request = false;
296
297 // One can trigger an ActivityPub request by adding `?activitypub` to the URL.
298 if ( isset( $wp_query->query_vars['activitypub'] ) || isset( $_GET['activitypub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
299 \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
300 $this->is_activitypub_request = true;
301
302 // The other (more common) option to make an ActivityPub request is to send an Accept header.
303 } elseif ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
304 $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
305
306 /*
307 * $accept can be a single value, or a comma separated list of values.
308 * We want to support both scenarios,
309 * and return true when the header includes at least one of the following:
310 * - application/activity+json
311 * - application/ld+json
312 * - application/json
313 */
314 if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
315 \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
316 $this->is_activitypub_request = true;
317 }
318 }
319 }
320
321 /**
322 * Filters whether the current request is an ActivityPub request.
323 *
324 * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise.
325 */
326 return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request );
327 }
328
329 /**
330 * Check if content negotiation is allowed for a request.
331 *
332 * @return bool True if content negotiation is allowed, false otherwise.
333 */
334 public function should_negotiate_content() {
335 $return = false;
336 $always_negotiate = array( 'p', 'c', 'author', 'actor', 'stamp', 'preview', 'activitypub' );
337 $url = \wp_parse_url( $this->get_request_url(), PHP_URL_QUERY );
338 $query = array();
339 \wp_parse_str( $url, $query );
340
341 // Check if any of the query params are in the `$always_negotiate` array.
342 if ( \array_intersect( \array_keys( $query ), $always_negotiate ) ) {
343 $return = true;
344 }
345
346 if ( \get_option( 'activitypub_content_negotiation', '1' ) ) {
347 $return = true;
348 }
349
350 if ( \is_author() && \get_user_option( 'activitypub_use_permalink_as_id', \get_queried_object_id() ) ) {
351 $return = true;
352 }
353
354 /**
355 * Filters whether content negotiation should be forced.
356 *
357 * @param bool $return Whether content negotiation should be forced.
358 */
359 return \apply_filters( 'activitypub_should_negotiate_content', $return );
360 }
361
362 /**
363 * Check if the current request is from the old host.
364 *
365 * @return bool True if the request is from the old host, false otherwise.
366 */
367 public function is_old_host_request() {
368 if ( isset( $this->is_old_host_request ) ) {
369 return $this->is_old_host_request;
370 }
371
372 $old_host = \get_option( 'activitypub_old_host' );
373
374 if ( ! $old_host ) {
375 $this->is_old_host_request = false;
376 return false;
377 }
378
379 $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
380 $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
381
382 // Check if the domain matches either the request domain or referer.
383 $check = $old_host === $request_host || $old_host === $referer_host;
384 $this->is_old_host_request = $check;
385
386 return $check;
387 }
388
389 /**
390 * Fake an old host request.
391 *
392 * @param bool $state Optional. The state to set. Default true.
393 */
394 public function set_old_host_request( $state = true ) {
395 $this->is_old_host_request = $state;
396 }
397
398 /**
399 * Maybe get a QuoteAuthorization object from a stamp.
400 *
401 * @return bool True if the object was prepared, false otherwise.
402 */
403 private function maybe_get_stamp() {
404 require_once ABSPATH . 'wp-admin/includes/post.php';
405
406 $stamp = \get_query_var( 'stamp' );
407 $meta = \get_post_meta_by_id( (int) $stamp );
408
409 if ( ! $meta ) {
410 return false;
411 }
412
413 $post = $this->get_queried_object();
414
415 /*
416 * Only quote-authorization meta may be reflected as a stamp, and only for the queried
417 * post. Checking the post id alone would still let an unauthenticated request read any
418 * of that post's meta rows (e.g. _edit_lock or private custom fields) by guessing a
419 * meta_id, so the meta key is verified too.
420 */
421 if ( '_activitypub_quoted_by' !== $meta->meta_key || (int) $meta->post_id !== $post->ID ) {
422 return false;
423 }
424
425 $user_uri = get_user_id( $post->post_author );
426
427 if ( ! $user_uri ) {
428 return false;
429 }
430
431 $stamp_uri = \add_query_arg(
432 array(
433 'p' => $post->ID,
434 'stamp' => $meta->meta_id,
435 ),
436 \home_url( '/' )
437 );
438
439 $activitypub_object = new Quote_Authorization();
440 $activitypub_object->set_id( $stamp_uri );
441 $activitypub_object->set_attributed_to( $user_uri );
442 $activitypub_object->set_interacting_object( $meta->meta_value );
443 $activitypub_object->set_interaction_target( get_post_id( $post->ID ) );
444
445 $this->activitypub_object = $activitypub_object;
446 $this->activitypub_object_id = $activitypub_object->get_id();
447
448 return true;
449 }
450
451 /**
452 * Maybe get a FeatureAuthorization object from an actor-scoped stamp.
453 *
454 * Resolves URLs of the form `?actor=USER_ID&stamp=STAMP_ID` against the
455 * actor's stamp store, see {@see Feature_Request::get_stamp()}. Ownership
456 * is enforced by resolving the stamp scoped to the queried actor, which
457 * includes the blog actor (`actor=0`).
458 *
459 * @return bool True if a FeatureAuthorization was prepared, false otherwise.
460 */
461 private function maybe_get_actor_stamp() {
462 $stamp_id = (int) \get_query_var( 'stamp' );
463 $actor_var = \get_query_var( 'actor' );
464
465 if ( ! $stamp_id ) {
466 return false;
467 }
468
469 if ( '' === $actor_var ) {
470 $queried = $this->get_queried_object();
471 if ( ! $queried instanceof \WP_User ) {
472 return false;
473 }
474
475 $actor_id = (int) $queried->ID;
476 } else {
477 // Values like '0e1' or '1.5' pass is_numeric() but cast to 0/1 and alias
478 // an actor, so require a plain decimal integer before casting.
479 if ( ! \ctype_digit( (string) $actor_var ) ) {
480 return false;
481 }
482
483 $actor_id = (int) $actor_var;
484 }
485
486 $instrument = Feature_Request::get_stamp( $actor_id, $stamp_id );
487 if ( null === $instrument ) {
488 return false;
489 }
490
491 $actor = Actors::get_by_id( $actor_id );
492 if ( \is_wp_error( $actor ) ) {
493 return false;
494 }
495
496 $stamp_url = \add_query_arg(
497 array(
498 'actor' => $actor_id,
499 'stamp' => $stamp_id,
500 ),
501 \home_url( '/' )
502 );
503
504 $authorization = new Feature_Authorization();
505 $authorization->set_id( $stamp_url );
506 $authorization->set_attributed_to( $actor->get_id() );
507 $authorization->set_interacting_object( $instrument );
508 $authorization->set_interaction_target( $actor->get_id() );
509
510 $this->activitypub_object = $authorization;
511 $this->activitypub_object_id = $authorization->get_id();
512
513 return true;
514 }
515 }
516