diff --git a/activitypub.php b/activitypub.php index c07103e3a..b15eedc35 100644 --- a/activitypub.php +++ b/activitypub.php @@ -80,6 +80,7 @@ function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ), 1 ); \add_action( 'init', array( __NAMESPACE__ . '\Move', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Options', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Post_Types', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Search', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Signature', 'init' ) ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index b1e1bb0e4..d2af98adb 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -7,14 +7,10 @@ namespace Activitypub; -use Activitypub\Activity\Activity; use Activitypub\Collection\Actors; -use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; use Activitypub\Collection\Following; -use Activitypub\Collection\Inbox; use Activitypub\Collection\Outbox; -use Activitypub\Collection\Remote_Actors; /** * ActivityPub Class. @@ -29,9 +25,6 @@ public static function init() { \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 ); \add_action( 'init', array( self::class, 'theme_compat' ), 11 ); \add_action( 'init', array( self::class, 'register_user_meta' ), 11 ); - \add_action( 'init', array( self::class, 'register_post_types' ), 11 ); - - \add_action( 'rest_api_init', array( self::class, 'register_ap_actor_rest_field' ) ); \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 ); \add_action( 'template_redirect', array( self::class, 'template_redirect' ) ); @@ -45,18 +38,8 @@ public static function init() { \add_action( 'user_register', array( self::class, 'user_register' ) ); - \add_filter( 'add_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 ); - \add_filter( 'update_post_metadata', array( self::class, 'prevent_empty_post_meta' ), 10, 4 ); - \add_filter( 'default_post_metadata', array( self::class, 'default_post_metadata' ), 10, 3 ); - - \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 ); \add_action( 'activitypub_add_user_block', array( Followers::class, 'remove_blocked_actors' ), 10, 3 ); \add_action( 'activitypub_add_user_block', array( Following::class, 'remove_blocked_actors' ), 10, 3 ); - - // Add support for ActivityPub to custom post types. - foreach ( \get_option( 'activitypub_support_post_types', array( 'post' ) ) as $post_type ) { - \add_post_type_support( $post_type, 'activitypub' ); - } } /** @@ -471,369 +454,6 @@ public static function theme_compat() { } } - /** - * Register Custom Post Types. - */ - public static function register_post_types() { - \register_post_type( - Remote_Actors::POST_TYPE, - array( - 'labels' => array( - 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), - ), - 'public' => false, - 'show_in_rest' => true, - 'hierarchical' => false, - 'rewrite' => false, - 'query_var' => false, - 'delete_with_user' => false, - 'can_export' => true, - 'supports' => array(), - ) - ); - - \register_post_meta( - Remote_Actors::POST_TYPE, - '_activitypub_inbox', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => 'sanitize_url', - ) - ); - - \register_post_meta( - Remote_Actors::POST_TYPE, - '_activitypub_errors', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - if ( ! is_string( $value ) ) { - throw new \Exception( 'Error message is no valid string' ); - } - - return esc_sql( $value ); - }, - ) - ); - - \register_post_meta( - Remote_Actors::POST_TYPE, - Followers::FOLLOWER_META_KEY, - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - return esc_sql( $value ); - }, - ) - ); - - // Register Inbox Post-Type. - \register_post_type( - Inbox::POST_TYPE, - array( - 'labels' => array( - 'name' => _x( 'Inbox', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Inbox Item', 'post_type single name', 'activitypub' ), - ), - 'capabilities' => array( - 'create_posts' => false, - ), - 'map_meta_cap' => true, - 'public' => false, - 'show_in_rest' => true, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), - 'delete_with_user' => true, - 'can_export' => true, - 'exclude_from_search' => true, - ) - ); - - // Register Inbox Post-Meta. - \register_post_meta( - Inbox::POST_TYPE, - '_activitypub_object_id', - array( - 'type' => 'string', - 'single' => true, - 'description' => 'The ID (ActivityPub URI) of the object that the inbox item is about.', - 'sanitize_callback' => 'sanitize_url', - ) - ); - - \register_post_meta( - Inbox::POST_TYPE, - '_activitypub_activity_type', - array( - 'type' => 'string', - 'description' => 'The type of the activity', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $value = ucfirst( strtolower( $value ) ); - $schema = array( - 'type' => 'string', - 'enum' => Activity::TYPES, - 'default' => 'Create', - ); - - if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - \register_post_meta( - Inbox::POST_TYPE, - '_activitypub_activity_actor', - array( - 'type' => 'string', - 'single' => true, - 'description' => 'The type of the local actor that received the activity.', - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( 'application', 'blog', 'user' ), - 'default' => 'user', - ); - - if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - \register_post_meta( - Inbox::POST_TYPE, - '_activitypub_activity_remote_actor', - array( - 'type' => 'string', - 'single' => true, - 'description' => 'The ID (ActivityPub URI) of the remote actor that sent the activity.', - 'sanitize_callback' => 'sanitize_url', - ) - ); - - \register_post_meta( - Inbox::POST_TYPE, - 'activitypub_content_visibility', - array( - 'type' => 'string', - 'single' => true, - 'description' => 'The visibility of the content.', - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( 'public', 'unlisted', 'private', 'direct' ), - 'default' => 'public', - ); - - if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - // Register Outbox Post-Type. - \register_post_type( - Outbox::POST_TYPE, - array( - 'labels' => array( - 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ), - ), - 'capabilities' => array( - 'create_posts' => false, - ), - 'map_meta_cap' => true, - 'public' => false, - 'show_in_rest' => true, - 'rewrite' => false, - 'query_var' => false, - 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), - 'delete_with_user' => true, - 'can_export' => true, - 'exclude_from_search' => true, - ) - ); - - /** - * Register Activity Type meta for Outbox items. - * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types - */ - \register_post_meta( - Outbox::POST_TYPE, - '_activitypub_activity_type', - array( - 'type' => 'string', - 'description' => 'The type of the activity', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $value = ucfirst( strtolower( $value ) ); - $schema = array( - 'type' => 'string', - 'enum' => Activity::TYPES, - 'default' => 'Announce', - ); - - if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - \register_post_meta( - Outbox::POST_TYPE, - '_activitypub_activity_actor', - array( - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( 'application', 'blog', 'user' ), - 'default' => 'user', - ); - - if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - \register_post_meta( - Outbox::POST_TYPE, - '_activitypub_outbox_offset', - array( - 'type' => 'integer', - 'single' => true, - 'description' => 'Keeps track of the followers offset when processing outbox items.', - 'sanitize_callback' => 'absint', - 'default' => 0, - ) - ); - - \register_post_meta( - Outbox::POST_TYPE, - '_activitypub_object_id', - array( - 'type' => 'string', - 'single' => true, - 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.', - 'sanitize_callback' => 'sanitize_url', - ) - ); - - \register_post_meta( - Outbox::POST_TYPE, - 'activitypub_content_visibility', - array( - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), - 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, - ); - - if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - // Both User and Blog Extra Fields types have the same args. - $extra_field_args = array( - 'labels' => array( - 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ), - 'add_new' => __( 'Add new', 'activitypub' ), - 'add_new_item' => __( 'Add new extra field', 'activitypub' ), - 'new_item' => __( 'New extra field', 'activitypub' ), - 'edit_item' => __( 'Edit extra field', 'activitypub' ), - 'view_item' => __( 'View extra field', 'activitypub' ), - 'all_items' => __( 'All extra fields', 'activitypub' ), - ), - 'public' => false, - 'hierarchical' => false, - 'query_var' => false, - 'has_archive' => false, - 'publicly_queryable' => false, - 'show_in_menu' => false, - 'delete_with_user' => true, - 'can_export' => true, - 'exclude_from_search' => true, - 'show_in_rest' => true, - 'map_meta_cap' => true, - 'show_ui' => true, - 'supports' => array( 'title', 'editor', 'page-attributes' ), - ); - - \register_post_type( Extra_Fields::USER_POST_TYPE, $extra_field_args ); - \register_post_type( Extra_Fields::BLOG_POST_TYPE, $extra_field_args ); - - /** - * Fires after ActivityPub custom post types have been registered. - */ - \do_action( 'activitypub_after_register_post_type' ); - } - - /** - * Register REST field for ap_actor posts. - */ - public static function register_ap_actor_rest_field() { - \register_rest_field( - Remote_Actors::POST_TYPE, - 'activitypub_json', - array( - /** - * Get the raw post content without WordPress content filtering. - * - * @param array $response Prepared response array. - * @return string The raw post content. - */ - 'get_callback' => function ( $response ) { - return \get_post_field( 'post_content', $response['id'] ); - }, - 'schema' => array( - 'description' => 'Raw ActivityPub JSON data without WordPress content filtering', - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - ), - ) - ); - } - /** * Add the 'activitypub' capability to users who can publish posts. * @@ -846,67 +466,6 @@ public static function user_register( $user_id ) { } } - /** - * Prevent empty or default meta values. - * - * @param null|bool $check Whether to allow updating metadata for the given type. - * @param int $object_id ID of the object metadata is for. - * @param string $meta_key Metadata key. - * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. - */ - public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) { - $post_metas = array( - 'activitypub_content_visibility' => '', - 'activitypub_content_warning' => '', - 'activitypub_interaction_policy_quote' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, - 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), - ); - - if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) { - if ( 'update_post_metadata' === current_action() ) { - \delete_post_meta( $object_id, $meta_key ); - } - - $check = true; - } - - return $check; - } - - /** - * Adjusts default post meta values. - * - * @param mixed $meta_value The meta value. - * @param int $object_id ID of the object metadata is for. - * @param string $meta_key Metadata key. - * - * @return mixed The meta value. - */ - public static function default_post_metadata( $meta_value, $object_id, $meta_key ) { - // Check if the meta key is `activitypub_content_visibility`. - if ( 'activitypub_content_visibility' !== $meta_key ) { - return $meta_value; - } - - // If meta value is already explicitly set, respect the author's choice. - if ( null !== $meta_value ) { - return $meta_value; - } - - // If the post is federated, return the default visibility. - if ( 'federated' === \get_post_meta( $object_id, 'activitypub_status', true ) ) { - return $meta_value; - } - - // If the post is not federated and older than a year, return local visibility. - $date = \get_the_date( 'U', $object_id ); - if ( $date < \strtotime( '-1 month' ) ) { - return ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL; - } - - return $meta_value; - } - /** * Register user meta. */ @@ -950,7 +509,7 @@ public static function register_user_meta() { $blog_prefix . 'activitypub_description', array( 'type' => 'string', - 'description' => 'The user’s description.', + 'description' => 'The user description.', 'single' => true, 'default' => '', 'sanitize_callback' => function ( $value ) { @@ -964,7 +523,7 @@ public static function register_user_meta() { $blog_prefix . 'activitypub_icon', array( 'type' => 'integer', - 'description' => 'The attachment ID for user’s profile image.', + 'description' => 'The attachment ID for user profile image.', 'single' => true, 'default' => 0, 'sanitize_callback' => 'absint', @@ -976,7 +535,7 @@ public static function register_user_meta() { $blog_prefix . 'activitypub_header_image', array( 'type' => 'integer', - 'description' => 'The attachment ID for the user’s header image.', + 'description' => 'The attachment ID for the user header image.', 'single' => true, 'default' => 0, 'sanitize_callback' => 'absint', diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 5d574422c..4c1a2430e 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -23,7 +23,6 @@ public static function init() { \add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) ); // Add editor plugin. \add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) ); - \add_action( 'init', array( self::class, 'register_postmeta' ), 11 ); \add_action( 'rest_api_init', array( self::class, 'register_rest_fields' ) ); \add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 ); @@ -32,83 +31,6 @@ public static function init() { \add_filter( 'activitypub_the_content', array( self::class, 'remove_post_transformation_callbacks' ) ); } - /** - * Register post meta for content warnings. - */ - public static function register_postmeta() { - $ap_post_types = \get_post_types_by_support( 'activitypub' ); - foreach ( $ap_post_types as $post_type ) { - \register_post_meta( - $post_type, - 'activitypub_content_warning', - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', - ) - ); - - \register_post_meta( - $post_type, - 'activitypub_content_visibility', - array( - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), - 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, - ); - - if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - - \register_post_meta( - $post_type, - 'activitypub_max_image_attachments', - array( - 'type' => 'integer', - 'single' => true, - 'show_in_rest' => true, - 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), - 'sanitize_callback' => 'absint', - ) - ); - - \register_post_meta( - $post_type, - 'activitypub_interaction_policy_quote', - array( - 'type' => 'string', - 'single' => true, - 'show_in_rest' => true, - 'sanitize_callback' => function ( $value ) { - $schema = array( - 'type' => 'string', - 'enum' => array( ACTIVITYPUB_INTERACTION_POLICY_ANYONE, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, ACTIVITYPUB_INTERACTION_POLICY_ME ), - 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, - ); - - if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { - return $schema['default']; - } - - return $value; - }, - ) - ); - } - } - /** * Enqueue the block editor assets. */ diff --git a/includes/class-post-types.php b/includes/class-post-types.php new file mode 100644 index 000000000..d45ce83f2 --- /dev/null +++ b/includes/class-post-types.php @@ -0,0 +1,548 @@ + array( + 'name' => \_x( 'Followers', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Follower', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'show_in_rest' => true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) + ); + + // Register meta for Remote Actors post type. + \register_post_meta( + Remote_Actors::POST_TYPE, + '_activitypub_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Remote_Actors::POST_TYPE, + '_activitypub_errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + Remote_Actors::POST_TYPE, + Followers::FOLLOWER_META_KEY, + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + } + + /** + * Register the Inbox post type and its meta. + */ + public static function register_inbox_post_type() { + \register_post_type( + Inbox::POST_TYPE, + array( + 'labels' => array( + 'name' => \_x( 'Inbox', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Inbox Item', 'post_type single name', 'activitypub' ), + ), + 'capabilities' => array( + 'create_posts' => false, + ), + 'map_meta_cap' => true, + 'public' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + ) + ); + + // Register meta for Inbox post type. + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_object_id', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the object that the inbox item is about.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_activity_type', + array( + 'type' => 'string', + 'description' => 'The type of the activity', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $value = ucfirst( strtolower( $value ) ); + $schema = array( + 'type' => 'string', + 'enum' => Activity::TYPES, + 'default' => 'Create', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_activity_actor', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The type of the local actor that received the activity.', + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( 'application', 'blog', 'user' ), + 'default' => 'user', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_activity_remote_actor', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the remote actor that sent the activity.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + } + + /** + * Register the Outbox post type and its meta. + */ + public static function register_outbox_post_type() { + \register_post_type( + Outbox::POST_TYPE, + array( + 'labels' => array( + 'name' => \_x( 'Outbox', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Outbox Item', 'post_type single name', 'activitypub' ), + ), + 'capabilities' => array( + 'create_posts' => false, + ), + 'map_meta_cap' => true, + 'public' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + ) + ); + + // Register meta for Outbox post type. + /** + * Register Activity Type meta for Outbox items. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + */ + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_activity_type', + array( + 'type' => 'string', + 'description' => 'The type of the activity', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $value = ucfirst( strtolower( $value ) ); + $schema = array( + 'type' => 'string', + 'enum' => Activity::TYPES, + 'default' => 'Announce', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_activity_actor', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( 'application', 'blog', 'user' ), + 'default' => 'user', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_outbox_offset', + array( + 'type' => 'integer', + 'single' => true, + 'description' => 'Keeps track of the followers offset when processing outbox items.', + 'sanitize_callback' => 'absint', + 'default' => 0, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_object_id', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + } + + /** + * Register the Extra Fields post types. + */ + public static function register_extra_fields_post_types() { + $extra_field_args = array( + 'labels' => array( + 'name' => \_x( 'Extra fields', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Extra field', 'post_type single name', 'activitypub' ), + 'add_new' => \__( 'Add new', 'activitypub' ), + 'add_new_item' => \__( 'Add new extra field', 'activitypub' ), + 'new_item' => \__( 'New extra field', 'activitypub' ), + 'edit_item' => \__( 'Edit extra field', 'activitypub' ), + 'view_item' => \__( 'View extra field', 'activitypub' ), + 'all_items' => \__( 'All extra fields', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'query_var' => false, + 'has_archive' => false, + 'publicly_queryable' => false, + 'show_in_menu' => false, + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + 'show_in_rest' => true, + 'map_meta_cap' => true, + 'show_ui' => true, + 'supports' => array( 'title', 'editor', 'page-attributes' ), + ); + + \register_post_type( Extra_Fields::USER_POST_TYPE, $extra_field_args ); + \register_post_type( Extra_Fields::BLOG_POST_TYPE, $extra_field_args ); + + /** + * Fires after ActivityPub custom post types have been registered. + */ + \do_action( 'activitypub_after_register_post_type' ); + } + + /** + * Register post meta for ActivityPub supported post types. + */ + public static function register_activitypub_post_meta() { + $ap_post_types = \get_post_types_by_support( 'activitypub' ); + foreach ( $ap_post_types as $post_type ) { + \register_post_meta( + $post_type, + 'activitypub_content_warning', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_max_image_attachments', + array( + 'type' => 'integer', + 'single' => true, + 'show_in_rest' => true, + 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), + 'sanitize_callback' => 'absint', + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_interaction_policy_quote', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_INTERACTION_POLICY_ANYONE, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, ACTIVITYPUB_INTERACTION_POLICY_ME ), + 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + } + } + + /** + * Register REST field for ap_actor posts. + */ + public static function register_ap_actor_rest_field() { + \register_rest_field( + Remote_Actors::POST_TYPE, + 'activitypub_json', + array( + /** + * Get the raw post content without WordPress content filtering. + * + * @param array $response Prepared response array. + * @return string The raw post content. + */ + 'get_callback' => function ( $response ) { + return \get_post_field( 'post_content', $response['id'] ); + }, + 'schema' => array( + 'description' => 'Raw ActivityPub JSON data without WordPress content filtering', + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + } + + /** + * Prevent empty or default meta values. + * + * @param null|bool $check Whether to allow updating metadata for the given type. + * @param int $object_id ID of the object metadata is for. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + */ + public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) { + $post_metas = array( + 'activitypub_content_visibility' => '', + 'activitypub_content_warning' => '', + 'activitypub_interaction_policy_quote' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, + 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), + ); + + if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) { + if ( 'update_post_metadata' === current_action() ) { + \delete_post_meta( $object_id, $meta_key ); + } + + $check = true; + } + + return $check; + } + + /** + * Adjusts default post meta values. + * + * @param mixed $meta_value The meta value. + * @param int $object_id ID of the object metadata is for. + * @param string $meta_key Metadata key. + * + * @return string|null The meta value. + */ + public static function default_post_meta_data( $meta_value, $object_id, $meta_key ) { + if ( 'activitypub_content_visibility' !== $meta_key ) { + return $meta_value; + } + + // If meta value is already explicitly set, respect the author's choice. + if ( null !== $meta_value ) { + return $meta_value; + } + + // If the post is federated, return the default visibility. + if ( 'federated' === \get_post_meta( $object_id, 'activitypub_status', true ) ) { + return null; + } + + // If the post is not federated and older than a month, return local visibility. + if ( \get_the_date( 'U', $object_id ) < \strtotime( '-1 month' ) ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL; + } + + return null; + } +} diff --git a/tests/includes/class-test-activitypub.php b/tests/includes/class-test-activitypub.php index 15bc66498..7d13b6050 100644 --- a/tests/includes/class-test-activitypub.php +++ b/tests/includes/class-test-activitypub.php @@ -104,7 +104,7 @@ function () { * Test activity type meta sanitization. * * @dataProvider activity_meta_sanitization_provider - * @covers ::register_post_types + * @covers \Activitypub\Post_Types::register_outbox_post_type * * @param string $meta_key Meta key. * @param mixed $meta_value Meta value. @@ -294,80 +294,4 @@ public function test_no_trailing_redirect() { // Clean up. set_query_var( 'actor', null ); } - - /** - * Test prevent_empty_post_meta method. - * - * @covers ::prevent_empty_post_meta - */ - public function test_prevent_empty_post_meta() { - $post_id = self::factory()->post->create( - array( - 'post_author' => 1, - ) - ); - - \update_post_meta( $post_id, 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ); - $this->assertEmpty( \get_post_meta( $post_id, 'activitypub_max_image_attachments', true ) ); - \delete_post_meta( $post_id, 'activitypub_max_image_attachments' ); - - \update_post_meta( $post_id, 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS + 3 ); - $this->assertEquals( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS + 3, \get_post_meta( $post_id, 'activitypub_max_image_attachments', true ) ); - \delete_post_meta( $post_id, 'activitypub_max_image_attachments' ); - - \wp_delete_post( $post_id, true ); - } - - /** - * Test get_post_metadata method. - * - * @covers ::default_post_metadata - */ - public function test_get_post_metadata() { - // Create a test post. - $post_id = self::factory()->post->create( - array( - 'post_author' => 1, - 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 months' ) ), // Post older than a month. - ) - ); - - // Test 1: When meta_key is not 'activitypub_content_visibility', should return the original value. - $result = Activitypub::default_post_metadata( 'original_value', $post_id, 'some_other_key' ); - $this->assertEquals( 'original_value', $result, 'Should return original value for non-matching meta key.' ); - - // Test 2: When post is federated, should return the original value. - \update_post_meta( $post_id, 'activitypub_status', 'federated' ); - $result = Activitypub::default_post_metadata( 'original_value', $post_id, 'activitypub_content_visibility' ); - $this->assertEquals( 'original_value', $result, 'Should return original value for federated posts.' ); - - // Test 3: When post is not federated and older than a month, should return local visibility. - \update_post_meta( $post_id, 'activitypub_status', 'pending' ); - $result = Activitypub::default_post_metadata( null, $post_id, 'activitypub_content_visibility' ); - $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, $result, 'Should return local visibility for old non-federated posts.' ); - - // Test 4: When post is not federated but less than a month old, should return original value. - $recent_post_id = self::factory()->post->create( - array( - 'post_author' => 1, - 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 weeks' ) ), // Recent post. - ) - ); - \update_post_meta( $recent_post_id, 'activitypub_status', 'pending' ); - $result = Activitypub::default_post_metadata( null, $recent_post_id, 'activitypub_content_visibility' ); - $this->assertEquals( null, $result, 'Should return original value for recent non-federated posts.' ); - - // Test 5: When meta value is already set (not null), should respect author's explicit choice. - \update_post_meta( $post_id, 'activitypub_status', 'pending' ); // Ensure not federated. - $result = Activitypub::default_post_metadata( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $post_id, 'activitypub_content_visibility' ); - $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should respect explicitly set public visibility even for old unfederated posts.' ); - - // Test 6: Only apply local visibility when meta value is null (no explicit setting). - $result = Activitypub::default_post_metadata( null, $post_id, 'activitypub_content_visibility' ); - $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, $result, 'Should return local visibility when no explicit value is set for old unfederated posts.' ); - - // Clean up. - \wp_delete_post( $post_id, true ); - \wp_delete_post( $recent_post_id, true ); - } } diff --git a/tests/includes/class-test-blocks.php b/tests/includes/class-test-blocks.php index c2707db2d..783b1f0c9 100644 --- a/tests/includes/class-test-blocks.php +++ b/tests/includes/class-test-blocks.php @@ -19,7 +19,7 @@ class Test_Blocks extends \WP_UnitTestCase { /** * Test register_post_meta. * - * @covers ::register_postmeta + * @covers \Activitypub\Post_Types::register_activitypub_post_meta */ public function test_register_post_meta() { // Empty option should not trigger _doing_it_wrong() notice. diff --git a/tests/includes/class-test-post-types.php b/tests/includes/class-test-post-types.php new file mode 100644 index 000000000..099c94655 --- /dev/null +++ b/tests/includes/class-test-post-types.php @@ -0,0 +1,99 @@ +post->create( array( 'post_author' => 1 ) ); + + \update_post_meta( $post_id, 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ); + $this->assertEmpty( \get_post_meta( $post_id, 'activitypub_max_image_attachments', true ) ); + \delete_post_meta( $post_id, 'activitypub_max_image_attachments' ); + + \update_post_meta( $post_id, 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS + 3 ); + $this->assertEquals( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS + 3, \get_post_meta( $post_id, 'activitypub_max_image_attachments', true ) ); + \delete_post_meta( $post_id, 'activitypub_max_image_attachments' ); + + \wp_delete_post( $post_id, true ); + } + + /** + * Test get_post_metadata method. + * + * @covers ::default_post_meta_data + */ + public function test_get_post_metadata() { + // Create a test post. + $post_id = self::factory()->post->create( + array( + 'post_author' => 1, + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 months' ) ), // Post older than a month. + ) + ); + + // Test 1: When meta_key is not 'activitypub_content_visibility', should return the original value. + $result = Post_Types::default_post_meta_data( 'original_value', $post_id, 'some_other_key' ); + $this->assertEquals( 'original_value', $result, 'Should return original value for non-matching meta key.' ); + + // Test 2: When post is federated, should return the original value. + \update_post_meta( $post_id, 'activitypub_status', 'federated' ); + $result = Post_Types::default_post_meta_data( 'original_value', $post_id, 'activitypub_content_visibility' ); + $this->assertEquals( 'original_value', $result, 'Should return original value for federated posts.' ); + + // Test 3: When post is not federated and older than a month, should return local visibility. + \update_post_meta( $post_id, 'activitypub_status', 'pending' ); + $result = Post_Types::default_post_meta_data( null, $post_id, 'activitypub_content_visibility' ); + $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, $result, 'Should return local visibility for old non-federated posts.' ); + + // Test 4: When post is not federated but less than a month old, should return original value. + $recent_post_id = self::factory()->post->create( + array( + 'post_author' => 1, + 'post_date' => gmdate( 'Y-m-d H:i:s', strtotime( '-2 weeks' ) ), // Recent post. + ) + ); + \update_post_meta( $recent_post_id, 'activitypub_status', 'pending' ); + $result = Post_Types::default_post_meta_data( null, $recent_post_id, 'activitypub_content_visibility' ); + $this->assertEquals( null, $result, 'Should return original value for recent non-federated posts.' ); + + // Test 5: When meta value is already set (not null), should respect author's explicit choice. + \update_post_meta( $post_id, 'activitypub_status', 'pending' ); // Ensure not federated. + $result = Post_Types::default_post_meta_data( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $post_id, 'activitypub_content_visibility' ); + $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, $result, 'Should respect explicitly set public visibility even for old unfederated posts.' ); + + // Test 6: Only apply local visibility when meta value is null (no explicit setting). + $result = Post_Types::default_post_meta_data( null, $post_id, 'activitypub_content_visibility' ); + $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, $result, 'Should return local visibility when no explicit value is set for old unfederated posts.' ); + + // Clean up. + \wp_delete_post( $post_id, true ); + \wp_delete_post( $recent_post_id, true ); + } +} diff --git a/tests/includes/collection/class-test-inbox.php b/tests/includes/collection/class-test-inbox.php index 20f27379d..32e8898f0 100644 --- a/tests/includes/collection/class-test-inbox.php +++ b/tests/includes/collection/class-test-inbox.php @@ -9,8 +9,8 @@ use Activitypub\Activity\Activity; use Activitypub\Activity\Base_Object; -use Activitypub\Activitypub; use Activitypub\Collection\Inbox; +use Activitypub\Post_Types; /** * Test class for Inbox collection. @@ -214,7 +214,7 @@ public function test_duplicate_activity_prevention() { * Test post meta registration exists. */ public function test_post_meta_registration() { - Activitypub::register_post_types(); + Post_Types::register_inbox_post_type(); // Verify that post meta is registered for inbox post type. $registered_meta = \get_registered_meta_keys( 'post', Inbox::POST_TYPE );