PluginProbe
ActivityPub / 7.8.2
ActivityPub v7.8.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
activitypub / includes / class-query.php

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

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