PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.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 7.8.0, at includes/rest/class-inbox-controller.php

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