PluginProbe
ActivityPub / 0.14.1
ActivityPub v0.14.1
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 0.14.1, at includes/rest/class-inbox.php

478 lines 12.8 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 /**
5 * ActivityPub Inbox REST-Class
6 *
7 * @author Matthias Pfefferle
8 *
9 * @see https://www.w3.org/TR/activitypub/#inbox
10 */
11 class Inbox {
12 /**
13 * Initialize the class, registering WordPress hooks
14 */
15 public static function init() {
16 \add_action( 'rest_api_init', array( '\Activitypub\Rest\Inbox', 'register_routes' ) );
17 \add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 );
18 \add_action( 'activitypub_inbox_follow', array( '\Activitypub\Rest\Inbox', 'handle_follow' ), 10, 2 );
19 \add_action( 'activitypub_inbox_undo', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 );
20 //\add_action( 'activitypub_inbox_like', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 );
21 //\add_action( 'activitypub_inbox_announce', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 );
22 \add_action( 'activitypub_inbox_create', array( '\Activitypub\Rest\Inbox', 'handle_create' ), 10, 2 );
23 }
24
25 /**
26 * Register routes
27 */
28 public static function register_routes() {
29 \register_rest_route(
30 'activitypub/1.0',
31 '/inbox',
32 array(
33 array(
34 'methods' => \WP_REST_Server::EDITABLE,
35 'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ),
36 'args' => self::shared_inbox_request_parameters(),
37 'permission_callback' => '__return_true',
38 ),
39 )
40 );
41
42 \register_rest_route(
43 'activitypub/1.0',
44 '/users/(?P<user_id>\d+)/inbox',
45 array(
46 array(
47 'methods' => \WP_REST_Server::EDITABLE,
48 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ),
49 'args' => self::user_inbox_request_parameters(),
50 'permission_callback' => '__return_true',
51 ),
52 array(
53 'methods' => \WP_REST_Server::READABLE,
54 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ),
55 'permission_callback' => '__return_true',
56 ),
57 )
58 );
59 }
60
61 /**
62 * Hooks into the REST API request to verify the signature.
63 *
64 * @param bool $served Whether the request has already been served.
65 * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response.
66 * @param WP_REST_Request $request Request used to generate the response.
67 * @param WP_REST_Server $server Server instance.
68 *
69 * @return true
70 */
71 public static function serve_request( $served, $result, $request, $server ) {
72 if ( '/activitypub' !== \substr( $request->get_route(), 0, 12 ) ) {
73 return $served;
74 }
75
76 $signature = $request->get_header( 'signature' );
77
78 if ( ! $signature ) {
79 return $served;
80 }
81
82 $headers = $request->get_headers();
83
84 // verify signature
85 //\Activitypub\Signature::verify_signature( $headers, $key );
86
87 return $served;
88 }
89
90 /**
91 * Renders the user-inbox
92 *
93 * @param WP_REST_Request $request
94 * @return WP_REST_Response
95 */
96 public static function user_inbox_get( $request ) {
97 $user_id = $request->get_param( 'user_id' );
98 $page = $request->get_param( 'page', 0 );
99
100 /*
101 * Action triggerd prior to the ActivityPub profile being created and sent to the client
102 */
103 \do_action( 'activitypub_inbox_pre' );
104
105 $json = new \stdClass();
106
107 $json->{'@context'} = \Activitypub\get_context();
108 $json->id = \home_url( \add_query_arg( null, null ) );
109 $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
110 $json->type = 'OrderedCollectionPage';
111 $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/inbox" ); // phpcs:ignore
112
113 $json->totalItems = 0; // phpcs:ignore
114
115 $json->orderedItems = array(); // phpcs:ignore
116
117 $json->first = $json->partOf; // phpcs:ignore
118
119 // filter output
120 $json = \apply_filters( 'activitypub_inbox_array', $json );
121
122 /*
123 * Action triggerd after the ActivityPub profile has been created and sent to the client
124 */
125 \do_action( 'activitypub_inbox_post' );
126
127 $response = new \WP_REST_Response( $json, 200 );
128
129 $response->header( 'Content-Type', 'application/activity+json' );
130
131 return $response;
132 }
133
134 /**
135 * Handles user-inbox requests
136 *
137 * @param WP_REST_Request $request
138 *
139 * @return WP_REST_Response
140 */
141 public static function user_inbox_post( $request ) {
142 $user_id = $request->get_param( 'user_id' );
143
144 $data = $request->get_params();
145 $type = $request->get_param( 'type' );
146 $type = \strtolower( $type );
147
148 \do_action( 'activitypub_inbox', $data, $user_id, $type );
149 \do_action( "activitypub_inbox_{$type}", $data, $user_id );
150
151 return new \WP_REST_Response( array(), 202 );
152 }
153
154 /**
155 * The shared inbox
156 *
157 * @param WP_REST_Request $request
158 *
159 * @return WP_REST_Response
160 */
161 public static function shared_inbox_post( $request ) {
162 $data = $request->get_params();
163 $type = $request->get_param( 'type' );
164 $users = self::extract_recipients( $data );
165
166 if ( ! $users ) {
167 return new \WP_Error(
168 'rest_invalid_param',
169 \__( 'No recipients found', 'activitypub' ),
170 array(
171 'status' => 404,
172 'params' => array(
173 'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
174 'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
175 'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ),
176 'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ),
177 'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ),
178 ),
179 )
180 );
181 }
182
183 foreach ( $users as $user ) {
184 $type = \strtolower( $type );
185
186 \do_action( 'activitypub_inbox', $data, $user->ID, $type );
187 \do_action( "activitypub_inbox_{$type}", $data, $user->ID );
188 }
189
190 return new \WP_REST_Response( array(), 202 );
191 }
192
193 /**
194 * The supported parameters
195 *
196 * @return array list of parameters
197 */
198 public static function user_inbox_request_parameters() {
199 $params = array();
200
201 $params['page'] = array(
202 'type' => 'integer',
203 );
204
205 $params['user_id'] = array(
206 'required' => true,
207 'type' => 'integer',
208 );
209
210 $params['id'] = array(
211 'required' => true,
212 'sanitize_callback' => 'esc_url_raw',
213 );
214
215 $params['actor'] = array(
216 'required' => true,
217 'sanitize_callback' => function( $param, $request, $key ) {
218 if ( ! \is_string( $param ) ) {
219 $param = $param['id'];
220 }
221 return \esc_url_raw( $param );
222 },
223 );
224
225 $params['type'] = array(
226 'required' => true,
227 //'type' => 'enum',
228 //'enum' => array( 'Create' ),
229 //'sanitize_callback' => function( $param, $request, $key ) {
230 // return \strtolower( $param );
231 //},
232 );
233
234 $params['object'] = array(
235 'required' => true,
236 );
237
238 return $params;
239 }
240
241 /**
242 * The supported parameters
243 *
244 * @return array list of parameters
245 */
246 public static function shared_inbox_request_parameters() {
247 $params = array();
248
249 $params['page'] = array(
250 'type' => 'integer',
251 );
252
253 $params['id'] = array(
254 'required' => true,
255 'type' => 'string',
256 'sanitize_callback' => 'esc_url_raw',
257 );
258
259 $params['actor'] = array(
260 'required' => true,
261 //'type' => array( 'object', 'string' ),
262 'sanitize_callback' => function( $param, $request, $key ) {
263 if ( ! \is_string( $param ) ) {
264 $param = $param['id'];
265 }
266 return \esc_url_raw( $param );
267 },
268 );
269
270 $params['type'] = array(
271 'required' => true,
272 //'type' => 'enum',
273 //'enum' => array( 'Create' ),
274 //'sanitize_callback' => function( $param, $request, $key ) {
275 // return \strtolower( $param );
276 //},
277 );
278
279 $params['object'] = array(
280 'required' => true,
281 //'type' => 'object',
282 );
283
284 $params['to'] = array(
285 'required' => false,
286 'sanitize_callback' => function( $param, $request, $key ) {
287 if ( \is_string( $param ) ) {
288 $param = array( $param );
289 }
290
291 return $param;
292 },
293 );
294
295 $params['cc'] = array(
296 'sanitize_callback' => function( $param, $request, $key ) {
297 if ( \is_string( $param ) ) {
298 $param = array( $param );
299 }
300
301 return $param;
302 },
303 );
304
305 $params['bcc'] = array(
306 'sanitize_callback' => function( $param, $request, $key ) {
307 if ( \is_string( $param ) ) {
308 $param = array( $param );
309 }
310
311 return $param;
312 },
313 );
314
315 return $params;
316 }
317
318 /**
319 * Handles "Follow" requests
320 *
321 * @param array $object The activity-object
322 * @param int $user_id The id of the local blog-user
323 */
324 public static function handle_follow( $object, $user_id ) {
325 // save follower
326 \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id );
327
328 // get inbox
329 $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] );
330
331 // send "Accept" activity
332 $activity = new \Activitypub\Model\Activity( 'Accept', \Activitypub\Model\Activity::TYPE_SIMPLE );
333 $activity->set_object( $object );
334 $activity->set_actor( \get_author_posts_url( $user_id ) );
335 $activity->set_to( $object['actor'] );
336 $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) );
337
338 $activity = $activity->to_simple_json();
339
340 $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
341 }
342
343 /**
344 * Handles "Unfollow" requests
345 *
346 * @param array $object The activity-object
347 * @param int $user_id The id of the local blog-user
348 */
349 public static function handle_unfollow( $object, $user_id ) {
350 if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) {
351 \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id );
352 }
353 }
354
355 /**
356 * Handles "Reaction" requests
357 *
358 * @param array $object The activity-object
359 * @param int $user_id The id of the local blog-user
360 */
361 public static function handle_reaction( $object, $user_id ) {
362 $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
363
364 $comment_post_id = \url_to_postid( $object['object'] );
365
366 // save only replys and reactions
367 if ( ! $comment_post_id ) {
368 return false;
369 }
370
371 $commentdata = array(
372 'comment_post_ID' => $comment_post_id,
373 'comment_author' => \esc_attr( $meta['name'] ),
374 'comment_author_email' => '',
375 'comment_author_url' => \esc_url_raw( $object['actor'] ),
376 'comment_content' => \esc_url_raw( $object['actor'] ),
377 'comment_type' => \esc_attr( \strtolower( $object['type'] ) ),
378 'comment_parent' => 0,
379 'comment_meta' => array(
380 'source_url' => \esc_url_raw( $object['id'] ),
381 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
382 'protocol' => 'activitypub',
383 ),
384 );
385
386 // disable flood control
387 \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
388
389 // do not require email for AP entries
390 \add_filter( 'pre_option_require_name_email', '__return_false' );
391
392 $state = \wp_new_comment( $commentdata, true );
393
394 \remove_filter( 'pre_option_require_name_email', '__return_false' );
395
396 // re-add flood control
397 \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
398 }
399
400 /**
401 * Handles "Create" requests
402 *
403 * @param array $object The activity-object
404 * @param int $user_id The id of the local blog-user
405 */
406 public static function handle_create( $object, $user_id ) {
407 $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
408
409 if ( ! isset( $object['object']['inReplyTo'] ) ) {
410 return;
411 }
412
413 $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
414
415 // save only replys and reactions
416 if ( ! $comment_post_id ) {
417 return false;
418 }
419
420 $commentdata = array(
421 'comment_post_ID' => $comment_post_id,
422 'comment_author' => \esc_attr( $meta['name'] ),
423 'comment_author_url' => \esc_url_raw( $object['actor'] ),
424 'comment_content' => \wp_filter_kses( $object['object']['content'] ),
425 'comment_type' => '',
426 'comment_author_email' => '',
427 'comment_parent' => 0,
428 'comment_meta' => array(
429 'source_url' => \esc_url_raw( $object['object']['url'] ),
430 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
431 'protocol' => 'activitypub',
432 ),
433 );
434
435 // disable flood control
436 \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
437
438 // do not require email for AP entries
439 \add_filter( 'pre_option_require_name_email', '__return_false' );
440
441 $state = \wp_new_comment( $commentdata, true );
442
443 \remove_filter( 'pre_option_require_name_email', '__return_false' );
444
445 // re-add flood control
446 \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
447 }
448
449 public static function extract_recipients( $data ) {
450 $recipients = array();
451 $users = array();
452
453 foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
454 if ( array_key_exists( $i, $data ) ) {
455 $recipients = array_merge( $recipients, $data[ $i ] );
456 }
457
458 if ( array_key_exists( $i, $data['object'] ) ) {
459 $recipients = array_merge( $recipients, $data[ $i ] );
460 }
461 }
462
463 $recipients = array_unique( $recipients );
464
465 foreach ( $recipients as $recipient ) {
466 $user_id = \Activitypub\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