PluginProbe
ActivityPub / 1.2.0
ActivityPub v1.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.php

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

515 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub\Rest;
3
4 use WP_Error;
5 use WP_REST_Server;
6 use WP_REST_Response;
7 use Activitypub\Activity\Activity;
8 use Activitypub\Collection\Users as User_Collection;
9
10 use function Activitypub\get_context;
11 use function Activitypub\url_to_authorid;
12 use function Activitypub\get_rest_url_by_path;
13 use function Activitypub\get_remote_metadata_by_actor;
14
15 /**
16 * ActivityPub Inbox REST-Class
17 *
18 * @author Matthias Pfefferle
19 *
20 * @see https://www.w3.org/TR/activitypub/#inbox
21 */
22 class Inbox {
23 /**
24 * Initialize the class, registering WordPress hooks
25 */
26 public static function init() {
27 self::register_routes();
28
29 \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
30 }
31
32 /**
33 * Register routes
34 */
35 public static function register_routes() {
36 \register_rest_route(
37 ACTIVITYPUB_REST_NAMESPACE,
38 '/inbox',
39 array(
40 array(
41 'methods' => WP_REST_Server::CREATABLE,
42 'callback' => array( self::class, 'shared_inbox_post' ),
43 'args' => self::shared_inbox_post_parameters(),
44 'permission_callback' => '__return_true',
45 ),
46 )
47 );
48
49 \register_rest_route(
50 ACTIVITYPUB_REST_NAMESPACE,
51 '/users/(?P<user_id>[\w\-\.]+)/inbox',
52 array(
53 array(
54 'methods' => WP_REST_Server::CREATABLE,
55 'callback' => array( self::class, 'user_inbox_post' ),
56 'args' => self::user_inbox_post_parameters(),
57 'permission_callback' => '__return_true',
58 ),
59 array(
60 'methods' => WP_REST_Server::READABLE,
61 'callback' => array( self::class, 'user_inbox_get' ),
62 'args' => self::user_inbox_get_parameters(),
63 'permission_callback' => '__return_true',
64 ),
65 )
66 );
67 }
68
69 /**
70 * Renders the user-inbox
71 *
72 * @param WP_REST_Request $request
73 * @return WP_REST_Response
74 */
75 public static function user_inbox_get( $request ) {
76 $user_id = $request->get_param( 'user_id' );
77 $user = User_Collection::get_by_various( $user_id );
78
79 if ( is_wp_error( $user ) ) {
80 return $user;
81 }
82
83 $page = $request->get_param( 'page', 0 );
84
85 /*
86 * Action triggerd prior to the ActivityPub profile being created and sent to the client
87 */
88 \do_action( 'activitypub_rest_inbox_pre' );
89
90 $json = new \stdClass();
91
92 $json->{'@context'} = get_context();
93 $json->id = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) );
94 $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
95 $json->type = 'OrderedCollectionPage';
96 $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore
97
98 $json->totalItems = 0; // phpcs:ignore
99
100 $json->orderedItems = array(); // phpcs:ignore
101
102 $json->first = $json->partOf; // phpcs:ignore
103
104 // filter output
105 $json = \apply_filters( 'activitypub_rest_inbox_array', $json );
106
107 /*
108 * Action triggerd after the ActivityPub profile has been created and sent to the client
109 */
110 \do_action( 'activitypub_inbox_post' );
111
112 $rest_response = new WP_REST_Response( $json, 200 );
113 $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
114
115 return $rest_response;
116 }
117
118 /**
119 * Handles user-inbox requests
120 *
121 * @param WP_REST_Request $request
122 *
123 * @return WP_REST_Response
124 */
125 public static function user_inbox_post( $request ) {
126 $user_id = $request->get_param( 'user_id' );
127 $user = User_Collection::get_by_various( $user_id );
128
129 if ( is_wp_error( $user ) ) {
130 return $user;
131 }
132
133 $data = $request->get_json_params();
134 $type = $request->get_param( 'type' );
135 $type = \strtolower( $type );
136
137 \do_action( 'activitypub_inbox', $data, $user->get__id(), $type );
138 \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() );
139
140 $rest_response = new WP_REST_Response( array(), 202 );
141 $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
142
143 return $rest_response;
144 }
145
146 /**
147 * The shared inbox
148 *
149 * @param WP_REST_Request $request
150 *
151 * @return WP_REST_Response
152 */
153 public static function shared_inbox_post( $request ) {
154 $data = $request->get_json_params();
155 $type = $request->get_param( 'type' );
156 $users = self::extract_recipients( $data );
157
158 if ( ! $users ) {
159 return new WP_Error(
160 'rest_invalid_param',
161 \__( 'No recipients found', 'activitypub' ),
162 array(
163 'status' => 400,
164 'params' => array(
165 'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
166 'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
167 'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ),
168 'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ),
169 'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ),
170 ),
171 )
172 );
173 }
174
175 foreach ( $users as $user ) {
176 $user = User_Collection::get_by_various( $user );
177
178 if ( is_wp_error( $user ) ) {
179 continue;
180 }
181
182 $type = \strtolower( $type );
183
184 \do_action( 'activitypub_inbox', $data, $user->ID, $type );
185 \do_action( "activitypub_inbox_{$type}", $data, $user->ID );
186 }
187
188 $rest_response = new WP_REST_Response( array(), 202 );
189 $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
190
191 return $rest_response;
192 }
193
194 /**
195 * The supported parameters
196 *
197 * @return array list of parameters
198 */
199 public static function user_inbox_get_parameters() {
200 $params = array();
201
202 $params['page'] = array(
203 'type' => 'integer',
204 );
205
206 $params['user_id'] = array(
207 'required' => true,
208 'type' => 'string',
209 );
210
211 return $params;
212 }
213
214 /**
215 * The supported parameters
216 *
217 * @return array list of parameters
218 */
219 public static function user_inbox_post_parameters() {
220 $params = array();
221
222 $params['page'] = array(
223 'type' => 'integer',
224 );
225
226 $params['user_id'] = array(
227 'required' => true,
228 'type' => 'string',
229 );
230
231 $params['id'] = array(
232 'required' => true,
233 'sanitize_callback' => 'esc_url_raw',
234 );
235
236 $params['actor'] = array(
237 'required' => true,
238 'sanitize_callback' => function( $param, $request, $key ) {
239 if ( ! \is_string( $param ) ) {
240 $param = $param['id'];
241 }
242 return \esc_url_raw( $param );
243 },
244 );
245
246 $params['type'] = array(
247 'required' => true,
248 //'type' => 'enum',
249 //'enum' => array( 'Create' ),
250 //'sanitize_callback' => function( $param, $request, $key ) {
251 // return \strtolower( $param );
252 //},
253 );
254
255 $params['object'] = array(
256 'required' => true,
257 );
258
259 return $params;
260 }
261
262 /**
263 * The supported parameters
264 *
265 * @return array list of parameters
266 */
267 public static function shared_inbox_post_parameters() {
268 $params = array();
269
270 $params['page'] = array(
271 'type' => 'integer',
272 );
273
274 $params['id'] = array(
275 'required' => true,
276 'type' => 'string',
277 'sanitize_callback' => 'esc_url_raw',
278 );
279
280 $params['actor'] = array(
281 'required' => true,
282 //'type' => array( 'object', 'string' ),
283 'sanitize_callback' => function( $param, $request, $key ) {
284 if ( ! \is_string( $param ) ) {
285 $param = $param['id'];
286 }
287 return \esc_url_raw( $param );
288 },
289 );
290
291 $params['type'] = array(
292 'required' => true,
293 //'type' => 'enum',
294 //'enum' => array( 'Create' ),
295 //'sanitize_callback' => function( $param, $request, $key ) {
296 // return \strtolower( $param );
297 //},
298 );
299
300 $params['object'] = array(
301 'required' => true,
302 //'type' => 'object',
303 );
304
305 $params['to'] = array(
306 'required' => false,
307 'sanitize_callback' => function( $param, $request, $key ) {
308 if ( \is_string( $param ) ) {
309 $param = array( $param );
310 }
311
312 return $param;
313 },
314 );
315
316 $params['cc'] = array(
317 'sanitize_callback' => function( $param, $request, $key ) {
318 if ( \is_string( $param ) ) {
319 $param = array( $param );
320 }
321
322 return $param;
323 },
324 );
325
326 $params['bcc'] = array(
327 'sanitize_callback' => function( $param, $request, $key ) {
328 if ( \is_string( $param ) ) {
329 $param = array( $param );
330 }
331
332 return $param;
333 },
334 );
335
336 return $params;
337 }
338
339 /**
340 * Handles "Create" requests
341 *
342 * @param array $object The activity-object
343 * @param int $user_id The id of the local blog-user
344 */
345 public static function handle_create( $object, $user_id ) {
346 $meta = get_remote_metadata_by_actor( $object['actor'] );
347
348 if ( ! isset( $object['object']['inReplyTo'] ) ) {
349 return;
350 }
351
352 // check if Activity is public or not
353 if ( ! self::is_activity_public( $object ) ) {
354 // @todo maybe send email
355 return;
356 }
357
358 $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
359
360 // save only replys and reactions
361 if ( ! $comment_post_id ) {
362 return false;
363 }
364
365 $commentdata = array(
366 'comment_post_ID' => $comment_post_id,
367 'comment_author' => \esc_attr( $meta['name'] ),
368 'comment_author_url' => \esc_url_raw( $object['actor'] ),
369 'comment_content' => addslashes( \wp_kses( $object['object']['content'], 'pre_comment_content' ) ),
370 'comment_type' => 'comment',
371 'comment_author_email' => '',
372 'comment_parent' => 0,
373 'comment_meta' => array(
374 'source_url' => \esc_url_raw( $object['object']['url'] ),
375 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
376 'protocol' => 'activitypub',
377 ),
378 );
379
380 // disable flood control
381 \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
382
383 // do not require email for AP entries
384 \add_filter( 'pre_option_require_name_email', '__return_false' );
385
386 // No nonce possible for this submission route
387 \add_filter(
388 'akismet_comment_nonce',
389 function() {
390 return 'inactive';
391 }
392 );
393
394 \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
395
396 $state = \wp_new_comment( $commentdata, true );
397
398 \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
399 \remove_filter( 'pre_option_require_name_email', '__return_false' );
400
401 // re-add flood control
402 \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
403
404 do_action( 'activitypub_handled_create', $object, $user_id, $state, $commentdata );
405 }
406
407 /**
408 * Extract recipient URLs from Activity object
409 *
410 * @param array $data
411 *
412 * @return array The list of user URLs
413 */
414 public static function extract_recipients( $data ) {
415 $recipient_items = array();
416
417 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
418 if ( array_key_exists( $i, $data ) ) {
419 if ( is_array( $data[ $i ] ) ) {
420 $recipient = $data[ $i ];
421 } else {
422 $recipient = array( $data[ $i ] );
423 }
424 $recipient_items = array_merge( $recipient_items, $recipient );
425 }
426
427 if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
428 if ( is_array( $data['object'][ $i ] ) ) {
429 $recipient = $data['object'][ $i ];
430 } else {
431 $recipient = array( $data['object'][ $i ] );
432 }
433 $recipient_items = array_merge( $recipient_items, $recipient );
434 }
435 }
436
437 $recipients = array();
438
439 // flatten array
440 foreach ( $recipient_items as $recipient ) {
441 if ( is_array( $recipient ) ) {
442 // check if recipient is an object
443 if ( array_key_exists( 'id', $recipient ) ) {
444 $recipients[] = $recipient['id'];
445 }
446 } else {
447 $recipients[] = $recipient;
448 }
449 }
450
451 return array_unique( $recipients );
452 }
453
454 /**
455 * Get local user recipients
456 *
457 * @param array $data
458 *
459 * @return array The list of local users
460 */
461 public static function get_recipients( $data ) {
462 $recipients = self::extract_recipients( $data );
463 $users = array();
464
465 foreach ( $recipients as $recipient ) {
466 $user_id = url_to_authorid( $recipient );
467
468 $user = get_user_by( 'id', $user_id );
469
470 if ( $user ) {
471 $users[] = $user;
472 }
473 }
474
475 return $users;
476 }
477
478 /**
479 * Check if passed Activity is Public
480 *
481 * @param array $data
482 * @return boolean
483 */
484 public static function is_activity_public( $data ) {
485 $recipients = self::extract_recipients( $data );
486
487 return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
488 }
489
490 /**
491 * Adds line breaks to the list of allowed comment tags.
492 *
493 * @param array $allowedtags Allowed HTML tags.
494 * @param string $context Context.
495 * @return array Filtered tag list.
496 */
497 public static function allowed_comment_html( $allowedtags, $context = '' ) {
498 if ( 'pre_comment_content' !== $context ) {
499 // Do nothing.
500 return $allowedtags;
501 }
502
503 // Add `p` and `br` to the list of allowed tags.
504 if ( ! array_key_exists( 'br', $allowedtags ) ) {
505 $allowedtags['br'] = array();
506 }
507
508 if ( ! array_key_exists( 'p', $allowedtags ) ) {
509 $allowedtags['p'] = array();
510 }
511
512 return $allowedtags;
513 }
514 }
515