PluginProbe
ActivityPub / 8.2.0
ActivityPub v8.2.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 / rest / class-inbox-controller.php

class-inbox-controller.php in ActivityPub 8.2.0, at includes/rest/class-inbox-controller.php

434 lines 14.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Inbox_Controller file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Rest;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Collection\Actors;
12 use Activitypub\Collection\Following;
13 use Activitypub\Collection\Inbox;
14 use Activitypub\Http;
15 use Activitypub\Moderation;
16
17 use function Activitypub\camel_to_snake_case;
18 use function Activitypub\extract_recipients_from_activity;
19 use function Activitypub\is_activity_public;
20 use function Activitypub\is_collection;
21 use function Activitypub\is_same_domain;
22 use function Activitypub\user_can_activitypub;
23
24 /**
25 * Inbox_Controller class.
26 *
27 * @author Matthias Pfefferle
28 *
29 * @see https://www.w3.org/TR/activitypub/#inbox
30 */
31 class Inbox_Controller extends \WP_REST_Controller {
32 use Verification;
33 use Language_Map;
34
35 /**
36 * The namespace of this controller's route.
37 *
38 * @var string
39 */
40 protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
41
42 /**
43 * The base of this controller's route.
44 *
45 * @var string
46 */
47 protected $rest_base = 'inbox';
48
49 /**
50 * Register routes.
51 */
52 public function register_routes() {
53 \register_rest_route(
54 $this->namespace,
55 '/' . $this->rest_base,
56 array(
57 array(
58 'methods' => \WP_REST_Server::CREATABLE,
59 'callback' => array( $this, 'create_item' ),
60 'permission_callback' => array( $this, 'verify_signature' ),
61 'args' => array(
62 'id' => array(
63 'description' => 'The unique identifier for the activity.',
64 'type' => 'string',
65 'format' => 'uri',
66 'required' => true,
67 ),
68 'actor' => array(
69 'description' => 'The actor performing the activity.',
70 'type' => 'string',
71 'required' => true,
72 'sanitize_callback' => '\Activitypub\object_to_uri',
73 ),
74 'type' => array(
75 'description' => 'The type of the activity.',
76 'type' => 'string',
77 'required' => true,
78 'sanitize_callback' => 'sanitize_html_class',
79 'validate_callback' => static function ( $param ) {
80 // Reject values that sanitize to empty so dynamic hook names always have a suffix.
81 return '' !== \sanitize_html_class( (string) $param );
82 },
83 ),
84 'object' => array(
85 'description' => 'The object of the activity.',
86 'required' => true,
87 'sanitize_callback' => array( $this, 'localize_language_maps' ),
88 'validate_callback' => static function ( $param, $request, $key ) {
89 /**
90 * Filter the ActivityPub object validation.
91 *
92 * @param bool $validate The validation result.
93 * @param array $param The object data.
94 * @param \WP_REST_Request $request The request object.
95 * @param string $key The key.
96 */
97 return \apply_filters( 'activitypub_validate_object', true, $param, $request, $key );
98 },
99 ),
100 'to' => array(
101 'description' => 'The primary recipients of the activity.',
102 'type' => array( 'string', 'array' ),
103 'required' => false,
104 'sanitize_callback' => static function ( $param ) {
105 if ( \is_string( $param ) ) {
106 $param = array( $param );
107 }
108
109 return $param;
110 },
111 ),
112 'cc' => array(
113 'description' => 'The secondary recipients of the activity.',
114 'type' => array( 'string', 'array' ),
115 'sanitize_callback' => static function ( $param ) {
116 if ( \is_string( $param ) ) {
117 $param = array( $param );
118 }
119
120 return $param;
121 },
122 ),
123 'bcc' => array(
124 'description' => 'The private recipients of the activity.',
125 'type' => array( 'string', 'array' ),
126 'sanitize_callback' => static function ( $param ) {
127 if ( \is_string( $param ) ) {
128 $param = array( $param );
129 }
130
131 return $param;
132 },
133 ),
134 ),
135 ),
136 'schema' => array( $this, 'get_item_schema' ),
137 )
138 );
139 }
140
141 /**
142 * The shared inbox.
143 *
144 * @param \WP_REST_Request $request The request object.
145 *
146 * @return \WP_REST_Response|\WP_Error Response object or WP_Error.
147 */
148 public function create_item( $request ) {
149 $data = $request->get_json_params();
150 $type = camel_to_snake_case( $request->get_param( 'type' ) );
151
152 /* @var Activity $activity Activity object.*/
153 $activity = Activity::init_from_array( $data );
154
155 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
156 if ( Moderation::activity_is_blocked( $activity ) ) {
157 /**
158 * ActivityPub inbox disallowed activity.
159 *
160 * @param array $data The data array.
161 * @param null $user_id The user ID.
162 * @param string $type The type of the activity.
163 * @param Activity|\WP_Error $activity The Activity object.
164 */
165 \do_action( 'activitypub_rest_inbox_disallowed', $data, null, $type, $activity );
166 } else {
167 $recipients = $this->get_local_recipients( $data );
168
169 // Filter out blocked recipients.
170 $allowed_recipients = array();
171 foreach ( $recipients as $user_id ) {
172 if ( Moderation::activity_is_blocked_for_user( $activity, $user_id ) ) {
173 /**
174 * ActivityPub inbox disallowed activity for specific user.
175 *
176 * @param array $data The data array.
177 * @param int $user_id The user ID.
178 * @param string $type The type of the activity.
179 * @param Activity|\WP_Error $activity The Activity object.
180 */
181 \do_action( 'activitypub_rest_inbox_disallowed', $data, $user_id, $type, $activity );
182 } else {
183 $allowed_recipients[] = $user_id;
184
185 /**
186 * ActivityPub inbox action.
187 *
188 * @deprecated 7.6.0 Support activitypub_inbox_shared instead to avoid duplicate processing.
189 *
190 * @param array $data The data array.
191 * @param int $user_id The user ID.
192 * @param string $type The type of the activity.
193 * @param Activity|\WP_Error $activity The Activity object.
194 * @param string $context The context of the request (shared_inbox when called from shared inbox endpoint).
195 */
196 \do_action( 'activitypub_inbox', $data, $user_id, $type, $activity, Inbox::CONTEXT_SHARED_INBOX );
197
198 /**
199 * ActivityPub inbox action for specific activity types.
200 *
201 * @deprecated 7.6.0 Support activitypub_inbox_shared_{type} instead to avoid duplicate processing.
202 *
203 * @param array $data The data array.
204 * @param int $user_id The user ID.
205 * @param Activity|\WP_Error $activity The Activity object.
206 * @param string $context The context of the request (shared_inbox when called from shared inbox endpoint).
207 */
208 \do_action( 'activitypub_inbox_' . $type, $data, $user_id, $activity, Inbox::CONTEXT_SHARED_INBOX );
209 }
210 }
211
212 /**
213 * ActivityPub shared inbox action.
214 *
215 * This hook fires once per activity with all recipients.
216 * Preferred for new implementations to avoid duplication.
217 *
218 * @since 7.6.0
219 *
220 * @param array $data The data array.
221 * @param array $recipients Array of user IDs.
222 * @param string $type The type of the activity.
223 * @param Activity|\WP_Error $activity The Activity object.
224 * @param string $context The context of the request.
225 */
226 \do_action( 'activitypub_inbox_shared', $data, $allowed_recipients, $type, $activity, Inbox::CONTEXT_SHARED_INBOX );
227
228 /**
229 * ActivityPub shared inbox action for specific activity types.
230 *
231 * This hook fires once per activity with all recipients.
232 * Preferred for new implementations to avoid duplication.
233 *
234 * @since 7.6.0
235 *
236 * @param array $data The data array.
237 * @param array $recipients Array of user IDs.
238 * @param Activity|\WP_Error $activity The Activity object.
239 * @param string $context The context of the request.
240 */
241 \do_action( 'activitypub_inbox_shared_' . $type, $data, $allowed_recipients, $activity, Inbox::CONTEXT_SHARED_INBOX );
242
243 /**
244 * Filter to skip inbox storage.
245 *
246 * Skip inbox storage for debugging purposes or to reduce load for
247 * certain Activity-Types, like "Delete".
248 *
249 * @param bool $skip Whether to skip inbox storage.
250 * @param array $data The activity data array.
251 *
252 * @return bool Whether to skip inbox storage.
253 */
254 $skip = \apply_filters( 'activitypub_skip_inbox_storage', false, $data );
255
256 if ( ! $skip ) {
257 $result = Inbox::add( $activity, $allowed_recipients );
258
259 /**
260 * Fires after an ActivityPub Inbox activity has been handled.
261 *
262 * @param array $data The data array.
263 * @param array $user_ids The user IDs.
264 * @param string $type The type of the activity.
265 * @param Activity|\WP_Error $activity The Activity object.
266 * @param \WP_Error|int $result The ID of the inbox item that was created, or WP_Error if failed.
267 * @param string $context The context of the request ('inbox' or 'shared_inbox').
268 */
269 \do_action( 'activitypub_handled_inbox', $data, $allowed_recipients, $type, $activity, $result, Inbox::CONTEXT_SHARED_INBOX );
270
271 /**
272 * Fires after an ActivityPub Inbox activity has been handled.
273 *
274 * @param array $data The data array.
275 * @param array $user_ids The user IDs.
276 * @param Activity|\WP_Error $activity The Activity object.
277 * @param \WP_Error|int $result The ID of the inbox item that was created, or WP_Error if failed.
278 * @param string $context The context of the request ('inbox' or 'shared_inbox').
279 */
280 \do_action( 'activitypub_handled_inbox_' . $type, $data, $allowed_recipients, $activity, $result, Inbox::CONTEXT_SHARED_INBOX );
281 }
282 }
283
284 $response = \rest_ensure_response(
285 array(
286 'type' => 'https://w3id.org/fep/c180#approval-required',
287 'title' => 'Approval Required',
288 'status' => '202',
289 'detail' => 'This activity requires approval before it can be processed.',
290 )
291 );
292 $response->set_status( 202 );
293 $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
294
295 return $response;
296 }
297
298 /**
299 * Retrieves the schema for a single inbox item, conforming to JSON Schema.
300 *
301 * @return array Item schema data.
302 */
303 public function get_item_schema() {
304 if ( $this->schema ) {
305 return $this->add_additional_fields_schema( $this->schema );
306 }
307
308 $schema = array(
309 '$schema' => 'https://json-schema.org/draft-04/schema#',
310 'title' => 'activity',
311 'type' => 'object',
312 'properties' => array(
313 '@context' => array(
314 'description' => 'The JSON-LD context for the activity.',
315 'type' => array( 'string', 'array', 'object' ),
316 'required' => true,
317 ),
318 'id' => array(
319 'description' => 'The unique identifier for the activity.',
320 'type' => 'string',
321 'format' => 'uri',
322 'required' => true,
323 ),
324 'type' => array(
325 'description' => 'The type of the activity.',
326 'type' => 'string',
327 'required' => true,
328 ),
329 'actor' => array(
330 'description' => 'The actor performing the activity.',
331 'type' => array( 'string', 'object' ),
332 'format' => 'uri',
333 'required' => true,
334 ),
335 'object' => array(
336 'description' => 'The object of the activity.',
337 'type' => array( 'string', 'object' ),
338 'required' => true,
339 ),
340 'to' => array(
341 'description' => 'The primary recipients of the activity.',
342 'type' => 'array',
343 'items' => array(
344 'type' => 'string',
345 'format' => 'uri',
346 ),
347 ),
348 'cc' => array(
349 'description' => 'The secondary recipients of the activity.',
350 'type' => 'array',
351 'items' => array(
352 'type' => 'string',
353 'format' => 'uri',
354 ),
355 ),
356 'bcc' => array(
357 'description' => 'The private recipients of the activity.',
358 'type' => 'array',
359 'items' => array(
360 'type' => 'string',
361 'format' => 'uri',
362 ),
363 ),
364 ),
365 );
366
367 $this->schema = $schema;
368
369 return $this->add_additional_fields_schema( $this->schema );
370 }
371
372 /**
373 * Extract recipients from the given Activity.
374 *
375 * @param array $activity The activity data.
376 *
377 * @return array An array of user IDs who are the recipients of the activity.
378 */
379 private function get_local_recipients( $activity ) {
380 $user_ids = array();
381
382 if ( is_activity_public( $activity ) ) {
383 $user_ids = Following::get_follower_ids( $activity['actor'] );
384 }
385
386 $recipients = extract_recipients_from_activity( $activity );
387
388 foreach ( $recipients as $recipient ) {
389 // Skip public audience identifiers - they're not actual recipients to fetch.
390 if ( \in_array( $recipient, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS, true ) ) {
391 continue;
392 }
393
394 if ( ! is_same_domain( $recipient ) ) {
395 $collection = Http::get_remote_object( $recipient );
396
397 // If it is a remote actor we can skip it.
398 if ( \is_wp_error( $collection ) ) {
399 continue;
400 }
401
402 if ( is_collection( $collection ) ) {
403 $_user_ids = Following::get_follower_ids( $activity['actor'] );
404 $user_ids = array_merge( $user_ids, $_user_ids );
405 continue;
406 }
407 }
408
409 $user_id = Actors::get_id_by_resource( $recipient );
410
411 if ( \is_wp_error( $user_id ) ) {
412 continue;
413 }
414
415 if ( ! user_can_activitypub( $user_id ) ) {
416 continue;
417 }
418
419 $user_ids[] = $user_id;
420 }
421
422 // Check for an Actor in the Object field.
423 if ( empty( $user_ids ) && ! empty( $activity['object'] ) ) {
424 $user_id = Actors::get_id_by_resource( $activity['object'] );
425
426 if ( ! \is_wp_error( $user_id ) && user_can_activitypub( $user_id ) ) {
427 $user_ids[] = $user_id;
428 }
429 }
430
431 return array_unique( array_map( 'intval', $user_ids ) );
432 }
433 }
434