PluginProbe
ActivityPub / 9.1.0
ActivityPub v9.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
activitypub / includes / class-query.php

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

515 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 plugin currently supports one virtual object: The Blog-Actor.
240 *
241 * @see \Activitypub\Model\Blog
242 *
243 * @return object|null The virtual object.
244 */
245 protected function maybe_get_virtual_object() {
246 $url = $this->get_request_url();
247
248 if ( ! $url ) {
249 return null;
250 }
251
252 $author_id = url_to_authorid( $url );
253
254 if ( ! \is_numeric( $author_id ) ) {
255 $author_id = $url;
256 }
257
258 $user = Actors::get_by_various( $author_id );
259
260 if ( \is_wp_error( $user ) || ! $user ) {
261 return null;
262 }
263
264 return $user;
265 }
266
267 /**
268 * Get the request URL.
269 *
270 * @return string|null The request URL.
271 */
272 public function get_request_url() {
273 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
274 return null;
275 }
276
277 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
278 $url = \wp_unslash( $_SERVER['REQUEST_URI'] );
279 $url = \WP_Http::make_absolute_url( $url, \home_url() );
280 $url = \sanitize_url( $url );
281
282 return $url;
283 }
284
285 /**
286 * Check if the current request is an ActivityPub request.
287 *
288 * @return bool True if the request is an ActivityPub request, false otherwise.
289 */
290 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 $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
304
305 /*
306 * $accept can be a single value, or a comma separated list of values.
307 * We want to support both scenarios,
308 * and return true when the header includes at least one of the following:
309 * - application/activity+json
310 * - application/ld+json
311 * - application/json
312 */
313 if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
314 \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
315 $this->is_activitypub_request = true;
316 }
317 }
318 }
319
320 /**
321 * Filters whether the current request is an ActivityPub request.
322 *
323 * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise.
324 */
325 return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request );
326 }
327
328 /**
329 * Check if content negotiation is allowed for a request.
330 *
331 * @return bool True if content negotiation is allowed, false otherwise.
332 */
333 public function should_negotiate_content() {
334 $return = false;
335 $always_negotiate = array( 'p', 'c', 'author', 'actor', 'stamp', 'preview', 'activitypub' );
336 $url = \wp_parse_url( $this->get_request_url(), PHP_URL_QUERY );
337 $query = array();
338 \wp_parse_str( $url, $query );
339
340 // Check if any of the query params are in the `$always_negotiate` array.
341 if ( \array_intersect( \array_keys( $query ), $always_negotiate ) ) {
342 $return = true;
343 }
344
345 if ( \get_option( 'activitypub_content_negotiation', '1' ) ) {
346 $return = true;
347 }
348
349 if ( \is_author() && \get_user_option( 'activitypub_use_permalink_as_id', \get_queried_object_id() ) ) {
350 $return = true;
351 }
352
353 /**
354 * Filters whether content negotiation should be forced.
355 *
356 * @param bool $return Whether content negotiation should be forced.
357 */
358 return \apply_filters( 'activitypub_should_negotiate_content', $return );
359 }
360
361 /**
362 * Check if the current request is from the old host.
363 *
364 * @return bool True if the request is from the old host, false otherwise.
365 */
366 public function is_old_host_request() {
367 if ( isset( $this->is_old_host_request ) ) {
368 return $this->is_old_host_request;
369 }
370
371 $old_host = \get_option( 'activitypub_old_host' );
372
373 if ( ! $old_host ) {
374 $this->is_old_host_request = false;
375 return false;
376 }
377
378 $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
379 $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
380
381 // Check if the domain matches either the request domain or referer.
382 $check = $old_host === $request_host || $old_host === $referer_host;
383 $this->is_old_host_request = $check;
384
385 return $check;
386 }
387
388 /**
389 * Fake an old host request.
390 *
391 * @param bool $state Optional. The state to set. Default true.
392 */
393 public function set_old_host_request( $state = true ) {
394 $this->is_old_host_request = $state;
395 }
396
397 /**
398 * Maybe get a QuoteAuthorization object from a stamp.
399 *
400 * @return bool True if the object was prepared, false otherwise.
401 */
402 private function maybe_get_stamp() {
403 require_once ABSPATH . 'wp-admin/includes/post.php';
404
405 $stamp = \get_query_var( 'stamp' );
406 $meta = \get_post_meta_by_id( (int) $stamp );
407
408 if ( ! $meta ) {
409 return false;
410 }
411
412 $post = $this->get_queried_object();
413
414 /*
415 * Only quote-authorization meta may be reflected as a stamp, and only for the queried
416 * post. Checking the post id alone would still let an unauthenticated request read any
417 * of that post's meta rows (e.g. _edit_lock or private custom fields) by guessing a
418 * meta_id, so the meta key is verified too.
419 */
420 if ( '_activitypub_quoted_by' !== $meta->meta_key || (int) $meta->post_id !== $post->ID ) {
421 return false;
422 }
423
424 $user_uri = get_user_id( $post->post_author );
425
426 if ( ! $user_uri ) {
427 return false;
428 }
429
430 $stamp_uri = \add_query_arg(
431 array(
432 'p' => $post->ID,
433 'stamp' => $meta->meta_id,
434 ),
435 \home_url( '/' )
436 );
437
438 $activitypub_object = new Quote_Authorization();
439 $activitypub_object->set_id( $stamp_uri );
440 $activitypub_object->set_attributed_to( $user_uri );
441 $activitypub_object->set_interacting_object( $meta->meta_value );
442 $activitypub_object->set_interaction_target( get_post_id( $post->ID ) );
443
444 $this->activitypub_object = $activitypub_object;
445 $this->activitypub_object_id = $activitypub_object->get_id();
446
447 return true;
448 }
449
450 /**
451 * Maybe get a FeatureAuthorization object from an actor-scoped stamp.
452 *
453 * Resolves URLs of the form `?actor=USER_ID&stamp=STAMP_ID` against the
454 * actor's stamp store, see {@see Feature_Request::get_stamp()}. Ownership
455 * is enforced by resolving the stamp scoped to the queried actor, which
456 * includes the blog actor (`actor=0`).
457 *
458 * @return bool True if a FeatureAuthorization was prepared, false otherwise.
459 */
460 private function maybe_get_actor_stamp() {
461 $stamp_id = (int) \get_query_var( 'stamp' );
462 $actor_var = \get_query_var( 'actor' );
463
464 if ( ! $stamp_id ) {
465 return false;
466 }
467
468 if ( '' === $actor_var ) {
469 $queried = $this->get_queried_object();
470 if ( ! $queried instanceof \WP_User ) {
471 return false;
472 }
473
474 $actor_id = (int) $queried->ID;
475 } else {
476 // Values like '0e1' or '1.5' pass is_numeric() but cast to 0/1 and alias
477 // an actor, so require a plain decimal integer before casting.
478 if ( ! \ctype_digit( (string) $actor_var ) ) {
479 return false;
480 }
481
482 $actor_id = (int) $actor_var;
483 }
484
485 $instrument = Feature_Request::get_stamp( $actor_id, $stamp_id );
486 if ( null === $instrument ) {
487 return false;
488 }
489
490 $actor = Actors::get_by_id( $actor_id );
491 if ( \is_wp_error( $actor ) ) {
492 return false;
493 }
494
495 $stamp_url = \add_query_arg(
496 array(
497 'actor' => $actor_id,
498 'stamp' => $stamp_id,
499 ),
500 \home_url( '/' )
501 );
502
503 $authorization = new Feature_Authorization();
504 $authorization->set_id( $stamp_url );
505 $authorization->set_attributed_to( $actor->get_id() );
506 $authorization->set_interacting_object( $instrument );
507 $authorization->set_interaction_target( $actor->get_id() );
508
509 $this->activitypub_object = $authorization;
510 $this->activitypub_object_id = $authorization->get_id();
511
512 return true;
513 }
514 }
515