PluginProbe
ActivityPub / trunk
ActivityPub vtrunk
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 trunk, at includes/class-post-types.php

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