PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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 7.7.0, at includes/class-query.php

419 lines 10.7 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 // Try to get Author by ID.
202 if ( ! $queried_object ) {
203 $url = $this->get_request_url();
204 $author_id = url_to_authorid( $url );
205 if ( $author_id ) {
206 $queried_object = \get_user_by( 'id', $author_id );
207 }
208 }
209
210 /**
211 * Filters the queried object.
212 *
213 * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
214 */
215 return apply_filters( 'activitypub_queried_object', $queried_object );
216 }
217
218 /**
219 * Get the virtual object.
220 *
221 * Virtual objects are objects that are not stored in the database, but are created on the fly.
222 * The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
223 *
224 * @see \Activitypub\Model\Blog
225 * @see \Activitypub\Model\Application
226 *
227 * @return object|null The virtual object.
228 */
229 protected function maybe_get_virtual_object() {
230 $url = $this->get_request_url();
231
232 if ( ! $url ) {
233 return null;
234 }
235
236 $author_id = url_to_authorid( $url );
237
238 if ( ! is_numeric( $author_id ) ) {
239 $author_id = $url;
240 }
241
242 $user = Actors::get_by_various( $author_id );
243
244 if ( \is_wp_error( $user ) || ! $user ) {
245 return null;
246 }
247
248 return $user;
249 }
250
251 /**
252 * Get the request URL.
253 *
254 * @return string|null The request URL.
255 */
256 public function get_request_url() {
257 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
258 return null;
259 }
260
261 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
262 $url = \wp_unslash( $_SERVER['REQUEST_URI'] );
263 $url = \WP_Http::make_absolute_url( $url, \home_url() );
264 $url = \sanitize_url( $url );
265
266 return $url;
267 }
268
269 /**
270 * Check if the current request is an ActivityPub request.
271 *
272 * @return bool True if the request is an ActivityPub request, false otherwise.
273 */
274 public function is_activitypub_request() {
275 if ( ! isset( $this->is_activitypub_request ) ) {
276 global $wp_query;
277
278 $this->is_activitypub_request = false;
279
280 // One can trigger an ActivityPub request by adding `?activitypub` to the URL.
281 if ( isset( $wp_query->query_vars['activitypub'] ) || isset( $_GET['activitypub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
282 \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
283 $this->is_activitypub_request = true;
284
285 // The other (more common) option to make an ActivityPub request is to send an Accept header.
286 } elseif ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
287 $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
288
289 /*
290 * $accept can be a single value, or a comma separated list of values.
291 * We want to support both scenarios,
292 * and return true when the header includes at least one of the following:
293 * - application/activity+json
294 * - application/ld+json
295 * - application/json
296 */
297 if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
298 \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
299 $this->is_activitypub_request = true;
300 }
301 }
302 }
303
304 /**
305 * Filters whether the current request is an ActivityPub request.
306 *
307 * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise.
308 */
309 return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request );
310 }
311
312 /**
313 * Check if content negotiation is allowed for a request.
314 *
315 * @return bool True if content negotiation is allowed, false otherwise.
316 */
317 public function should_negotiate_content() {
318 $return = false;
319 $always_negotiate = array( 'p', 'c', 'author', 'actor', 'stamp', 'preview', 'activitypub' );
320 $url = \wp_parse_url( $this->get_request_url(), PHP_URL_QUERY );
321 $query = array();
322 \wp_parse_str( $url, $query );
323
324 // Check if any of the query params are in the `$always_negotiate` array.
325 if ( \array_intersect( \array_keys( $query ), $always_negotiate ) ) {
326 $return = true;
327 }
328
329 if ( \get_option( 'activitypub_content_negotiation', '1' ) ) {
330 $return = true;
331 }
332
333 /**
334 * Filters whether content negotiation should be forced.
335 *
336 * @param bool $return Whether content negotiation should be forced.
337 */
338 return \apply_filters( 'activitypub_should_negotiate_content', $return );
339 }
340
341 /**
342 * Check if the current request is from the old host.
343 *
344 * @return bool True if the request is from the old host, false otherwise.
345 */
346 public function is_old_host_request() {
347 if ( isset( $this->is_old_host_request ) ) {
348 return $this->is_old_host_request;
349 }
350
351 $old_host = \get_option( 'activitypub_old_host' );
352
353 if ( ! $old_host ) {
354 $this->is_old_host_request = false;
355 return false;
356 }
357
358 $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
359 $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
360
361 // Check if the domain matches either the request domain or referer.
362 $check = $old_host === $request_host || $old_host === $referer_host;
363 $this->is_old_host_request = $check;
364
365 return $check;
366 }
367
368 /**
369 * Fake an old host request.
370 *
371 * @param bool $state Optional. The state to set. Default true.
372 */
373 public function set_old_host_request( $state = true ) {
374 $this->is_old_host_request = $state;
375 }
376
377 /**
378 * Maybe get a QuoteAuthorization object from a stamp.
379 *
380 * @return bool True if the object was prepared, false otherwise.
381 */
382 private function maybe_get_stamp() {
383 require_once ABSPATH . 'wp-admin/includes/post.php';
384
385 $stamp = \get_query_var( 'stamp' );
386 $meta = \get_post_meta_by_id( (int) $stamp );
387
388 if ( ! $meta ) {
389 return false;
390 }
391
392 $post = $this->get_queried_object();
393 $user_uri = get_user_id( $post->post_author );
394
395 if ( ! $user_uri ) {
396 return false;
397 }
398
399 $stamp_uri = \add_query_arg(
400 array(
401 'p' => $post->ID,
402 'stamp' => $meta->meta_id,
403 ),
404 \home_url( '/' )
405 );
406
407 $activitypub_object = new Quote_Authorization();
408 $activitypub_object->set_id( $stamp_uri );
409 $activitypub_object->set_attributed_to( $user_uri );
410 $activitypub_object->set_interacting_object( $meta->meta_value );
411 $activitypub_object->set_interaction_target( get_post_id( $post->ID ) );
412
413 $this->activitypub_object = $activitypub_object;
414 $this->activitypub_object_id = $activitypub_object->get_id();
415
416 return true;
417 }
418 }
419