PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.2
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.0.2, at includes/class-post-types.php

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