PluginProbe
ActivityPub / 9.0.0
ActivityPub v9.0.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.0.0, at includes/class-query.php

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