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

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

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