PluginProbe
ActivityPub / 8.2.1
ActivityPub v8.2.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 / class-post-types.php

class-post-types.php in ActivityPub 8.2.1, at includes/class-post-types.php

992 lines 29.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post Types class for consolidating all custom post type and related meta registrations.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Collection\Extra_Fields;
12 use Activitypub\Collection\Followers;
13 use Activitypub\Collection\Following;
14 use Activitypub\Collection\Inbox;
15 use Activitypub\Collection\Outbox;
16 use Activitypub\Collection\Remote_Actors;
17 use Activitypub\Collection\Remote_Posts;
18 use Activitypub\OAuth\Client;
19 use Activitypub\OAuth\Scope;
20 use Activitypub\OAuth\Token;
21
22 /**
23 * Post Types class.
24 */
25 class Post_Types {
26 /**
27 * Initialize the class, registering all custom post types and post meta.
28 */
29 public static function init() {
30 \add_action( 'init', array( self::class, 'register_remote_actors_post_type' ), 11 );
31 \add_action( 'init', array( self::class, 'register_inbox_post_type' ), 11 );
32 \add_action( 'init', array( self::class, 'register_outbox_post_type' ), 11 );
33 \add_action( 'init', array( self::class, 'register_post_post_type' ), 11 );
34 \add_action( 'init', array( self::class, 'register_extra_fields_post_types' ), 11 );
35 \add_action( 'init', array( self::class, 'register_activitypub_post_meta' ), 11 );
36 \add_action( 'init', array( self::class, 'register_oauth_post_types' ), 11 );
37
38 \add_action( 'rest_api_init', array( self::class, 'register_ap_actor_rest_field' ) );
39 \add_action( 'rest_api_init', array( self::class, 'register_ap_post_actor_rest_field' ) );
40 \add_action( 'rest_api_init', array( self::class, 'register_ap_post_rest_params' ) );
41
42 \add_filter( 'rest_ap_post_query', array( self::class, 'filter_ap_post_by_user' ), 10, 2 );
43 \add_filter( 'rest_ap_object_type_query', array( self::class, 'filter_object_type_by_user' ), 10, 2 );
44 \add_filter( 'rest_ap_object_type_collection_params', array( self::class, 'register_object_type_user_param' ) );
45
46 \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
47
48 \add_filter( 'add_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 );
49 \add_filter( 'update_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 );
50
51 // Add support for ActivityPub to custom post types.
52 foreach ( \get_option( 'activitypub_support_post_types', array( 'post' ) ) as $post_type ) {
53 \add_post_type_support( $post_type, 'activitypub' );
54 }
55 }
56
57 /**
58 * Register the Remote Actors post type and its meta.
59 */
60 public static function register_remote_actors_post_type() {
61 \register_post_type(
62 Remote_Actors::POST_TYPE,
63 array(
64 'labels' => array(
65 'name' => \_x( 'Followers', 'post_type plural name', 'activitypub' ),
66 'singular_name' => \_x( 'Follower', 'post_type single name', 'activitypub' ),
67 ),
68 'public' => false,
69 'show_in_rest' => true,
70 'hierarchical' => false,
71 'rewrite' => false,
72 'query_var' => false,
73 'delete_with_user' => false,
74 'can_export' => true,
75 'supports' => array( 'custom-fields' ),
76 )
77 );
78
79 // Register meta for Remote Actors post type.
80 \register_post_meta(
81 Remote_Actors::POST_TYPE,
82 '_activitypub_inbox',
83 array(
84 'type' => 'string',
85 'single' => true,
86 'sanitize_callback' => 'sanitize_url',
87 )
88 );
89
90 \register_post_meta(
91 Remote_Actors::POST_TYPE,
92 '_activitypub_errors',
93 array(
94 'type' => 'string',
95 'single' => false,
96 'sanitize_callback' => 'sanitize_text_field',
97 )
98 );
99
100 \register_post_meta(
101 Remote_Actors::POST_TYPE,
102 Followers::FOLLOWER_META_KEY,
103 array(
104 'type' => 'string',
105 'single' => false,
106 'show_in_rest' => true,
107 'sanitize_callback' => 'sanitize_text_field',
108 )
109 );
110 }
111
112 /**
113 * Register the Inbox post type and its meta.
114 */
115 public static function register_inbox_post_type() {
116 \register_post_type(
117 Inbox::POST_TYPE,
118 array(
119 'labels' => array(
120 'name' => \_x( 'Inbox', 'post_type plural name', 'activitypub' ),
121 'singular_name' => \_x( 'Inbox Item', 'post_type single name', 'activitypub' ),
122 ),
123 'capabilities' => array(
124 'create_posts' => false,
125 ),
126 'map_meta_cap' => true,
127 'public' => false,
128 'show_in_rest' => false,
129 'rewrite' => false,
130 'query_var' => false,
131 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
132 'delete_with_user' => true,
133 'can_export' => true,
134 'exclude_from_search' => true,
135 )
136 );
137
138 // Register meta for Inbox post type.
139 \register_post_meta(
140 Inbox::POST_TYPE,
141 '_activitypub_object_id',
142 array(
143 'type' => 'string',
144 'single' => true,
145 'description' => 'The ID (ActivityPub URI) of the object that the inbox item is about.',
146 'sanitize_callback' => 'sanitize_url',
147 )
148 );
149
150 \register_post_meta(
151 Inbox::POST_TYPE,
152 '_activitypub_activity_type',
153 array(
154 'type' => 'string',
155 'description' => 'The type of the activity',
156 'single' => true,
157 'show_in_rest' => true,
158 'sanitize_callback' => static function ( $value ) {
159 $schema = array(
160 'type' => 'string',
161 'enum' => Activity::TYPES,
162 'default' => 'Create',
163 );
164
165 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
166 return $schema['default'];
167 }
168
169 return $value;
170 },
171 )
172 );
173
174 \register_post_meta(
175 Inbox::POST_TYPE,
176 '_activitypub_activity_remote_actor',
177 array(
178 'type' => 'string',
179 'single' => true,
180 'description' => 'The ID (ActivityPub URI) of the remote actor that sent the activity.',
181 'sanitize_callback' => 'sanitize_url',
182 )
183 );
184
185 \register_post_meta(
186 Inbox::POST_TYPE,
187 'activitypub_content_visibility',
188 array(
189 'type' => 'string',
190 'single' => true,
191 'show_in_rest' => true,
192 'sanitize_callback' => static function ( $value ) {
193 $schema = array(
194 'type' => 'string',
195 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
196 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
197 );
198
199 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
200 return $schema['default'];
201 }
202
203 return $value;
204 },
205 )
206 );
207
208 \register_post_meta(
209 Inbox::POST_TYPE,
210 '_activitypub_user_id',
211 array(
212 'type' => 'integer',
213 'single' => false, // Allow multiple values - one per recipient.
214 'description' => 'User ID of a recipient of this activity. Multiple entries allowed.',
215 'sanitize_callback' => 'absint',
216 'show_in_rest' => true,
217 )
218 );
219 }
220
221 /**
222 * Register the Outbox post type and its meta.
223 */
224 public static function register_outbox_post_type() {
225 \register_post_type(
226 Outbox::POST_TYPE,
227 array(
228 'labels' => array(
229 'name' => \_x( 'Outbox', 'post_type plural name', 'activitypub' ),
230 'singular_name' => \_x( 'Outbox Item', 'post_type single name', 'activitypub' ),
231 ),
232 'capabilities' => array(
233 'create_posts' => false,
234 ),
235 'map_meta_cap' => true,
236 'public' => false,
237 'show_in_rest' => false,
238 'rewrite' => false,
239 'query_var' => false,
240 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
241 'delete_with_user' => true,
242 'can_export' => true,
243 'exclude_from_search' => true,
244 )
245 );
246
247 // Register meta for Outbox post type.
248 /**
249 * Register Activity Type meta for Outbox items.
250 *
251 * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
252 */
253 \register_post_meta(
254 Outbox::POST_TYPE,
255 '_activitypub_activity_type',
256 array(
257 'type' => 'string',
258 'description' => 'The type of the activity',
259 'single' => true,
260 'show_in_rest' => true,
261 'sanitize_callback' => static function ( $value ) {
262 $schema = array(
263 'type' => 'string',
264 'enum' => Activity::TYPES,
265 'default' => 'Announce',
266 );
267
268 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
269 return $schema['default'];
270 }
271
272 return $value;
273 },
274 )
275 );
276
277 \register_post_meta(
278 Outbox::POST_TYPE,
279 '_activitypub_activity_actor',
280 array(
281 'type' => 'string',
282 'single' => true,
283 'show_in_rest' => true,
284 'sanitize_callback' => static function ( $value ) {
285 $schema = array(
286 'type' => 'string',
287 'enum' => array( 'application', 'blog', 'user' ),
288 'default' => 'user',
289 );
290
291 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
292 return $schema['default'];
293 }
294
295 return $value;
296 },
297 )
298 );
299
300 \register_post_meta(
301 Outbox::POST_TYPE,
302 '_activitypub_outbox_offset',
303 array(
304 'type' => 'integer',
305 'single' => true,
306 'description' => 'Keeps track of the followers offset when processing outbox items.',
307 'sanitize_callback' => 'absint',
308 'default' => 0,
309 )
310 );
311
312 \register_post_meta(
313 Outbox::POST_TYPE,
314 '_activitypub_object_id',
315 array(
316 'type' => 'string',
317 'single' => true,
318 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.',
319 'sanitize_callback' => 'sanitize_url',
320 )
321 );
322
323 \register_post_meta(
324 Outbox::POST_TYPE,
325 'activitypub_content_visibility',
326 array(
327 'type' => 'string',
328 'single' => true,
329 'show_in_rest' => true,
330 'sanitize_callback' => static function ( $value ) {
331 $schema = array(
332 'type' => 'string',
333 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
334 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
335 );
336
337 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
338 return $schema['default'];
339 }
340
341 return $value;
342 },
343 )
344 );
345 }
346
347 /**
348 * Register the Post post type.
349 */
350 public static function register_post_post_type() {
351 \register_post_type(
352 Remote_Posts::POST_TYPE,
353 array(
354 'labels' => array(
355 'name' => \_x( 'Posts', 'post_type plural name', 'activitypub' ),
356 'singular_name' => \_x( 'Post', 'post_type single name', 'activitypub' ),
357 ),
358 'capabilities' => array(
359 'activitypub' => true,
360 ),
361 'map_meta_cap' => true,
362 'public' => false,
363 'show_in_rest' => true,
364 'rewrite' => false,
365 'query_var' => false,
366 'supports' => array( 'title', 'editor', 'author', 'custom-fields', 'excerpt', 'comments' ),
367 'delete_with_user' => true,
368 'can_export' => true,
369 'exclude_from_search' => true,
370 'taxonomies' => array( 'ap_tag', 'ap_object_type' ),
371 )
372 );
373
374 \register_taxonomy(
375 'ap_tag',
376 array( Remote_Posts::POST_TYPE ),
377 array(
378 'public' => false,
379 'query_var' => true,
380 'show_in_rest' => true,
381 )
382 );
383
384 \register_taxonomy(
385 'ap_object_type',
386 array( Remote_Posts::POST_TYPE ),
387 array(
388 'public' => false,
389 'query_var' => true,
390 'show_in_rest' => true,
391 )
392 );
393
394 \register_post_meta(
395 Remote_Posts::POST_TYPE,
396 '_activitypub_remote_actor_id',
397 array(
398 'type' => 'integer',
399 'single' => true,
400 'description' => 'The local ID of the remote actor that created the object.',
401 'sanitize_callback' => 'absint',
402 )
403 );
404
405 \register_post_meta(
406 Remote_Posts::POST_TYPE,
407 '_activitypub_user_id',
408 array(
409 'type' => 'integer',
410 'single' => true,
411 'description' => 'The ID of the local user that received the activity.',
412 'sanitize_callback' => 'absint',
413 )
414 );
415 }
416
417 /**
418 * Register the Extra Fields post types.
419 */
420 public static function register_extra_fields_post_types() {
421 $extra_field_args = array(
422 'labels' => array(
423 'name' => \_x( 'Extra fields', 'post_type plural name', 'activitypub' ),
424 'singular_name' => \_x( 'Extra field', 'post_type single name', 'activitypub' ),
425 'add_new' => \__( 'Add new', 'activitypub' ),
426 'add_new_item' => \__( 'Add new extra field', 'activitypub' ),
427 'new_item' => \__( 'New extra field', 'activitypub' ),
428 'edit_item' => \__( 'Edit extra field', 'activitypub' ),
429 'view_item' => \__( 'View extra field', 'activitypub' ),
430 'all_items' => \__( 'All extra fields', 'activitypub' ),
431 ),
432 'public' => false,
433 'hierarchical' => false,
434 'query_var' => false,
435 'has_archive' => false,
436 'publicly_queryable' => false,
437 'show_in_menu' => false,
438 'delete_with_user' => true,
439 'can_export' => true,
440 'exclude_from_search' => true,
441 'show_in_rest' => true,
442 'map_meta_cap' => true,
443 'show_ui' => true,
444 'supports' => array( 'title', 'editor', 'page-attributes', 'author' ),
445 'capabilities' => array(
446 'create_posts' => 'activitypub', // Require activitypub capability to create extra fields.
447 'edit_others_posts' => 'do_not_allow', // Disallow editing others' Extra Fields (only own ones).
448 ),
449 );
450
451 \register_post_type( Extra_Fields::USER_POST_TYPE, $extra_field_args );
452
453 // Blog Extra Fields require manage_options capability.
454 $extra_field_args['capabilities'] = array( 'create_posts' => 'manage_options' );
455 \register_post_type( Extra_Fields::BLOG_POST_TYPE, $extra_field_args );
456
457 /**
458 * Fires after ActivityPub custom post types have been registered.
459 */
460 \do_action( 'activitypub_after_register_post_type' );
461 }
462
463 /**
464 * Register OAuth 2.0 post types for C2S support.
465 *
466 * Registers post type for OAuth clients.
467 * Note: Tokens are stored in user meta and authorization codes in transients.
468 */
469 public static function register_oauth_post_types() {
470 // OAuth Clients post type.
471 \register_post_type(
472 Client::POST_TYPE,
473 array(
474 'labels' => array(
475 'name' => \_x( 'OAuth Clients', 'post_type plural name', 'activitypub' ),
476 'singular_name' => \_x( 'OAuth Client', 'post_type single name', 'activitypub' ),
477 ),
478 'public' => false,
479 'show_in_rest' => false,
480 'hierarchical' => false,
481 'rewrite' => false,
482 'query_var' => false,
483 'delete_with_user' => false,
484 'can_export' => true,
485 'supports' => array( 'title', 'editor', 'custom-fields' ),
486 'exclude_from_search' => true,
487 )
488 );
489
490 // OAuth Client meta.
491 \register_post_meta(
492 Client::POST_TYPE,
493 '_activitypub_client_id',
494 array(
495 'type' => 'string',
496 'single' => true,
497 'description' => 'Unique OAuth client identifier (UUID).',
498 'sanitize_callback' => 'sanitize_text_field',
499 )
500 );
501
502 \register_post_meta(
503 Client::POST_TYPE,
504 '_activitypub_client_secret_hash',
505 array(
506 'type' => 'string',
507 'single' => true,
508 'description' => 'SHA-256 hash of the client secret (null for public clients).',
509 'sanitize_callback' => 'sanitize_text_field',
510 )
511 );
512
513 \register_post_meta(
514 Client::POST_TYPE,
515 '_activitypub_redirect_uris',
516 array(
517 'type' => 'array',
518 'single' => true,
519 'description' => 'Allowed redirect URIs for this client.',
520 'sanitize_callback' => static function ( $value ) {
521 if ( ! is_array( $value ) ) {
522 return array();
523 }
524 return array_map( array( Sanitize::class, 'redirect_uri' ), $value );
525 },
526 )
527 );
528
529 \register_post_meta(
530 Client::POST_TYPE,
531 '_activitypub_allowed_scopes',
532 array(
533 'type' => 'array',
534 'single' => true,
535 'description' => 'Allowed OAuth scopes for this client.',
536 'sanitize_callback' => array( Scope::class, 'sanitize' ),
537 )
538 );
539
540 \register_post_meta(
541 Client::POST_TYPE,
542 '_activitypub_is_public',
543 array(
544 'type' => 'boolean',
545 'single' => true,
546 'description' => 'Whether this is a public client (PKCE-only, no secret).',
547 'sanitize_callback' => 'rest_sanitize_boolean',
548 'default' => true,
549 )
550 );
551
552 \register_post_meta(
553 Client::POST_TYPE,
554 Token::USER_META_KEY,
555 array(
556 'type' => 'integer',
557 'single' => false,
558 'description' => 'User IDs that have active tokens for this client.',
559 'sanitize_callback' => 'absint',
560 )
561 );
562 }
563
564 /**
565 * Register post meta for ActivityPub supported post types.
566 */
567 public static function register_activitypub_post_meta() {
568 $ap_post_types = \get_post_types_by_support( 'activitypub' );
569 foreach ( $ap_post_types as $post_type ) {
570 \register_post_meta(
571 $post_type,
572 'activitypub_content_warning',
573 array(
574 'show_in_rest' => true,
575 'single' => true,
576 'type' => 'string',
577 'sanitize_callback' => 'sanitize_text_field',
578 )
579 );
580
581 \register_post_meta(
582 $post_type,
583 'activitypub_content_visibility',
584 array(
585 'type' => 'string',
586 'single' => true,
587 'show_in_rest' => true,
588 'sanitize_callback' => static function ( $value ) {
589 $schema = array(
590 'type' => 'string',
591 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
592 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
593 );
594
595 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
596 return $schema['default'];
597 }
598
599 return $value;
600 },
601 )
602 );
603
604 \register_post_meta(
605 $post_type,
606 'activitypub_max_image_attachments',
607 array(
608 'type' => 'integer',
609 'single' => true,
610 'show_in_rest' => true,
611 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ),
612 'sanitize_callback' => 'absint',
613 )
614 );
615
616 \register_post_meta(
617 $post_type,
618 'activitypub_interaction_policy_quote',
619 array(
620 'type' => 'string',
621 'single' => true,
622 'show_in_rest' => true,
623 'default' => \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE ),
624 'sanitize_callback' => static function ( $value ) {
625 $schema = array(
626 'type' => 'string',
627 'enum' => array( ACTIVITYPUB_INTERACTION_POLICY_ANYONE, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, ACTIVITYPUB_INTERACTION_POLICY_ME ),
628 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE,
629 );
630
631 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
632 return $schema['default'];
633 }
634
635 return $value;
636 },
637 )
638 );
639
640 \register_post_meta(
641 $post_type,
642 'activitypub_status',
643 array(
644 'type' => 'string',
645 'single' => true,
646 'show_in_rest' => true,
647 'sanitize_callback' => static function ( $value ) {
648 // Allow empty values to pass through without setting a default.
649 if ( empty( $value ) ) {
650 return '';
651 }
652
653 $schema = array(
654 'type' => 'string',
655 'enum' => array(
656 ACTIVITYPUB_OBJECT_STATE_PENDING,
657 ACTIVITYPUB_OBJECT_STATE_FEDERATED,
658 ACTIVITYPUB_OBJECT_STATE_FAILED,
659 ACTIVITYPUB_OBJECT_STATE_DELETED,
660 ),
661 'default' => '',
662 );
663
664 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
665 return $schema['default'];
666 }
667
668 return $value;
669 },
670 )
671 );
672 }
673 }
674
675 /**
676 * Register REST field for ap_actor posts.
677 */
678 public static function register_ap_actor_rest_field() {
679 \register_rest_field(
680 Remote_Actors::POST_TYPE,
681 'activitypub_json',
682 array(
683 /**
684 * Get the raw post content without WordPress content filtering.
685 *
686 * @param array $response Prepared response array.
687 * @return string The raw post content.
688 */
689 'get_callback' => static function ( $response ) {
690 return \get_post_field( 'post_content', $response['id'] );
691 },
692 'schema' => array(
693 'description' => 'Raw ActivityPub JSON data without WordPress content filtering',
694 'type' => 'string',
695 'context' => array( 'view', 'edit' ),
696 ),
697 )
698 );
699
700 // Add formatted actor data field.
701 \register_rest_field(
702 Remote_Actors::POST_TYPE,
703 'actor_info',
704 array(
705 'get_callback' => function ( $response ) {
706 $actor = Remote_Actors::get_actor( $response['id'] );
707 if ( \is_wp_error( $actor ) ) {
708 return null;
709 }
710 return array(
711 'username' => $actor->get_preferred_username(),
712 'name' => $actor->get_name() ?? $actor->get_preferred_username(),
713 'icon' => object_to_uri( $actor->get_icon() ),
714 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ),
715 'webfinger' => Remote_Actors::get_acct( $response['id'] ),
716 'identifier' => $actor->get_id(),
717 );
718 },
719 'schema' => array(
720 'description' => 'Parsed ActivityPub actor information',
721 'type' => 'object',
722 'context' => array( 'view', 'edit' ),
723 ),
724 )
725 );
726
727 // Add follow status field.
728 \register_rest_field(
729 Remote_Actors::POST_TYPE,
730 'follow_status',
731 array(
732 'get_callback' => function ( $response ) {
733 $current_user_id = \get_current_user_id();
734 if ( ! $current_user_id ) {
735 return array( 'follows_back' => false );
736 }
737 return array(
738 'follows_back' => Following::check_status( $current_user_id, $response['id'] ),
739 );
740 },
741 'schema' => array(
742 'description' => 'Follow relationship status',
743 'type' => 'object',
744 'context' => array( 'view', 'edit' ),
745 ),
746 )
747 );
748
749 // Add custom query parameter for filtering by follower relationships.
750 \add_filter( 'rest_ap_actor_query', array( self::class, 'filter_ap_actor_query_by_follower' ), 10, 2 );
751 }
752
753 /**
754 * Filter WP_Query args to support follower_of parameter.
755 *
756 * @param array $args Array of arguments for WP_Query.
757 * @param \WP_REST_Request $request The REST API request.
758 * @return array Modified query arguments.
759 */
760 public static function filter_ap_actor_query_by_follower( $args, $request ) {
761 if ( ! empty( $request['follower_of'] ) ) {
762 // Add meta_query to filter by _activitypub_following.
763 if ( ! isset( $args['meta_query'] ) ) {
764 $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
765 }
766
767 $args['meta_query'][] = array(
768 'key' => Followers::FOLLOWER_META_KEY,
769 'value' => $request['follower_of'],
770 );
771 }
772
773 return $args;
774 }
775
776 /**
777 * Register a REST field for the ap_post post type to embed remote actor data.
778 */
779 public static function register_ap_post_actor_rest_field() {
780 \register_rest_field(
781 Remote_Posts::POST_TYPE,
782 'actor_info',
783 array(
784 /**
785 * Get the remote actor data for an ap_post.
786 *
787 * @param array $response Prepared response array.
788 * @return array|null The actor data or null if not found.
789 */
790 'get_callback' => function ( $response ) {
791 $id = \get_post_meta( $response['id'], '_activitypub_remote_actor_id', true );
792 $actor = Remote_Actors::get_actor( $id );
793
794 if ( \is_wp_error( $actor ) ) {
795 return null;
796 }
797
798 return array(
799 'username' => $actor->get_preferred_username(),
800 'name' => $actor->get_name() ?? $actor->get_preferred_username(),
801 'icon' => object_to_uri( $actor->get_icon() ),
802 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ),
803 'webfinger' => Remote_Actors::get_acct( $id ),
804 'identifier' => $actor->get_id(),
805 );
806 },
807 'schema' => array(
808 'description' => 'Remote actor data',
809 'type' => 'object',
810 'context' => array( 'view', 'edit' ),
811 ),
812 )
813 );
814 }
815
816 /**
817 * Register custom REST API parameters for ap_post endpoint.
818 */
819 public static function register_ap_post_rest_params() {
820 \add_filter(
821 'rest_' . Remote_Posts::POST_TYPE . '_collection_params',
822 function ( $params ) {
823 $params['user_id'] = array(
824 'description' => __( 'Filter posts by user ID (0 for site/blog actor).', 'activitypub' ),
825 'type' => 'integer',
826 'sanitize_callback' => 'absint',
827 );
828
829 $params['ap_object_type'] = array(
830 'description' => 'Filter posts by ActivityPub object type.',
831 'type' => 'array',
832 'items' => array(
833 'type' => 'integer',
834 'minimum' => 0,
835 ),
836 );
837
838 $params['ap_tag'] = array(
839 'description' => 'Filter posts by ActivityPub tag (term IDs).',
840 'type' => 'array',
841 'items' => array(
842 'type' => 'integer',
843 'minimum' => 0,
844 ),
845 );
846
847 return $params;
848 }
849 );
850 }
851
852 /**
853 * Filter ap_post REST query to only show posts for the current user.
854 *
855 * @param array $args Query arguments.
856 * @param \WP_REST_Request $request The REST API request.
857 *
858 * @return array Modified query arguments.
859 */
860 public static function filter_ap_post_by_user( $args, $request ) {
861 $ap_tag = $request->get_param( 'ap_tag' );
862 if ( ! empty( $ap_tag ) ) {
863 if ( ! isset( $args['tax_query'] ) ) {
864 $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
865 }
866
867 $args['tax_query'][] = array(
868 'taxonomy' => 'ap_tag',
869 'field' => 'term_id',
870 'terms' => $ap_tag,
871 );
872
873 return $args;
874 }
875
876 // Filter by user_id (defaults to current user, use 0 for site/blog actor).
877 $user_id = isset( $request['user_id'] ) ? (int) $request['user_id'] : \get_current_user_id();
878
879 if ( ! isset( $args['meta_query'] ) ) {
880 $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
881 }
882
883 $args['meta_query'][] = array(
884 'key' => '_activitypub_user_id',
885 'value' => $user_id,
886 'compare' => '=',
887 );
888
889 // Filter by object type if provided.
890 if ( ! empty( $request['ap_object_type'] ) ) {
891 if ( ! isset( $args['tax_query'] ) ) {
892 $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
893 }
894
895 $args['tax_query'][] = array(
896 'taxonomy' => 'ap_object_type',
897 'field' => 'term_id',
898 'terms' => $request['ap_object_type'],
899 );
900 }
901
902 return $args;
903 }
904
905 /**
906 * Register user_id parameter for ap_object_type taxonomy REST API.
907 *
908 * @param array $params Existing collection parameters.
909 *
910 * @return array Modified collection parameters.
911 */
912 public static function register_object_type_user_param( $params ) {
913 $params['user_id'] = array(
914 'description' => __( 'Filter terms to those with posts from this user ID.', 'activitypub' ),
915 'type' => 'integer',
916 'sanitize_callback' => 'absint',
917 );
918
919 return $params;
920 }
921
922 /**
923 * Filter ap_object_type REST query to only return terms that have posts for the given user.
924 *
925 * Uses a direct SQL query to efficiently get term IDs without loading all post IDs.
926 *
927 * @param array $args Query arguments.
928 * @param \WP_REST_Request $request The REST API request.
929 *
930 * @return array Modified query arguments.
931 */
932 public static function filter_object_type_by_user( $args, $request ) {
933 $user_id = $request->get_param( 'user_id' );
934 if ( null === $user_id ) {
935 return $args;
936 }
937
938 global $wpdb;
939
940 // Get term IDs that have at least one ap_post for this user.
941 $term_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
942 $wpdb->prepare(
943 "SELECT DISTINCT tt.term_id
944 FROM {$wpdb->term_taxonomy} tt
945 INNER JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
946 INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
947 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
948 WHERE tt.taxonomy = 'ap_object_type'
949 AND p.post_type = 'ap_post'
950 AND pm.meta_key = '_activitypub_user_id'
951 AND pm.meta_value = %s",
952 $user_id
953 )
954 );
955
956 if ( empty( $term_ids ) ) {
957 // Force empty result.
958 $term_ids = array( 0 );
959 }
960
961 $args['include'] = \array_map( 'intval', $term_ids );
962
963 return $args;
964 }
965
966 /**
967 * Prevent empty or default meta values.
968 *
969 * @param null|bool $check Whether to allow updating metadata for the given type.
970 * @param int $object_id ID of the object metadata is for.
971 * @param string $meta_key Metadata key.
972 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
973 */
974 public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) {
975 $post_metas = array(
976 'activitypub_content_visibility' => '',
977 'activitypub_content_warning' => '',
978 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ),
979 );
980
981 if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) {
982 if ( 'update_post_metadata' === current_action() ) {
983 \delete_post_meta( $object_id, $meta_key );
984 }
985
986 $check = true;
987 }
988
989 return $check;
990 }
991 }
992