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

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

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