PluginProbe
ActivityPub / 9.0.1
ActivityPub v9.0.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 9.0.1, at includes/class-post-types.php

1,027 lines 30.4 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 'labels' => array(
578 'name' => \_x( 'Tombstones', 'post_type plural name', 'activitypub' ),
579 'singular_name' => \_x( 'Tombstone', 'post_type single name', 'activitypub' ),
580 ),
581 'public' => false,
582 'publicly_queryable' => false,
583 'show_ui' => false,
584 'show_in_menu' => false,
585 'show_in_nav_menus' => false,
586 'show_in_admin_bar' => false,
587 'show_in_rest' => false,
588 'exclude_from_search' => true,
589 'has_archive' => false,
590 'rewrite' => false,
591 'query_var' => false,
592 'can_export' => false,
593 'delete_with_user' => false,
594 'supports' => array(),
595 )
596 );
597 }
598
599 /**
600 * Register post meta for ActivityPub supported post types.
601 */
602 public static function register_activitypub_post_meta() {
603 $ap_post_types = \get_post_types_by_support( 'activitypub' );
604 foreach ( $ap_post_types as $post_type ) {
605 \register_post_meta(
606 $post_type,
607 'activitypub_content_warning',
608 array(
609 'show_in_rest' => true,
610 'single' => true,
611 'type' => 'string',
612 'sanitize_callback' => 'sanitize_text_field',
613 )
614 );
615
616 \register_post_meta(
617 $post_type,
618 'activitypub_content_visibility',
619 array(
620 'type' => 'string',
621 'single' => true,
622 'show_in_rest' => true,
623 'sanitize_callback' => static function ( $value ) {
624 $schema = array(
625 'type' => 'string',
626 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
627 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
628 );
629
630 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
631 return $schema['default'];
632 }
633
634 return $value;
635 },
636 )
637 );
638
639 \register_post_meta(
640 $post_type,
641 'activitypub_max_image_attachments',
642 array(
643 'type' => 'integer',
644 'single' => true,
645 'show_in_rest' => true,
646 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ),
647 'sanitize_callback' => 'absint',
648 )
649 );
650
651 \register_post_meta(
652 $post_type,
653 'activitypub_interaction_policy_quote',
654 array(
655 'type' => 'string',
656 'single' => true,
657 'show_in_rest' => true,
658 'default' => \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE ),
659 'sanitize_callback' => static function ( $value ) {
660 $schema = array(
661 'type' => 'string',
662 'enum' => array( ACTIVITYPUB_INTERACTION_POLICY_ANYONE, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, ACTIVITYPUB_INTERACTION_POLICY_ME ),
663 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE,
664 );
665
666 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
667 return $schema['default'];
668 }
669
670 return $value;
671 },
672 )
673 );
674
675 \register_post_meta(
676 $post_type,
677 'activitypub_status',
678 array(
679 'type' => 'string',
680 'single' => true,
681 'show_in_rest' => true,
682 'sanitize_callback' => static function ( $value ) {
683 // Allow empty values to pass through without setting a default.
684 if ( empty( $value ) ) {
685 return '';
686 }
687
688 $schema = array(
689 'type' => 'string',
690 'enum' => array(
691 ACTIVITYPUB_OBJECT_STATE_PENDING,
692 ACTIVITYPUB_OBJECT_STATE_FEDERATED,
693 ACTIVITYPUB_OBJECT_STATE_FAILED,
694 ACTIVITYPUB_OBJECT_STATE_DELETED,
695 ),
696 'default' => '',
697 );
698
699 if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) {
700 return $schema['default'];
701 }
702
703 return $value;
704 },
705 )
706 );
707 }
708 }
709
710 /**
711 * Register REST field for ap_actor posts.
712 */
713 public static function register_ap_actor_rest_field() {
714 \register_rest_field(
715 Remote_Actors::POST_TYPE,
716 'activitypub_json',
717 array(
718 /**
719 * Get the raw post content without WordPress content filtering.
720 *
721 * @param array $response Prepared response array.
722 * @return string The raw post content.
723 */
724 'get_callback' => static function ( $response ) {
725 return \get_post_field( 'post_content', $response['id'] );
726 },
727 'schema' => array(
728 'description' => 'Raw ActivityPub JSON data without WordPress content filtering',
729 'type' => 'string',
730 'context' => array( 'view', 'edit' ),
731 ),
732 )
733 );
734
735 // Add formatted actor data field.
736 \register_rest_field(
737 Remote_Actors::POST_TYPE,
738 'actor_info',
739 array(
740 'get_callback' => function ( $response ) {
741 $actor = Remote_Actors::get_actor( $response['id'] );
742 if ( \is_wp_error( $actor ) ) {
743 return null;
744 }
745 return array(
746 'username' => $actor->get_preferred_username(),
747 'name' => $actor->get_name() ?? $actor->get_preferred_username(),
748 'icon' => object_to_uri( $actor->get_icon() ),
749 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ),
750 'webfinger' => Remote_Actors::get_acct( $response['id'] ),
751 'identifier' => $actor->get_id(),
752 );
753 },
754 'schema' => array(
755 'description' => 'Parsed ActivityPub actor information',
756 'type' => 'object',
757 'context' => array( 'view', 'edit' ),
758 ),
759 )
760 );
761
762 // Add follow status field.
763 \register_rest_field(
764 Remote_Actors::POST_TYPE,
765 'follow_status',
766 array(
767 'get_callback' => function ( $response ) {
768 $current_user_id = \get_current_user_id();
769 if ( ! $current_user_id ) {
770 return array( 'follows_back' => false );
771 }
772 return array(
773 'follows_back' => Following::check_status( $current_user_id, $response['id'] ),
774 );
775 },
776 'schema' => array(
777 'description' => 'Follow relationship status',
778 'type' => 'object',
779 'context' => array( 'view', 'edit' ),
780 ),
781 )
782 );
783
784 // Add custom query parameter for filtering by follower relationships.
785 \add_filter( 'rest_ap_actor_query', array( self::class, 'filter_ap_actor_query_by_follower' ), 10, 2 );
786 }
787
788 /**
789 * Filter WP_Query args to support follower_of parameter.
790 *
791 * @param array $args Array of arguments for WP_Query.
792 * @param \WP_REST_Request $request The REST API request.
793 * @return array Modified query arguments.
794 */
795 public static function filter_ap_actor_query_by_follower( $args, $request ) {
796 if ( ! empty( $request['follower_of'] ) ) {
797 // Add meta_query to filter by _activitypub_following.
798 if ( ! isset( $args['meta_query'] ) ) {
799 $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
800 }
801
802 $args['meta_query'][] = array(
803 'key' => Followers::FOLLOWER_META_KEY,
804 'value' => $request['follower_of'],
805 );
806 }
807
808 return $args;
809 }
810
811 /**
812 * Register a REST field for the ap_post post type to embed remote actor data.
813 */
814 public static function register_ap_post_actor_rest_field() {
815 \register_rest_field(
816 Remote_Posts::POST_TYPE,
817 'actor_info',
818 array(
819 /**
820 * Get the remote actor data for an ap_post.
821 *
822 * @param array $response Prepared response array.
823 * @return array|null The actor data or null if not found.
824 */
825 'get_callback' => function ( $response ) {
826 $id = \get_post_meta( $response['id'], '_activitypub_remote_actor_id', true );
827 $actor = Remote_Actors::get_actor( $id );
828
829 if ( \is_wp_error( $actor ) ) {
830 return null;
831 }
832
833 return array(
834 'username' => $actor->get_preferred_username(),
835 'name' => $actor->get_name() ?? $actor->get_preferred_username(),
836 'icon' => object_to_uri( $actor->get_icon() ),
837 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ),
838 'webfinger' => Remote_Actors::get_acct( $id ),
839 'identifier' => $actor->get_id(),
840 );
841 },
842 'schema' => array(
843 'description' => 'Remote actor data',
844 'type' => 'object',
845 'context' => array( 'view', 'edit' ),
846 ),
847 )
848 );
849 }
850
851 /**
852 * Register custom REST API parameters for ap_post endpoint.
853 */
854 public static function register_ap_post_rest_params() {
855 \add_filter(
856 'rest_' . Remote_Posts::POST_TYPE . '_collection_params',
857 function ( $params ) {
858 $params['user_id'] = array(
859 'description' => __( 'Filter posts by user ID (0 for site/blog actor).', 'activitypub' ),
860 'type' => 'integer',
861 'sanitize_callback' => 'absint',
862 );
863
864 $params['ap_object_type'] = array(
865 'description' => 'Filter posts by ActivityPub object type.',
866 'type' => 'array',
867 'items' => array(
868 'type' => 'integer',
869 'minimum' => 0,
870 ),
871 );
872
873 $params['ap_tag'] = array(
874 'description' => 'Filter posts by ActivityPub tag (term IDs).',
875 'type' => 'array',
876 'items' => array(
877 'type' => 'integer',
878 'minimum' => 0,
879 ),
880 );
881
882 return $params;
883 }
884 );
885 }
886
887 /**
888 * Filter ap_post REST query to only show posts for the current user.
889 *
890 * @param array $args Query arguments.
891 * @param \WP_REST_Request $request The REST API request.
892 *
893 * @return array Modified query arguments.
894 */
895 public static function filter_ap_post_by_user( $args, $request ) {
896 $ap_tag = $request->get_param( 'ap_tag' );
897 if ( ! empty( $ap_tag ) ) {
898 if ( ! isset( $args['tax_query'] ) ) {
899 $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
900 }
901
902 $args['tax_query'][] = array(
903 'taxonomy' => 'ap_tag',
904 'field' => 'term_id',
905 'terms' => $ap_tag,
906 );
907
908 return $args;
909 }
910
911 // Filter by user_id (defaults to current user, use 0 for site/blog actor).
912 $user_id = isset( $request['user_id'] ) ? (int) $request['user_id'] : \get_current_user_id();
913
914 if ( ! isset( $args['meta_query'] ) ) {
915 $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
916 }
917
918 $args['meta_query'][] = array(
919 'key' => '_activitypub_user_id',
920 'value' => $user_id,
921 'compare' => '=',
922 );
923
924 // Filter by object type if provided.
925 if ( ! empty( $request['ap_object_type'] ) ) {
926 if ( ! isset( $args['tax_query'] ) ) {
927 $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
928 }
929
930 $args['tax_query'][] = array(
931 'taxonomy' => 'ap_object_type',
932 'field' => 'term_id',
933 'terms' => $request['ap_object_type'],
934 );
935 }
936
937 return $args;
938 }
939
940 /**
941 * Register user_id parameter for ap_object_type taxonomy REST API.
942 *
943 * @param array $params Existing collection parameters.
944 *
945 * @return array Modified collection parameters.
946 */
947 public static function register_object_type_user_param( $params ) {
948 $params['user_id'] = array(
949 'description' => __( 'Filter terms to those with posts from this user ID.', 'activitypub' ),
950 'type' => 'integer',
951 'sanitize_callback' => 'absint',
952 );
953
954 return $params;
955 }
956
957 /**
958 * Filter ap_object_type REST query to only return terms that have posts for the given user.
959 *
960 * Uses a direct SQL query to efficiently get term IDs without loading all post IDs.
961 *
962 * @param array $args Query arguments.
963 * @param \WP_REST_Request $request The REST API request.
964 *
965 * @return array Modified query arguments.
966 */
967 public static function filter_object_type_by_user( $args, $request ) {
968 $user_id = $request->get_param( 'user_id' );
969 if ( null === $user_id ) {
970 return $args;
971 }
972
973 global $wpdb;
974
975 // Get term IDs that have at least one ap_post for this user.
976 $term_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
977 $wpdb->prepare(
978 "SELECT DISTINCT tt.term_id
979 FROM {$wpdb->term_taxonomy} tt
980 INNER JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
981 INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID
982 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
983 WHERE tt.taxonomy = 'ap_object_type'
984 AND p.post_type = 'ap_post'
985 AND pm.meta_key = '_activitypub_user_id'
986 AND pm.meta_value = %s",
987 $user_id
988 )
989 );
990
991 if ( empty( $term_ids ) ) {
992 // Force empty result.
993 $term_ids = array( 0 );
994 }
995
996 $args['include'] = \array_map( 'intval', $term_ids );
997
998 return $args;
999 }
1000
1001 /**
1002 * Prevent empty or default meta values.
1003 *
1004 * @param null|bool $check Whether to allow updating metadata for the given type.
1005 * @param int $object_id ID of the object metadata is for.
1006 * @param string $meta_key Metadata key.
1007 * @param mixed $meta_value Metadata value. Must be serializable if non-scalar.
1008 */
1009 public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) {
1010 $post_metas = array(
1011 'activitypub_content_visibility' => '',
1012 'activitypub_content_warning' => '',
1013 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ),
1014 );
1015
1016 if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) {
1017 if ( 'update_post_metadata' === current_action() ) {
1018 \delete_post_meta( $object_id, $meta_key );
1019 }
1020
1021 $check = true;
1022 }
1023
1024 return $check;
1025 }
1026 }
1027