From 5565c266323faba2ed1660ae825484af48a563d1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Dec 2023 15:46:12 +0100 Subject: [PATCH 001/116] init --- includes/class-activitypub.php | 22 +++++++++- includes/class-handler.php | 55 ++++++++++++++++++++++- includes/class-scheduler.php | 52 ---------------------- includes/collection/class-outbox.php | 9 ++++ includes/transformer/class-base.php | 55 +++++++++++++++++++++++ includes/transformer/class-post.php | 65 ++++++++-------------------- 6 files changed, 156 insertions(+), 102 deletions(-) create mode 100644 includes/collection/class-outbox.php create mode 100644 includes/transformer/class-base.php diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6f654c5c4..091e25433 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -4,6 +4,7 @@ use Exception; use Activitypub\Signature; use Activitypub\Collection\Users; +use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; use function Activitypub\sanitize_url; @@ -336,11 +337,12 @@ public static function plugin_update_message( $data ) { } /** - * Register the "Followers" Taxonomy + * Register Custom Post Types * * @return void */ private static function register_post_types() { + // register Followers Post-Type register_post_type( Followers::POST_TYPE, array( @@ -409,5 +411,23 @@ private static function register_post_types() { ); do_action( 'activitypub_after_register_post_type' ); + + // 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' ), + ), + 'public' => true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) + ); } } diff --git a/includes/class-handler.php b/includes/class-handler.php index fcabd63c7..130b82a01 100644 --- a/includes/class-handler.php +++ b/includes/class-handler.php @@ -15,13 +15,15 @@ class Handler { * Initialize the class, registering WordPress hooks */ public static function init() { - self::register_handlers(); + self::register_inbox_handlers(); + + \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); } /** * Register handlers. */ - public static function register_handlers() { + public static function register_inbox_handlers() { Create::init(); Delete::init(); Follow::init(); @@ -30,4 +32,53 @@ public static function register_handlers() { do_action( 'activitypub_register_handlers' ); } + + /** + * Schedule Activities. + * + * @param string $new_status New post status. + * @param string $old_status Old post status. + * @param WP_Post $post Post object. + */ + public static function schedule_post_activity( $new_status, $old_status, $post ) { + // Do not send activities if post is password protected. + if ( \post_password_required( $post ) ) { + return; + } + + // Check if post-type supports ActivityPub. + $post_types = \get_post_types_by_support( 'activitypub' ); + if ( ! \in_array( $post->post_type, $post_types, true ) ) { + return; + } + + $type = false; + + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + $type = 'Create'; + } elseif ( 'publish' === $new_status ) { + $type = 'Update'; + } elseif ( 'trash' === $new_status ) { + $type = 'Delete'; + } + + if ( ! $type ) { + return; + } + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $post, $type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $type ) + ), + array( $post ) + ); + } } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 11f40dafb..c88ce2e8c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -16,8 +16,6 @@ class Scheduler { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); - \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); @@ -49,56 +47,6 @@ public static function deregister_schedules() { wp_unschedule_hook( 'activitypub_cleanup_followers' ); } - - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $type = false; - - if ( 'publish' === $new_status && 'publish' !== $old_status ) { - $type = 'Create'; - } elseif ( 'publish' === $new_status ) { - $type = 'Update'; - } elseif ( 'trash' === $new_status ) { - $type = 'Delete'; - } - - if ( ! $type ) { - return; - } - - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $post, $type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $post ) - ); - } - /** * Update followers * diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php new file mode 100644 index 000000000..4ca9d1912 --- /dev/null +++ b/includes/collection/class-outbox.php @@ -0,0 +1,9 @@ +object = $object; + } + + /** + * Transform the WordPress Object into an ActivityPub Object. + * + * @return Activitypub\Activity\Base_Object + */ + abstract public function to_object(); + + /** + * Transform the WordPress Object into an ActivityPub Activity. + * + * @param string $type The type of Activity to transform to. + * + * @return Activitypub\Activity\Activity + */ + //abstract public function to_activity( $type ); +} diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 721bbec0f..80ed7b918 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -2,10 +2,11 @@ namespace Activitypub\Transformer; use WP_Post; -use Activitypub\Collection\Users; +use Activitypub\Shortcodes; use Activitypub\Model\Blog_User; +use Activitypub\Transformer\Base; +use Activitypub\Collection\Users; use Activitypub\Activity\Base_Object; -use Activitypub\Shortcodes; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; @@ -22,37 +23,7 @@ * * - Activitypub\Activity\Base_Object */ -class Post { - - /** - * The WP_Post object. - * - * @var WP_Post - */ - protected $wp_post; - - /** - * Static function to Transform a WP_Post Object. - * - * This helps to chain the output of the Transformer. - * - * @param WP_Post $wp_post The WP_Post object - * - * @return void - */ - public static function transform( WP_Post $wp_post ) { - return new static( $wp_post ); - } - - /** - * - * - * @param WP_Post $wp_post - */ - public function __construct( WP_Post $wp_post ) { - $this->wp_post = $wp_post; - } - +class Post extends Base { /** * Transforms the WP_Post object to an ActivityPub Object * @@ -61,7 +32,7 @@ public function __construct( WP_Post $wp_post ) { * @return \Activitypub\Activity\Base_Object The ActivityPub Object */ public function to_object() { - $wp_post = $this->wp_post; + $wp_post = $this->object; $object = new Base_Object(); $object->set_id( $this->get_id() ); @@ -115,7 +86,7 @@ public function get_id() { * @return string The Posts URL. */ public function get_url() { - $post = $this->wp_post; + $post = $this->object; if ( 'trash' === get_post_status( $post ) ) { $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); @@ -139,7 +110,7 @@ protected function get_attributed_to() { return $user->get_url(); } - return Users::get_by_id( $this->wp_post->post_author )->get_url(); + return Users::get_by_id( $this->object->post_author )->get_url(); } /** @@ -152,7 +123,7 @@ protected function get_attachments() { // We maintain the image-centric naming for backwards compatibility. $max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) ); - if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) { + if ( site_supports_blocks() && \has_blocks( $this->object->post_content ) ) { return $this->get_block_attachments( $max_media ); } @@ -172,7 +143,7 @@ protected function get_block_attachments( $max_media ) { return array(); } - $id = $this->wp_post->ID; + $id = $this->object->ID; $media_ids = array(); @@ -182,7 +153,7 @@ protected function get_block_attachments( $max_media ) { } if ( $max_media > 0 ) { - $blocks = \parse_blocks( $this->wp_post->post_content ); + $blocks = \parse_blocks( $this->object->post_content ); $media_ids = self::get_media_ids_from_blocks( $blocks, $media_ids, $max_media ); } @@ -203,7 +174,7 @@ protected function get_classic_editor_images( $max_images ) { return array(); } - $id = $this->wp_post->ID; + $id = $this->object->ID; $image_ids = array(); @@ -402,10 +373,10 @@ protected function get_object_type() { // Default to Article. $object_type = 'Article'; - $post_type = \get_post_type( $this->wp_post ); + $post_type = \get_post_type( $this->object ); switch ( $post_type ) { case 'post': - $post_format = \get_post_format( $this->wp_post ); + $post_format = \get_post_format( $this->object ); switch ( $post_format ) { case 'aside': case 'status': @@ -484,7 +455,7 @@ protected function get_cc() { protected function get_tags() { $tags = array(); - $post_tags = \get_the_tags( $this->wp_post->ID ); + $post_tags = \get_the_tags( $this->object->ID ); if ( $post_tags ) { foreach ( $post_tags as $post_tag ) { $tag = array( @@ -531,7 +502,7 @@ protected function get_content() { do_action( 'activitypub_before_get_content', $post ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->wp_post; + $post = $this->object; $content = $this->get_post_content_template(); // Register our shortcodes just in time. @@ -580,7 +551,7 @@ protected function get_post_content_template() { * @return array The list of @-Mentions. */ protected function get_mentions() { - return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post ); + return apply_filters( 'activitypub_extract_mentions', array(), $this->object->post_content, $this->object ); } /** @@ -589,7 +560,7 @@ protected function get_mentions() { * @return string The locale of the post. */ public function get_locale() { - $post_id = $this->wp_post->ID; + $post_id = $this->object->ID; $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); /** @@ -601,6 +572,6 @@ public function get_locale() { * * @return string The filtered locale of the post. */ - return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post ); + return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->object ); } } From 7448cd664f2e04ad0fac58e811f7f7a4a06bb779 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 8 Dec 2023 15:51:30 +0100 Subject: [PATCH 002/116] outbox should not be public! --- includes/class-activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 091e25433..a05c7a61a 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -420,7 +420,7 @@ private static function register_post_types() { 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ), 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ), ), - 'public' => true, + 'public' => false, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, From 6ac167f908f5f718bc02c13b71da8bcd67de3416 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 11 Dec 2023 10:41:02 +0100 Subject: [PATCH 003/116] add basic `to_activity` function --- includes/transformer/class-base.php | 4 ++-- includes/transformer/class-post.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index e26e9cf82..ca787f12d 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -45,11 +45,11 @@ public function __construct( $object ) { abstract public function to_object(); /** - * Transform the WordPress Object into an ActivityPub Activity. + * Transform the ActivityPub Object into an Activity. * * @param string $type The type of Activity to transform to. * * @return Activitypub\Activity\Activity */ - //abstract public function to_activity( $type ); + abstract public function to_activity( $type ); } diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 80ed7b918..dbd573be1 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -6,6 +6,7 @@ use Activitypub\Model\Blog_User; use Activitypub\Transformer\Base; use Activitypub\Collection\Users; +use Activitypub\Activity\Activity; use Activitypub\Activity\Base_Object; use function Activitypub\esc_hashtag; @@ -71,6 +72,23 @@ public function to_object() { return $object; } + /** + * Transforms the ActivityPub Object to an Activity + * + * @param string $type The Activity-Type. + * + * @return \Activitypub\Activity\Activity The Activity. + */ + public function to_activity( $type ) { + $object = $this->to_object(); + + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); + + return $activity; + } + /** * Returns the ID of the Post. * From 89ece407ae0c6d286490848f0d82b8782ed97224 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 11 Dec 2023 18:48:28 +0100 Subject: [PATCH 004/116] do not allow to instance post transformer --- includes/handler/class-update.php | 4 ++-- includes/transformer/class-base.php | 2 +- templates/post-json.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/handler/class-update.php b/includes/handler/class-update.php index 00e0430b6..6286bc652 100644 --- a/includes/handler/class-update.php +++ b/includes/handler/class-update.php @@ -20,8 +20,8 @@ public static function init() { /** * Handle "Update" requests * - * @param array $array The activity-object - * @param int $user_id The id of the local blog-user + * @param array $array The activity-object + * @param int $user_id The id of the local blog-user */ public static function handle_update( $array, $user_id ) { $object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : ''; diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index ca787f12d..43c4b5379 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -33,7 +33,7 @@ public static function transform( $object ) { * * @param stdClass $object */ - public function __construct( $object ) { + private function __construct( $object ) { $this->object = $object; } diff --git a/templates/post-json.php b/templates/post-json.php index 89467c466..80a9af8be 100644 --- a/templates/post-json.php +++ b/templates/post-json.php @@ -2,8 +2,8 @@ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post = \get_post(); -$object = new \Activitypub\Transformer\Post( $post ); -$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() ); +$object = \Activitypub\Transformer\Post::transform( $post )->to_object(); +$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_array() ); // filter output $json = \apply_filters( 'activitypub_json_post_array', $json ); From efda4a565493696c6775a7ab3814ddb2ada0d6b6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 12 Dec 2023 14:10:21 +0100 Subject: [PATCH 005/116] fix tests --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 578d1164d..388594eb4 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -565,7 +565,7 @@ protected function get_post_content_template() { break; } - return apply_filters( 'activitypub_object_content_template', $template, $this->wp_post ); + return apply_filters( 'activitypub_object_content_template', $template, $this->object ); } /** From 5d6649483dd7c3883e1c91ea8f75e3529f326008 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 27 Sep 2024 14:51:58 +0200 Subject: [PATCH 006/116] fix phpcs --- includes/transformer/class-post.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 3d9ba0517..8c747c847 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -114,9 +114,9 @@ public function to_activity( $type ) { $activity->set_object( $object ); return $activity; - } - - /** + } + + /** * Returns the User-Object of the Author of the Post. * * If `single_user` mode is enabled, the Blog-User is returned. From b1d260d40d573fca84a771e1b8cb87cf7be705f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 27 Sep 2024 14:53:11 +0200 Subject: [PATCH 007/116] revert change --- includes/class-handler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-handler.php b/includes/class-handler.php index 03da7c44b..7ca397248 100644 --- a/includes/class-handler.php +++ b/includes/class-handler.php @@ -17,7 +17,7 @@ class Handler { * Initialize the class, registering WordPress hooks */ public static function init() { - self::register_inbox_handlers(); + self::register_handlers(); \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); } From 81b0b88326a57a767d8dd1ac84f4b9a4a1488dc4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 27 Sep 2024 14:54:39 +0200 Subject: [PATCH 008/116] remove unused `use` declarations --- includes/transformer/class-post.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 8c747c847..938db3d4f 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -5,9 +5,8 @@ use Activitypub\Shortcodes; use Activitypub\Model\Blog; use Activitypub\Collection\Users; -use Activitypub\Activity\Activity; -use Activitypub\Activity\Base_Object; use Activitypub\Transformer\Base; +use Activitypub\Activity\Activity; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; From 8c0c3fa886f28d04598563d8179a2633455105b4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 27 Sep 2024 14:56:54 +0200 Subject: [PATCH 009/116] the handler should not handle outgoing stuff --- includes/class-handler.php | 51 -------------------------------------- 1 file changed, 51 deletions(-) diff --git a/includes/class-handler.php b/includes/class-handler.php index 7ca397248..ef5245007 100644 --- a/includes/class-handler.php +++ b/includes/class-handler.php @@ -18,8 +18,6 @@ class Handler { */ public static function init() { self::register_handlers(); - - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); } /** @@ -39,53 +37,4 @@ public static function register_handlers() { do_action( 'activitypub_register_handlers' ); } - - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $type = false; - - if ( 'publish' === $new_status && 'publish' !== $old_status ) { - $type = 'Create'; - } elseif ( 'publish' === $new_status ) { - $type = 'Update'; - } elseif ( 'trash' === $new_status ) { - $type = 'Delete'; - } - - if ( ! $type ) { - return; - } - - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $post, $type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $post ) - ); - } } From 43a3343d376b07cc0781f6605b4fe8d8e70341ab Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 17:29:17 +0200 Subject: [PATCH 010/116] Update class-activitypub.php --- includes/class-activitypub.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 0ebbf6085..9c94c8a84 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -8,9 +8,6 @@ namespace Activitypub; use Exception; -use Activitypub\Signature; -use Activitypub\Collection\Users; -use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; use Activitypub\Collection\Extra_Fields; From 87464476454c6950804c1c4382e7645527b3b670 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 17:31:04 +0200 Subject: [PATCH 011/116] fix PHPCS --- includes/class-activitypub.php | 2 +- includes/transformer/class-base.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 9c94c8a84..12c54f0df 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -516,7 +516,7 @@ private static function register_post_types() { ) ); - // register Outbox Post-Type + // Register Outbox Post-Type. register_post_type( Outbox::POST_TYPE, array( diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 6d4c202a6..0efb44dca 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -11,8 +11,8 @@ use WP_Comment; use Activitypub\Activity\Activity; -use Activitypub\Activity\Base_Object; use Activitypub\Collection\Replies; +use Activitypub\Activity\Base_Object; /** * WordPress Base Transformer. From 242dea40da4aa3c918c070ae749d6a10898a8f31 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 17:34:40 +0200 Subject: [PATCH 012/116] more PHPCS fixes --- includes/collection/class-outbox.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 4ca9d1912..0234db76f 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -1,4 +1,10 @@ Date: Tue, 22 Oct 2024 17:37:27 +0200 Subject: [PATCH 013/116] fix namespace issue --- includes/class-activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 12c54f0df..2d420d64a 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -8,6 +8,7 @@ namespace Activitypub; use Exception; +use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; use Activitypub\Collection\Extra_Fields; From 5ca19a64eba7f83d47e68524ab3186ec45233af2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 17:42:31 +0200 Subject: [PATCH 014/116] remove unneeded function --- includes/transformer/class-post.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 2b4170a29..a1cff1ea0 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -87,23 +87,6 @@ public function to_object() { return $object; } - /** - * Transforms the ActivityPub Object to an Activity - * - * @param string $type The Activity-Type. - * - * @return \Activitypub\Activity\Activity The Activity. - */ - public function to_activity( $type ) { - $object = $this->to_object(); - - $activity = new Activity(); - $activity->set_type( $type ); - $activity->set_object( $object ); - - return $activity; - } - /** * Returns the User-Object of the Author of the Post. * From 9f525b0f75d3d2563e94114bffae68762b7e1e07 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 20:54:17 +0200 Subject: [PATCH 015/116] fix sticky post endpoint /cc @mattwiebe --- includes/rest/class-collection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index 06f2203b8..a37d91df3 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -218,7 +218,7 @@ public static function featured_get( $request ) { if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) { $posts = array(); - } elseif ( is_array( $sticky_posts ) ) { + } elseif ( $sticky_posts && is_array( $sticky_posts ) ) { $args = array( 'post__in' => $sticky_posts, 'ignore_sticky_posts' => 1, From e0aeefdb9b607acdd2456e6b1d6a54d313c3aa5b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 22 Oct 2024 21:03:20 +0200 Subject: [PATCH 016/116] no need to check for User-ID --- includes/rest/class-actors.php | 5 ----- includes/rest/class-collection.php | 5 ----- includes/rest/class-followers.php | 5 ----- includes/rest/class-following.php | 5 ----- includes/rest/class-inbox.php | 13 ------------- includes/rest/class-outbox.php | 5 ----- 6 files changed, 38 deletions(-) diff --git a/includes/rest/class-actors.php b/includes/rest/class-actors.php index 60f03d298..aa15c0c24 100644 --- a/includes/rest/class-actors.php +++ b/includes/rest/class-actors.php @@ -151,11 +151,6 @@ public static function request_parameters() { 'type' => 'string', ); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - return $params; } } diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php index a37d91df3..0df17ba91 100644 --- a/includes/rest/class-collection.php +++ b/includes/rest/class-collection.php @@ -292,11 +292,6 @@ public static function moderators_get() { public static function request_parameters() { $params = array(); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - return $params; } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 1b38187f3..50bec2458 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -138,11 +138,6 @@ public static function request_parameters() { 'enum' => array( 'asc', 'desc' ), ); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - $params['context'] = array( 'type' => 'string', 'default' => 'simple', diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index cda058962..4b0f4674e 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -111,11 +111,6 @@ public static function request_parameters() { 'type' => 'integer', ); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - return $params; } diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b5f5fa9b1..15837bc9d 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -215,11 +215,6 @@ public static function user_inbox_get_parameters() { 'type' => 'integer', ); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - return $params; } @@ -231,11 +226,6 @@ public static function user_inbox_get_parameters() { public static function user_inbox_post_parameters() { $params = array(); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - $params['id'] = array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', @@ -276,9 +266,6 @@ public static function user_inbox_post_parameters() { public static function shared_inbox_post_parameters() { $params = self::user_inbox_post_parameters(); - // A shared Inbox does not need a User-ID. - unset( $params['user_id'] ); - $params['to'] = array( 'required' => false, 'sanitize_callback' => function ( $param ) { diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 461d42861..1dff85ec6 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -163,11 +163,6 @@ public static function request_parameters() { 'default' => 1, ); - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - return $params; } } From 934599164caff3fdfaf9e314916e70d8769fe290 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 5 Nov 2024 11:46:55 +0100 Subject: [PATCH 017/116] support JSON and Arrays beside WP_Comments and WP_Posts --- includes/activity/class-base-object.php | 10 +-- includes/collection/class-outbox.php | 42 ++++++++++ .../transformer/class-activity-object.php | 31 +++++++ includes/transformer/class-attachment.php | 6 +- includes/transformer/class-base.php | 24 ++++-- includes/transformer/class-comment.php | 26 +++--- includes/transformer/class-factory.php | 14 +++- includes/transformer/class-json.php | 41 ++++++++++ includes/transformer/class-post.php | 82 +++++++++---------- .../class-seriously-simple-podcasting.php | 4 +- 10 files changed, 203 insertions(+), 77 deletions(-) create mode 100644 includes/transformer/class-activity-object.php create mode 100644 includes/transformer/class-json.php diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index ab765174f..a75647ff2 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -585,7 +585,7 @@ public static function init_from_json( $json ) { $array = \json_decode( $json, true ); if ( ! is_array( $array ) ) { - $array = array(); + return new WP_Error( 'invalid_json', __( 'Invalid JSON', 'activitypub' ), array( 'status' => 400 ) ); } return self::init_from_array( $array ); @@ -600,15 +600,11 @@ public static function init_from_json( $json ) { */ public static function init_from_array( $data ) { if ( ! is_array( $data ) ) { - return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 404 ) ); + return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 400 ) ); } $object = new static(); - - foreach ( $data as $key => $value ) { - $key = camel_to_snake_case( $key ); - call_user_func( array( $object, 'set_' . $key ), $value ); - } + $object->from_array( $data ); return $object; } diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 0234db76f..acda0053b 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -7,9 +7,51 @@ namespace Activitypub\Collection; +use Activitypub\Transformer\Factory; + /** * ActivityPub Outbox Collection */ class Outbox { const POST_TYPE = 'ap_outbox'; + + /** + * Add an Item to the outbox. + * + * @param string|array|Base_Object|WP_Post|WP_Comment $item The item to add. + * @param int $user_id The user ID. + * @param string $activity_type The activity + * + * @return mixed The added item or an error. + */ + public static function add_item( $item, $user_id, $activity_type = 'Create' ) { + $transformer = Factory::get_transformer( $item ); + $object = $transformer->transform(); + + if ( ! $object || is_wp_error( $object ) ) { + return $object; + } + + $outbox_item = array( + 'post_type' => self::POST_TYPE, + 'post_title' => $object->get_id(), + 'post_content' => $object->to_json(), + 'post_author' => $user_id, + 'post_status' => 'draft', + ); + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + $result = \wp_insert_post( $outbox_item, true ); + + if ( $has_kses ) { + \kses_init_filters(); + } + + return $result; + } } diff --git a/includes/transformer/class-activity-object.php b/includes/transformer/class-activity-object.php new file mode 100644 index 000000000..bfe567813 --- /dev/null +++ b/includes/transformer/class-activity-object.php @@ -0,0 +1,31 @@ +item; + } + + /** + * Get the ID of the WordPress Object. + * + * @return string The ID of the WordPress Object. + */ + protected function get_id() { + return ''; + } +} diff --git a/includes/transformer/class-attachment.php b/includes/transformer/class-attachment.php index 98aaf8bf4..ef3e1d1fd 100644 --- a/includes/transformer/class-attachment.php +++ b/includes/transformer/class-attachment.php @@ -24,7 +24,7 @@ class Attachment extends Post { * @return array The Attachments. */ protected function get_attachment() { - $mime_type = get_post_mime_type( $this->wp_object->ID ); + $mime_type = get_post_mime_type( $this->item->ID ); $media_type = preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type ); $type = ''; @@ -40,11 +40,11 @@ protected function get_attachment() { $attachment = array( 'type' => $type, - 'url' => wp_get_attachment_url( $this->wp_object->ID ), + 'url' => wp_get_attachment_url( $this->item->ID ), 'mediaType' => $mime_type, ); - $alt = \get_post_meta( $this->wp_object->ID, '_wp_attachment_image_alt', true ); + $alt = \get_post_meta( $this->item->ID, '_wp_attachment_image_alt', true ); if ( $alt ) { $attachment['name'] = $alt; } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 0efb44dca..bca105f3a 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -26,6 +26,15 @@ abstract class Base { * * This is the source object of the transformer. * + * @var WP_Post|WP_Comment|Base_Object|string|array + */ + protected $item; + + /** + * The WP_Post or WP_Comment object. + * + * @deprecated version 5.0.0 + * * @var WP_Post|WP_Comment */ protected $wp_object; @@ -35,21 +44,22 @@ abstract class Base { * * This helps to chain the output of the Transformer. * - * @param WP_Post|WP_Comment $wp_object The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array $item The WordPress object. * * @return Base */ - public static function transform( $wp_object ) { - return new static( $wp_object ); + public static function transform( $item ) { + return new static( $item ); } /** * Base constructor. * - * @param WP_Post|WP_Comment $wp_object The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array $item The WordPress object. */ - public function __construct( $wp_object ) { - $this->wp_object = $wp_object; + public function __construct( $item ) { + $this->item = $item; + $this->wp_object = $item; } /** @@ -122,7 +132,7 @@ abstract protected function get_id(); * Get the replies Collection. */ public function get_replies() { - return Replies::get_collection( $this->wp_object ); + return Replies::get_collection( $this->item ); } /** diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index 71fbb2dd4..d9ee1e0e9 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -33,7 +33,7 @@ class Comment extends Base { * @return int The User-ID of the WordPress Comment */ public function get_wp_user_id() { - return $this->wp_object->user_id; + return $this->item->user_id; } /** @@ -42,7 +42,7 @@ public function get_wp_user_id() { * @param int $user_id The new user ID. */ public function change_wp_user_id( $user_id ) { - $this->wp_object->user_id = $user_id; + $this->item->user_id = $user_id; } /** @@ -53,7 +53,7 @@ public function change_wp_user_id( $user_id ) { * @return \Activitypub\Activity\Base_Object The ActivityPub Object. */ public function to_object() { - $comment = $this->wp_object; + $comment = $this->item; $object = parent::to_object(); $object->set_url( $this->get_id() ); @@ -97,7 +97,7 @@ protected function get_attributed_to() { return $user->get_id(); } - return Users::get_by_id( $this->wp_object->user_id )->get_id(); + return Users::get_by_id( $this->item->user_id )->get_id(); } /** @@ -108,7 +108,7 @@ protected function get_attributed_to() { * @return string The content. */ protected function get_content() { - $comment = $this->wp_object; + $comment = $this->item; $content = $comment->comment_content; /** @@ -141,7 +141,7 @@ protected function get_content() { * @return false|string|null The URL of the in-reply-to. */ protected function get_in_reply_to() { - $comment = $this->wp_object; + $comment = $this->item; $parent_comment = null; if ( $comment->comment_parent ) { @@ -169,7 +169,7 @@ protected function get_in_reply_to() { * @return string ActivityPub URI for comment */ protected function get_id() { - $comment = $this->wp_object; + $comment = $this->item; return Comment_Utils::generate_id( $comment ); } @@ -235,7 +235,7 @@ protected function get_mentions() { * * @return array The filtered list of mentions. */ - return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object ); + return apply_filters( 'activitypub_extract_mentions', array(), $this->item->comment_content, $this->item ); } /** @@ -244,7 +244,7 @@ protected function get_mentions() { * @return array The list of ancestors. */ protected function get_comment_ancestors() { - $ancestors = get_comment_ancestors( $this->wp_object ); + $ancestors = get_comment_ancestors( $this->item ); // Now that we have the full tree of ancestors, only return the ones received from the fediverse. return array_filter( @@ -264,8 +264,8 @@ function ( $comment_id ) { * @return array The list of all Repliers. */ public function extract_reply_context( $mentions ) { - // Check if `$this->wp_object` is a WP_Comment. - if ( 'WP_Comment' !== get_class( $this->wp_object ) ) { + // Check if `$this->item` is a WP_Comment. + if ( 'WP_Comment' !== get_class( $this->item ) ) { return $mentions; } @@ -294,7 +294,7 @@ public function extract_reply_context( $mentions ) { * @return string The locale of the post. */ public function get_locale() { - $comment_id = $this->wp_object->ID; + $comment_id = $this->item->ID; $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); /** @@ -306,6 +306,6 @@ public function get_locale() { * * @return string The filtered locale of the comment. */ - return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->wp_object ); + return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->item ); } } diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index a619423af..ae67a6f16 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -21,12 +21,14 @@ class Factory { * @return Base|WP_Error The transformer to use, or an error. */ public static function get_transformer( $data ) { - if ( ! \is_object( $data ) ) { + if ( \is_array( $data ) || \is_string( $data ) ) { + $class = 'json'; + } elseif ( \is_object( $data ) ) { + $class = \get_class( $data ); + } else { return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } - $class = \get_class( $data ); - /** * Filter the transformer for a given object. * @@ -78,8 +80,12 @@ public static function get_transformer( $data ) { return new Post( $data ); case 'WP_Comment': return new Comment( $data ); + case 'Base_Object': + return new Activity_Object( $data ); + case 'json': + return new Json( $data ); default: - return null; + return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } } } diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php new file mode 100644 index 000000000..1eaeb5062 --- /dev/null +++ b/includes/transformer/class-json.php @@ -0,0 +1,41 @@ +item ) ) { + $activitypub_object = Base_Object::init_from_array( $this->item ); + } else { + $activitypub_object = Base_Object::init_from_json( $this->item ); + } + + return $activitypub_object; + } + + /** + * Get the ID of the WordPress Object. + * + * @return string The ID of the WordPress Object. + */ + protected function get_id() { + return ''; + } +} diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 23dd46e67..8644113e4 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -44,7 +44,7 @@ class Post extends Base { * @return int The ID of the WordPress Post */ public function get_wp_user_id() { - return $this->wp_object->post_author; + return $this->item->post_author; } /** @@ -55,7 +55,7 @@ public function get_wp_user_id() { * @return Post The Post Object. */ public function change_wp_user_id( $user_id ) { - $this->wp_object->post_author = $user_id; + $this->item->post_author = $user_id; return $this; } @@ -68,7 +68,7 @@ public function change_wp_user_id( $user_id ) { * @return \Activitypub\Activity\Base_Object The ActivityPub Object */ public function to_object() { - $post = $this->wp_object; + $post = $this->item; $object = parent::to_object(); $content_warning = get_content_warning( $post ); @@ -113,7 +113,7 @@ protected function get_actor_object() { return $blog_user; } - $user = Users::get_by_id( $this->wp_object->post_author ); + $user = Users::get_by_id( $this->item->post_author ); if ( $user && ! is_wp_error( $user ) ) { $this->actor_object = $user; @@ -130,7 +130,7 @@ protected function get_actor_object() { */ public function get_id() { $last_legacy_id = (int) \get_option( 'activitypub_last_post_with_permalink_as_id', 0 ); - $post_id = (int) $this->wp_object->ID; + $post_id = (int) $this->item->ID; if ( $post_id > $last_legacy_id ) { // Generate URI based on post ID. @@ -146,7 +146,7 @@ public function get_id() { * @return string The Posts URL. */ public function get_url() { - $post = $this->wp_object; + $post = $this->item; switch ( \get_post_status( $post ) ) { case 'trash': @@ -186,7 +186,7 @@ protected function get_attributed_to() { */ protected function get_attachment() { // Remove attachments from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { + if ( 'draft' === \get_post_status( $this->item ) ) { return array(); } @@ -204,7 +204,7 @@ protected function get_attachment() { 'video' => array(), 'image' => array(), ); - $id = $this->wp_object->ID; + $id = $this->item->ID; // List post thumbnail first if this post has one. if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) { @@ -213,13 +213,13 @@ protected function get_attachment() { $media = $this->get_enclosures( $media ); - if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) { + if ( site_supports_blocks() && \has_blocks( $this->item->post_content ) ) { $media = $this->get_block_attachments( $media, $max_media ); } else { $media = $this->get_classic_editor_images( $media, $max_media ); } - $media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object ); + $media = self::filter_media_by_object_type( $media, \get_post_format( $this->item ), $this->item ); $unique_ids = \array_unique( \array_column( $media, 'id' ) ); $media = \array_intersect_key( $media, $unique_ids ); $media = \array_slice( $media, 0, $max_media ); @@ -228,11 +228,11 @@ protected function get_attachment() { * Filter the attachment IDs for a post. * * @param array $media The media array grouped by type. - * @param WP_Post $this->wp_object The post object. + * @param WP_Post $this->item The post object. * * @return array The filtered attachment IDs. */ - $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->wp_object ); + $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->item ); $attachments = \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) ); @@ -240,11 +240,11 @@ protected function get_attachment() { * Filter the attachments for a post. * * @param array $attachments The attachments. - * @param WP_Post $this->wp_object The post object. + * @param WP_Post $this->item The post object. * * @return array The filtered attachments. */ - return \apply_filters( 'activitypub_attachments', $attachments, $this->wp_object ); + return \apply_filters( 'activitypub_attachments', $attachments, $this->item ); } /** @@ -255,7 +255,7 @@ protected function get_attachment() { * @return array The media array extended with enclosures. */ public function get_enclosures( $media ) { - $enclosures = get_enclosures( $this->wp_object->ID ); + $enclosures = get_enclosures( $this->item->ID ); if ( ! $enclosures ) { return $media; @@ -303,7 +303,7 @@ protected function get_block_attachments( $media, $max_media ) { return array(); } - $blocks = \parse_blocks( $this->wp_object->post_content ); + $blocks = \parse_blocks( $this->item->post_content ); return self::get_media_from_blocks( $blocks, $media ); } @@ -425,7 +425,7 @@ protected function get_classic_editor_image_embeds( $max_images ) { $images = array(); $base = \wp_get_upload_dir()['baseurl']; - $content = \get_post_field( 'post_content', $this->wp_object ); + $content = \get_post_field( 'post_content', $this->item ); $tags = new \WP_HTML_Tag_Processor( $content ); // This linter warning is a false positive - we have to re-count each time here as we modify $images. @@ -489,7 +489,7 @@ protected function get_classic_editor_image_attachments( $max_images ) { $images = array(); $query = new \WP_Query( array( - 'post_parent' => $this->wp_object->ID, + 'post_parent' => $this->item->ID, 'post_status' => 'inherit', 'post_type' => 'attachment', 'post_mime_type' => 'image', @@ -513,20 +513,20 @@ protected function get_classic_editor_image_attachments( $max_images ) { * * @param array $media The media array grouped by type. * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param WP_Post $item The post object. * * @return array The filtered media IDs. */ - protected static function filter_media_by_object_type( $media, $type, $wp_object ) { + protected static function filter_media_by_object_type( $media, $type, $item ) { /** * Filter the object type for media attachments. * * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param WP_Post $item The post object. * * @return string The filtered object type. */ - $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object ); + $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $item ); if ( ! empty( $media[ $type ] ) ) { return $media[ $type ]; @@ -664,13 +664,13 @@ protected function get_type() { return \ucfirst( $post_format_setting ); } - $has_title = \post_type_supports( $this->wp_object->post_type, 'title' ); - $content = \wp_strip_all_tags( $this->wp_object->post_content ); + $has_title = \post_type_supports( $this->item->post_type, 'title' ); + $content = \wp_strip_all_tags( $this->item->post_content ); // Check if the post has a title. if ( ! $has_title || - ! $this->wp_object->post_title || + ! $this->item->post_title || \strlen( $content ) <= ACTIVITYPUB_NOTE_LENGTH ) { return 'Note'; @@ -681,10 +681,10 @@ protected function get_type() { $post_format = 'standard'; if ( \get_theme_support( 'post-formats' ) ) { - $post_format = \get_post_format( $this->wp_object ); + $post_format = \get_post_format( $this->item ); } - $post_type = \get_post_type( $this->wp_object ); + $post_type = \get_post_type( $this->item ); switch ( $post_type ) { case 'post': switch ( $post_format ) { @@ -767,7 +767,7 @@ public function get_audience() { protected function get_tag() { $tags = array(); - $post_tags = \get_the_tags( $this->wp_object->ID ); + $post_tags = \get_the_tags( $this->item->ID ); if ( $post_tags ) { foreach ( $post_tags as $post_tag ) { $tag = array( @@ -812,7 +812,7 @@ protected function get_summary() { return \__( '(This post is being modified)', 'activitypub' ); } - return generate_post_summary( $this->wp_object ); + return generate_post_summary( $this->item ); } /** @@ -828,7 +828,7 @@ protected function get_name() { return null; } - $title = \get_the_title( $this->wp_object->ID ); + $title = \get_the_title( $this->item->ID ); if ( $title ) { return \wp_strip_all_tags( @@ -852,7 +852,7 @@ protected function get_content() { add_filter( 'activitypub_reply_block', '__return_empty_string' ); // Remove Content from drafts. - if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->wp_object ) ) { + if ( 'draft' === \get_post_status( $this->wp_object ) ) { return \__( '(This post is being modified)', 'activitypub' ); } @@ -870,7 +870,7 @@ protected function get_content() { add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->wp_object; + $post = $this->item; $content = $this->get_post_content_template(); // It seems that shortcodes are only applied to published posts. @@ -918,7 +918,7 @@ protected function get_post_content_template() { $template .= '[ap_content]'; } - return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object ); + return apply_filters( 'activitypub_object_content_template', $template, $this->item ); } /** @@ -939,8 +939,8 @@ protected function get_mentions() { return apply_filters( 'activitypub_extract_mentions', array(), - $this->wp_object->post_content . ' ' . $this->wp_object->post_excerpt, - $this->wp_object + $this->item->post_content . ' ' . $this->item->post_excerpt, + $this->item ); } @@ -950,7 +950,7 @@ protected function get_mentions() { * @return string The locale of the post. */ public function get_locale() { - $post_id = $this->wp_object->ID; + $post_id = $this->item->ID; $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); /** @@ -962,7 +962,7 @@ public function get_locale() { * * @return string The filtered locale of the post. */ - return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object ); + return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->item ); } /** @@ -973,7 +973,7 @@ public function get_locale() { * @return string|null The in-reply-to URL of the post. */ public function get_in_reply_to() { - $blocks = \parse_blocks( $this->wp_object->post_content ); + $blocks = \parse_blocks( $this->item->post_content ); foreach ( $blocks as $block ) { if ( 'activitypub/reply' === $block['blockName'] ) { @@ -991,7 +991,7 @@ public function get_in_reply_to() { * @return string The published date of the post. */ public function get_published() { - $published = \strtotime( $this->wp_object->post_date_gmt ); + $published = \strtotime( $this->item->post_date_gmt ); return \gmdate( 'Y-m-d\TH:i:s\Z', $published ); } @@ -1002,8 +1002,8 @@ public function get_published() { * @return string|null The updated date of the post. */ public function get_updated() { - $published = \strtotime( $this->wp_object->post_date_gmt ); - $updated = \strtotime( $this->wp_object->post_modified_gmt ); + $published = \strtotime( $this->item->post_date_gmt ); + $updated = \strtotime( $this->item->post_modified_gmt ); if ( $updated > $published ) { return \gmdate( 'Y-m-d\TH:i:s\Z', $updated ); diff --git a/integration/class-seriously-simple-podcasting.php b/integration/class-seriously-simple-podcasting.php index 8ca343b45..38df757aa 100644 --- a/integration/class-seriously-simple-podcasting.php +++ b/integration/class-seriously-simple-podcasting.php @@ -28,7 +28,7 @@ class Seriously_Simple_Podcasting extends Post { * @return array The attachments array. */ public function get_attachment() { - $post = $this->wp_object; + $post = $this->item; $attachment = array( 'type' => \esc_attr( ucfirst( \get_post_meta( $post->ID, 'episode_type', true ) ?? 'Audio' ) ), 'url' => \esc_url( \get_post_meta( $post->ID, 'audio_file', true ) ), @@ -62,6 +62,6 @@ public function get_type() { * @return string The content. */ public function get_content() { - return generate_post_summary( $this->wp_object ); + return generate_post_summary( $this->item ); } } From d1e98393e5da09e072cbd786d964de0d5bcafa87 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 5 Nov 2024 11:48:54 +0100 Subject: [PATCH 018/116] fix auto-complete issue --- includes/collection/class-outbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index acda0053b..36d95d836 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -20,7 +20,7 @@ class Outbox { * * @param string|array|Base_Object|WP_Post|WP_Comment $item The item to add. * @param int $user_id The user ID. - * @param string $activity_type The activity + * @param string $activity_type The activity type. * * @return mixed The added item or an error. */ From 59ab8b9607760d9a0a99fd7fe3807a74958c994e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 5 Nov 2024 13:43:16 +0100 Subject: [PATCH 019/116] simplify code. --- includes/collection/class-replies.php | 4 ++-- includes/transformer/class-base.php | 19 +++++++++++++++++++ includes/transformer/class-comment.php | 21 --------------------- includes/transformer/class-post.php | 21 --------------------- 4 files changed, 21 insertions(+), 44 deletions(-) diff --git a/includes/collection/class-replies.php b/includes/collection/class-replies.php index 2f10c004b..3981e2636 100644 --- a/includes/collection/class-replies.php +++ b/includes/collection/class-replies.php @@ -74,7 +74,7 @@ private static function get_id( $wp_object ) { } elseif ( $wp_object instanceof WP_Comment ) { return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) ); } else { - return new WP_Error(); + return new WP_Error( 'unsupported_object', 'The object is not a post or comment.' ); } } @@ -88,7 +88,7 @@ private static function get_id( $wp_object ) { public static function get_collection( $wp_object ) { $id = self::get_id( $wp_object ); - if ( ! $id ) { + if ( ! $id || is_wp_error( $id ) ) { return null; } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index bca105f3a..80deb2644 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,4 +146,23 @@ abstract public function get_wp_user_id(); * @param int $user_id The new user ID. */ abstract public function change_wp_user_id( $user_id ); + + /** + * Returns a generic locale based on the Blog settings. + * + * @return string The locale of the blog. + */ + public function get_locale() { + $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); + + /** + * Filter the locale of the post. + * + * @param string $lang The locale of the post. + * @param mixed $item The post object. + * + * @return string The filtered locale of the post. + */ + return apply_filters( 'activitypub_locale', $lang, $this->item ); + } } diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index d9ee1e0e9..bf35ab184 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -287,25 +287,4 @@ public function extract_reply_context( $mentions ) { return $mentions; } - - /** - * Returns the locale of the post. - * - * @return string The locale of the post. - */ - public function get_locale() { - $comment_id = $this->item->ID; - $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); - - /** - * Filter the locale of the comment. - * - * @param string $lang The locale of the comment. - * @param int $comment_id The comment ID. - * @param \WP_Post $post The comment object. - * - * @return string The filtered locale of the comment. - */ - return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->item ); - } } diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 8644113e4..beb37697a 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -944,27 +944,6 @@ protected function get_mentions() { ); } - /** - * Returns the locale of the post. - * - * @return string The locale of the post. - */ - public function get_locale() { - $post_id = $this->item->ID; - $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); - - /** - * Filter the locale of the post. - * - * @param string $lang The locale of the post. - * @param int $post_id The post ID. - * @param WP_Post $post The post object. - * - * @return string The filtered locale of the post. - */ - return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->item ); - } - /** * Returns the in-reply-to URL of the post. * From 8133a1ce963c9e0bac5a36ecf1341dc1f3398d0b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 5 Nov 2024 13:57:16 +0100 Subject: [PATCH 020/116] convert JSON to Activity_Object to not have everything duplicated --- includes/transformer/class-base.php | 8 ++++---- includes/transformer/class-json.php | 28 ++++++++++------------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 80deb2644..02e46067d 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -44,7 +44,7 @@ abstract class Base { * * This helps to chain the output of the Transformer. * - * @param WP_Post|WP_Comment|Base_Object|string|array $item The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array $item The item that should be transformed. * * @return Base */ @@ -55,7 +55,7 @@ public static function transform( $item ) { /** * Base constructor. * - * @param WP_Post|WP_Comment|Base_Object|string|array $item The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array $item The item that should be transformed. */ public function __construct( $item ) { $this->item = $item; @@ -65,9 +65,9 @@ public function __construct( $item ) { /** * Transform all properties with available get(ter) functions. * - * @param Base_Object|object $activitypub_object The ActivityPub Object. + * @param Base_Object $activitypub_object The ActivityPub Object. * - * @return Base_Object|object + * @return Base_Object The transformed ActivityPub Object. */ protected function transform_object_properties( $activitypub_object ) { $vars = $activitypub_object->get_object_var_keys(); diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php index 1eaeb5062..b518bfbb1 100644 --- a/includes/transformer/class-json.php +++ b/includes/transformer/class-json.php @@ -12,30 +12,22 @@ /** * String Transformer Class file. */ -class Json extends Base { +class Json extends Activity_Object { + /** - * Transform the WordPress Object into an ActivityPub Object. + * JSON constructor. * - * @return Base_Object The ActivityPub Object. + * @param WP_Post|WP_Comment|Base_Object|string|array $item The item that should be transformed. */ - public function to_object() { - $activitypub_object = null; + public function __construct( $item ) { + $item = new Base_Object(); if ( is_array( $this->item ) ) { - $activitypub_object = Base_Object::init_from_array( $this->item ); - } else { - $activitypub_object = Base_Object::init_from_json( $this->item ); + $item = Base_Object::init_from_array( $this->item ); + } elseif ( is_string( $this->item ) ) { + $item = Base_Object::init_from_json( $this->item ); } - return $activitypub_object; - } - - /** - * Get the ID of the WordPress Object. - * - * @return string The ID of the WordPress Object. - */ - protected function get_id() { - return ''; + parent::__construct( $item ); } } From 0bdb30d535cdf47f4e0bd222e0f9e18d1564efa8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 5 Nov 2024 15:14:03 +0100 Subject: [PATCH 021/116] add support for maps and mentions --- .../transformer/class-activity-object.php | 126 +++++++++++++++++- includes/transformer/class-base.php | 29 ++-- includes/transformer/class-comment.php | 55 +++++--- includes/transformer/class-post.php | 17 +-- 4 files changed, 188 insertions(+), 39 deletions(-) diff --git a/includes/transformer/class-activity-object.php b/includes/transformer/class-activity-object.php index bfe567813..13b3aac19 100644 --- a/includes/transformer/class-activity-object.php +++ b/includes/transformer/class-activity-object.php @@ -17,7 +17,7 @@ class Activity_Object extends Base { * @return Base_Object The ActivityPub Object. */ public function to_object() { - return $this->item; + return $this->transform_object_properties( $this->item ); } /** @@ -28,4 +28,128 @@ public function to_object() { protected function get_id() { return ''; } + + /** + * Helper function to get the @-Mentions from the post content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + /** + * Filter the mentions in the post content. + * + * @param array $mentions The mentions. + * @param string $content The post content. + * @param WP_Post $post The post object. + * + * @return array The filtered mentions. + */ + return apply_filters( + 'activitypub_extract_mentions', + array(), + $this->item->get_content() . ' ' . $this->item->get_summary(), + $this->item + ); + } + + /** + * Returns a list of Mentions, used in the Post. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#Mention + * + * @return array The list of Mentions. + */ + protected function get_cc() { + $cc = array(); + $mentions = $this->get_mentions(); + + if ( $mentions ) { + foreach ( $mentions as $url ) { + $cc[] = $url; + } + } + + return $cc; + } + + /** + * Returns the content map for the post. + * + * @return array The content map for the post. + */ + protected function get_content_map() { + $content = $this->item->get_content(); + + if ( ! $content ) { + return null; + } + + return array( + $this->get_locale() => $content, + ); + } + + /** + * Returns the name map for the post. + * + * @return array The name map for the post. + */ + protected function get_name_map() { + $name = $this->item->get_name(); + + if ( ! $name ) { + return null; + } + + return array( + $this->get_locale() => $name, + ); + } + + /** + * Returns the summary map for the post. + * + * @return array The summary map for the post. + */ + protected function get_summary_map() { + $summary = $this->item->get_summary(); + + if ( ! $summary ) { + return null; + } + + return array( + $this->get_locale() => $summary, + ); + } + + /** + * Returns a list of Tags, used in the Comment. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tag() { + $tags = $this->item->get_tags(); + + if ( ! $tags ) { + $tags = array(); + } + + $mentions = $this->get_mentions(); + + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $tag = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + $tags[] = $tag; + } + } + + return \array_unique( $tags, SORT_REGULAR ); + } } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 02e46067d..3f1accd13 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -128,13 +128,6 @@ public function to_activity( $type ) { */ abstract protected function get_id(); - /** - * Get the replies Collection. - */ - public function get_replies() { - return Replies::get_collection( $this->item ); - } - /** * Returns the ID of the WordPress Object. */ @@ -152,7 +145,7 @@ abstract public function change_wp_user_id( $user_id ); * * @return string The locale of the blog. */ - public function get_locale() { + protected function get_locale() { $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); /** @@ -165,4 +158,24 @@ public function get_locale() { */ return apply_filters( 'activitypub_locale', $lang, $this->item ); } + + /** + * Returns the recipient of the post. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to + * + * @return array The recipient URLs of the post. + */ + protected function get_to() { + return array( + 'https://www.w3.org/ns/activitystreams#Public', + ); + } + + /** + * Get the replies Collection. + */ + public function get_replies() { + return Replies::get_collection( $this->item ); + } } diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index bf35ab184..a51c16c75 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -27,6 +27,13 @@ * - Activitypub\Activity\Base_Object */ class Comment extends Base { + /** + * The User as Actor Object. + * + * @var \Activitypub\Activity\Actor + */ + private $actor_object = null; + /** * Returns the User-ID of the WordPress Comment. * @@ -72,14 +79,6 @@ public function to_object() { $this->get_locale() => $this->get_content(), ) ); - $path = sprintf( 'actors/%d/followers', intval( $comment->comment_author ) ); - - $object->set_to( - array( - 'https://www.w3.org/ns/activitystreams#Public', - get_rest_url_by_path( $path ), - ) - ); return $object; } @@ -92,12 +91,7 @@ public function to_object() { * @return string The User-URL. */ protected function get_attributed_to() { - if ( is_single_user() ) { - $user = new Blog(); - return $user->get_id(); - } - - return Users::get_by_id( $this->item->user_id )->get_id(); + return $this->get_actor_object()->get_id(); } /** @@ -173,6 +167,35 @@ protected function get_id() { return Comment_Utils::generate_id( $comment ); } + /** + * Returns the User-Object of the Author of the Post. + * + * If `single_user` mode is enabled, the Blog-User is returned. + * + * @return \Activitypub\Activity\Actor The User-Object. + */ + protected function get_actor_object() { + if ( $this->actor_object ) { + return $this->actor_object; + } + + $blog_user = new Blog(); + $this->actor_object = $blog_user; + + if ( is_single_user() ) { + return $blog_user; + } + + $user = Users::get_by_id( $this->item->user_id ); + + if ( $user && ! is_wp_error( $user ) ) { + $this->actor_object = $user; + return $user; + } + + return $blog_user; + } + /** * Returns a list of Mentions, used in the Comment. * @@ -181,7 +204,9 @@ protected function get_id() { * @return array The list of Mentions. */ protected function get_cc() { - $cc = array(); + $cc = array( + $this->get_actor_object()->get_followers(), + ); $mentions = $this->get_mentions(); if ( $mentions ) { diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index beb37697a..7077dd42f 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -254,7 +254,7 @@ protected function get_attachment() { * * @return array The media array extended with enclosures. */ - public function get_enclosures( $media ) { + protected function get_enclosures( $media ) { $enclosures = get_enclosures( $this->item->ID ); if ( ! $enclosures ) { @@ -708,19 +708,6 @@ protected function get_type() { return $object_type; } - /** - * Returns the recipient of the post. - * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to - * - * @return array The recipient URLs of the post. - */ - public function get_to() { - return array( - 'https://www.w3.org/ns/activitystreams#Public', - ); - } - /** * Returns a list of Mentions, used in the Post. * @@ -748,7 +735,7 @@ protected function get_cc() { * * @return string|null The audience. */ - public function get_audience() { + protected function get_audience() { if ( is_single_user() ) { return null; } else { From 748a266f932b928b012998d4297255de669d2b89 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 12 Nov 2024 13:09:40 +0100 Subject: [PATCH 022/116] it takes a string (JSON) or an array --- includes/transformer/class-json.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php index b518bfbb1..8e8dbf05c 100644 --- a/includes/transformer/class-json.php +++ b/includes/transformer/class-json.php @@ -17,7 +17,7 @@ class Json extends Activity_Object { /** * JSON constructor. * - * @param WP_Post|WP_Comment|Base_Object|string|array $item The item that should be transformed. + * @param string|array $item The item that should be transformed. */ public function __construct( $item ) { $item = new Base_Object(); From 22aea420cc917daed3c3e752ce6fc6331779df2c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 13 Nov 2024 19:34:41 +0100 Subject: [PATCH 023/116] simplified based on the feedback of @obenland --- includes/transformer/class-post.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 0d0d1bc5b..e80867348 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -716,16 +716,8 @@ protected function get_type() { * @return array The list of Mentions. */ protected function get_cc() { - $cc = array( - $this->get_actor_object()->get_followers(), - ); - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $url ) { - $cc[] = $url; - } - } + $cc = array_values( $this->get_mentions() ); + $cc[] = $this->get_actor_object()->get_followers(); return $cc; } From 9b20505039d71d6368b590346211bb479201a0bd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 14 Nov 2024 14:32:28 +0100 Subject: [PATCH 024/116] rearrange code a bit --- includes/class-scheduler.php | 249 ++------------------------- includes/collection/class-outbox.php | 2 +- includes/scheduler/class-actor.php | 99 +++++++++++ includes/scheduler/class-comment.php | 89 ++++++++++ includes/scheduler/class-post.php | 105 +++++++++++ 5 files changed, 309 insertions(+), 235 deletions(-) create mode 100644 includes/scheduler/class-actor.php create mode 100644 includes/scheduler/class-comment.php create mode 100644 includes/scheduler/class-post.php diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index e810aaa4c..6299f0608 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -20,63 +20,27 @@ class Scheduler { * Initialize the class, registering WordPress hooks. */ public static function init() { - // Post transitions. - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); - \add_action( - 'edit_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'publish', 'publish', $post_id ); - } - ); - \add_action( - 'add_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'publish', '', $post_id ); - } - ); - \add_action( - 'delete_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'trash', '', $post_id ); - } - ); - - if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) { - // Comment transitions. - \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 ); - \add_action( - 'edit_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', 'approved', $comment_id ); - } - ); - \add_action( - 'wp_insert_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', '', $comment_id ); - } - ); - } + self::register_schedulers(); // Follower Cleanups. \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + } - // Profile updates for blog options. - if ( ! is_user_type_disabled( 'blog' ) ) { - \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) ); - \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) ); - \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) ); - \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) ); - \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) ); - } + /** + * Register handlers. + */ + public static function register_schedulers() { + Post::init(); + Comment::init(); + Post::init(); - // Profile updates for user options. - if ( ! is_user_type_disabled( 'user' ) ) { - \add_action( 'wp_update_user', array( self::class, 'user_update' ) ); - \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 ); - // @todo figure out a feasible way of updating the header image since it's not unique to any user. - } + /** + * Register additional schedulers. + * + * @since 5.0.0 + */ + do_action( 'activitypub_register_schedulers' ); } /** @@ -102,124 +66,6 @@ public static function deregister_schedules() { wp_unschedule_hook( 'activitypub_cleanup_followers' ); } - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param \WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - $post = get_post( $post ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - if ( 'ap_extrafield' === $post->post_type ) { - self::schedule_profile_update( $post->post_author ); - return; - } - - if ( 'ap_extrafield_blog' === $post->post_type ) { - self::schedule_profile_update( 0 ); - return; - } - - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $type = false; - - if ( - 'publish' === $new_status && - 'publish' !== $old_status - ) { - $type = 'Create'; - } elseif ( - 'publish' === $new_status || - // We want to send updates for posts that are published and then moved to draft. - ( 'draft' === $new_status && - 'publish' === $old_status ) - ) { - $type = 'Update'; - } elseif ( 'trash' === $new_status ) { - $type = 'Delete'; - } - - if ( empty( $type ) ) { - return; - } - - $hook = 'activitypub_send_post'; - $args = array( $post->ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $post, 'federate' ); - \wp_schedule_single_event( \time() + 10, $hook, $args ); - } - } - - /** - * Schedule Comment Activities. - * - * @see transition_comment_status() - * - * @param string $new_status New comment status. - * @param string $old_status Old comment status. - * @param \WP_Comment $comment Comment object. - */ - public static function schedule_comment_activity( $new_status, $old_status, $comment ) { - $comment = get_comment( $comment ); - - // Federate only comments that are written by a registered user. - if ( ! $comment || ! $comment->user_id ) { - return; - } - - $type = false; - - if ( - 'approved' === $new_status && - 'approved' !== $old_status - ) { - $type = 'Create'; - } elseif ( 'approved' === $new_status ) { - $type = 'Update'; - \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true ); - } elseif ( - 'trash' === $new_status || - 'spam' === $new_status - ) { - $type = 'Delete'; - } - - if ( empty( $type ) ) { - return; - } - - // Check if comment should be federated or not. - if ( ! should_comment_be_federated( $comment ) ) { - return; - } - - $hook = 'activitypub_send_comment'; - $args = array( $comment->comment_ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $comment, 'federate' ); - \wp_schedule_single_event( \time(), $hook, $args ); - } - } - /** * Update followers. */ @@ -289,69 +135,4 @@ public static function cleanup_followers() { } } } - - /** - * Send a profile update when relevant user meta is updated. - * - * @param int $meta_id Meta ID being updated. - * @param int $user_id User ID being updated. - * @param string $meta_key Meta key being updated. - */ - public static function user_meta_update( $meta_id, $user_id, $meta_key ) { - // Don't bother if the user can't publish. - if ( ! \user_can( $user_id, 'activitypub' ) ) { - return; - } - - // The user meta fields that affect a profile. - $fields = array( - 'activitypub_description', - 'activitypub_header_image', - 'description', - 'user_url', - 'display_name', - ); - if ( in_array( $meta_key, $fields, true ) ) { - self::schedule_profile_update( $user_id ); - } - } - - /** - * Send a profile update when a user is updated. - * - * @param int $user_id User ID being updated. - */ - public static function user_update( $user_id ) { - // Don't bother if the user can't publish. - if ( ! \user_can( $user_id, 'activitypub' ) ) { - return; - } - - self::schedule_profile_update( $user_id ); - } - - /** - * Theme mods only have a dynamic filter so we fudge it like this. - * - * @param mixed $value Optional. The value to be updated. Default null. - * - * @return mixed - */ - public static function blog_user_update( $value = null ) { - self::schedule_profile_update( 0 ); - return $value; - } - - /** - * Send a profile update to all followers. Gets hooked into all relevant options/meta etc. - * - * @param int $user_id The user ID to update (Could be 0 for Blog-User). - */ - public static function schedule_profile_update( $user_id ) { - \wp_schedule_single_event( - \time() + 10, - 'activitypub_send_update_profile_activity', - array( $user_id ) - ); - } } diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 36d95d836..45dca6d1e 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -24,7 +24,7 @@ class Outbox { * * @return mixed The added item or an error. */ - public static function add_item( $item, $user_id, $activity_type = 'Create' ) { + public static function add_item( $item, $user_id, $activity_type = 'Create' ) { // phpcs:ignore $transformer = Factory::get_transformer( $item ); $object = $transformer->transform(); diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php new file mode 100644 index 000000000..b4ea906b9 --- /dev/null +++ b/includes/scheduler/class-actor.php @@ -0,0 +1,99 @@ +user_id ) { + return; + } + + $type = false; + + if ( + 'approved' === $new_status && + 'approved' !== $old_status + ) { + $type = 'Create'; + } elseif ( 'approved' === $new_status ) { + $type = 'Update'; + \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true ); + } elseif ( + 'trash' === $new_status || + 'spam' === $new_status + ) { + $type = 'Delete'; + } + + if ( empty( $type ) ) { + return; + } + + // Check if comment should be federated or not. + if ( ! should_comment_be_federated( $comment ) ) { + return; + } + + $hook = 'activitypub_send_comment'; + $args = array( $comment->comment_ID, $type ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + set_wp_object_state( $comment, 'federate' ); + \wp_schedule_single_event( \time(), $hook, $args ); + } + } +} diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php new file mode 100644 index 000000000..496923b38 --- /dev/null +++ b/includes/scheduler/class-post.php @@ -0,0 +1,105 @@ +post_type ) { + self::schedule_profile_update( $post->post_author ); + return; + } + + if ( 'ap_extrafield_blog' === $post->post_type ) { + self::schedule_profile_update( 0 ); + return; + } + + // Do not send activities if post is password protected. + if ( \post_password_required( $post ) ) { + return; + } + + // Check if post-type supports ActivityPub. + $post_types = \get_post_types_by_support( 'activitypub' ); + if ( ! \in_array( $post->post_type, $post_types, true ) ) { + return; + } + + $type = false; + + if ( + 'publish' === $new_status && + 'publish' !== $old_status + ) { + $type = 'Create'; + } elseif ( + 'publish' === $new_status || + // We want to send updates for posts that are published and then moved to draft. + ( 'draft' === $new_status && + 'publish' === $old_status ) + ) { + $type = 'Update'; + } elseif ( 'trash' === $new_status ) { + $type = 'Delete'; + } + + if ( empty( $type ) ) { + return; + } + + $hook = 'activitypub_send_post'; + $args = array( $post->ID, $type ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + set_wp_object_state( $post, 'federate' ); + \wp_schedule_single_event( \time() + 10, $hook, $args ); + } + } +} From 617eff7df982c0ab50998d3c74c4fd183e5d66f5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 14 Nov 2024 15:29:53 +0100 Subject: [PATCH 025/116] add missing uses --- includes/class-scheduler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 6299f0608..c101093dd 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -7,6 +7,9 @@ namespace Activitypub; +use Activitypun\Scheduler\Post; +use Activitypub\Scheduler\Actor; +use Activitypub\Scheduler\Comment; use Activitypub\Collection\Followers; /** From ad5f35a7aee895dbaef46c65e513042871356ea9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 14 Nov 2024 15:37:49 +0100 Subject: [PATCH 026/116] fix typo --- includes/class-scheduler.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index c101093dd..36ad1b809 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -7,7 +7,7 @@ namespace Activitypub; -use Activitypun\Scheduler\Post; +use Activitypub\Scheduler\Post; use Activitypub\Scheduler\Actor; use Activitypub\Scheduler\Comment; use Activitypub\Collection\Followers; @@ -35,8 +35,8 @@ public static function init() { */ public static function register_schedulers() { Post::init(); + Actor::init(); Comment::init(); - Post::init(); /** * Register additional schedulers. From c1f63b58f80eaecd5de0db4ca0b4ad70fbfe7d47 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 14 Nov 2024 15:45:04 +0100 Subject: [PATCH 027/116] add missing uses --- includes/scheduler/class-actor.php | 2 ++ includes/scheduler/class-comment.php | 3 +++ includes/scheduler/class-post.php | 3 +++ 3 files changed, 8 insertions(+) diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php index b4ea906b9..89acf64f0 100644 --- a/includes/scheduler/class-actor.php +++ b/includes/scheduler/class-actor.php @@ -7,6 +7,8 @@ namespace Activitypub\Scheduler; +use function Activitypub\is_user_type_disabled; + /** * Post scheduler class. */ diff --git a/includes/scheduler/class-comment.php b/includes/scheduler/class-comment.php index c6e963afe..ecb7f8e85 100644 --- a/includes/scheduler/class-comment.php +++ b/includes/scheduler/class-comment.php @@ -7,6 +7,9 @@ namespace Activitypub\Scheduler; +use function Activitypub\set_wp_object_state; +use function Activitypub\should_comment_be_federated; + /** * Post scheduler class. */ diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 496923b38..67a988d8b 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,6 +7,9 @@ namespace Activitypub\Scheduler; +use function Activitypub\is_post_disabled; +use function Activitypub\set_wp_object_state; + /** * Post scheduler class. */ From fe29ea4984cb18f8382e4b93948cf189f12c9e2b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 22 Nov 2024 13:23:23 +0100 Subject: [PATCH 028/116] Added Changelog-Entry --- CHANGELOG.md | 1 + readme.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e17d9985..0b3295f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Fediverse Preview on post-overview page * GitHub action to enforce Changelog updates. +* Outbox queue ### Improved diff --git a/readme.txt b/readme.txt index 473b53b60..64d7bdfa5 100644 --- a/readme.txt +++ b/readme.txt @@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other * Added: Fediverse Preview on post-overview page * Added: GitHub action to enforce Changelog updates. +* Added: Outbox queue * Improved: Outsource Constants to a separate file * Improved: Better handling of `readme.txt` and `README.md` From 8f4f8b137f14cfdca22a6a036e1c8d91edba6e5d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 09:29:38 +0100 Subject: [PATCH 029/116] apply changes from @obenland --- includes/scheduler/class-post.php | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 67a988d8b..e6f1420d1 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -75,22 +75,21 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) return; } - $type = false; - - if ( - 'publish' === $new_status && - 'publish' !== $old_status - ) { - $type = 'Create'; - } elseif ( - 'publish' === $new_status || - // We want to send updates for posts that are published and then moved to draft. - ( 'draft' === $new_status && - 'publish' === $old_status ) - ) { - $type = 'Update'; - } elseif ( 'trash' === $new_status ) { - $type = 'Delete'; + switch ( $new_status ) { + case 'publish': + $type = ( 'publish' === $old_status ) ? 'Update' : 'Create'; + break; + + case 'draft': + $type = ( 'publish' === $old_status ) ? 'Update' : false; + break; + + case 'trash': + $type = 'federated' === get_wp_object_state( $post ) ? 'Delete' : false; + break; + + default: + $type = false; } if ( empty( $type ) ) { From f26eb3df61bc406a6a927344ea1237b80fba6a80 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 09:35:11 +0100 Subject: [PATCH 030/116] delay scheduling a bit --- includes/scheduler/class-comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/scheduler/class-comment.php b/includes/scheduler/class-comment.php index ecb7f8e85..c92227f4b 100644 --- a/includes/scheduler/class-comment.php +++ b/includes/scheduler/class-comment.php @@ -86,7 +86,7 @@ public static function schedule_comment_activity( $new_status, $old_status, $com if ( false === wp_next_scheduled( $hook, $args ) ) { set_wp_object_state( $comment, 'federate' ); - \wp_schedule_single_event( \time(), $hook, $args ); + \wp_schedule_single_event( \time() + 10, $hook, $args ); } } } From 77f8f06652b9c645535b790895c70c91f708d7a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 13:51:20 +0100 Subject: [PATCH 031/116] change scheduling and add possibility to add items to outbox --- includes/collection/class-followers.php | 2 +- includes/collection/class-outbox.php | 29 ++++----- includes/functions.php | 48 +++++++++++++- includes/scheduler/class-actor.php | 11 ++-- includes/scheduler/class-comment.php | 9 +-- includes/scheduler/class-post.php | 11 +--- .../transformer/class-activity-object.php | 18 ++++- includes/transformer/class-comment.php | 2 +- ...s-test-activitypub-activity-dispatcher.php | 2 +- tests/class-test-activitypub-followers.php | 2 +- tests/class-test-activitypub-mention.php | 2 +- ...ass-test-activitypub-outbox-collection.php | 65 +++++++++++++++++++ tests/class-test-enable-mastodon-apps.php | 2 +- 13 files changed, 158 insertions(+), 45 deletions(-) create mode 100644 tests/class-test-activitypub-outbox-collection.php diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index dc4e31507..5991bba25 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -266,7 +266,7 @@ public static function count_followers( $user_id ) { } /** - * Returns all Inboxes for a Users Followers. + * Returns all Inboxes for a Actors Followers. * * @param int $user_id The ID of the WordPress User. * diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 45dca6d1e..803125768 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -7,8 +7,6 @@ namespace Activitypub\Collection; -use Activitypub\Transformer\Factory; - /** * ActivityPub Outbox Collection */ @@ -18,24 +16,17 @@ class Outbox { /** * Add an Item to the outbox. * - * @param string|array|Base_Object|WP_Post|WP_Comment $item The item to add. - * @param int $user_id The user ID. - * @param string $activity_type The activity type. + * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. + * @param string $activity_type The activity type. + * @param int $user_id The user ID. * * @return mixed The added item or an error. */ - public static function add_item( $item, $user_id, $activity_type = 'Create' ) { // phpcs:ignore - $transformer = Factory::get_transformer( $item ); - $object = $transformer->transform(); - - if ( ! $object || is_wp_error( $object ) ) { - return $object; - } - + public static function add( $activity_object, $activity_type = 'Create', $user_id ) { // phpcs:ignore $outbox_item = array( 'post_type' => self::POST_TYPE, - 'post_title' => $object->get_id(), - 'post_content' => $object->to_json(), + 'post_title' => $activity_object->get_id(), + 'post_content' => $activity_object->to_json(), 'post_author' => $user_id, 'post_status' => 'draft', ); @@ -46,12 +37,16 @@ public static function add_item( $item, $user_id, $activity_type = 'Create' ) { \kses_remove_filters(); } - $result = \wp_insert_post( $outbox_item, true ); + $id = \wp_insert_post( $outbox_item, true ); if ( $has_kses ) { \kses_init_filters(); } - return $result; + if ( ! $id || \is_wp_error( $id ) ) { + return false; + } + + return $id; } } diff --git a/includes/functions.php b/includes/functions.php index eecc689dc..15f4fb2e6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -9,9 +9,11 @@ use WP_Error; use Activitypub\Activity\Activity; -use Activitypub\Collection\Followers; use Activitypub\Collection\Actors; +use Activitypub\Collection\Outbox; +use Activitypub\Collection\Followers; use Activitypub\Transformer\Post; +use Activitypub\Transformer\Factory as Transformer_Factory; /** * Returns the ActivityPub default JSON-context. @@ -1529,3 +1531,47 @@ function is_self_ping( $id ) { return false; } + +/** + * Add an object to the outbox. + * + * @param mixed $data The object to add to the outbox. + * @param string $type The type of the Activity. + * @param integer $user_id The User-ID. + * + * @return boolean|int The ID of the outbox item or false on failure. + */ +function add_to_outbox( $data, $type = 'Create', $user_id = 0 ) { + $transformer = Transformer_Factory::get_transformer( $data ); + + if ( ! $transformer || is_wp_error( $transformer ) ) { + return false; + } + + $activity = $transformer->to_object(); + + if ( ! $activity || is_wp_error( $activity ) ) { + return false; + } + + set_wp_object_state( $data, 'federate' ); + + $id = Outbox::add( $activity, $type, $user_id ); + + if ( ! $id ) { + return false; + } + + $hook = 'activitypub_process_outbox'; + $args = array( $id ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + \wp_schedule_single_event( + \time() + 10, + $hook, + $args + ); + } + + return $id; +} diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php index 89acf64f0..17830cf91 100644 --- a/includes/scheduler/class-actor.php +++ b/includes/scheduler/class-actor.php @@ -7,6 +7,9 @@ namespace Activitypub\Scheduler; +use Activitypub\Collection\Actors; + +use function Activitypub\add_to_outbox; use function Activitypub\is_user_type_disabled; /** @@ -92,10 +95,8 @@ public static function blog_user_update( $value = null ) { * @param int $user_id The user ID to update (Could be 0 for Blog-User). */ public static function schedule_profile_update( $user_id ) { - \wp_schedule_single_event( - \time() + 10, - 'activitypub_send_update_profile_activity', - array( $user_id ) - ); + $actor = Actors::get_by_id( $user_id ); + + add_to_outbox( $actor->get_id(), 'Update', $user_id ); } } diff --git a/includes/scheduler/class-comment.php b/includes/scheduler/class-comment.php index c92227f4b..a6d6bbd1c 100644 --- a/includes/scheduler/class-comment.php +++ b/includes/scheduler/class-comment.php @@ -7,6 +7,7 @@ namespace Activitypub\Scheduler; +use function Activitypub\add_to_outbox; use function Activitypub\set_wp_object_state; use function Activitypub\should_comment_be_federated; @@ -81,12 +82,6 @@ public static function schedule_comment_activity( $new_status, $old_status, $com return; } - $hook = 'activitypub_send_comment'; - $args = array( $comment->comment_ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $comment, 'federate' ); - \wp_schedule_single_event( \time() + 10, $hook, $args ); - } + add_to_outbox( $comment, $type, $comment->user_id ); } } diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index e6f1420d1..ff3dbeeb9 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,8 +7,9 @@ namespace Activitypub\Scheduler; +use function Activitypub\add_to_outbox; use function Activitypub\is_post_disabled; -use function Activitypub\set_wp_object_state; +use function Activitypub\get_wp_object_state; /** * Post scheduler class. @@ -96,12 +97,6 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) return; } - $hook = 'activitypub_send_post'; - $args = array( $post->ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $post, 'federate' ); - \wp_schedule_single_event( \time() + 10, $hook, $args ); - } + add_to_outbox( $post, $type, $post->post_author ); } } diff --git a/includes/transformer/class-activity-object.php b/includes/transformer/class-activity-object.php index 13b3aac19..b96348f17 100644 --- a/includes/transformer/class-activity-object.php +++ b/includes/transformer/class-activity-object.php @@ -131,7 +131,7 @@ protected function get_summary_map() { * @return array The list of Tags. */ protected function get_tag() { - $tags = $this->item->get_tags(); + $tags = $this->item->get_tag(); if ( ! $tags ) { $tags = array(); @@ -152,4 +152,20 @@ protected function get_tag() { return \array_unique( $tags, SORT_REGULAR ); } + + /** + * Returns the ID of the WordPress Object. + */ + public function get_wp_user_id() { + return ''; + } + + /** + * Change the User-ID of the WordPress Post. + * + * @param int $user_id The new user ID. + */ + public function change_wp_user_id( $user_id ) { + // do nothing. + } } diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index e6dd76ec4..84768099a 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -186,7 +186,7 @@ protected function get_actor_object() { return $blog_user; } - $user = Users::get_by_id( $this->item->user_id ); + $user = Actors::get_by_id( $this->item->user_id ); if ( $user && ! is_wp_error( $user ) ) { $this->actor_object = $user; diff --git a/tests/class-test-activitypub-activity-dispatcher.php b/tests/class-test-activitypub-activity-dispatcher.php index bf463d401..048c61aa5 100644 --- a/tests/class-test-activitypub-activity-dispatcher.php +++ b/tests/class-test-activitypub-activity-dispatcher.php @@ -13,7 +13,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HTTP { /** - * Users. + * Actors. * * @var array[] $users */ diff --git a/tests/class-test-activitypub-followers.php b/tests/class-test-activitypub-followers.php index 79f2176c0..5dc4873e9 100644 --- a/tests/class-test-activitypub-followers.php +++ b/tests/class-test-activitypub-followers.php @@ -13,7 +13,7 @@ class Test_Activitypub_Followers extends WP_UnitTestCase { /** - * Users. + * Actors. * * @var array[] */ diff --git a/tests/class-test-activitypub-mention.php b/tests/class-test-activitypub-mention.php index 1de18ec46..de41830f0 100644 --- a/tests/class-test-activitypub-mention.php +++ b/tests/class-test-activitypub-mention.php @@ -13,7 +13,7 @@ class Test_Activitypub_Mention extends ActivityPub_TestCase_Cache_HTTP { /** - * Users. + * Actors. * * @var array */ diff --git a/tests/class-test-activitypub-outbox-collection.php b/tests/class-test-activitypub-outbox-collection.php new file mode 100644 index 000000000..c5b8dfe3b --- /dev/null +++ b/tests/class-test-activitypub-outbox-collection.php @@ -0,0 +1,65 @@ +assertIsInt( $id ); + + $post = get_post( $id ); + + $this->assertInstanceOf( 'WP_Post', $post ); + $this->assertEquals( 'draft', $post->post_status ); + //$this->assertEquals( $json, $post->post_content ); + } + + /** + * Data provider for test_add. + * + * @return array + */ + public function activity_object_provider() { + return array( + array( + array( + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/1', + 'type' => 'Note', + 'content' => 'This is a note', + ), + 'Create', + 1, + '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","id":"https:\/\/example.com\/1","type":"Note","content":"This is a note"}', + ), + array( + array( + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/2', + 'type' => 'Note', + 'content' => 'This is another note', + ), + 'Create', + 2, + '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","id":"https:\/\/example.com\/2","type":"Note","content":"This is another note"}', + ), + ); + } +} diff --git a/tests/class-test-enable-mastodon-apps.php b/tests/class-test-enable-mastodon-apps.php index 3c3a0914e..f6169f496 100644 --- a/tests/class-test-enable-mastodon-apps.php +++ b/tests/class-test-enable-mastodon-apps.php @@ -13,7 +13,7 @@ class Test_Enable_Mastodon_Apps extends WP_UnitTestCase { /** - * Users. + * Actors. * * @var array[] */ From abef3dc0b8f43f12b62ef3acef2b9ea44287194d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 14:10:00 +0100 Subject: [PATCH 032/116] remove deprecated functions --- includes/collection/class-outbox.php | 2 +- includes/transformer/class-base.php | 12 ------------ includes/transformer/class-comment.php | 18 ------------------ includes/transformer/class-post.php | 22 ---------------------- 4 files changed, 1 insertion(+), 53 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 803125768..0ae3f52f0 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -22,7 +22,7 @@ class Outbox { * * @return mixed The added item or an error. */ - public static function add( $activity_object, $activity_type = 'Create', $user_id ) { // phpcs:ignore + public static function add( $activity_object, $activity_type, $user_id ) { // phpcs:ignore $outbox_item = array( 'post_type' => self::POST_TYPE, 'post_title' => $activity_object->get_id(), diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 3f1accd13..a8ac663f8 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -128,18 +128,6 @@ public function to_activity( $type ) { */ abstract protected function get_id(); - /** - * Returns the ID of the WordPress Object. - */ - abstract public function get_wp_user_id(); - - /** - * Change the User-ID of the WordPress Post. - * - * @param int $user_id The new user ID. - */ - abstract public function change_wp_user_id( $user_id ); - /** * Returns a generic locale based on the Blog settings. * diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index 84768099a..e83059280 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -34,24 +34,6 @@ class Comment extends Base { */ private $actor_object = null; - /** - * Returns the User-ID of the WordPress Comment. - * - * @return int The User-ID of the WordPress Comment - */ - public function get_wp_user_id() { - return $this->item->user_id; - } - - /** - * Change the User-ID of the WordPress Comment. - * - * @param int $user_id The new user ID. - */ - public function change_wp_user_id( $user_id ) { - $this->item->user_id = $user_id; - } - /** * Transforms the WP_Comment object to an ActivityPub Object. * diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index de7dad16d..df189840e 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -39,28 +39,6 @@ class Post extends Base { */ private $actor_object = null; - /** - * Returns the ID of the WordPress Post. - * - * @return int The ID of the WordPress Post - */ - public function get_wp_user_id() { - return $this->item->post_author; - } - - /** - * Change the User-ID of the WordPress Post. - * - * @param int $user_id The new user ID. - * - * @return Post The Post Object. - */ - public function change_wp_user_id( $user_id ) { - $this->item->post_author = $user_id; - - return $this; - } - /** * Transforms the WP_Post object to an ActivityPub Object * From b93568d8ba3062048477a47c111422e21ace7a0f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 14:43:35 +0100 Subject: [PATCH 033/116] add filter --- includes/transformer/class-base.php | 33 ++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index a8ac663f8..2837c9441 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -65,12 +65,12 @@ public function __construct( $item ) { /** * Transform all properties with available get(ter) functions. * - * @param Base_Object $activitypub_object The ActivityPub Object. + * @param Base_Object $activity_object The ActivityPub Object. * * @return Base_Object The transformed ActivityPub Object. */ - protected function transform_object_properties( $activitypub_object ) { - $vars = $activitypub_object->get_object_var_keys(); + protected function transform_object_properties( $activity_object ) { + $vars = $activity_object->get_object_var_keys(); foreach ( $vars as $var ) { $getter = 'get_' . $var; @@ -81,22 +81,39 @@ protected function transform_object_properties( $activitypub_object ) { if ( isset( $value ) ) { $setter = 'set_' . $var; - call_user_func( array( $activitypub_object, $setter ), $value ); + /** + * Filter the value before it is set to the Activity-Object `$activity_object`. + * + * @param mixed $value The value that should be set. + * @param mixed $item The Object. + */ + $value = apply_filters( 'activitypub_transform_' . $setter, $value, $this->item ); + + /** + * Filter the value before it is set to the Activity-Object `$activity_object`. + * + * @param mixed $value The value that should be set. + * @param string $var The variable name. + * @param mixed $item The Object. + */ + $value = apply_filters( 'activitypub_transform_set', $value, $var, $this->item ); + + call_user_func( array( $activity_object, $setter ), $value ); } } } - return $activitypub_object; + return $activity_object; } /** * Transform the WordPress Object into an ActivityPub Object. * - * @return Base_Object|object The ActivityPub Object. + * @return Base_Object|object The Activity-Object. */ public function to_object() { - $activitypub_object = new Base_Object(); + $activity_object = new Base_Object(); - return $this->transform_object_properties( $activitypub_object ); + return $this->transform_object_properties( $activity_object ); } /** From 17cc89b50cde6b1daeeb181d8578faad53278fe8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 14:49:21 +0100 Subject: [PATCH 034/116] fix indents --- includes/transformer/class-base.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index af14f846b..4e11e3b7a 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -186,11 +186,11 @@ protected function get_to() { ); } - /** + /** * Get the replies Collection. * * @return array The replies collection. - */ + */ public function get_replies() { return Replies::get_collection( $this->item ); } From 535899cb182c20c2367221d01495f3487832fbb6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 6 Dec 2024 14:54:21 +0100 Subject: [PATCH 035/116] removed deprecated functions --- .../transformer/class-activity-object.php | 25 ------------------- includes/transformer/class-base.php | 5 ---- 2 files changed, 30 deletions(-) diff --git a/includes/transformer/class-activity-object.php b/includes/transformer/class-activity-object.php index b96348f17..23e7cf0ad 100644 --- a/includes/transformer/class-activity-object.php +++ b/includes/transformer/class-activity-object.php @@ -20,15 +20,6 @@ public function to_object() { return $this->transform_object_properties( $this->item ); } - /** - * Get the ID of the WordPress Object. - * - * @return string The ID of the WordPress Object. - */ - protected function get_id() { - return ''; - } - /** * Helper function to get the @-Mentions from the post content. * @@ -152,20 +143,4 @@ protected function get_tag() { return \array_unique( $tags, SORT_REGULAR ); } - - /** - * Returns the ID of the WordPress Object. - */ - public function get_wp_user_id() { - return ''; - } - - /** - * Change the User-ID of the WordPress Post. - * - * @param int $user_id The new user ID. - */ - public function change_wp_user_id( $user_id ) { - // do nothing. - } } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 4e11e3b7a..1406a5875 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -140,11 +140,6 @@ public function to_activity( $type ) { return $activity; } - /** - * Get the ID of the WordPress Object. - */ - abstract protected function get_id(); - /** * Returns a generic locale based on the Blog settings. * From 9d8c270a8d046081d9e2a94e0b244e1c25fd397c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 9 Dec 2024 14:47:35 +0100 Subject: [PATCH 036/116] add visibility and normalize autor id --- includes/collection/class-outbox.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 0ae3f52f0..6ea0d665a 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -22,12 +22,13 @@ class Outbox { * * @return mixed The added item or an error. */ - public static function add( $activity_object, $activity_type, $user_id ) { // phpcs:ignore + public static function add( $activity_object, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore $outbox_item = array( 'post_type' => self::POST_TYPE, 'post_title' => $activity_object->get_id(), 'post_content' => $activity_object->to_json(), - 'post_author' => $user_id, + // ensure that user ID is always above 0. + 'post_author' => \max( $user_id, 0 ), 'post_status' => 'draft', ); From 757c8c92892c39cfaf0348f6f48c1c4104d7d516 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 11 Dec 2024 15:14:43 +0100 Subject: [PATCH 037/116] add taxonomies to store actor and actitiy-type informations --- includes/class-activitypub.php | 41 ++++++++++++++++++++++++++-- includes/collection/class-outbox.php | 20 +++++++++++++- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 7a7274e1d..5c49607b6 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -534,15 +534,50 @@ private static function register_post_types() { 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ), 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ), ), - 'public' => false, - 'hierarchical' => false, + 'capabilities' => array( + 'create_posts' => false, + //'edit_posts' => false, + //'delete_posts' => false, + ), + 'map_meta_cap' => true, + 'public' => true, + 'hierarchical' => true, 'rewrite' => false, 'query_var' => false, - 'delete_with_user' => false, + 'delete_with_user' => true, 'can_export' => true, 'supports' => array(), + 'taxonomies' => array( 'ap_actor', 'ap_activity_type' ), + ) + ); + + \register_taxonomy( + 'ap_actor', + Outbox::POST_TYPE, + array( + 'labels' => array( + 'name' => _x( 'Actor', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( '', 'post_type single name', 'activitypub' ), + ), + 'hierarchical' => true, + 'public' => false, + ) + ); + \register_taxonomy_for_object_type( 'ap_actor', Outbox::POST_TYPE ); + + \register_taxonomy( + 'ap_activity_type', + Outbox::POST_TYPE, + array( + 'labels' => array( + 'name' => _x( 'Activity Type', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( '', 'post_type single name', 'activitypub' ), + ), + 'hierarchical' => true, + 'public' => false, ) ); + \register_taxonomy_for_object_type( 'ap_activity_type', Outbox::POST_TYPE ); // Both User and Blog Extra Fields types have the same args. $args = array( diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 6ea0d665a..8b7210d1e 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -23,11 +23,23 @@ class Outbox { * @return mixed The added item or an error. */ public static function add( $activity_object, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore + switch ( $user_id ) { + case -1: + $actor = 'application'; + break; + case 0: + $actor = 'blog'; + break; + default: + $actor = 'user'; + break; + } + $outbox_item = array( 'post_type' => self::POST_TYPE, 'post_title' => $activity_object->get_id(), 'post_content' => $activity_object->to_json(), - // ensure that user ID is always above 0. + // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), 'post_status' => 'draft', ); @@ -48,6 +60,12 @@ public static function add( $activity_object, $activity_type, $user_id, $visibil return false; } + // Set the actor type. + \wp_set_object_terms( $id, array( $actor ), 'ap_actor' ); + + // Set the activity type. + \wp_set_object_terms( $id, array( strtolower( $activity_type ) ), 'ap_activity_type' ); + return $id; } } From 3fc94d31ba5236c1ab252287dd7ef1bb4dfef90f Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Mon, 13 Jan 2025 13:58:49 -0600 Subject: [PATCH 038/116] Fix phpcs --- includes/class-activitypub.php | 12 ++++-------- includes/collection/class-outbox.php | 3 ++- includes/transformer/class-post.php | 8 ++++---- .../collection/class-test-outbox.php} | 16 +++++++++++----- 4 files changed, 21 insertions(+), 18 deletions(-) rename tests/{class-test-activitypub-outbox-collection.php => includes/collection/class-test-outbox.php} (72%) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 5a859e902..128ad0ba2 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -550,8 +550,6 @@ private static function register_post_types() { ), 'capabilities' => array( 'create_posts' => false, - //'edit_posts' => false, - //'delete_posts' => false, ), 'map_meta_cap' => true, 'public' => true, @@ -569,9 +567,8 @@ private static function register_post_types() { 'ap_actor', Outbox::POST_TYPE, array( - 'labels' => array( - 'name' => _x( 'Actor', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( '', 'post_type single name', 'activitypub' ), + 'labels' => array( + 'name' => _x( 'Actor', 'post_type plural name', 'activitypub' ), ), 'hierarchical' => true, 'public' => false, @@ -583,9 +580,8 @@ private static function register_post_types() { 'ap_activity_type', Outbox::POST_TYPE, array( - 'labels' => array( - 'name' => _x( 'Activity Type', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( '', 'post_type single name', 'activitypub' ), + 'labels' => array( + 'name' => _x( 'Activity Type', 'post_type plural name', 'activitypub' ), ), 'hierarchical' => true, 'public' => false, diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 8b7210d1e..0174c709c 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -19,8 +19,9 @@ class Outbox { * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. * @param string $activity_type The activity type. * @param int $user_id The user ID. + * @param string $visibility Optional. The visibility of the content. Default 'public'. * - * @return mixed The added item or an error. + * @return false|int|\WP_Error The added item or an error. */ public static function add( $activity_object, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore switch ( $user_id ) { diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 2e1eb6c60..fb4e47cd2 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -634,13 +634,13 @@ protected function get_classic_editor_image_attachments( $max_images ) { /** * Filter media IDs by object type. * - * @param array $media The media array grouped by type. - * @param string $type The object type. - * @param WP_Post $item The post object. + * @param array $media The media array grouped by type. + * @param string $type The object type. + * @param WP_Post $item The post object. * * @return array The filtered media IDs. */ - protected function filter_media_by_object_type( $media, $type, $wp_object ) { + protected function filter_media_by_object_type( $media, $type, $item ) { /** * Filter the object type for media attachments. * diff --git a/tests/class-test-activitypub-outbox-collection.php b/tests/includes/collection/class-test-outbox.php similarity index 72% rename from tests/class-test-activitypub-outbox-collection.php rename to tests/includes/collection/class-test-outbox.php index c5b8dfe3b..c9ff66ae5 100644 --- a/tests/class-test-activitypub-outbox-collection.php +++ b/tests/includes/collection/class-test-outbox.php @@ -1,16 +1,18 @@ assertIsInt( $id ); @@ -28,7 +34,7 @@ public function asd_test_add( $data, $type, $user_id, $json ) { $this->assertInstanceOf( 'WP_Post', $post ); $this->assertEquals( 'draft', $post->post_status ); - //$this->assertEquals( $json, $post->post_content ); + $this->assertEquals( $json, $post->post_content ); } /** From 6589bed2b945340f8745ec6dcba1a97e407a5466 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 14 Jan 2025 09:46:00 -0600 Subject: [PATCH 039/116] Fix shortcode tests --- tests/includes/class-test-shortcodes.php | 112 +++++++++-------------- 1 file changed, 44 insertions(+), 68 deletions(-) diff --git a/tests/includes/class-test-shortcodes.php b/tests/includes/class-test-shortcodes.php index 2819e0851..34358022c 100644 --- a/tests/includes/class-test-shortcodes.php +++ b/tests/includes/class-test-shortcodes.php @@ -7,6 +7,7 @@ namespace Activitypub\Tests; +use Activitypub\Scheduler\Post; use Activitypub\Shortcodes; /** @@ -17,103 +18,86 @@ class Test_Shortcodes extends \WP_UnitTestCase { /** - * Test the content shortcode. + * Post object. + * + * @var \WP_Post */ - public function test_content() { + protected $post; + + public function set_up() { + parent::set_up(); + + remove_action( 'transition_post_status', array( Post::class, 'schedule_post_activity' ), 33 ); Shortcodes::register(); + + // Create a post. + $this->post = self::factory()->post->create_and_get( + array( + 'post_title' => 'Test title for shortcode', + 'post_content' => 'Lorem ipsum dolor sit amet, consectetur.', + 'post_excerpt' => '', + ) + ); + } + + public function tear_down() { + parent::tear_down(); + + Shortcodes::unregister(); + + // Delete the post. + wp_delete_post( $this->post->ID, true ); + } + /** + * Test the content shortcode. + */ + public function test_content() { global $post; - $post_id = -99; // Negative ID, to avoid clash with a valid post. - $post = new \stdClass(); - $post->ID = $post_id; - $post->post_author = 1; - $post->post_date = current_time( 'mysql' ); - $post->post_date_gmt = current_time( 'mysql', 1 ); - $post->post_title = 'Some title or other'; - $post->post_content = 'hallo'; - $post->post_status = 'publish'; - $post->comment_status = 'closed'; - $post->ping_status = 'closed'; - $post->post_name = 'fake-post-' . wp_rand( 1, 99999 ); // Append random number to avoid clash. - $post->post_type = 'post'; - $post->filter = 'raw'; // important! - - $content = '[ap_content]'; + $post = $this->post; + $post->post_content = 'hallo'; // Fill in the shortcodes. setup_postdata( $post ); - $content = do_shortcode( $content ); + $content = do_shortcode( '[ap_content]' ); wp_reset_postdata(); $this->assertEquals( '

hallo

', $content ); - Shortcodes::unregister(); } /** * Test the content shortcode with password protected content. */ public function test_password_protected_content() { - Shortcodes::register(); global $post; - $post_id = -98; // Negative ID, to avoid clash with a valid post. - $post = new \stdClass(); - $post->ID = $post_id; - $post->post_author = 1; - $post->post_date = current_time( 'mysql' ); - $post->post_date_gmt = current_time( 'mysql', 1 ); - $post->post_title = 'Some title or other'; - $post->post_content = 'hallo'; - $post->comment_status = 'closed'; - $post->ping_status = 'closed'; - $post->post_name = 'fake-post-' . wp_rand( 1, 99999 ); // Append random number to avoid clash. - $post->post_type = 'post'; - $post->filter = 'raw'; // important! - $post->post_password = 'abc'; - - $content = '[ap_content]'; + $post = $this->post; + $post->post_password = 'abc'; // Fill in the shortcodes. setup_postdata( $post ); - $content = do_shortcode( $content ); + $content = do_shortcode( '[ap_content]' ); wp_reset_postdata(); $this->assertEquals( '', $content ); - Shortcodes::unregister(); } /** * Test the excerpt shortcode. */ public function test_excerpt() { - Shortcodes::register(); global $post; - $post_id = -97; // Negative ID, to avoid clash with a valid post. - $post = new \stdClass(); - $post->ID = $post_id; - $post->post_author = 1; - $post->post_date = current_time( 'mysql' ); - $post->post_date_gmt = current_time( 'mysql', 1 ); - $post->post_title = 'Some title or other'; - $post->post_content = 'Lorem ipsum dolor sit amet, consectetur.'; - $post->post_status = 'publish'; - $post->comment_status = 'closed'; - $post->ping_status = 'closed'; - $post->post_name = 'fake-post-' . wp_rand( 1, 99999 ); // Append random number to avoid clash. - $post->post_type = 'post'; - $post->filter = 'raw'; // important! - - $content = '[ap_excerpt length="25"]'; + $post = $this->post; + $post->post_content = 'Lorem ipsum dolor sit amet, consectetur.'; // Fill in the shortcodes. setup_postdata( $post ); - $content = do_shortcode( $content ); + $content = do_shortcode( '[ap_excerpt length="25"]' ); wp_reset_postdata(); $this->assertEquals( "

Lorem ipsum dolor […]

\n", $content ); - Shortcodes::unregister(); } /** @@ -122,22 +106,14 @@ public function test_excerpt() { * @covers ::title */ public function test_title() { - Shortcodes::register(); global $post; - $post = self::factory()->post->create_and_get( - array( - 'post_title' => 'Test title for shortcode', - ) - ); - - $content = '[ap_title]'; + $post = $this->post; // Fill in the shortcodes. setup_postdata( $post ); - $content = do_shortcode( $content ); + $content = do_shortcode( '[ap_title]' ); wp_reset_postdata(); - Shortcodes::unregister(); $this->assertEquals( 'Test title for shortcode', $content ); } From fad425e8a938c26ff05d631620a21ed409f73e85 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Tue, 14 Jan 2025 09:47:40 -0600 Subject: [PATCH 040/116] Update json tests. --- tests/includes/collection/class-test-outbox.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index c9ff66ae5..f3c603513 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -53,7 +53,7 @@ public function activity_object_provider() { ), 'Create', 1, - '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","id":"https:\/\/example.com\/1","type":"Note","content":"This is a note"}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"type":"Object","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', ), array( array( @@ -64,7 +64,7 @@ public function activity_object_provider() { ), 'Create', 2, - '{"@context":"https:\/\/www.w3.org\/ns\/activitystreams","id":"https:\/\/example.com\/2","type":"Note","content":"This is another note"}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"type":"Object","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', ), ); } From 1142be1d654a30ebfd001a3990724f89b1e67939 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 10:45:08 +0100 Subject: [PATCH 041/116] move map functions to base transformer --- includes/transformer/class-base.php | 41 +++++++++++++++++++++++++++++ includes/transformer/class-post.php | 41 ----------------------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 1406a5875..bfd3d1ee2 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -189,4 +189,45 @@ protected function get_to() { public function get_replies() { return Replies::get_collection( $this->item ); } + + /** + * Returns the content map for the post. + * + * @return array The content map for the post. + */ + public function get_content_map() { + return array( + $this->get_locale() => $this->get_content(), + ); + } + + /** + * Returns the name map for the post. + * + * @return array The name map for the post. + */ + public function get_name_map() { + if ( ! $this->get_name() ) { + return null; + } + + return array( + $this->get_locale() => $this->get_name(), + ); + } + + /** + * Returns the summary map for the post. + * + * @return array The summary map for the post. + */ + public function get_summary_map() { + if ( ! $this->get_summary() ) { + return null; + } + + return array( + $this->get_locale() => $this->get_summary(), + ); + } } diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index fb4e47cd2..ac6d92baa 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -1097,47 +1097,6 @@ public function get_updated() { return null; } - /** - * Returns the content map for the post. - * - * @return array The content map for the post. - */ - public function get_content_map() { - return array( - $this->get_locale() => $this->get_content(), - ); - } - - /** - * Returns the name map for the post. - * - * @return array The name map for the post. - */ - public function get_name_map() { - if ( ! $this->get_name() ) { - return null; - } - - return array( - $this->get_locale() => $this->get_name(), - ); - } - - /** - * Returns the summary map for the post. - * - * @return array The summary map for the post. - */ - public function get_summary_map() { - if ( ! $this->get_summary() ) { - return null; - } - - return array( - $this->get_locale() => $this->get_summary(), - ); - } - /** * Transform Embed blocks to block level link. * From 1824b4bc941a9f6bc65779d2076761e8cb1bfd8b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 10:45:24 +0100 Subject: [PATCH 042/116] use item instead of wp_object --- includes/transformer/class-comment.php | 40 +++----------------------- 1 file changed, 4 insertions(+), 36 deletions(-) diff --git a/includes/transformer/class-comment.php b/includes/transformer/class-comment.php index a7eff5e0e..7145c926f 100644 --- a/includes/transformer/class-comment.php +++ b/includes/transformer/class-comment.php @@ -305,35 +305,14 @@ public function extract_reply_context( $mentions = array() ) { return $mentions; } - /** - * Returns the locale of the post. - * - * @return string The locale of the post. - */ - public function get_locale() { - $comment_id = $this->wp_object->ID; - $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); - - /** - * Filter the locale of the comment. - * - * @param string $lang The locale of the comment. - * @param int $comment_id The comment ID. - * @param \WP_Post $post The comment object. - * - * @return string The filtered locale of the comment. - */ - return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->wp_object ); - } - /** * Returns the updated date of the comment. * * @return string|null The updated date of the comment. */ public function get_updated() { - $updated = \get_comment_meta( $this->wp_object->comment_ID, 'activitypub_comment_modified', true ); - $published = \get_comment_meta( $this->wp_object->comment_ID, 'activitypub_comment_published', true ); + $updated = \get_comment_meta( $this->item->comment_ID, 'activitypub_comment_modified', true ); + $published = \get_comment_meta( $this->item->comment_ID, 'activitypub_comment_published', true ); if ( $updated > $published ) { return \gmdate( 'Y-m-d\TH:i:s\Z', $updated ); @@ -348,7 +327,7 @@ public function get_updated() { * @return string The published date of the comment. */ public function get_published() { - return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $this->wp_object->comment_date_gmt ) ); + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $this->item->comment_date_gmt ) ); } /** @@ -375,22 +354,11 @@ public function get_type() { * @return array The to of the comment. */ public function get_to() { - $path = sprintf( 'actors/%d/followers', intval( $this->wp_object->comment_author ) ); + $path = sprintf( 'actors/%d/followers', intval( $this->item->comment_author ) ); return array( 'https://www.w3.org/ns/activitystreams#Public', get_rest_url_by_path( $path ), ); } - - /** - * Returns the content map for the comment. - * - * @return array The content map for the comment. - */ - public function get_content_map() { - return array( - $this->get_locale() => $this->get_content(), - ); - } } From f9ca53d4e4e4b6c1e1ec2a4e0201bd3da917ddb5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 11:21:35 +0100 Subject: [PATCH 043/116] protect functions --- includes/transformer/class-base.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index bfd3d1ee2..3e8e1026e 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -195,7 +195,7 @@ public function get_replies() { * * @return array The content map for the post. */ - public function get_content_map() { + protected function get_content_map() { return array( $this->get_locale() => $this->get_content(), ); @@ -206,7 +206,7 @@ public function get_content_map() { * * @return array The name map for the post. */ - public function get_name_map() { + protected function get_name_map() { if ( ! $this->get_name() ) { return null; } @@ -221,7 +221,7 @@ public function get_name_map() { * * @return array The summary map for the post. */ - public function get_summary_map() { + protected function get_summary_map() { if ( ! $this->get_summary() ) { return null; } From 58a84d907260faf6918107d8a54c2813d0604593 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 11:32:34 +0100 Subject: [PATCH 044/116] fix tests and remove legacy tests --- includes/transformer/class-base.php | 4 +- includes/transformer/class-post.php | 28 +- .../class-test-activity-dispatcher.php | 364 ------------------ tests/includes/class-test-scheduler.php | 221 ----------- 4 files changed, 16 insertions(+), 601 deletions(-) delete mode 100644 tests/includes/class-test-activity-dispatcher.php delete mode 100644 tests/includes/class-test-scheduler.php diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 3e8e1026e..23ce1c603 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -207,7 +207,7 @@ protected function get_content_map() { * @return array The name map for the post. */ protected function get_name_map() { - if ( ! $this->get_name() ) { + if ( ! \method_exists( $this, 'get_name' ) || ! $this->get_name() ) { return null; } @@ -222,7 +222,7 @@ protected function get_name_map() { * @return array The summary map for the post. */ protected function get_summary_map() { - if ( ! $this->get_summary() ) { + if ( ! \method_exists( $this, 'get_summary' ) || ! $this->get_summary() ) { return null; } diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index ac6d92baa..0a7fdb106 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -93,7 +93,7 @@ public function get_actor_object() { return $blog_user; } - $user = Actors::get_by_id( $this->wp_object->post_author ); + $user = Actors::get_by_id( $this->item->post_author ); if ( $user && ! is_wp_error( $user ) ) { $this->actor_object = $user; @@ -165,7 +165,7 @@ protected function get_attributed_to() { * @return array|null The Image or null if no image is available. */ protected function get_image() { - $post_id = $this->wp_object->ID; + $post_id = $this->item->ID; // List post thumbnail first if this post has one. if ( @@ -218,7 +218,7 @@ protected function get_image() { * @return array|null The Icon or null if no icon is available. */ protected function get_icon() { - $post_id = $this->wp_object->ID; + $post_id = $this->item->ID; // List post thumbnail first if this post has one. if ( \has_post_thumbnail( $post_id ) ) { @@ -315,7 +315,7 @@ protected function get_attachment() { $media = $this->get_classic_editor_images( $media, $max_media ); } - $media = $this->filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object ); + $media = $this->filter_media_by_object_type( $media, \get_post_format( $this->item ), $this->item ); $unique_ids = \array_unique( \array_column( $media, 'id' ) ); $media = \array_intersect_key( $media, $unique_ids ); $media = \array_slice( $media, 0, $max_media ); @@ -534,7 +534,7 @@ protected function get_classic_editor_image_embeds( $max_images ) { $images = array(); $base = get_upload_baseurl(); - $content = \get_post_field( 'post_content', $this->wp_object ); + $content = \get_post_field( 'post_content', $this->item ); $tags = new \WP_HTML_Tag_Processor( $content ); // This linter warning is a false positive - we have to re-count each time here as we modify $images. @@ -805,11 +805,11 @@ protected function get_type() { // Default to Note. $object_type = 'Note'; - $post_type = \get_post_type( $this->wp_object ); + $post_type = \get_post_type( $this->item ); if ( 'page' === $post_type ) { $object_type = 'Page'; - } elseif ( ! \get_post_format( $this->wp_object ) ) { + } elseif ( ! \get_post_format( $this->item ) ) { $object_type = 'Article'; } @@ -897,7 +897,7 @@ protected function get_summary() { } // Remove Teaser from drafts. - if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->wp_object ) ) { + if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) { return \__( '(This post is being modified)', 'activitypub' ); } @@ -941,7 +941,7 @@ protected function get_content() { add_filter( 'activitypub_reply_block', '__return_empty_string' ); // Remove Content from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { + if ( 'draft' === \get_post_status( $this->item ) ) { return \__( '(This post is being modified)', 'activitypub' ); } @@ -1022,9 +1022,9 @@ protected function get_post_content_template() { * generation. * * @param string $template The template string containing shortcodes. - * @param WP_Post $wp_object The WordPress post object being transformed. + * @param WP_Post $item The WordPress post object being transformed. */ - return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object ); + return apply_filters( 'activitypub_object_content_template', $template, $this->item ); } /** @@ -1057,7 +1057,7 @@ protected function get_mentions() { * * @return string|null The in-reply-to URL of the post. */ - public function get_in_reply_to() { + protected function get_in_reply_to() { $blocks = \parse_blocks( $this->item->post_content ); foreach ( $blocks as $block ) { @@ -1075,7 +1075,7 @@ public function get_in_reply_to() { * * @return string The published date of the post. */ - public function get_published() { + protected function get_published() { $published = \strtotime( $this->item->post_date_gmt ); return \gmdate( 'Y-m-d\TH:i:s\Z', $published ); @@ -1086,7 +1086,7 @@ public function get_published() { * * @return string|null The updated date of the post. */ - public function get_updated() { + protected function get_updated() { $published = \strtotime( $this->item->post_date_gmt ); $updated = \strtotime( $this->item->post_modified_gmt ); diff --git a/tests/includes/class-test-activity-dispatcher.php b/tests/includes/class-test-activity-dispatcher.php deleted file mode 100644 index e143cea3f..000000000 --- a/tests/includes/class-test-activity-dispatcher.php +++ /dev/null @@ -1,364 +0,0 @@ - array( - 'id' => 'https://example.org/users/username', - 'url' => 'https://example.org/users/username', - 'inbox' => 'https://example.org/users/username/inbox', - 'name' => 'username', - 'preferredUsername' => 'username', - ), - 'jon@example.com' => array( - 'id' => 'https://example.com/author/jon', - 'url' => 'https://example.com/author/jon', - 'inbox' => 'https://example.com/author/jon/inbox', - 'name' => 'jon', - 'preferredUsername' => 'jon', - ), - ); - - /** - * Set up the test case. - */ - public function set_up() { - parent::set_up(); - add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); - _delete_all_posts(); - } - - /** - * Tear down the test case. - */ - public function tear_down() { - remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); - parent::tear_down(); - } - - /** - * Test dispatch activity. - * - * @covers ::send_activity - */ - public function test_dispatch_activity() { - $followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' ); - - foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( 1, $follower ); - } - - $post = \wp_insert_post( - array( - 'post_author' => 1, - 'post_content' => 'hello', - ) - ); - - $pre_http_request = new \MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - - Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); - - $this->assertSame( 2, $pre_http_request->get_call_count() ); - $all_args = $pre_http_request->get_args(); - $first_call_args = array_shift( $all_args ); - - $this->assertEquals( 'https://example.com/author/jon/inbox', $first_call_args[2] ); - - $second_call_args = array_shift( $all_args ); - $this->assertEquals( 'https://example.org/users/username/inbox', $second_call_args[2] ); - - $json = json_decode( $second_call_args[1]['body'] ); - $this->assertEquals( 'Create', $json->type ); - $this->assertEquals( 'http://example.org/?author=1', $json->actor ); - $this->assertEquals( 'http://example.org/?author=1', $json->object->attributedTo ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); - } - - /** - * Test dispatch mentions. - * - * @covers ::send_activity - */ - public function test_dispatch_mentions() { - $post = \wp_insert_post( - array( - 'post_author' => 1, - 'post_content' => '@alex hello', - ) - ); - - self::$users['https://example.com/alex'] = array( - 'id' => 'https://example.com/alex', - 'url' => 'https://example.com/alex', - 'inbox' => 'https://example.com/alex/inbox', - 'name' => 'alex', - ); - - add_filter( - 'activitypub_extract_mentions', - function ( $mentions ) { - $mentions[] = 'https://example.com/alex'; - return $mentions; - } - ); - - $pre_http_request = new \MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - - Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); - - $this->assertSame( 1, $pre_http_request->get_call_count() ); - $all_args = $pre_http_request->get_args(); - $first_call_args = $all_args[0]; - $this->assertEquals( 'https://example.com/alex/inbox', $first_call_args[2] ); - - $body = json_decode( $first_call_args[1]['body'], true ); - $this->assertArrayHasKey( 'id', $body ); - - remove_all_filters( 'activitypub_from_post_object' ); - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); - } - - /** - * Test dispatch mentions. - * - * @covers ::send_activity_or_announce - */ - public function test_dispatch_announce() { - add_filter( 'activitypub_is_user_type_disabled', '__return_false' ); - - $followers = array( 'https://example.com/author/jon' ); - - foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Actors::BLOG_USER_ID, $follower ); - } - - $post = \wp_insert_post( - array( - 'post_author' => 1, - 'post_content' => 'hello', - ) - ); - - $pre_http_request = new \MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - - Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); - - $all_args = $pre_http_request->get_args(); - $first_call_args = $all_args[0]; - - $this->assertSame( 1, $pre_http_request->get_call_count() ); - - $user = new \Activitypub\Model\Blog(); - - $json = json_decode( $first_call_args[1]['body'] ); - $this->assertEquals( 'Announce', $json->type ); - $this->assertEquals( $user->get_id(), $json->actor ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); - } - - /** - * Test dispatch blog activity. - * - * @covers ::send_activity_or_announce - */ - public function test_dispatch_blog_activity() { - $followers = array( 'https://example.com/author/jon' ); - - add_filter( - 'activitypub_is_user_type_disabled', - function ( $value, $type ) { - if ( 'blog' === $type ) { - return false; - } else { - return true; - } - }, - 10, - 2 - ); - - $this->assertTrue( \Activitypub\is_single_user() ); - - foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Actors::BLOG_USER_ID, $follower ); - } - - $post = \wp_insert_post( - array( - 'post_author' => 1, - 'post_content' => 'hello', - ) - ); - - $pre_http_request = new \MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - - Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); - - $all_args = $pre_http_request->get_args(); - $first_call_args = $all_args[0]; - - $this->assertSame( 1, $pre_http_request->get_call_count() ); - - $user = new \Activitypub\Model\Blog(); - - $json = json_decode( $first_call_args[1]['body'] ); - $this->assertEquals( 'Create', $json->type ); - $this->assertEquals( $user->get_id(), $json->actor ); - $this->assertEquals( $user->get_id(), $json->object->attributedTo ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); - } - - /** - * Test dispatch fallback activity. - * - * @covers ::send_activity - */ - public function test_dispatch_fallback_activity() { - $followers = array( 'https://example.com/author/jon' ); - - add_filter( 'activitypub_is_user_type_disabled', '__return_false' ); - - add_filter( - 'activitypub_is_user_disabled', - function ( $disabled, $user_id ) { - if ( 1 === (int) $user_id ) { - return true; - } - - return false; - }, - 10, - 2 - ); - - $this->assertFalse( \Activitypub\is_single_user() ); - - foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Actors::BLOG_USER_ID, $follower ); - \Activitypub\Collection\Followers::add_follower( 1, $follower ); - } - - $post = \wp_insert_post( - array( - 'post_author' => 1, - 'post_content' => 'hello', - ) - ); - - $pre_http_request = new \MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - - Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); - - $all_args = $pre_http_request->get_args(); - $first_call_args = $all_args[0]; - - $this->assertSame( 1, $pre_http_request->get_call_count() ); - - $user = new \Activitypub\Model\Blog(); - - $json = json_decode( $first_call_args[1]['body'] ); - $this->assertEquals( 'Create', $json->type ); - $this->assertEquals( $user->get_id(), $json->actor ); - $this->assertEquals( $user->get_id(), $json->object->attributedTo ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); - } - - /** - * Filters remote metadata by actor. - * - * @param array|bool $pre The metadata for the given URL. - * @param string $actor The URL of the actor. - * @return array|bool - */ - public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { - if ( isset( self::$users[ $actor ] ) ) { - return self::$users[ $actor ]; - } - foreach ( self::$users as $data ) { - if ( $data['url'] === $actor ) { - return $data; - } - } - return $pre; - } - - /** - * Filters the arguments used in an HTTP request. - * - * @param array $args The arguments for the HTTP request. - * @param string $url The request URL. - * @return array - */ - public static function http_request_args( $args, $url ) { - if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) { - $args['reject_unsafe_urls'] = false; - } - return $args; - } - - /** - * Filters the return value of an HTTP request. - * - * @param bool $preempt Whether to preempt an HTTP request's return value. - * @param array $request { - * Array of HTTP request arguments. - * - * @type string $method Request method. - * @type string $body Request body. - * } - * @param string $url The request URL. - * @return array Array containing 'headers', 'body', 'response'. - */ - public static function pre_http_request( $preempt, $request, $url ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - return array( - 'headers' => array( - 'content-type' => 'text/json', - ), - 'body' => '', - 'response' => array( - 'code' => 202, - ), - ); - } - - /** - * Filters the return value of an HTTP request. - * - * @param array $response Response array. - * @param array $args Request arguments. - * @param string $url Request URL. - * @return array - */ - public static function http_response( $response, $args, $url ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - return $response; - } -} diff --git a/tests/includes/class-test-scheduler.php b/tests/includes/class-test-scheduler.php deleted file mode 100644 index d7df36643..000000000 --- a/tests/includes/class-test-scheduler.php +++ /dev/null @@ -1,221 +0,0 @@ -post = self::factory()->post->create_and_get( - array( - 'post_title' => 'Test Post', - 'post_content' => 'Test Content', - 'post_status' => 'draft', - 'post_author' => 1, - ) - ); - } - - /** - * Clean up test resources after each test. - * - * Deletes the test post. - */ - public function tear_down() { - wp_delete_post( $this->post->ID, true ); - parent::tear_down(); - } - - /** - * Test that moving a draft post to trash does not schedule federation. - * - * @covers ::schedule_post_activity - */ - public function test_draft_to_trash_should_not_schedule_federation() { - Scheduler::schedule_post_activity( 'trash', 'draft', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Delete' ) ), - 'Draft to trash transition should not schedule federation' - ); - } - - /** - * Test that moving a published post to trash schedules a delete activity only if federated. - * - * @covers ::schedule_post_activity - */ - public function test_publish_to_trash_should_schedule_delete_only_if_federated() { - wp_publish_post( $this->post->ID ); - $this->post = get_post( $this->post->ID ); - - // Test without federation state. - Scheduler::schedule_post_activity( 'trash', 'publish', $this->post ); - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Delete' ) ), - 'Published to trash transition should not schedule delete activity when not federated' - ); - - // Test with federation state. - \Activitypub\set_wp_object_state( $this->post, 'federated' ); - Scheduler::schedule_post_activity( 'trash', 'publish', $this->post ); - - $this->assertNotFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Delete' ) ), - 'Published to trash transition should schedule delete activity when federated' - ); - } - - /** - * Test that updating a draft post does not schedule federation. - * - * @covers ::schedule_post_activity - */ - public function test_draft_to_draft_should_not_schedule_federation() { - Scheduler::schedule_post_activity( 'draft', 'draft', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Draft to draft transition should not schedule federation' - ); - } - - /** - * Test that moving a published post to draft schedules an update activity. - * - * @covers ::schedule_post_activity - */ - public function test_publish_to_draft_should_schedule_update() { - wp_publish_post( $this->post->ID ); - $this->post = get_post( $this->post->ID ); - Scheduler::schedule_post_activity( 'draft', 'publish', $this->post ); - - $this->assertNotFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Published to draft transition should schedule update activity' - ); - } - - /** - * Test that publishing a draft post schedules a create activity. - * - * @covers ::schedule_post_activity - */ - public function test_draft_to_publish_should_schedule_create() { - Scheduler::schedule_post_activity( 'publish', 'draft', $this->post ); - - $this->assertNotFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Create' ) ), - 'Draft to publish transition should schedule create activity' - ); - } - - /** - * Test that updating a published post schedules an update activity. - * - * @covers ::schedule_post_activity - */ - public function test_publish_to_publish_should_schedule_update() { - wp_publish_post( $this->post->ID ); - $this->post = get_post( $this->post->ID ); - Scheduler::schedule_post_activity( 'publish', 'publish', $this->post ); - - $this->assertNotFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Published to published transition should schedule update activity' - ); - } - - /** - * Test that various non-standard status transitions do not schedule federation. - * - * Tests transitions from pending, private, and future statuses. - * - * @covers ::schedule_post_activity - */ - public function test_other_status_transitions_should_not_schedule_federation() { - // Test pending to draft. - Scheduler::schedule_post_activity( 'draft', 'pending', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Pending to draft transition should not schedule federation' - ); - - // Test private to draft. - Scheduler::schedule_post_activity( 'draft', 'private', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Private to draft transition should not schedule federation' - ); - - // Test future to draft. - Scheduler::schedule_post_activity( 'draft', 'future', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Update' ) ), - 'Future to draft transition should not schedule federation' - ); - } - - /** - * Test that disabled posts do not schedule federation activities. - * - * @covers ::schedule_post_activity - */ - public function test_disabled_post_should_not_schedule_federation() { - update_post_meta( $this->post->ID, 'activitypub_content_visibility', ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ); - Scheduler::schedule_post_activity( 'publish', 'draft', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Create' ) ), - 'Disabled posts should not schedule federation activities' - ); - } - - /** - * Test that password protected posts do not schedule federation activities. - * - * @covers ::schedule_post_activity - */ - public function test_password_protected_post_should_not_schedule_federation() { - wp_update_post( - array( - 'ID' => $this->post->ID, - 'post_password' => 'test-password', - ) - ); - $this->post = get_post( $this->post->ID ); - Scheduler::schedule_post_activity( 'publish', 'draft', $this->post ); - - $this->assertFalse( - wp_next_scheduled( 'activitypub_send_post', array( $this->post->ID, 'Create' ) ), - 'Password protected posts should not schedule federation activities' - ); - } -} From c291d03553a4e09d0fb97ed94162179ce6372dbd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 11:34:22 +0100 Subject: [PATCH 045/116] fix phpcs --- tests/includes/class-test-shortcodes.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/includes/class-test-shortcodes.php b/tests/includes/class-test-shortcodes.php index 34358022c..2ae7f188c 100644 --- a/tests/includes/class-test-shortcodes.php +++ b/tests/includes/class-test-shortcodes.php @@ -24,6 +24,9 @@ class Test_Shortcodes extends \WP_UnitTestCase { */ protected $post; + /** + * Set up the test. + */ public function set_up() { parent::set_up(); @@ -41,6 +44,9 @@ public function set_up() { ); } + /** + * Tear down the test. + */ public function tear_down() { parent::tear_down(); @@ -49,6 +55,7 @@ public function tear_down() { // Delete the post. wp_delete_post( $this->post->ID, true ); } + /** * Test the content shortcode. */ From 1aeb48617feef3d1bc3698c36495b8b163d90a60 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 11:42:40 +0100 Subject: [PATCH 046/116] add unittests --- .../transformer/class-test-factory.php | 193 ++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 tests/includes/transformer/class-test-factory.php diff --git a/tests/includes/transformer/class-test-factory.php b/tests/includes/transformer/class-test-factory.php new file mode 100644 index 000000000..9d8e38b54 --- /dev/null +++ b/tests/includes/transformer/class-test-factory.php @@ -0,0 +1,193 @@ +post->create(); + + // Create test attachment. + self::$attachment_id = $factory->attachment->create_object( + array( + 'post_type' => 'attachment', + 'post_mime_type' => 'image/jpeg', + ) + ); + + // Create test comment. + self::$comment_id = $factory->comment->create( + array( + 'comment_post_ID' => self::$post_id, + ) + ); + } + + /** + * Clean up after tests. + */ + public static function wpTearDownAfterClass() { + wp_delete_post( self::$post_id, true ); + wp_delete_post( self::$attachment_id, true ); + wp_delete_comment( self::$comment_id, true ); + } + + /** + * Test get_transformer with invalid input. + * + * @covers ::get_transformer + */ + public function test_get_transformer_invalid_input() { + $result = Factory::get_transformer( null ); + $this->assertWPError( $result ); + $this->assertEquals( 'invalid_object', $result->get_error_code() ); + } + + /** + * Test get_transformer with post. + * + * @covers ::get_transformer + */ + public function test_get_transformer_post() { + $post = get_post( self::$post_id ); + $transformer = Factory::get_transformer( $post ); + + $this->assertInstanceOf( Post::class, $transformer ); + } + + /** + * Test get_transformer with attachment. + * + * @covers ::get_transformer + */ + public function test_get_transformer_attachment() { + $attachment = get_post( self::$attachment_id ); + $transformer = Factory::get_transformer( $attachment ); + + $this->assertInstanceOf( Attachment::class, $transformer ); + } + + /** + * Test get_transformer with comment. + * + * @covers ::get_transformer + */ + public function test_get_transformer_comment() { + $comment = get_comment( self::$comment_id ); + $transformer = Factory::get_transformer( $comment ); + + $this->assertInstanceOf( Comment::class, $transformer ); + } + + /** + * Test get_transformer with JSON data. + * + * @covers ::get_transformer + */ + public function test_get_transformer_json() { + $json_string = '{"type": "Note", "content": "Test"}'; + $transformer = Factory::get_transformer( $json_string ); + + $this->assertInstanceOf( Json::class, $transformer ); + + $json_array = array( + 'type' => 'Note', + 'content' => 'Test', + ); + $transformer = Factory::get_transformer( $json_array ); + + $this->assertInstanceOf( Json::class, $transformer ); + } + + /** + * Test get_transformer with custom filter. + * + * @covers ::get_transformer + */ + public function test_get_transformer_filter() { + add_filter( + 'activitypub_transformer', + // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.classFound + function ( $transformer, $data, $class ) { + if ( 'WP_Post' === $class && 'post' === $data->post_type ) { + return new Activity_Object( $data ); + } + return $transformer; + }, + 10, + 3 + ); + + $post = get_post( self::$post_id ); + $transformer = Factory::get_transformer( $post ); + + $this->assertInstanceOf( Activity_Object::class, $transformer ); + + remove_all_filters( 'activitypub_transformer' ); + } + + /** + * Test get_transformer with invalid filter return. + * + * @covers ::get_transformer + */ + public function test_get_transformer_invalid_filter() { + add_filter( + 'activitypub_transformer', + function () { + return 'invalid'; + } + ); + + $post = get_post( self::$post_id ); + $result = Factory::get_transformer( $post ); + + $this->assertWPError( $result ); + $this->assertEquals( 'invalid_transformer', $result->get_error_code() ); + + remove_all_filters( 'activitypub_transformer' ); + } +} From 99a0a4802443d457d7d5eceef09eaba40f3f9556 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 12:26:50 +0100 Subject: [PATCH 047/116] fix attachment issues and add tests --- includes/class-shortcodes.php | 30 ++-- includes/transformer/class-attachment.php | 8 +- .../transformer/class-test-attachment.php | 167 ++++++++++++++++++ 3 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 tests/includes/transformer/class-test-attachment.php diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 6ac73c03c..10018dd47 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -166,24 +166,26 @@ public static function content( $atts, $content, $tag ) { if ( empty( $content ) ) { $content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true ); } - } else { - $content = \get_post_field( 'post_content', $item ); + } - if ( 'yes' === $atts['apply_filters'] ) { - /** This filter is documented in wp-includes/post-template.php */ - $content = \apply_filters( 'the_content', $content ); - } else { - $content = do_blocks( $content ); - $content = wptexturize( $content ); - $content = wp_filter_content_tags( $content ); - } + if ( empty( $content ) ) { + $content = \get_post_field( 'post_content', $item ); + } - // Replace script and style elements. - $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); - $content = strip_shortcodes( $content ); - $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + if ( 'yes' === $atts['apply_filters'] ) { + /** This filter is documented in wp-includes/post-template.php */ + $content = \apply_filters( 'the_content', $content ); + } else { + $content = do_blocks( $content ); + $content = wptexturize( $content ); + $content = wp_filter_content_tags( $content ); } + // Replace script and style elements. + $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); + $content = strip_shortcodes( $content ); + $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) ); return $content; diff --git a/includes/transformer/class-attachment.php b/includes/transformer/class-attachment.php index ef3e1d1fd..65f500ca8 100644 --- a/includes/transformer/class-attachment.php +++ b/includes/transformer/class-attachment.php @@ -24,11 +24,11 @@ class Attachment extends Post { * @return array The Attachments. */ protected function get_attachment() { - $mime_type = get_post_mime_type( $this->item->ID ); - $media_type = preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type ); - $type = ''; + $mime_type = \get_post_mime_type( $this->item->ID ); + $mime_type_parts = \explode( '/', $mime_type ); + $type = ''; - switch ( $media_type ) { + switch ( $mime_type_parts[0] ) { case 'audio': case 'video': $type = 'Document'; diff --git a/tests/includes/transformer/class-test-attachment.php b/tests/includes/transformer/class-test-attachment.php new file mode 100644 index 000000000..6887da527 --- /dev/null +++ b/tests/includes/transformer/class-test-attachment.php @@ -0,0 +1,167 @@ +attachment->create_object( + array( + 'post_type' => 'attachment', + 'post_mime_type' => 'image/jpeg', + 'post_title' => 'Test Image', + 'post_content' => 'Test Image Description', + ) + ); + } + + /** + * Clean up after tests. + */ + public static function wpTearDownAfterClass() { + wp_delete_post( self::$attachment_id, true ); + } + + /** + * Test get_type method. + * + * @covers ::get_type + */ + public function test_get_type() { + $attachment = get_post( self::$attachment_id ); + $transformer = new Attachment( $attachment ); + $type = $this->get_protected_method( $transformer, 'get_type' ); + + $this->assertEquals( 'Note', $type ); + } + + /** + * Test get_attachment method with different mime types. + * + * @covers ::get_attachment + * @dataProvider provide_mime_types + */ + public function test_get_attachment( $mime_type, $expected_type ) { + $attachment_id = self::factory()->attachment->create_object( + array( + 'post_type' => 'attachment', + 'post_mime_type' => $mime_type, + ) + ); + + $attachment = get_post( $attachment_id ); + $transformer = new Attachment( $attachment ); + $result = $this->get_protected_method( $transformer, 'get_attachment' ); + + $this->assertIsArray( $result ); + $this->assertEquals( $expected_type, $result['type'] ); + $this->assertEquals( $mime_type, $result['mediaType'] ); + $this->assertArrayHasKey( 'url', $result ); + + wp_delete_post( $attachment_id, true ); + } + + /** + * Test get_attachment method with alt text. + * + * @covers ::get_attachment + */ + public function test_get_attachment_with_alt() { + $alt_text = 'Test Alt Text'; + update_post_meta( self::$attachment_id, '_wp_attachment_image_alt', $alt_text ); + + $attachment = get_post( self::$attachment_id ); + $transformer = new Attachment( $attachment ); + $result = $this->get_protected_method( $transformer, 'get_attachment' ); + + $this->assertArrayHasKey( 'name', $result ); + $this->assertEquals( $alt_text, $result['name'] ); + } + + /** + * Test to_object method. + * + * @covers ::to_object + */ + public function test_to_object() { + $attachment = get_post( self::$attachment_id ); + $transformer = new Attachment( $attachment ); + $object = $transformer->to_object(); + + $this->assertEquals( 'Note', $object->get_type() ); + $this->assertEquals( home_url( '?p=' . self::$attachment_id ), $object->get_id() ); + $this->assertNull( $object->get_name() ); + } + + /** + * Data provider for mime types. + * + * @return array Test data. + */ + public function provide_mime_types() { + return array( + 'image' => array( + 'image/jpeg', + 'Image', + ), + 'audio' => array( + 'audio/mpeg', + 'Document', + ), + 'video' => array( + 'video/mp4', + 'Document', + ), + 'pdf' => array( + 'application/pdf', + '', + ), + 'text' => array( + 'text/plain', + '', + ), + ); + } + + /** + * Helper method to access protected methods. + * + * @param object $object Object instance. + * @param string $method_name Method name. + * @param array $parameters Optional parameters. + * + * @return mixed Method result. + */ + protected function get_protected_method( $object, $method_name, $parameters = array() ) { + $reflection = new \ReflectionClass( get_class( $object ) ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + + return $method->invokeArgs( $object, $parameters ); + } +} From 5d89584e912e87a503dd01b6ea8955b24ae40515 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 12:46:53 +0100 Subject: [PATCH 048/116] add tests for JSON transformer and fix detected problems --- includes/transformer/class-base.php | 4 + includes/transformer/class-json.php | 12 +- .../includes/collection/class-test-outbox.php | 4 +- .../includes/transformer/class-test-json.php | 132 ++++++++++++++++++ 4 files changed, 144 insertions(+), 8 deletions(-) create mode 100644 tests/includes/transformer/class-test-json.php diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 23ce1c603..897fc9592 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -70,6 +70,10 @@ public function __construct( $item ) { * @return Base_Object The transformed ActivityPub Object. */ protected function transform_object_properties( $activity_object ) { + if ( ! $activity_object || \is_wp_error( $activity_object ) ) { + return $activity_object; + } + $vars = $activity_object->get_object_var_keys(); foreach ( $vars as $var ) { diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php index 8e8dbf05c..6b7c0288e 100644 --- a/includes/transformer/class-json.php +++ b/includes/transformer/class-json.php @@ -20,14 +20,14 @@ class Json extends Activity_Object { * @param string|array $item The item that should be transformed. */ public function __construct( $item ) { - $item = new Base_Object(); + $object = new Base_Object(); - if ( is_array( $this->item ) ) { - $item = Base_Object::init_from_array( $this->item ); - } elseif ( is_string( $this->item ) ) { - $item = Base_Object::init_from_json( $this->item ); + if ( is_array( $item ) ) { + $object = Base_Object::init_from_array( $item ); + } elseif ( is_string( $item ) ) { + $object = Base_Object::init_from_json( $item ); } - parent::__construct( $item ); + parent::__construct( $object ); } } diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index f3c603513..5ed566435 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -53,7 +53,7 @@ public function activity_object_provider() { ), 'Create', 1, - '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"type":"Object","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/1","type":"Note","content":"This is a note","contentMap":{"en":"This is a note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', ), array( array( @@ -64,7 +64,7 @@ public function activity_object_provider() { ), 'Create', 2, - '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"type":"Object","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/2","type":"Note","content":"This is another note","contentMap":{"en":"This is another note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', ), ); } diff --git a/tests/includes/transformer/class-test-json.php b/tests/includes/transformer/class-test-json.php new file mode 100644 index 000000000..d08f92a25 --- /dev/null +++ b/tests/includes/transformer/class-test-json.php @@ -0,0 +1,132 @@ + 'Note', + 'content' => 'Test Content', + 'id' => 'https://example.com/test', + ) + ); + + $transformer = new Json( $json_string ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( Base_Object::class, $object ); + $this->assertEquals( 'Note', $object->get_type() ); + $this->assertEquals( 'Test Content', $object->get_content() ); + $this->assertEquals( 'https://example.com/test', $object->get_id() ); + } + + /** + * Test constructor with array. + * + * @covers ::__construct + */ + public function test_constructor_with_array() { + $array = array( + 'type' => 'Article', + 'name' => 'Test Title', + 'content' => 'Test Content', + 'url' => 'https://example.com/article', + ); + + $transformer = new Json( $array ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( Base_Object::class, $object ); + $this->assertEquals( 'Article', $object->get_type() ); + $this->assertEquals( 'Test Title', $object->get_name() ); + $this->assertEquals( 'Test Content', $object->get_content() ); + $this->assertEquals( 'https://example.com/article', $object->get_url() ); + } + + /** + * Test constructor with invalid JSON string. + * + * @covers ::__construct + */ + public function test_constructor_with_invalid_json() { + $invalid_json = '{invalid json string}'; + + $transformer = new Json( $invalid_json ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( 'WP_Error', $object ); + } + + /** + * Test constructor with empty input. + * + * @covers ::__construct + */ + public function test_constructor_with_empty_input() { + $transformer = new Json( '' ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( 'WP_Error', $object ); + } + + /** + * Test constructor with complex nested data. + * + * @covers ::__construct + */ + public function test_constructor_with_nested_data() { + $data = array( + 'type' => 'Note', + 'content' => 'Test Content', + 'attachment' => array( + array( + 'type' => 'Image', + 'mediaType' => 'image/jpeg', + 'url' => 'https://example.com/image.jpg', + ), + ), + 'tag' => array( + array( + 'type' => 'Mention', + 'name' => '@test', + 'href' => 'https://example.com/@test', + ), + ), + ); + + $transformer = new Json( $data ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( Base_Object::class, $object ); + $this->assertEquals( 'Note', $object->get_type() ); + $this->assertEquals( 'Test Content', $object->get_content() ); + + $attachment = $object->get_attachment(); + $this->assertIsArray( $attachment ); + $this->assertEquals( 'Image', $attachment[0]['type'] ); + + $tags = $object->get_tag(); + $this->assertIsArray( $tags ); + $this->assertEquals( 'Mention', $tags[0]['type'] ); + } +} From 8bea3685e6582c05b5a8301079581495aa289955 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 12:49:57 +0100 Subject: [PATCH 049/116] fix phpcs issues --- .../includes/transformer/class-test-attachment.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/includes/transformer/class-test-attachment.php b/tests/includes/transformer/class-test-attachment.php index 6887da527..f1f2965c1 100644 --- a/tests/includes/transformer/class-test-attachment.php +++ b/tests/includes/transformer/class-test-attachment.php @@ -29,7 +29,7 @@ class Test_Attachment extends WP_UnitTestCase { * @param WP_UnitTest_Factory $factory Helper that creates fake data. */ public static function wpSetUpBeforeClass( $factory ) { - // Create test attachment + // Create test attachment. self::$attachment_id = $factory->attachment->create_object( array( 'post_type' => 'attachment', @@ -65,6 +65,9 @@ public function test_get_type() { * * @covers ::get_attachment * @dataProvider provide_mime_types + * + * @param string $mime_type The mime type of the attachment. + * @param string $expected_type The expected type of the attachment. */ public function test_get_attachment( $mime_type, $expected_type ) { $attachment_id = self::factory()->attachment->create_object( @@ -151,17 +154,17 @@ public function provide_mime_types() { /** * Helper method to access protected methods. * - * @param object $object Object instance. + * @param object $obj Object instance. * @param string $method_name Method name. * @param array $parameters Optional parameters. * * @return mixed Method result. */ - protected function get_protected_method( $object, $method_name, $parameters = array() ) { - $reflection = new \ReflectionClass( get_class( $object ) ); + protected function get_protected_method( $obj, $method_name, $parameters = array() ) { + $reflection = new \ReflectionClass( get_class( $obj ) ); $method = $reflection->getMethod( $method_name ); $method->setAccessible( true ); - return $method->invokeArgs( $object, $parameters ); + return $method->invokeArgs( $obj, $parameters ); } } From 1c29caf96c02b3ab1c9b07f564aba71d04ff3cde Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 13:04:15 +0100 Subject: [PATCH 050/116] add activity-object transformer tests --- .../class-test-activity-object.php | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 tests/includes/transformer/class-test-activity-object.php diff --git a/tests/includes/transformer/class-test-activity-object.php b/tests/includes/transformer/class-test-activity-object.php new file mode 100644 index 000000000..0a437eca1 --- /dev/null +++ b/tests/includes/transformer/class-test-activity-object.php @@ -0,0 +1,216 @@ +test_object = new Base_Object(); + $this->test_object->set_content( 'Test content with @mention and another @mention2' ); + $this->test_object->set_summary( 'Test summary with @mention3' ); + $this->test_object->set_name( 'Test name' ); + $this->test_object->set_type( 'Note' ); + } + + /** + * Test to_object method. + * + * @covers ::to_object + */ + public function test_to_object() { + $transformer = new Activity_Object( $this->test_object ); + $object = $transformer->to_object(); + + $this->assertInstanceOf( Base_Object::class, $object ); + $this->assertEquals( 'Test content with @mention and another @mention2', $object->get_content() ); + $this->assertEquals( 'Test name', $object->get_name() ); + $this->assertEquals( 'Note', $object->get_type() ); + } + + /** + * Test get_mentions method. + * + * @covers ::get_mentions + */ + public function test_get_mentions() { + add_filter( + 'activitypub_extract_mentions', + function () { + return array( + '@mention' => 'https://example.com/@mention', + '@mention2' => 'https://example.com/@mention2', + '@mention3' => 'https://example.com/@mention3', + ); + }, + 10, + 2 + ); + + $transformer = new Activity_Object( $this->test_object ); + $mentions = $this->get_protected_method( $transformer, 'get_mentions' ); + + $this->assertIsArray( $mentions ); + $this->assertCount( 3, $mentions ); + $this->assertEquals( 'https://example.com/@mention', $mentions['@mention'] ); + + remove_all_filters( 'activitypub_extract_mentions' ); + } + + /** + * Test get_cc method. + * + * @covers ::get_cc + */ + public function test_get_cc() { + add_filter( + 'activitypub_extract_mentions', + function () { + return array( + '@mention' => 'https://example.com/@mention', + '@mention2' => 'https://example.com/@mention2', + ); + } + ); + + $transformer = new Activity_Object( $this->test_object ); + $cc = $this->get_protected_method( $transformer, 'get_cc' ); + + $this->assertIsArray( $cc ); + $this->assertCount( 2, $cc ); + $this->assertContains( 'https://example.com/@mention', $cc ); + $this->assertContains( 'https://example.com/@mention2', $cc ); + + remove_all_filters( 'activitypub_extract_mentions' ); + } + + /** + * Test get_content_map method. + * + * @covers ::get_content_map + */ + public function test_get_content_map() { + $transformer = new Activity_Object( $this->test_object ); + $content_map = $this->get_protected_method( $transformer, 'get_content_map' ); + + $this->assertIsArray( $content_map ); + $this->assertArrayHasKey( $this->get_locale(), $content_map ); + $this->assertEquals( 'Test content with @mention and another @mention2', $content_map[ $this->get_locale() ] ); + + // Test with empty content. + $this->test_object->set_content( '' ); + $content_map = $this->get_protected_method( $transformer, 'get_content_map' ); + $this->assertNull( $content_map ); + } + + /** + * Test get_name_map method. + * + * @covers ::get_name_map + */ + public function test_get_name_map() { + $transformer = new Activity_Object( $this->test_object ); + $name_map = $this->get_protected_method( $transformer, 'get_name_map' ); + + $this->assertIsArray( $name_map ); + $this->assertArrayHasKey( $this->get_locale(), $name_map ); + $this->assertEquals( 'Test name', $name_map[ $this->get_locale() ] ); + + // Test with empty name. + $this->test_object->set_name( '' ); + $name_map = $this->get_protected_method( $transformer, 'get_name_map' ); + $this->assertNull( $name_map ); + } + + /** + * Test get_tag method. + * + * @covers ::get_tag + */ + public function test_get_tag() { + add_filter( + 'activitypub_extract_mentions', + function () { + return array( + '@mention' => 'https://example.com/@mention', + ); + } + ); + + $this->test_object->set_tag( + array( + array( + 'type' => 'Hashtag', + 'name' => '#test', + ), + ) + ); + + $transformer = new Activity_Object( $this->test_object ); + $tags = $this->get_protected_method( $transformer, 'get_tag' ); + + $this->assertIsArray( $tags ); + $this->assertCount( 2, $tags ); + + // Test hashtag. + $this->assertEquals( 'Hashtag', $tags[0]['type'] ); + $this->assertEquals( '#test', $tags[0]['name'] ); + + // Test mention. + $this->assertEquals( 'Mention', $tags[1]['type'] ); + $this->assertEquals( '@mention', $tags[1]['name'] ); + $this->assertEquals( 'https://example.com/@mention', $tags[1]['href'] ); + + remove_all_filters( 'activitypub_extract_mentions' ); + } + + /** + * Helper method to access protected methods. + * + * @param object $obj Object instance. + * @param string $method_name Method name. + * @param array $parameters Optional parameters. + * + * @return mixed Method result. + */ + protected function get_protected_method( $obj, $method_name, $parameters = array() ) { + $reflection = new \ReflectionClass( get_class( $obj ) ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + + return $method->invokeArgs( $obj, $parameters ); + } +} From b654081388cd8dd4d5e46e737c2ce5e003e089ae Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 14:21:32 +0100 Subject: [PATCH 051/116] simplify code --- includes/transformer/class-base.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 897fc9592..e52ca7ba8 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -83,15 +83,13 @@ protected function transform_object_properties( $activity_object ) { $value = call_user_func( array( $this, $getter ) ); if ( isset( $value ) ) { - $setter = 'set_' . $var; - /** * Filter the value before it is set to the Activity-Object `$activity_object`. * * @param mixed $value The value that should be set. * @param mixed $item The Object. */ - $value = apply_filters( 'activitypub_transform_' . $setter, $value, $this->item ); + $value = apply_filters( "activitypub_transform_set_{$var}", $value, $this->item ); /** * Filter the value before it is set to the Activity-Object `$activity_object`. From 2077a01d5e3eb908379d5580a25f7a1d17d9fb3e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 14:22:23 +0100 Subject: [PATCH 052/116] define as global functions --- includes/transformer/class-base.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index e52ca7ba8..b42f77820 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -79,8 +79,8 @@ protected function transform_object_properties( $activity_object ) { foreach ( $vars as $var ) { $getter = 'get_' . $var; - if ( method_exists( $this, $getter ) ) { - $value = call_user_func( array( $this, $getter ) ); + if ( \method_exists( $this, $getter ) ) { + $value = \call_user_func( array( $this, $getter ) ); if ( isset( $value ) ) { /** @@ -89,7 +89,7 @@ protected function transform_object_properties( $activity_object ) { * @param mixed $value The value that should be set. * @param mixed $item The Object. */ - $value = apply_filters( "activitypub_transform_set_{$var}", $value, $this->item ); + $value = \apply_filters( "activitypub_transform_set_{$var}", $value, $this->item ); /** * Filter the value before it is set to the Activity-Object `$activity_object`. @@ -98,9 +98,9 @@ protected function transform_object_properties( $activity_object ) { * @param string $var The variable name. * @param mixed $item The Object. */ - $value = apply_filters( 'activitypub_transform_set', $value, $var, $this->item ); + $value = \apply_filters( 'activitypub_transform_set', $value, $var, $this->item ); - call_user_func( array( $activity_object, $setter ), $value ); + \call_user_func( array( $activity_object, $setter ), $value ); } } } From 96d1146a84eed224b62a8f61ac6fd0ab4699226f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 15 Jan 2025 14:40:51 +0100 Subject: [PATCH 053/116] re-added setter --- includes/transformer/class-base.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index b42f77820..9e382c71b 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -83,13 +83,15 @@ protected function transform_object_properties( $activity_object ) { $value = \call_user_func( array( $this, $getter ) ); if ( isset( $value ) ) { + $setter = 'set_' . $var; + /** * Filter the value before it is set to the Activity-Object `$activity_object`. * * @param mixed $value The value that should be set. * @param mixed $item The Object. */ - $value = \apply_filters( "activitypub_transform_set_{$var}", $value, $this->item ); + $value = \apply_filters( "activitypub_transform_{$setter}", $value, $this->item ); /** * Filter the value before it is set to the Activity-Object `$activity_object`. From 476250c0b5a11911f85525c34fadc61a637985f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 16 Jan 2025 16:34:00 +0100 Subject: [PATCH 054/116] add content visibility --- includes/collection/class-outbox.php | 3 +++ includes/functions.php | 4 ++-- includes/scheduler/class-post.php | 7 ++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 0174c709c..c3bb105c2 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -67,6 +67,9 @@ public static function add( $activity_object, $activity_type, $user_id, $visibil // Set the activity type. \wp_set_object_terms( $id, array( strtolower( $activity_type ) ), 'ap_activity_type' ); + // Set the content visibility. + \update_post_meta( $id, 'activitypub_content_visibility', $content_visibility, true ); + return $id; } } diff --git a/includes/functions.php b/includes/functions.php index 50c231dae..6d96b5c4e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1605,7 +1605,7 @@ function is_self_ping( $id ) { * * @return boolean|int The ID of the outbox item or false on failure. */ -function add_to_outbox( $data, $type = 'Create', $user_id = 0 ) { +function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = null ) { $transformer = Transformer_Factory::get_transformer( $data ); if ( ! $transformer || is_wp_error( $transformer ) ) { @@ -1620,7 +1620,7 @@ function add_to_outbox( $data, $type = 'Create', $user_id = 0 ) { set_wp_object_state( $data, 'federate' ); - $id = Outbox::add( $activity, $type, $user_id ); + $id = Outbox::add( $activity, $type, $user_id, $content_visibility ); if ( ! $id ) { return false; diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index ff3dbeeb9..6312ff379 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -93,10 +93,15 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) $type = false; } + // Do not send Activities if `$type` is not set or unknown. if ( empty( $type ) ) { return; } - add_to_outbox( $post, $type, $post->post_author ); + // Get the content visibility. + $content_visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true ); + + // Add the post to the outbox. + add_to_outbox( $post, $type, $post->post_author, $content_visibility ); } } From 511b422f35bfed6c6b1ffe67559d681f1afb819d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 16 Jan 2025 16:38:37 +0100 Subject: [PATCH 055/116] fix PHPCS --- includes/collection/class-outbox.php | 14 ++++++++------ includes/functions.php | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index c3bb105c2..478475b43 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -16,14 +16,14 @@ class Outbox { /** * Add an Item to the outbox. * - * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. - * @param string $activity_type The activity type. - * @param int $user_id The user ID. - * @param string $visibility Optional. The visibility of the content. Default 'public'. + * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. + * @param string $activity_type The activity type. + * @param int $user_id The user ID. + * @param string $content_visibility Optional. The visibility of the content. Default 'public'. * * @return false|int|\WP_Error The added item or an error. */ - public static function add( $activity_object, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore + public static function add( $activity_object, $activity_type, $user_id, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore switch ( $user_id ) { case -1: $actor = 'application'; @@ -68,7 +68,9 @@ public static function add( $activity_object, $activity_type, $user_id, $visibil \wp_set_object_terms( $id, array( strtolower( $activity_type ) ), 'ap_activity_type' ); // Set the content visibility. - \update_post_meta( $id, 'activitypub_content_visibility', $content_visibility, true ); + if ( $content_visibility ) { + \update_post_meta( $id, 'activitypub_content_visibility', $content_visibility, true ); + } return $id; } diff --git a/includes/functions.php b/includes/functions.php index 6d96b5c4e..ea86f057a 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1599,13 +1599,14 @@ function is_self_ping( $id ) { /** * Add an object to the outbox. * - * @param mixed $data The object to add to the outbox. - * @param string $type The type of the Activity. - * @param integer $user_id The User-ID. + * @param mixed $data The object to add to the outbox. + * @param string $type The type of the Activity. + * @param integer $user_id The User-ID. + * @param string $content_visibility The visibility of the content. * * @return boolean|int The ID of the outbox item or false on failure. */ -function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = null ) { +function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { $transformer = Transformer_Factory::get_transformer( $data ); if ( ! $transformer || is_wp_error( $transformer ) ) { From 3720f30e78caa3324521ced831ff182e01574fbc Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 16 Jan 2025 10:17:25 -0600 Subject: [PATCH 056/116] Outbox: Use post meta instead of taxonomies (#1173) * Outbox: Use post meta instead of taxonomies * Add tests for meta values when adding Outbox items * fix indents --------- Co-authored-by: Matthias Pfefferle --- includes/class-activitypub.php | 58 +++++++++++++------ includes/collection/class-outbox.php | 16 ++--- tests/includes/class-test-activitypub.php | 55 ++++++++++++++++++ .../includes/collection/class-test-outbox.php | 3 + 4 files changed, 104 insertions(+), 28 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index ec973e4e9..7f8577541 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -554,35 +554,59 @@ private static function register_post_types() { 'delete_with_user' => true, 'can_export' => true, 'supports' => array(), - 'taxonomies' => array( 'ap_actor', 'ap_activity_type' ), ) ); - \register_taxonomy( - 'ap_actor', + /** + * 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( - 'labels' => array( - 'name' => _x( 'Actor', 'post_type plural name', 'activitypub' ), - ), - 'hierarchical' => true, - 'public' => false, + 'type' => 'string', + 'description' => 'The type of the activity', + 'single' => true, + 'sanitize_callback' => function ( $value ) { + $value = ucfirst( strtolower( $value ) ); + $schema = array( + 'type' => 'string', + 'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ), + 'default' => 'Announce', + ); + + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, ) ); - \register_taxonomy_for_object_type( 'ap_actor', Outbox::POST_TYPE ); - \register_taxonomy( - 'ap_activity_type', + \register_post_meta( Outbox::POST_TYPE, + '_activitypub_activity_actor', array( - 'labels' => array( - 'name' => _x( 'Activity Type', 'post_type plural name', 'activitypub' ), - ), - 'hierarchical' => true, - 'public' => false, + 'type' => 'integer', + 'single' => 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_taxonomy_for_object_type( 'ap_activity_type', Outbox::POST_TYPE ); // Both User and Blog Extra Fields types have the same args. $args = array( diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 478475b43..f816af5f2 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -43,6 +43,11 @@ public static function add( $activity_object, $activity_type, $user_id, $content // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), 'post_status' => 'draft', + 'meta_input' => array( + '_activitypub_activity_type' => $activity_type, + '_activitypub_activity_actor' => $actor, + 'activitypub_content_visibility' => $content_visibility, + ), ); $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); @@ -61,17 +66,6 @@ public static function add( $activity_object, $activity_type, $user_id, $content return false; } - // Set the actor type. - \wp_set_object_terms( $id, array( $actor ), 'ap_actor' ); - - // Set the activity type. - \wp_set_object_terms( $id, array( strtolower( $activity_type ) ), 'ap_activity_type' ); - - // Set the content visibility. - if ( $content_visibility ) { - \update_post_meta( $id, 'activitypub_content_visibility', $content_visibility, true ); - } - return $id; } } diff --git a/tests/includes/class-test-activitypub.php b/tests/includes/class-test-activitypub.php index bf25b155c..796063a53 100644 --- a/tests/includes/class-test-activitypub.php +++ b/tests/includes/class-test-activitypub.php @@ -7,6 +7,8 @@ namespace Activitypub\Tests; +use Activitypub\Collection\Outbox; + /** * Test class for Activitypub. * @@ -14,6 +16,14 @@ */ class Test_Activitypub extends \WP_UnitTestCase { + /** + * Set up test environment. + */ + public function setUp(): void { + parent::setUp(); + \Activitypub\Activitypub::init(); + } + /** * Test post type support. * @@ -55,5 +65,50 @@ function () { // Clean up. unset( $_SERVER['HTTP_ACCEPT'] ); + wp_delete_post( $post_id, true ); + } + + /** + * Test activity type meta sanitization. + * + * @dataProvider activity_meta_sanitization_provider + * @covers ::register_post_types + * + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. + * @param mixed $expected Expected value for invalid meta value. + */ + public function test_activity_meta_sanitization( $meta_key, $meta_value, $expected ) { + $post_id = self::factory()->post->create( + array( + 'post_type' => Outbox::POST_TYPE, + 'meta_input' => array( $meta_key => $meta_value ), + ) + ); + + $this->assertEquals( $meta_value, \get_post_meta( $post_id, $meta_key, true ) ); + + wp_update_post( + array( + 'ID' => $post_id, + 'meta_input' => array( $meta_key => 'InvalidType' ), + ) + ); + $this->assertEquals( $expected, \get_post_meta( $post_id, $meta_key, true ) ); + + wp_delete_post( $post_id, true ); + } + + /** + * Data provider for test_activity_meta_sanitization. + * + * @return array + */ + public function activity_meta_sanitization_provider() { + return array( + array( '_activitypub_activity_type', 'Create', 'Announce' ), + array( '_activitypub_activity_actor', 'user', 'user' ), + array( '_activitypub_activity_actor', 'blog', 'user' ), + ); } } diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 5ed566435..20e82c509 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -35,6 +35,9 @@ public function test_add( $data, $type, $user_id, $json ) { $this->assertInstanceOf( 'WP_Post', $post ); $this->assertEquals( 'draft', $post->post_status ); $this->assertEquals( $json, $post->post_content ); + + $this->assertEquals( $type, get_post_meta( $id, '_activitypub_activity_type', true ) ); + $this->assertEquals( 'user', get_post_meta( $id, '_activitypub_activity_actor', true ) ); } /** From 7e146bf99e36d7eb60bb1dc2efd29321c7c52958 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 16 Jan 2025 23:34:16 +0100 Subject: [PATCH 057/116] Update includes/scheduler/class-actor.php Co-authored-by: Matt Wiebe --- includes/scheduler/class-actor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php index 17830cf91..e210c2eea 100644 --- a/includes/scheduler/class-actor.php +++ b/includes/scheduler/class-actor.php @@ -85,7 +85,7 @@ public static function user_update( $user_id ) { * @return mixed */ public static function blog_user_update( $value = null ) { - self::schedule_profile_update( 0 ); + self::schedule_profile_update( Actors::BLOG_USER_ID ); return $value; } From 315493a0d55a433cc56958b8f7cb29fdf6198288 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 17 Jan 2025 09:36:03 +0100 Subject: [PATCH 058/116] register content-visibility meta --- includes/class-activitypub.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 7f8577541..e214f40db 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -608,6 +608,29 @@ private static function register_post_types() { ) ); + \register_post_meta( + Outbox::POST_TYPE, + 'activitypub_content_visibility', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, 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. $args = array( 'labels' => array( From 570d4d3f5d05b728ba103e4ef1fb294c651a6136 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:11:40 +0100 Subject: [PATCH 059/116] rename dispatcher --- includes/{class-activity-dispatcher.php => class-dispatcher.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename includes/{class-activity-dispatcher.php => class-dispatcher.php} (100%) diff --git a/includes/class-activity-dispatcher.php b/includes/class-dispatcher.php similarity index 100% rename from includes/class-activity-dispatcher.php rename to includes/class-dispatcher.php From 0093c9941fa02c4553e24e41863c35c5bde60318 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:48:31 +0100 Subject: [PATCH 060/116] Simple dispatcher based on the Outbox-Collection This is a simple rewrite of the current dispatcher system, to use the Outbox instead of the Scheduler. This is a first draft and will be improved over time, to better handle: * Re-tries * Errors * Logging * Batch processing --- activitypub.php | 2 +- includes/class-dispatcher.php | 225 +++++++--------------------------- 2 files changed, 42 insertions(+), 185 deletions(-) diff --git a/activitypub.php b/activitypub.php index 22a23ec49..85e71beab 100644 --- a/activitypub.php +++ b/activitypub.php @@ -65,7 +65,7 @@ function rest_init() { function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 670a3a2f2..422ae8195 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -1,37 +1,29 @@ get_wp_user_id() ) && - ! is_user_disabled( Actors::BLOG_USER_ID ) - ) { - $transformer->change_wp_user_id( Actors::BLOG_USER_ID ); - } - - if ( null !== $user_id ) { - $transformer->change_wp_user_id( $user_id ); - } - - $user_id = $transformer->get_wp_user_id(); - - if ( is_user_disabled( $user_id ) ) { - return; - } - - $activity = $transformer->to_activity( $type ); - - self::send_activity_to_followers( $activity, $user_id, $wp_object ); - } - - /** - * Send Announces to followers and mentioned users. + * Process the outbox. * - * @param mixed $wp_object The ActivityPub Post. - * @param string $type The Activity-Type. + * @param int $id The outbox ID. */ - public static function send_announce( $wp_object, $type ) { - if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) { - return; - } + public static function process_outbox( $id ) { + $activity = \get_post( $id ); - if ( is_user_disabled( Actors::BLOG_USER_ID ) ) { + // If the activity is not a post, return. + if ( ! $activity ) { return; } - $transformer = Factory::get_transformer( $wp_object ); + $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); - if ( \is_wp_error( $transformer ) ) { - return; + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $activity->post_author; + break; } - $user_id = Actors::BLOG_USER_ID; - $activity = $transformer->to_activity( $type ); - $user = Actors::get_by_id( Actors::BLOG_USER_ID ); - - $announce = new Activity(); - $announce->set_type( 'Announce' ); - $announce->set_object( $activity ); - $announce->set_actor( $user->get_id() ); + $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $activity->post_content ); + $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $announce, $user_id, $wp_object ); - } - - /** - * Send a "Update" Activity when a user updates their profile. - * - * @param int $user_id The user ID to send an update for. - */ - public static function send_profile_update( $user_id ) { - $user = Actors::get_by_various( $user_id ); - - // Bail if that's not a good user. - if ( is_wp_error( $user ) ) { - return; - } - - // Build the update. - $activity = new Activity(); - $activity->set_type( 'Update' ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $user->get_id() ); - $activity->set_to( array( 'https://www.w3.org/ns/activitystreams#Public' ) ); - - // Send the update. - self::send_activity_to_followers( $activity, $user_id, $user ); + self::send_activity_to_followers( $activity, $actor_id ); } /** * Send an Activity to all followers and mentioned users. * * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - private static function send_activity_to_followers( $activity, $user_id, $wp_object ) { + private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { /** * Filters whether to send an Activity to followers. * * @param bool $send_activity_to_followers Whether to send the Activity to followers. * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { return; } @@ -175,10 +90,10 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj * Filters the list of inboxes to send the Activity to. * * @param array $inboxes The list of inboxes to send to. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param Activity $activity The ActivityPub Activity. */ - $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity ); + $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $actor_id, $activity ); $inboxes = array_unique( $inboxes ); if ( empty( $inboxes ) ) { @@ -188,80 +103,22 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $actor_id ); } set_wp_object_state( $wp_object, 'federated' ); } - /** - * Send a "Create" or "Update" Activity for a WordPress Post. - * - * @param int $id The WordPress Post ID. - * @param string $type The Activity-Type. - */ - public static function send_post( $id, $type ) { - $post = get_post( $id ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - /** - * Fires when an Activity is being sent for any object type. - * - * @param WP_Post $post The WordPress Post. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $post, $type ); - - /** - * Fires when a specific type of Activity is being sent. - * - * @param WP_Post $post The WordPress Post. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Comment. - * - * @param int $id The WordPress Comment ID. - * @param string $type The Activity-Type. - */ - public static function send_comment( $id, $type ) { - $comment = get_comment( $id ); - - if ( ! $comment ) { - return; - } - - /** - * Fires when an Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $comment, $type ); - - /** - * Fires when a specific type of Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment ); - } - /** * Default filter to add Inboxes of Followers. * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_follower( $inboxes, $user_id ) { - $follower_inboxes = Followers::get_inboxes( $user_id ); + public static function add_inboxes_of_follower( $inboxes, $actor_id ) { + $follower_inboxes = Followers::get_inboxes( $actor_id ); return array_merge( $inboxes, $follower_inboxes ); } @@ -270,12 +127,12 @@ public static function add_inboxes_of_follower( $inboxes, $user_id ) { * Default filter to add Inboxes of Mentioned Actors * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes. */ - public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) { + public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) { $cc = $activity->get_cc() ?? array(); $to = $activity->get_to() ?? array(); @@ -302,12 +159,12 @@ function ( $actor ) { * Default filter to add Inboxes of Posts that are set as `in-reply-to` * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) { + public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) { $in_reply_to = $activity->get_in_reply_to(); if ( ! $in_reply_to ) { From d467d91f5409bee1e6f4a28d9dd4d2d84e9d9f49 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 15:07:39 +0100 Subject: [PATCH 061/116] update changelog --- CHANGELOG.md | 9 ++++++++- readme.txt | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2fc612b5..f3609a0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* Outbox queue + +### Changed + +* Rewrite the current dispatcher system, to use the Outbox instead of the Scheduler. + ### Removed * Built-in support for nodeinfo2. Use the [NodeInfo plugin](https://wordpress.org/plugins/nodeinfo/) instead. @@ -21,7 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Outbox queue * Comment counts get updated when the plugin is activated/deactivated/deleted * Added a filter to make custom comment types manageable in WP.com Calypso diff --git a/readme.txt b/readme.txt index 30bc0b586..ef84dba73 100644 --- a/readme.txt +++ b/readme.txt @@ -135,6 +135,7 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = * Added: Outbox queue +* Changed: Rewrite the current dispatcher system, to use the Outbox instead of a Scheduler. * Removed: Built-in support for nodeinfo2. Use the [NodeInfo plugin](https://wordpress.org/plugins/nodeinfo/) instead. = 4.7.1 = From a6cf6d956d6c65d36b1c2e2f6c94dc4e9e573974 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Mon, 20 Jan 2025 10:38:19 -0600 Subject: [PATCH 062/116] Outbox: Update API Endpoint (#1170) * Update file name * Extend WP_REST_Controller * First pass at updated outbox endpoint * Remove expectation of having cc in the response * Update object type to be a bit more descriptive * First pass at create endpoint * Fix tests * Get Activity Type from meta after #1173 * Fix rest_url_path Props @pfefferle * Return accurate number of outbox items Props @pfefferle * Add more tests and remove unnecessary out-of-bounds error * Remove POST endpoint for now We currently don't have a use-case for it. * Limit Outbox by activity type and visibility * Account for posts without visibility meta Props @pfefferle * phpcs * Allow logged in users to see all activity types * Add request context to remaining hooks * Default query to limit activities Adds an exception for requests with privileges. * Slightly improve query --- activitypub.php | 4 +- includes/collection/class-outbox.php | 6 +- includes/rest/class-outbox-controller.php | 306 +++++++++++ includes/rest/class-outbox.php | 181 ------- includes/transformer/class-json.php | 11 + .../includes/collection/class-test-outbox.php | 4 +- .../rest/class-test-outbox-controller.php | 511 ++++++++++++++++++ 7 files changed, 837 insertions(+), 186 deletions(-) create mode 100644 includes/rest/class-outbox-controller.php delete mode 100644 includes/rest/class-outbox.php create mode 100644 tests/includes/rest/class-test-outbox-controller.php diff --git a/activitypub.php b/activitypub.php index 5960bff86..83e943a00 100644 --- a/activitypub.php +++ b/activitypub.php @@ -40,7 +40,6 @@ */ function rest_init() { Rest\Actors::init(); - Rest\Outbox::init(); Rest\Inbox::init(); Rest\Followers::init(); Rest\Following::init(); @@ -48,8 +47,9 @@ function rest_init() { Rest\Server::init(); Rest\Collection::init(); Rest\Post::init(); - ( new Rest\Interaction_Controller() )->register_routes(); ( new Rest\Application_Controller() )->register_routes(); + ( new Rest\Interaction_Controller() )->register_routes(); + ( new Rest\Outbox_Controller() )->register_routes(); ( new Rest\Webfinger_Controller() )->register_routes(); // Load NodeInfo endpoints only if blog is public. diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index f816af5f2..04392a92a 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -62,7 +62,11 @@ public static function add( $activity_object, $activity_type, $user_id, $content \kses_init_filters(); } - if ( ! $id || \is_wp_error( $id ) ) { + if ( \is_wp_error( $id ) ) { + return $id; + } + + if ( ! $id ) { return false; } diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php new file mode 100644 index 000000000..b9aaf6355 --- /dev/null +++ b/includes/rest/class-outbox-controller.php @@ -0,0 +1,306 @@ +[\w\-\.]+)/outbox'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID of the user or actor.', + 'type' => 'string', + 'validate_callback' => array( $this, 'validate_user_id' ), + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'default' => 1, + 'minimum' => 1, + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + ), + ), + 'schema' => array( $this, 'get_collection_schema' ), + ) + ); + } + + /** + * Validates the user_id parameter. + * + * @param mixed $user_id The user_id parameter. + * @return bool|\WP_Error True if the user_id is valid, WP_Error otherwise. + */ + public function validate_user_id( $user_id ) { + $user = Actors::get_by_various( $user_id ); + if ( \is_wp_error( $user ) ) { + return $user; + } + + return true; + } + + /** + * Retrieves a collection of outbox items. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $page = $request->get_param( 'page' ); + $user = Actors::get_by_various( $user_id ); + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + * + * @param \WP_REST_Request $request The request object. + */ + \do_action( 'activitypub_rest_outbox_pre', $request ); + + /** + * Filters the list of activity types to include in the outbox. + * + * @param string[] $activity_types The list of activity types. + */ + $activity_types = apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); + + $args = array( + 'posts_per_page' => $request->get_param( 'per_page' ), + 'author' => $user_id > 0 ? $user_id : null, + 'paged' => $page, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_activitypub_activity_type', + 'value' => $activity_types, + 'compare' => 'IN', + ), + array( + 'relation' => 'OR', + array( + 'key' => 'activitypub_content_visibility', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'activitypub_content_visibility', + 'value' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ), + ), + ); + + if ( current_user_can( 'activitypub' ) ) { + unset( $args['meta_query'] ); + } + + /** + * Filters WP_Query arguments when querying Outbox items via the REST API. + * + * Enables adding extra arguments or setting defaults for an outbox collection request. + * + * @param array $args Array of arguments for WP_Query. + * @param \WP_REST_Request $request The REST API request. + */ + $args = apply_filters( 'rest_activitypub_outbox_query', $args, $request ); + + $outbox_query = new \WP_Query(); + $query_result = $outbox_query->query( $args ); + + $response = array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ), + 'generator' => 'https://wordpress.org/?v=' . \get_bloginfo( 'version' ), + 'actor' => $user->get_id(), + 'type' => 'OrderedCollectionPage', + 'partOf' => get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ), + 'totalItems' => $outbox_query->found_posts, + 'orderedItems' => array(), + ); + + update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + foreach ( $query_result as $outbox_item ) { + $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); + } + + $max_pages = \ceil( $response['totalItems'] / $request->get_param( 'per_page' ) ); + $response['first'] = \add_query_arg( 'page', 1, $response['partOf'] ); + $response['last'] = \add_query_arg( 'page', $max_pages, $response['partOf'] ); + + if ( $max_pages > $page ) { + $response['next'] = \add_query_arg( 'page', $page + 1, $response['partOf'] ); + } + + if ( $page > 1 ) { + $response['prev'] = \add_query_arg( 'page', $page - 1, $response['partOf'] ); + } + + /** + * Filter the ActivityPub outbox array. + * + * @param array $response The ActivityPub outbox array. + * @param \WP_REST_Request $request The request object. + */ + $response = \apply_filters( 'activitypub_rest_outbox_array', $response, $request ); + + /** + * Action triggered after the ActivityPub profile has been created and sent to the client. + * + * @param \WP_REST_Request $request The request object. + */ + \do_action( 'activitypub_outbox_post', $request ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param mixed $item WordPress representation of the item. + * @param \WP_REST_Request $request Request object. + * @return array Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $type = \get_post_meta( $item->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $item->post_content ); + $activity = $transformer->to_activity( $type ); + + return $activity->to_array( false ); + } + + /** + * Retrieves the outbox schema, conforming to JSON Schema. + * + * @return array Collection schema data. + */ + public function get_collection_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'outbox', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'description' => 'The JSON-LD context for the collection.', + 'type' => array( 'string', 'array', 'object' ), + 'required' => true, + ), + 'id' => array( + 'description' => 'The unique identifier for the collection.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'type' => array( + 'description' => 'The type of the collection.', + 'type' => 'string', + 'enum' => array( 'OrderedCollection', 'OrderedCollectionPage' ), + 'required' => true, + ), + 'actor' => array( + 'description' => 'The actor who owns this outbox.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'totalItems' => array( + 'description' => 'The total number of items in the collection.', + 'type' => 'integer', + 'minimum' => 0, + 'required' => true, + ), + 'orderedItems' => array( + 'description' => 'The items in the collection.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + ), + 'required' => true, + ), + 'first' => array( + 'description' => 'The first page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'last' => array( + 'description' => 'The last page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'next' => array( + 'description' => 'The next page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'prev' => array( + 'description' => 'The previous page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php deleted file mode 100644 index 22183bd29..000000000 --- a/includes/rest/class-outbox.php +++ /dev/null @@ -1,181 +0,0 @@ -[\w\-\.]+)/outbox', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'user_outbox_get' ), - 'args' => self::request_parameters(), - 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), - ), - ) - ); - } - - /** - * Renders the user-outbox - * - * @param \WP_REST_Request $request The request object. - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function user_outbox_get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = Actors::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ); - - $page = $request->get_param( 'page', 1 ); - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_outbox_pre' ); - - $json = new stdClass(); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); - $json->totalItems = 0; - - if ( $user_id > 0 ) { - $count_posts = \count_user_posts( $user_id, $post_types, true ); - $json->totalItems = \intval( $count_posts ); - } else { - foreach ( $post_types as $post_type ) { - $count_posts = \wp_count_posts( $post_type ); - $json->totalItems += \intval( $count_posts->publish ); - } - } - - $json->first = \add_query_arg( 'page', 1, $json->partOf ); - $json->last = \add_query_arg( 'page', \ceil( $json->totalItems / 10 ), $json->partOf ); - - if ( $page && ( ( \ceil( $json->totalItems / 10 ) ) > $page ) ) { - $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); - } - - if ( $page && ( $page > 1 ) ) { - $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); - } - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - - if ( $page ) { - $posts = \get_posts( - array( - 'posts_per_page' => 10, - 'author' => $user_id > 0 ? $user_id : null, - 'paged' => $page, - 'post_type' => $post_types, - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => 'activitypub_content_visibility', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => 'activitypub_content_visibility', - 'value' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, - 'compare' => '!=', - ), - ), - ) - ); - - foreach ( $posts as $post ) { - $transformer = Factory::get_transformer( $post ); - - if ( \is_wp_error( $transformer ) ) { - continue; - } - - $post = $transformer->to_object(); - $activity = new Activity(); - $activity->set_type( 'Create' ); - $activity->set_object( $post ); - $json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - } - - /** - * Filter the ActivityPub outbox array. - * - * @param array $json The ActivityPub outbox array. - */ - $json = \apply_filters( 'activitypub_rest_outbox_array', $json ); - - /** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ - \do_action( 'activitypub_outbox_post' ); - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function request_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'integer', - 'default' => 1, - ); - - return $params; - } -} diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php index 6b7c0288e..92974d5af 100644 --- a/includes/transformer/class-json.php +++ b/includes/transformer/class-json.php @@ -30,4 +30,15 @@ public function __construct( $item ) { parent::__construct( $object ); } + + /** + * Returns the public secondary audience of this object + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc + * + * @return array The secondary audience of this object. + */ + protected function get_cc() { + return $this->item->get( 'cc' ); + } } diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 20e82c509..653300589 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -56,7 +56,7 @@ public function activity_object_provider() { ), 'Create', 1, - '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/1","type":"Note","content":"This is a note","contentMap":{"en":"This is a note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/1","type":"Note","content":"This is a note","contentMap":{"en":"This is a note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"mediaType":"text/html","sensitive":false}', ), array( array( @@ -67,7 +67,7 @@ public function activity_object_provider() { ), 'Create', 2, - '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/2","type":"Note","content":"This is another note","contentMap":{"en":"This is another note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":[],"mediaType":"text/html","sensitive":false}', + '{"@context":["https://www.w3.org/ns/activitystreams",{"Hashtag":"as:Hashtag","sensitive":"as:sensitive"}],"id":"https://example.com/2","type":"Note","content":"This is another note","contentMap":{"en":"This is another note"},"tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"mediaType":"text/html","sensitive":false}', ), ); } diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php new file mode 100644 index 000000000..4b2f29b54 --- /dev/null +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -0,0 +1,511 @@ +post->create_many( 10 ); + } + + /** + * Clean up test fixtures. + */ + public static function wpTearDownAfterClass() { + foreach ( self::$post_ids as $post_id ) { + \wp_delete_post( $post_id, true ); + } + + \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); + } + + /** + * Set up test environment. + */ + public function set_up() { + parent::set_up(); + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + } + + /** + * Test route registration. + * + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/' . ACTIVITYPUB_REST_NAMESPACE . '/(?:users|actors)/(?P[\w\-\.]+)/outbox', $routes ); + } + + /** + * Test user ID validation. + * + * @covers ::validate_user_id + */ + public function test_validate_user_id() { + $controller = new Outbox_Controller(); + $this->assertTrue( $controller->validate_user_id( 0 ) ); + $this->assertTrue( $controller->validate_user_id( '1' ) ); + $this->assertWPError( $controller->validate_user_id( 'user-1' ) ); + } + + /** + * Test getting items. + * + * @covers ::get_items + */ + public function test_get_items() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test schema. + * + * @covers ::get_collection_schema + */ + public function test_get_collection_schema() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $schema = ( new Outbox_Controller() )->get_collection_schema(); + + $valid = \rest_validate_value_from_schema( $data, $schema ); + $this->assertNotWPError( $valid, 'Response failed schema validation: ' . ( \is_wp_error( $valid ) ? $valid->get_error_message() : '' ) ); + } + + /** + * Test getting items with pagination. + * + * @covers ::get_items + */ + public function test_get_items_pagination() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request->set_param( 'page', 2 ); + $request->set_param( 'per_page', 3 ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'prev', $data ); + $this->assertArrayHasKey( 'next', $data ); + $this->assertStringContainsString( 'page=1', $data['prev'] ); + $this->assertStringContainsString( 'page=3', $data['next'] ); + } + + /** + * Test getting items response structure. + * + * @covers ::get_items + */ + public function test_get_items_response_structure() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( '@context', $data ); + $this->assertArrayHasKey( 'id', $data ); + $this->assertArrayHasKey( 'type', $data ); + $this->assertArrayHasKey( 'totalItems', $data ); + $this->assertArrayHasKey( 'orderedItems', $data ); + $this->assertEquals( 'OrderedCollectionPage', $data['type'] ); + $this->assertIsArray( $data['orderedItems'] ); + + $headers = $response->get_headers(); + $this->assertEquals( 'application/activity+json; charset=' . \get_option( 'blog_charset' ), $headers['Content-Type'] ); + } + + /** + * Test getting items for specific user. + * + * @covers ::get_items + */ + public function test_get_items_specific_user() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'ap_outbox', + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/1', + 'post_content' => wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/1', + 'type' => 'Create', + 'actor' => 'https://example.org/user/' . $user_id, + 'object' => array( + 'id' => 'https://example.org/note/1', + 'type' => 'Note', + 'content' => 'Test content', + ), + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => 'Create', + '_activitypub_activity_actor' => 'user', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ) + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 1, (int) $data['totalItems'] ); + $this->assertStringContainsString( (string) $user_id, $data['actor'] ); + + \wp_delete_post( $post_id, true ); + \wp_delete_user( $user_id ); + } + + /** + * Test outbox filters. + * + * @covers ::get_items + */ + public function test_get_items_filters() { + $filter_called = false; + $pre_called = false; + $post_called = false; + + \add_filter( + 'activitypub_rest_outbox_array', + function ( $response ) use ( &$filter_called ) { + $filter_called = true; + return $response; + } + ); + + \add_action( + 'activitypub_rest_outbox_pre', + function () use ( &$pre_called ) { + $pre_called = true; + } + ); + + \add_action( + 'activitypub_outbox_post', + function () use ( &$post_called ) { + $post_called = true; + } + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + \rest_get_server()->dispatch( $request ); + + $this->assertTrue( $filter_called, 'activitypub_rest_outbox_array filter was not called.' ); + $this->assertTrue( $pre_called, 'activitypub_rest_outbox_pre action was not called.' ); + $this->assertTrue( $post_called, 'activitypub_outbox_post action was not called.' ); + + \remove_all_filters( 'activitypub_rest_outbox_array' ); + \remove_all_actions( 'activitypub_rest_outbox_pre' ); + \remove_all_actions( 'activitypub_outbox_post' ); + } + + /** + * Test getting items with minimum per_page. + * + * @covers ::get_items + */ + public function test_get_items_minimum_per_page() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request->set_param( 'per_page', 1 ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data['orderedItems'] ); + } + + /** + * Test getting items with maximum per_page. + * + * @covers ::get_items + */ + public function test_get_items_maximum_per_page() { + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request->set_param( 'per_page', 100 ); + $response = \rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Data provider for test_get_items_activity_type. + * + * @return array[] Test parameters. + */ + public function data_activity_types() { + return array( + 'create_activity' => array( + 'type' => 'Create', + 'object' => array( + 'id' => 'https://example.org/note/1', + 'type' => 'Note', + 'content' => 'Test content', + ), + 'allowed' => true, + ), + 'announce_activity' => array( + 'type' => 'Announce', + 'object' => 'https://example.org/note/2', + 'allowed' => true, + ), + 'like_activity' => array( + 'type' => 'Like', + 'object' => 'https://example.org/note/3', + 'allowed' => true, + ), + 'update_activity' => array( + 'type' => 'Update', + 'object' => array( + 'id' => 'https://example.org/note/4', + 'type' => 'Note', + 'content' => 'Updated content', + ), + 'allowed' => true, + ), + 'delete_activity' => array( + 'type' => 'Delete', + 'object' => 'https://example.org/note/5', + 'allowed' => false, + ), + 'follow_activity' => array( + 'type' => 'Follow', + 'object' => 'https://example.org/user/6', + 'allowed' => false, + ), + ); + } + + /** + * Test getting items with different activity types. + * + * @covers ::get_items + * @dataProvider data_activity_types + * + * @param string $type Activity type. + * @param string|array $activity Activity object. + * @param bool $allowed Whether the activity type is allowed for public users. + */ + public function test_get_items_activity_type( $type, $activity, $allowed ) { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => "https://example.org/activity/{$type}", + 'post_content' => \wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => "https://example.org/activity/{$type}", + 'type' => $type, + 'actor' => 'https://example.org/user/' . $user_id, + 'object' => $activity, + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => $type, + '_activitypub_activity_actor' => 'user', + 'activitypub_content_visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ) + ); + + // Test as logged-out user. + \wp_set_current_user( 0 ); + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $activity_types = \wp_list_pluck( $data['orderedItems'], 'type' ); + + if ( $allowed ) { + $this->assertContains( $type, $activity_types, sprintf( 'Activity type "%s" should be visible to logged-out users.', $type ) ); + $this->assertSame( 1, (int) $data['totalItems'], sprintf( 'Activity type "%s" should be included in total items for logged-out users.', $type ) ); + } else { + $this->assertNotContains( $type, $activity_types, sprintf( 'Activity type "%s" should not be visible to logged-out users.', $type ) ); + $this->assertSame( 0, (int) $data['totalItems'], sprintf( 'Activity type "%s" should not be included in total items for logged-out users.', $type ) ); + } + + // Test as logged-in user with activitypub capability. + \wp_set_current_user( $user_id ); + $this->assertTrue( \current_user_can( 'activitypub' ) ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $activity_types = \wp_list_pluck( $data['orderedItems'], 'type' ); + + $this->assertContains( $type, $activity_types, sprintf( 'Activity type "%s" should be visible to users with activitypub capability.', $type ) ); + $this->assertSame( 1, (int) $data['totalItems'], sprintf( 'Activity type "%s" should be included in total items for users with activitypub capability.', $type ) ); + + \wp_delete_post( $post_id, true ); + \wp_delete_user( $user_id ); + } + + /** + * Data provider for test_get_items_content_visibility. + * + * @return array[] Test parameters. + */ + public function data_content_visibility() { + return array( + 'no_visibility' => array( + 'visibility' => null, + 'public_visible' => true, + 'private_visible' => true, + ), + 'public' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + 'public_visible' => true, + 'private_visible' => true, + ), + 'quiet_public' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + 'public_visible' => false, + 'private_visible' => true, + ), + 'local' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + 'public_visible' => false, + 'private_visible' => true, + ), + ); + } + + /** + * Test content visibility for logged-in and logged-out users. + * + * @covers ::get_items + * @dataProvider data_content_visibility + * + * @param string|null $visibility Content visibility setting. + * @param bool $public_visible Whether content should be visible to public users. + * @param bool $private_visible Whether content should be visible to users with activitypub capability. + */ + public function test_get_items_content_visibility( $visibility, $public_visible, $private_visible ) { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + $meta_input = array( + '_activitypub_activity_type' => 'Create', + '_activitypub_activity_actor' => 'user', + ); + + if ( null !== $visibility ) { + $meta_input['activitypub_content_visibility'] = $visibility; + } + + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/1', + 'post_content' => \wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/1', + 'type' => 'Create', + 'actor' => 'https://example.org/user/' . $user_id, + 'object' => array( + 'id' => 'https://example.org/note/1', + 'type' => 'Note', + 'content' => 'Test content', + ), + ) + ), + 'meta_input' => $meta_input, + ) + ); + + // Test as logged-out user. + \wp_set_current_user( 0 ); + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( + (int) $public_visible, + (int) $data['totalItems'], + sprintf( + 'Content with visibility "%s" should%s be visible to logged-out users.', + $visibility ?? 'none', + $public_visible ? '' : ' not' + ) + ); + + // Test as logged-in user with activitypub capability. + \wp_set_current_user( $user_id ); + $this->assertTrue( \current_user_can( 'activitypub' ) ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( + (int) $private_visible, + (int) $data['totalItems'], + sprintf( + 'Content with visibility "%s" should%s be visible to users with activitypub capability.', + $visibility ?? 'none', + $private_visible ? '' : ' not' + ) + ); + + \wp_delete_post( $post_id, true ); + \wp_delete_user( $user_id ); + } + + /** + * Test get_item method. + * + * @doesNotPerformAssertions + */ + public function test_get_item() { + // Controller does not implement get_item(). + } + + /** + * Test get_item_schema method. + * + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } +} From aa884055d6351191aa6c651f38990f193156bf30 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 17:40:39 +0100 Subject: [PATCH 063/116] fix unittests --- includes/class-comment.php | 2 +- tests/includes/class-test-comment.php | 12 ++++++++++ .../transformer/class-test-factory.php | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index 3c42d02be..b0ad16bc2 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -229,7 +229,7 @@ public static function should_be_federated( $comment ) { return false; } - if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) { + if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) { // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user. $user_id = Actors::BLOG_USER_ID; } diff --git a/tests/includes/class-test-comment.php b/tests/includes/class-test-comment.php index 1ef7b33c5..145034963 100644 --- a/tests/includes/class-test-comment.php +++ b/tests/includes/class-test-comment.php @@ -293,6 +293,9 @@ public function ability_to_federate_comment() { 'comment_content' => 'This is a sent comment.', 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', + 'comment_meta' => array( + 'activitypub_status' => 'pending', + ), ), 'expected' => array( 'was_sent' => true, @@ -362,6 +365,9 @@ public function ability_to_federate_threaded_comment() { 'comment_content' => 'This is another comment.', 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', + 'comment_meta' => array( + 'activitypub_status' => 'pending', + ), ), 'expected' => array( 'was_sent' => false, @@ -386,6 +392,9 @@ public function ability_to_federate_threaded_comment() { 'comment_content' => 'This is yet another comment.', 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', + 'comment_meta' => array( + 'activitypub_status' => 'pending', + ), ), 'expected' => array( 'was_sent' => true, @@ -440,6 +449,9 @@ public function ability_to_federate_threaded_comment() { 'comment_content' => 'This is a parent comment that should not be possible.', 'comment_author_url' => 'https://example.com', 'comment_author_email' => '', + 'comment_meta' => array( + 'activitypub_status' => 'federated', + ), ), 'comment' => array( 'comment_type' => 'comment', diff --git a/tests/includes/transformer/class-test-factory.php b/tests/includes/transformer/class-test-factory.php index 9d8e38b54..59fff9c39 100644 --- a/tests/includes/transformer/class-test-factory.php +++ b/tests/includes/transformer/class-test-factory.php @@ -42,6 +42,13 @@ class Test_Factory extends WP_UnitTestCase { */ protected static $comment_id; + /** + * Test user ID. + * + * @var int + */ + protected static $user_id; + /** * Create fake data before tests run. * @@ -58,10 +65,20 @@ public static function wpSetUpBeforeClass( $factory ) { ) ); + self::$user_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + // Create test comment. self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id, + 'user_id' => self::$user_id, + 'comment_meta' => array( + 'activitypub_status' => 'pending', + ), ) ); } @@ -73,6 +90,7 @@ public static function wpTearDownAfterClass() { wp_delete_post( self::$post_id, true ); wp_delete_post( self::$attachment_id, true ); wp_delete_comment( self::$comment_id, true ); + wp_delete_user( self::$user_id, true ); } /** @@ -104,10 +122,16 @@ public function test_get_transformer_post() { * @covers ::get_transformer */ public function test_get_transformer_attachment() { + // Allow attachment to be federated. + \add_post_type_support( 'attachment', 'activitypub' ); + $attachment = get_post( self::$attachment_id ); $transformer = Factory::get_transformer( $attachment ); $this->assertInstanceOf( Attachment::class, $transformer ); + + // Remove support for attachment. + \remove_post_type_support( 'attachment', 'activitypub' ); } /** From 8a7b5ccbf3f55bc5ce85b04e7366e214a09d32e0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:17:39 +0100 Subject: [PATCH 064/116] pending seems to be the better status --- includes/collection/class-outbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 04392a92a..2ba028e84 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -42,7 +42,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'draft', + 'post_status' => 'pending', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor, From 902495a3a24dfd2fd72c40f65d1af30fa6979894 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:27:26 +0100 Subject: [PATCH 065/116] mark post as `publish` after federation id done --- includes/class-dispatcher.php | 33 +++++++++++++------------- includes/transformer/class-factory.php | 5 ++-- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 422ae8195..bf77e1651 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -37,14 +37,14 @@ public static function init() { * @param int $id The outbox ID. */ public static function process_outbox( $id ) { - $activity = \get_post( $id ); + $outbox_item = \get_post( $id ); // If the activity is not a post, return. - if ( ! $activity ) { + if ( ! $outbox_item ) { return; } - $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); + $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true ); switch ( $actor_type ) { case 'blog': @@ -59,30 +59,30 @@ public static function process_outbox( $id ) { break; } - $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); - $transformer = Factory::get_transformer( $activity->post_content ); + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $outbox_item->post_content ); $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $activity, $actor_id ); + self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); } /** * Send an Activity to all followers and mentioned users. * - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { + private static function send_activity_to_followers( $activity, $actor_id, $outbox_item = null ) { /** * Filters whether to send an Activity to followers. * - * @param bool $send_activity_to_followers Whether to send the Activity to followers. - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param bool $send_activity_to_followers Whether to send the Activity to followers. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $outbox_item ) ) { return; } @@ -106,7 +106,8 @@ private static function send_activity_to_followers( $activity, $actor_id, $wp_ob safe_remote_post( $inbox, $json, $actor_id ); } - set_wp_object_state( $wp_object, 'federated' ); + $outbox_item->post_status = 'publish'; + \wp_update_post( $outbox_item ); } /** diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index 95aa4c625..a4662eca4 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -8,10 +8,11 @@ namespace Activitypub\Transformer; use WP_Error; +use Activitypub\Comment as Comment_Helper; use function Activitypub\is_user_disabled; use function Activitypub\is_post_disabled; -use function Activitypub\is_local_comment; + /** * Transformer Factory. */ @@ -84,7 +85,7 @@ public static function get_transformer( $data ) { } break; case 'WP_Comment': - if ( ! is_local_comment( $data ) ) { + if ( Comment_Helper::should_be_federated( $data ) ) { return new Comment( $data ); } break; From f2500eacb110bade908cbc042f6e8bccf29ab838 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:33:57 +0100 Subject: [PATCH 066/116] show only published activities --- includes/rest/class-outbox-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index b9aaf6355..31eb82824 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,7 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( From ecbb777d658c227e760962db41b0077ffe0138d1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:35:56 +0100 Subject: [PATCH 067/116] fix missing rename --- includes/class-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index bf77e1651..c7a1cc5e4 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -55,7 +55,7 @@ public static function process_outbox( $id ) { break; case 'user': default: - $actor_id = $activity->post_author; + $actor_id = $outbox_item->post_author; break; } From 2d590473cc1095e6ceee7813676010b83692d2fb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 00:16:17 +0100 Subject: [PATCH 068/116] Revert "pending seems to be the better status" This reverts commit 8a7b5ccbf3f55bc5ce85b04e7366e214a09d32e0. --- includes/collection/class-outbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 2ba028e84..04392a92a 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -42,7 +42,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'pending', + 'post_status' => 'draft', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor, From 57a41996b00f0f7b75b36629dda7c7d61a5e4a54 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 00:18:09 +0100 Subject: [PATCH 069/116] use pending instead of draft --- includes/collection/class-outbox.php | 2 +- tests/includes/collection/class-test-outbox.php | 2 +- tests/includes/rest/class-test-outbox-controller.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 04392a92a..2ba028e84 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -42,7 +42,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'draft', + 'post_status' => 'pending', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor, diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 653300589..57724696e 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -33,7 +33,7 @@ public function test_add( $data, $type, $user_id, $json ) { $post = get_post( $id ); $this->assertInstanceOf( 'WP_Post', $post ); - $this->assertEquals( 'draft', $post->post_status ); + $this->assertEquals( 'pending', $post->post_status ); $this->assertEquals( $json, $post->post_content ); $this->assertEquals( $type, get_post_meta( $id, '_activitypub_activity_type', true ) ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 4b2f29b54..93e1e4a98 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -152,7 +152,7 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => wp_json_encode( array( @@ -323,7 +323,7 @@ public function test_get_items_activity_type( $type, $activity, $allowed ) { array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => "https://example.org/activity/{$type}", 'post_content' => \wp_json_encode( array( @@ -432,7 +432,7 @@ public function test_get_items_content_visibility( $visibility, $public_visible, array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => \wp_json_encode( array( From 38eeafa988a45192deeca5b782a2f6fd5cd31b23 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:08:47 +0100 Subject: [PATCH 070/116] do not check for post_status --- includes/rest/class-outbox-controller.php | 4 +--- tests/includes/rest/class-test-outbox-controller.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 31eb82824..00e1c57af 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,8 +120,6 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'publish', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( @@ -171,7 +169,7 @@ public function get_items( $request ) { 'orderedItems' => array(), ); - update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + \update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); foreach ( $query_result as $outbox_item ) { $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); } diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 93e1e4a98..ea7dfa4e8 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -152,8 +152,8 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', + 'post_status' => 'pending', 'post_content' => wp_json_encode( array( '@context' => array( 'https://www.w3.org/ns/activitystreams' ), From 186547d4f182d7985878d550a10c285d6df5e2e1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:23:11 +0100 Subject: [PATCH 071/116] fix tests props @obenland --- includes/rest/class-outbox-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 00e1c57af..b920c8385 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,6 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'any', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( From 106b3902de7d12ddd3d2539689c16f0587381636 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 21 Jan 2025 16:18:34 -0600 Subject: [PATCH 072/116] Send `Update`s to Blog Actor in dual mode --- includes/class-dispatcher.php | 31 ++++++++++++ tests/includes/class-test-dispatcher.php | 64 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/includes/class-test-dispatcher.php diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index c7a1cc5e4..781b26c74 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -29,6 +29,7 @@ public static function init() { \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); + \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'maybe_add_inboxes_of_blog_user' ), 10, 3 ); } /** @@ -203,4 +204,34 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi return $inboxes; } + + /** + * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return $inboxes; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return $inboxes; + } + // Only if this is an Update. + if ( 'Update' !== $activity->get_type() ) { + return $inboxes; + } + + $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); + $inboxes = array_merge( $inboxes, $blog_inboxes ); + $inboxes = array_unique( $inboxes ); + + return $inboxes; + } } diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php new file mode 100644 index 000000000..8dc91e90e --- /dev/null +++ b/tests/includes/class-test-dispatcher.php @@ -0,0 +1,64 @@ +createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when actor is blog user + */ + public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, Actors::BLOG_USER_ID, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when activity type is not Update + */ + public function test_maybe_add_inboxes_of_blog_user_not_update() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + // Mock the static method using reflection. + $activity->expects( $this->once() ) + ->method( '__call' ) + ->with( 'get_type' ) + ->willReturn( 'Create' ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } +} From 79f6b0ee1658a4f72e968a8d0e0862bd5f66e639 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 21 Jan 2025 18:24:08 -0600 Subject: [PATCH 073/116] Add logging --- includes/class-activitypub.php | 36 ++++++++++++++++++++++++++++++++++ includes/class-dispatcher.php | 18 ++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cee9a9e8f..1413345d8 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -615,6 +615,42 @@ private static function register_post_types() { ) ); + register_post_meta( + Outbox::POST_TYPE, + 'activitypub_sent_json', + array( + 'type' => 'string', + 'description' => 'JSON activity for regular user', + 'single' => true, + 'sanitize_callback' => null, + 'show_in_rest' => false, + ) + ); + + register_post_meta( + Outbox::POST_TYPE, + 'activitypub_sent_inboxes', + array( + 'type' => 'array', + 'description' => 'List of inboxes for regular user', + 'single' => true, + 'sanitize_callback' => null, + 'show_in_rest' => false, + ) + ); + + register_post_meta( + Outbox::POST_TYPE, + 'activitypub_send_log', + array( + 'type' => 'array', + 'description' => 'Delivery logs for the activity', + 'single' => false, + 'sanitize_callback' => null, + 'show_in_rest' => false, + ) + ); + // Both User and Blog Extra Fields types have the same args. $args = array( 'labels' => array( diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 781b26c74..ded8a83ca 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -103,8 +103,24 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo $json = $activity->to_json(); + // We will store the json as generated by the transformer, even though it 's also in plaintext in $outbox_item->post_content + // This will also allow us to keep the logs below leaner. + \add_post_meta( $outbox_item->ID, 'activitypub_sent_json', $json ); + // This will allow error checking later, that all inboxes have been sent. + \add_post_meta( $outbox_item->ID, 'activitypub_sent_inboxes', $inboxes, true ); + foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $actor_id ); + $response = safe_remote_post( $inbox, $json, $actor_id ); + $to_log = array( + 'error' => null, + 'inbox' => $inbox, + 'code' => $response['code'], + 'delivered' => 202 === $response['code'], + ); + if ( ! $to_log['delivered'] ) { + $to_log['error'] = $response['body']; + } + \add_post_meta( $outbox_item->ID, 'activitypub_send_log', $to_log ); } $outbox_item->post_status = 'publish'; From b0810d00f94b5f5dcf22298171e6126ef33b57d3 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 09:57:10 -0600 Subject: [PATCH 074/116] Outbox: Use callbacks that can be unhooked. (#1188) * Outbox: Use callbacks that can be unhooked. * Add unit tests * Use data provider in no-activity tests * Make sure user has activitypub cap * Use should_be_federated() instead See #1186. * Update with learnings from #1196 --- includes/scheduler/class-comment.php | 25 +-- includes/scheduler/class-post.php | 44 +++-- includes/transformer/class-factory.php | 5 +- .../includes/scheduler/class-test-comment.php | 178 ++++++++++++++++++ 4 files changed, 219 insertions(+), 33 deletions(-) create mode 100644 tests/includes/scheduler/class-test-comment.php diff --git a/includes/scheduler/class-comment.php b/includes/scheduler/class-comment.php index a6d6bbd1c..10e6015cd 100644 --- a/includes/scheduler/class-comment.php +++ b/includes/scheduler/class-comment.php @@ -25,18 +25,7 @@ public static function init() { // Comment transitions. \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 ); - \add_action( - 'edit_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', 'approved', $comment_id ); - } - ); - \add_action( - 'wp_insert_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', '', $comment_id ); - } - ); + \add_action( 'wp_insert_comment', array( self::class, 'schedule_comment_activity_on_insert' ), 10, 2 ); } /** @@ -84,4 +73,16 @@ public static function schedule_comment_activity( $new_status, $old_status, $com add_to_outbox( $comment, $type, $comment->user_id ); } + + /** + * Schedule Comment Activities on insert. + * + * @param int $comment_id Comment ID. + * @param \WP_Comment $comment Comment object. + */ + public static function schedule_comment_activity_on_insert( $comment_id, $comment ) { + if ( 1 === (int) $comment->comment_approved ) { + self::schedule_comment_activity( 'approved', '', $comment ); + } + } } diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 6312ff379..14e1ac325 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -21,32 +21,38 @@ class Post { public static function init() { // Post transitions. \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); - \add_action( - 'edit_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'publish', 'publish', $post_id ); - } - ); - \add_action( - 'add_attachment', - function ( $post_id ) { + + // Attachment transitions. + \add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) ); + \add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) ); + \add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) ); + } + + /** + * Schedules Activities for attachment transitions. + * + * @param int $post_id Attachment ID. + */ + public static function transition_attachment_status( $post_id ) { + switch ( current_action() ) { + case 'add_attachment': self::schedule_post_activity( 'publish', '', $post_id ); - } - ); - \add_action( - 'delete_attachment', - function ( $post_id ) { + break; + case 'edit_attachment': + self::schedule_post_activity( 'publish', 'publish', $post_id ); + break; + case 'delete_attachment': self::schedule_post_activity( 'trash', '', $post_id ); - } - ); + break; + } } /** * Schedule Activities. * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param \WP_Post $post Post object. + * @param string $new_status New post status. + * @param string $old_status Old post status. + * @param int|\WP_Post $post Post ID or post object. */ public static function schedule_post_activity( $new_status, $old_status, $post ) { $post = get_post( $post ); diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index 95aa4c625..a4662eca4 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -8,10 +8,11 @@ namespace Activitypub\Transformer; use WP_Error; +use Activitypub\Comment as Comment_Helper; use function Activitypub\is_user_disabled; use function Activitypub\is_post_disabled; -use function Activitypub\is_local_comment; + /** * Transformer Factory. */ @@ -84,7 +85,7 @@ public static function get_transformer( $data ) { } break; case 'WP_Comment': - if ( ! is_local_comment( $data ) ) { + if ( Comment_Helper::should_be_federated( $data ) ) { return new Comment( $data ); } break; diff --git a/tests/includes/scheduler/class-test-comment.php b/tests/includes/scheduler/class-test-comment.php new file mode 100644 index 000000000..62584ea87 --- /dev/null +++ b/tests/includes/scheduler/class-test-comment.php @@ -0,0 +1,178 @@ +user->create( array( 'role' => 'author' ) ); + self::$post_id = self::factory()->post->create( array( 'post_author' => self::$user_id ) ); + + // Add activitypub capability to the user. + get_user_by( 'id', self::$user_id )->add_cap( 'activitypub' ); + + add_filter( 'pre_schedule_event', '__return_false' ); + } + + /** + * Clean up test resources. + */ + public static function tear_down_after_class() { + wp_delete_post( self::$post_id, true ); + wp_delete_user( self::$user_id ); + + remove_filter( 'pre_schedule_event', '__return_false' ); + + $outbox_items = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => -1, + 'post_status' => 'any', + 'fields' => 'ids', + ) + ); + + foreach ( $outbox_items as $outbox_item ) { + \wp_delete_post( $outbox_item, true ); + } + } + + /** + * Test scheduling comment activity on approval. + */ + public function test_schedule_comment_activity_on_approval() { + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$user_id, + 'comment_approved' => 0, + ) + ); + $activitpub_id = \Activitypub\Comment::generate_id( $comment_id ); + + wp_set_comment_status( $comment_id, 'approve' ); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + + wp_delete_comment( $comment_id, true ); + } + + /** + * Test scheduling comment activity on direct insert with approval. + */ + public function test_schedule_comment_activity_on_insert() { + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$user_id, + 'comment_approved' => 1, + ) + ); + $activitpub_id = \Activitypub\Comment::generate_id( $comment_id ); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + + wp_delete_comment( $comment_id, true ); + } + + /** + * Data provider for no activity tests. + * + * @return array[] Test parameters. + */ + public function no_activity_comment_provider() { + return array( + 'unapproved_comment' => array( + array( + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$user_id, + 'comment_approved' => 0, + ), + ), + 'non_registered_user' => array( + array( + 'comment_post_ID' => self::$post_id, + 'comment_approved' => 1, + ), + ), + 'federation_disabled' => array( + array( + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$user_id, + 'comment_approved' => 1, + 'comment_meta' => array( + 'protocol' => 'activitypub', + ), + ), + ), + ); + } + + /** + * Test comment activity scheduling under various conditions. + * + * @dataProvider no_activity_comment_provider + * + * @param array $comment_data Comment data for creating the test comment. + */ + public function test_no_activity_scheduled( $comment_data ) { + $comment_id = self::factory()->comment->create( $comment_data ); + $activitpub_id = \Activitypub\Comment::generate_id( $comment_id ); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertNotEquals( $activitpub_id, $post->post_title ); + + wp_delete_comment( $comment_id, true ); + } + + /** + * Retrieve the latest Outbox item to compare against. + * + * @param string $title Title of the Outbox item. + * @return int|\WP_Post|null + */ + private function get_latest_outbox_item( $title = '' ) { + $outbox = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => 1, + 'post_status' => 'draft', + 'post_title' => $title, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return $outbox ? $outbox[0] : null; + } +} From d283db6cffa566de72b9ab16ca4914012d0d16e5 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 10:01:26 -0600 Subject: [PATCH 075/116] Outbox: Make sure Last page is linked correctly (#1195) Props @pfefferle --- includes/rest/class-outbox-controller.php | 2 +- tests/includes/rest/class-test-outbox-controller.php | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index b9aaf6355..9adbddbea 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -178,7 +178,7 @@ public function get_items( $request ) { $max_pages = \ceil( $response['totalItems'] / $request->get_param( 'per_page' ) ); $response['first'] = \add_query_arg( 'page', 1, $response['partOf'] ); - $response['last'] = \add_query_arg( 'page', $max_pages, $response['partOf'] ); + $response['last'] = \add_query_arg( 'page', \max( $max_pages, 1 ), $response['partOf'] ); if ( $max_pages > $page ) { $response['next'] = \add_query_arg( 'page', $page + 1, $response['partOf'] ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 4b2f29b54..047ed6237 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -117,6 +117,16 @@ public function test_get_items_pagination() { $this->assertArrayHasKey( 'next', $data ); $this->assertStringContainsString( 'page=1', $data['prev'] ); $this->assertStringContainsString( 'page=3', $data['next'] ); + + // Empty collection. + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/1/outbox' ); + $request->set_param( 'per_page', 3 ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertStringContainsString( 'page=1', $data['last'] ); + $this->assertArrayNotHasKey( 'prev', $data ); + $this->assertArrayNotHasKey( 'next', $data ); } /** From 0f1257cc9ed673d5a6a4052050d684cddf0cb98a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 19:33:04 +0100 Subject: [PATCH 076/116] Update includes/collection/class-outbox.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Menrath <99024746+Menrath@users.noreply.github.com> --- includes/collection/class-outbox.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 04392a92a..bfb029c5f 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -9,6 +9,8 @@ /** * ActivityPub Outbox Collection + * + * @link https://www.w3.org/TR/activitypub/#outbox */ class Outbox { const POST_TYPE = 'ap_outbox'; From c28d03e0d5a3ed73d99658b25a63294100356fdf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 19:33:22 +0100 Subject: [PATCH 077/116] Update includes/collection/class-outbox.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Menrath <99024746+Menrath@users.noreply.github.com> --- includes/collection/class-outbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index bfb029c5f..a0fb75d49 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -20,7 +20,7 @@ class Outbox { * * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. * @param string $activity_type The activity type. - * @param int $user_id The user ID. + * @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox. * @param string $content_visibility Optional. The visibility of the content. Default 'public'. * * @return false|int|\WP_Error The added item or an error. From 4fd5642cc1bd8e6e1b4f22e75292055e0c4e5af4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 19:35:47 +0100 Subject: [PATCH 078/116] Update includes/collection/class-outbox.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: André Menrath <99024746+Menrath@users.noreply.github.com> --- includes/collection/class-outbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index a0fb75d49..d7cd69683 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -18,7 +18,7 @@ class Outbox { /** * Add an Item to the outbox. * - * @param \Activitypub\Activity\Base_Object $activity_object The Activity-Object to add as JSON. + * @param \Activitypub\Activity\Base_Object $activity_object The object of the activity that will be added to the outbox. * @param string $activity_type The activity type. * @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox. * @param string $content_visibility Optional. The visibility of the content. Default 'public'. From 6bd1b8019f649af38bd430ef9e894bcf57345d34 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 19:37:24 +0100 Subject: [PATCH 079/116] fix type props @menrath --- includes/class-activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cee9a9e8f..f0cb83166 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -574,7 +574,7 @@ private static function register_post_types() { Outbox::POST_TYPE, '_activitypub_activity_actor', array( - 'type' => 'integer', + 'type' => 'string', 'single' => true, 'sanitize_callback' => function ( $value ) { $schema = array( From 00a5e2e44eccc0d85de4f5b0e3600faf10cfb056 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:11:40 +0100 Subject: [PATCH 080/116] rename dispatcher --- includes/{class-activity-dispatcher.php => class-dispatcher.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename includes/{class-activity-dispatcher.php => class-dispatcher.php} (100%) diff --git a/includes/class-activity-dispatcher.php b/includes/class-dispatcher.php similarity index 100% rename from includes/class-activity-dispatcher.php rename to includes/class-dispatcher.php From f224701c11ce6dd89b521878a6f1a7aaad3300a9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 14:48:31 +0100 Subject: [PATCH 081/116] Simple dispatcher based on the Outbox-Collection This is a simple rewrite of the current dispatcher system, to use the Outbox instead of the Scheduler. This is a first draft and will be improved over time, to better handle: * Re-tries * Errors * Logging * Batch processing --- activitypub.php | 2 +- includes/class-dispatcher.php | 225 +++++++--------------------------- 2 files changed, 42 insertions(+), 185 deletions(-) diff --git a/activitypub.php b/activitypub.php index cb52d0c16..4446452bf 100644 --- a/activitypub.php +++ b/activitypub.php @@ -65,7 +65,7 @@ function rest_init() { function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 670a3a2f2..422ae8195 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -1,37 +1,29 @@ get_wp_user_id() ) && - ! is_user_disabled( Actors::BLOG_USER_ID ) - ) { - $transformer->change_wp_user_id( Actors::BLOG_USER_ID ); - } - - if ( null !== $user_id ) { - $transformer->change_wp_user_id( $user_id ); - } - - $user_id = $transformer->get_wp_user_id(); - - if ( is_user_disabled( $user_id ) ) { - return; - } - - $activity = $transformer->to_activity( $type ); - - self::send_activity_to_followers( $activity, $user_id, $wp_object ); - } - - /** - * Send Announces to followers and mentioned users. + * Process the outbox. * - * @param mixed $wp_object The ActivityPub Post. - * @param string $type The Activity-Type. + * @param int $id The outbox ID. */ - public static function send_announce( $wp_object, $type ) { - if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) { - return; - } + public static function process_outbox( $id ) { + $activity = \get_post( $id ); - if ( is_user_disabled( Actors::BLOG_USER_ID ) ) { + // If the activity is not a post, return. + if ( ! $activity ) { return; } - $transformer = Factory::get_transformer( $wp_object ); + $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); - if ( \is_wp_error( $transformer ) ) { - return; + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $activity->post_author; + break; } - $user_id = Actors::BLOG_USER_ID; - $activity = $transformer->to_activity( $type ); - $user = Actors::get_by_id( Actors::BLOG_USER_ID ); - - $announce = new Activity(); - $announce->set_type( 'Announce' ); - $announce->set_object( $activity ); - $announce->set_actor( $user->get_id() ); + $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $activity->post_content ); + $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $announce, $user_id, $wp_object ); - } - - /** - * Send a "Update" Activity when a user updates their profile. - * - * @param int $user_id The user ID to send an update for. - */ - public static function send_profile_update( $user_id ) { - $user = Actors::get_by_various( $user_id ); - - // Bail if that's not a good user. - if ( is_wp_error( $user ) ) { - return; - } - - // Build the update. - $activity = new Activity(); - $activity->set_type( 'Update' ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $user->get_id() ); - $activity->set_to( array( 'https://www.w3.org/ns/activitystreams#Public' ) ); - - // Send the update. - self::send_activity_to_followers( $activity, $user_id, $user ); + self::send_activity_to_followers( $activity, $actor_id ); } /** * Send an Activity to all followers and mentioned users. * * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - private static function send_activity_to_followers( $activity, $user_id, $wp_object ) { + private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { /** * Filters whether to send an Activity to followers. * * @param bool $send_activity_to_followers Whether to send the Activity to followers. * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { return; } @@ -175,10 +90,10 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj * Filters the list of inboxes to send the Activity to. * * @param array $inboxes The list of inboxes to send to. - * @param int $user_id The user ID. + * @param int $actor_id The actor ID. * @param Activity $activity The ActivityPub Activity. */ - $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity ); + $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $actor_id, $activity ); $inboxes = array_unique( $inboxes ); if ( empty( $inboxes ) ) { @@ -188,80 +103,22 @@ private static function send_activity_to_followers( $activity, $user_id, $wp_obj $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $actor_id ); } set_wp_object_state( $wp_object, 'federated' ); } - /** - * Send a "Create" or "Update" Activity for a WordPress Post. - * - * @param int $id The WordPress Post ID. - * @param string $type The Activity-Type. - */ - public static function send_post( $id, $type ) { - $post = get_post( $id ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - /** - * Fires when an Activity is being sent for any object type. - * - * @param WP_Post $post The WordPress Post. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $post, $type ); - - /** - * Fires when a specific type of Activity is being sent. - * - * @param WP_Post $post The WordPress Post. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Comment. - * - * @param int $id The WordPress Comment ID. - * @param string $type The Activity-Type. - */ - public static function send_comment( $id, $type ) { - $comment = get_comment( $id ); - - if ( ! $comment ) { - return; - } - - /** - * Fires when an Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $comment, $type ); - - /** - * Fires when a specific type of Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment ); - } - /** * Default filter to add Inboxes of Followers. * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_follower( $inboxes, $user_id ) { - $follower_inboxes = Followers::get_inboxes( $user_id ); + public static function add_inboxes_of_follower( $inboxes, $actor_id ) { + $follower_inboxes = Followers::get_inboxes( $actor_id ); return array_merge( $inboxes, $follower_inboxes ); } @@ -270,12 +127,12 @@ public static function add_inboxes_of_follower( $inboxes, $user_id ) { * Default filter to add Inboxes of Mentioned Actors * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes. */ - public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) { + public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) { $cc = $activity->get_cc() ?? array(); $to = $activity->get_to() ?? array(); @@ -302,12 +159,12 @@ function ( $actor ) { * Default filter to add Inboxes of Posts that are set as `in-reply-to` * * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. + * @param int $actor_id The WordPress Actor-ID. * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ - public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) { + public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) { $in_reply_to = $activity->get_in_reply_to(); if ( ! $in_reply_to ) { From 318a2fd37965c2e6a39258927d652beb9be81eb4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 15:07:39 +0100 Subject: [PATCH 082/116] update changelog --- CHANGELOG.md | 9 ++++++++- readme.txt | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56e2fbc3..c7e8f3941 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for WPML post locale +### Added + +* Outbox queue + +### Changed + +* Rewrite the current dispatcher system, to use the Outbox instead of the Scheduler. + ### Removed * Built-in support for nodeinfo2. Use the [NodeInfo plugin](https://wordpress.org/plugins/nodeinfo/) instead. @@ -42,7 +50,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Outbox queue * Comment counts get updated when the plugin is activated/deactivated/deleted * Added a filter to make custom comment types manageable in WP.com Calypso diff --git a/readme.txt b/readme.txt index af290bc64..a47cf3af6 100644 --- a/readme.txt +++ b/readme.txt @@ -134,8 +134,8 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = - * Added: Outbox queue +* Changed: Rewrite the current dispatcher system, to use the Outbox instead of a Scheduler. * Changed: Improved content negotiation and AUTHORIZED_FETCH support for third-party plugins = 4.7.3 = From dcc68d33f9a1731cda9bf1e6e857d51ea95a21b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:27:26 +0100 Subject: [PATCH 083/116] mark post as `publish` after federation id done --- includes/class-dispatcher.php | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 422ae8195..bf77e1651 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -37,14 +37,14 @@ public static function init() { * @param int $id The outbox ID. */ public static function process_outbox( $id ) { - $activity = \get_post( $id ); + $outbox_item = \get_post( $id ); // If the activity is not a post, return. - if ( ! $activity ) { + if ( ! $outbox_item ) { return; } - $actor_type = \get_post_meta( $activity->ID, '_activitypub_activity_actor', true ); + $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true ); switch ( $actor_type ) { case 'blog': @@ -59,30 +59,30 @@ public static function process_outbox( $id ) { break; } - $type = \get_post_meta( $activity->ID, '_activitypub_activity_type', true ); - $transformer = Factory::get_transformer( $activity->post_content ); + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + $transformer = Factory::get_transformer( $outbox_item->post_content ); $activity = $transformer->to_activity( $type ); - self::send_activity_to_followers( $activity, $actor_id ); + self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); } /** * Send an Activity to all followers and mentioned users. * - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - private static function send_activity_to_followers( $activity, $actor_id, $wp_object = null ) { + private static function send_activity_to_followers( $activity, $actor_id, $outbox_item = null ) { /** * Filters whether to send an Activity to followers. * - * @param bool $send_activity_to_followers Whether to send the Activity to followers. - * @param Activity $activity The ActivityPub Activity. - * @param int $actor_id The actor ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. + * @param bool $send_activity_to_followers Whether to send the Activity to followers. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $wp_object ) ) { + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $outbox_item ) ) { return; } @@ -106,7 +106,8 @@ private static function send_activity_to_followers( $activity, $actor_id, $wp_ob safe_remote_post( $inbox, $json, $actor_id ); } - set_wp_object_state( $wp_object, 'federated' ); + $outbox_item->post_status = 'publish'; + \wp_update_post( $outbox_item ); } /** From f04c6825aa1e23ba5833a7158874f8ac146c5570 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:33:57 +0100 Subject: [PATCH 084/116] show only published activities --- includes/rest/class-outbox-controller.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 9adbddbea..20fb156b0 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,7 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'publish', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( From 3f4672bdb7481dcc122da888adcd79bfb1bbc72e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 20 Jan 2025 23:35:56 +0100 Subject: [PATCH 085/116] fix missing rename --- includes/class-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index bf77e1651..c7a1cc5e4 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -55,7 +55,7 @@ public static function process_outbox( $id ) { break; case 'user': default: - $actor_id = $activity->post_author; + $actor_id = $outbox_item->post_author; break; } From 5c6446d901c58d682d906ab2bf37d2ea2832145c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 00:18:09 +0100 Subject: [PATCH 086/116] use pending instead of draft --- includes/collection/class-outbox.php | 2 +- tests/includes/collection/class-test-outbox.php | 2 +- tests/includes/rest/class-test-outbox-controller.php | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index d7cd69683..f68a4fd60 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -44,7 +44,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'draft', + 'post_status' => 'pending', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor, diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 653300589..57724696e 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -33,7 +33,7 @@ public function test_add( $data, $type, $user_id, $json ) { $post = get_post( $id ); $this->assertInstanceOf( 'WP_Post', $post ); - $this->assertEquals( 'draft', $post->post_status ); + $this->assertEquals( 'pending', $post->post_status ); $this->assertEquals( $json, $post->post_content ); $this->assertEquals( $type, get_post_meta( $id, '_activitypub_activity_type', true ) ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 047ed6237..2e76ae12e 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -162,7 +162,7 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => wp_json_encode( array( @@ -333,7 +333,7 @@ public function test_get_items_activity_type( $type, $activity, $allowed ) { array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => "https://example.org/activity/{$type}", 'post_content' => \wp_json_encode( array( @@ -442,7 +442,7 @@ public function test_get_items_content_visibility( $visibility, $public_visible, array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => \wp_json_encode( array( From 7f28a615c3d3e580893aaa77214d4db358115faf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:08:47 +0100 Subject: [PATCH 087/116] do not check for post_status --- includes/rest/class-outbox-controller.php | 4 +--- tests/includes/rest/class-test-outbox-controller.php | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 20fb156b0..fededf618 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,8 +120,6 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'publish', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( @@ -171,7 +169,7 @@ public function get_items( $request ) { 'orderedItems' => array(), ); - update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + \update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); foreach ( $query_result as $outbox_item ) { $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); } diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 2e76ae12e..f287f3c9a 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -162,8 +162,8 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => 'ap_outbox', - 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', + 'post_status' => 'pending', 'post_content' => wp_json_encode( array( '@context' => array( 'https://www.w3.org/ns/activitystreams' ), From a1ce4e8abc65c4e911e5f8c85caf58aafc671db4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 21 Jan 2025 20:23:11 +0100 Subject: [PATCH 088/116] fix tests props @obenland --- includes/rest/class-outbox-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index fededf618..95ab97a5b 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -120,6 +120,7 @@ public function get_items( $request ) { 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'any', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( From 98bbfa15bde35ab16140450593172477f3304f17 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 21 Jan 2025 16:18:34 -0600 Subject: [PATCH 089/116] Send `Update`s to Blog Actor in dual mode --- includes/class-dispatcher.php | 31 ++++++++++++ tests/includes/class-test-dispatcher.php | 64 ++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 tests/includes/class-test-dispatcher.php diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index c7a1cc5e4..781b26c74 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -29,6 +29,7 @@ public static function init() { \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); + \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'maybe_add_inboxes_of_blog_user' ), 10, 3 ); } /** @@ -203,4 +204,34 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi return $inboxes; } + + /** + * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return $inboxes; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return $inboxes; + } + // Only if this is an Update. + if ( 'Update' !== $activity->get_type() ) { + return $inboxes; + } + + $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); + $inboxes = array_merge( $inboxes, $blog_inboxes ); + $inboxes = array_unique( $inboxes ); + + return $inboxes; + } } diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php new file mode 100644 index 000000000..8dc91e90e --- /dev/null +++ b/tests/includes/class-test-dispatcher.php @@ -0,0 +1,64 @@ +createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when actor is blog user + */ + public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, Actors::BLOG_USER_ID, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when activity type is not Update + */ + public function test_maybe_add_inboxes_of_blog_user_not_update() { + update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + // Mock the static method using reflection. + $activity->expects( $this->once() ) + ->method( '__call' ) + ->with( 'get_type' ) + ->willReturn( 'Create' ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $this->assertEquals( $inboxes, $result ); + } +} From d83147b72991f07ef686a565f086104ad7720b06 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:13 +0100 Subject: [PATCH 090/116] Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland --- includes/class-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 781b26c74..deafd0bae 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -23,7 +23,7 @@ class Dispatcher { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ), 10, 1 ); + \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) ); // Default filters to add Inboxes to sent to. \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); From bc9e6af6d607f73eadc3ece10335e4070fefa5c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:21 +0100 Subject: [PATCH 091/116] Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland --- includes/class-dispatcher.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index deafd0bae..5e37e80b7 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -107,8 +107,7 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo safe_remote_post( $inbox, $json, $actor_id ); } - $outbox_item->post_status = 'publish'; - \wp_update_post( $outbox_item ); + \wp_publish_post( $outbox_item ); } /** From 21d8e46a75be74546984a4eaa66aa2680405f9db Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 07:37:39 +0100 Subject: [PATCH 092/116] Update includes/rest/class-outbox-controller.php Co-authored-by: Konstantin Obenland --- includes/rest/class-outbox-controller.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 95ab97a5b..3ab15b63c 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -121,6 +121,7 @@ public function get_items( $request ) { 'paged' => $page, 'post_type' => Outbox::POST_TYPE, 'post_status' => 'any', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( From 06edf3bb83474014e275cd0de4cae492f3bde6fd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:13:52 +0100 Subject: [PATCH 093/116] Check if Activity should be sent to followers --- includes/class-dispatcher.php | 46 +++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 5e37e80b7..712d95266 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -26,7 +26,7 @@ public static function init() { \add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) ); // Default filters to add Inboxes to sent to. - \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 ); + \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 ); \add_filter( 'activitypub_send_to_inboxes', array( self::class, 'maybe_add_inboxes_of_blog_user' ), 10, 3 ); @@ -118,7 +118,11 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo * * @return array The filtered Inboxes */ - public static function add_inboxes_of_follower( $inboxes, $actor_id ) { + public static function add_inboxes_of_follower( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + $follower_inboxes = Followers::get_inboxes( $actor_id ); return array_merge( $inboxes, $follower_inboxes ); @@ -214,6 +218,10 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi * @return array The filtered Inboxes */ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + // Only if we're in both Blog and User modes. if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { return $inboxes; @@ -233,4 +241,38 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act return $inboxes; } + + /** + * Check if passed Activity is public. + * + * @param array $activity The Activity object as array. + * @param int $actor_id The Actor-ID. + * + * @return boolean True if public, false if not. + */ + protected static function should_send_to_followers( $activity, $actor_id ) { + // Check if follower endpoint is set. + $actor = Actors::get_by_id( $actor_id ); + + if ( ! $actor ) { + return $inboxes; + } + + // Check if follower endpoint is set. + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + if ( + // Check if activity is public. + in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) || + // ...or check if follower endpoint is set. + in_array( $actor->get_followers(), $audience, true ) + ) { + return true; + } + + return false; + } } From 7957a271d2c05676d2c478cdf0d07232b89185c3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:16:12 +0100 Subject: [PATCH 094/116] the unique check will be done `send_activity_to_followers` --- includes/class-dispatcher.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 712d95266..198f848a3 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -236,10 +236,8 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act } $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); - $inboxes = array_merge( $inboxes, $blog_inboxes ); - $inboxes = array_unique( $inboxes ); - return $inboxes; + return array_merge( $inboxes, $blog_inboxes ); } /** From 212c02f7aadc2fc32f7e6ad2e1fc50c6fddbab3c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:47:38 +0100 Subject: [PATCH 095/116] fix tests --- includes/class-dispatcher.php | 4 ++-- tests/includes/class-test-dispatcher.php | 26 ++++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 198f848a3..0d8cb0a1a 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -252,8 +252,8 @@ protected static function should_send_to_followers( $activity, $actor_id ) { // Check if follower endpoint is set. $actor = Actors::get_by_id( $actor_id ); - if ( ! $actor ) { - return $inboxes; + if ( ! $actor || is_wp_error( $actor ) ) { + return false; } // Check if follower endpoint is set. diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index 8dc91e90e..2ecbabe28 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -17,6 +17,7 @@ * @covers Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { + /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE */ @@ -26,7 +27,7 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); - $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); } @@ -50,15 +51,28 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); - $activity = $this->createMock( Activity::class ); + $activity = $this->createMock( Activity::class, array( '__call' ) ); // Mock the static method using reflection. - $activity->expects( $this->once() ) + $activity->expects( $this->any() ) ->method( '__call' ) - ->with( 'get_type' ) - ->willReturn( 'Create' ); + ->willReturnCallback( function( $name, $args ) { + if ( 'get_to' === $name ) { + return array( 'https://www.w3.org/ns/activitystreams#Public' ); + } + + if ( 'get_cc' === $name ) { + return array(); + } + + if ( 'get_type' === $name ) { + return 'Create'; + } + + return null; + } ); - $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 123, $activity ); + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); } } From 361d891495617a51c62753be585e27269e6ef0cc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 10:51:04 +0100 Subject: [PATCH 096/116] fix PHPCS --- includes/class-dispatcher.php | 1 + tests/includes/class-test-dispatcher.php | 26 +++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 0d8cb0a1a..738d19532 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -115,6 +115,7 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo * * @param array $inboxes The list of Inboxes. * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index 2ecbabe28..dbcf54168 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -56,21 +56,23 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { // Mock the static method using reflection. $activity->expects( $this->any() ) ->method( '__call' ) - ->willReturnCallback( function( $name, $args ) { - if ( 'get_to' === $name ) { - return array( 'https://www.w3.org/ns/activitystreams#Public' ); - } + ->willReturnCallback( + function ( $name ) { + if ( 'get_to' === $name ) { + return array( 'https://www.w3.org/ns/activitystreams#Public' ); + } - if ( 'get_cc' === $name ) { - return array(); - } + if ( 'get_cc' === $name ) { + return array(); + } - if ( 'get_type' === $name ) { - return 'Create'; - } + if ( 'get_type' === $name ) { + return 'Create'; + } - return null; - } ); + return null; + } + ); $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); $this->assertEquals( $inboxes, $result ); From 978e5b5fe8cdc186186b78e4f04f2f7f06fb4fca Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 11:07:11 +0100 Subject: [PATCH 097/116] move scheduler behind action --- includes/class-scheduler.php | 20 ++++++++++++++++++++ includes/functions.php | 26 +++++++++----------------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 36ad1b809..c174b7245 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -28,6 +28,8 @@ public static function init() { // Follower Cleanups. \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_item_for_federation' ) ); } /** @@ -138,4 +140,22 @@ public static function cleanup_followers() { } } } + + /** + * Schedule the outbox item for federation. + * + * @param int $id The ID of the outbox item. + */ + public static function schedule_outbox_item_for_federation( $id ) { + $hook = 'activitypub_process_outbox'; + $args = array( $id ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + \wp_schedule_single_event( + \time() + 10, + $hook, + $args + ); + } + } } diff --git a/includes/functions.php b/includes/functions.php index 380777e27..ec1da7a24 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1424,6 +1424,7 @@ function get_content_visibility( $post_id ) { $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; $options = array( ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, ); @@ -1554,43 +1555,34 @@ function is_self_ping( $id ) { * Add an object to the outbox. * * @param mixed $data The object to add to the outbox. - * @param string $type The type of the Activity. + * @param string $activity_type The type of the Activity. * @param integer $user_id The User-ID. * @param string $content_visibility The visibility of the content. * * @return boolean|int The ID of the outbox item or false on failure. */ -function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { +function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { $transformer = Transformer_Factory::get_transformer( $data ); if ( ! $transformer || is_wp_error( $transformer ) ) { return false; } - $activity = $transformer->to_object(); + $activity_object = $transformer->to_object(); - if ( ! $activity || is_wp_error( $activity ) ) { + if ( ! $activity_object || \is_wp_error( $activity_object ) ) { return false; } set_wp_object_state( $data, 'federate' ); - $id = Outbox::add( $activity, $type, $user_id, $content_visibility ); + $outbox_activity_id = Outbox::add( $activity_object, $activity_type, $user_id, $content_visibility ); - if ( ! $id ) { + if ( ! $outbox_activity_id ) { return false; } - $hook = 'activitypub_process_outbox'; - $args = array( $id ); + \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity_object, $user_id, $content_visibility ); - if ( false === wp_next_scheduled( $hook, $args ) ) { - \wp_schedule_single_event( - \time() + 10, - $hook, - $args - ); - } - - return $id; + return $outbox_activity_id; } From 72d2fd605f6dd6d2f085a146fb390c28cc4058e8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 11:07:30 +0100 Subject: [PATCH 098/116] Add `private` visibility --- includes/class-activitypub.php | 2 +- includes/constants.php | 1 + tests/includes/rest/class-test-outbox-controller.php | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index f0cb83166..c62a300d9 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -602,7 +602,7 @@ private static function register_post_types() { 'sanitize_callback' => function ( $value ) { $schema = array( 'type' => 'string', - 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ); diff --git a/includes/constants.php b/includes/constants.php index e12450205..98da5d4a4 100644 --- a/includes/constants.php +++ b/includes/constants.php @@ -71,4 +71,5 @@ // Post visibility constants. \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index f287f3c9a..86555d44e 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -409,6 +409,11 @@ public function data_content_visibility() { 'public_visible' => false, 'private_visible' => true, ), + 'private' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'public_visible' => false, + 'private_visible' => true, + ), 'local' => array( 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, 'public_visible' => false, From 99c13891f7d9a4c7fc2bf4b041d4ef8744b3a361 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:12:49 +0100 Subject: [PATCH 099/116] Add Announce activity --- includes/class-dispatcher.php | 4 +-- includes/class-scheduler.php | 4 +-- includes/scheduler/class-post.php | 43 ++++++++++++++++++++++++++ includes/transformer/class-factory.php | 2 +- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 738d19532..fe0d35052 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -9,7 +9,7 @@ use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; -use Activitypub\Transformer\Factory; +use Activitypub\Transformer\Factory as Transformer_Factory; /** * ActivityPub Dispatcher Class. @@ -61,7 +61,7 @@ public static function process_outbox( $id ) { } $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); - $transformer = Factory::get_transformer( $outbox_item->post_content ); + $transformer = Transformer_Factory::get_transformer( $outbox_item->post_content ); $activity = $transformer->to_activity( $type ); self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index c174b7245..cbcf5f96c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -29,7 +29,7 @@ public static function init() { \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_item_for_federation' ) ); + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) ); } /** @@ -146,7 +146,7 @@ public static function cleanup_followers() { * * @param int $id The ID of the outbox item. */ - public static function schedule_outbox_item_for_federation( $id ) { + public static function schedule_outbox_activity_for_federation( $id ) { $hook = 'activitypub_process_outbox'; $args = array( $id ); diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 14e1ac325..26a8806a9 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,6 +7,10 @@ namespace Activitypub\Scheduler; +use Activitypub\Collection\Outbox; +use Activitypub\Collection\Actors; +use Activitypub\Transformer\Factory; + use function Activitypub\add_to_outbox; use function Activitypub\is_post_disabled; use function Activitypub\get_wp_object_state; @@ -45,6 +49,8 @@ public static function transition_attachment_status( $post_id ) { self::schedule_post_activity( 'trash', '', $post_id ); break; } + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** @@ -110,4 +116,41 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) // Add the post to the outbox. add_to_outbox( $post, $type, $post->post_author, $content_visibility ); } + + /** + * Send announces. + * + * @param int $outbox_activity_id The outbox activity ID. + * @param array $activity_object The activity object. + * @param int $actor_id The actor ID. + * @param int $content_visibility The content visibility. + */ + public static function send_announces( $outbox_activity_id, $activity_object, $actor_id, $content_visibility ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return; + } + + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return; + } + + // Only if the content is public or quiet public. + if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) { + return; + } + + $activity_type = \get_post_meta( $outbox_activity_id, '_activitypub_activity_type', true ); + + // Only if the activity is a Create, Update or Delete. + if ( ! in_array( $activity_type, array( 'Create', 'Update', 'Delete' ), true ) ) { + return; + } + + $transformer = Factory::get_transformer( $activity_object ); + $activity = $transformer->to_activity( $activity_type ); + + Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + } } diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index a4662eca4..08eb70c4b 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -94,7 +94,7 @@ public static function get_transformer( $data ) { return new User( $data ); } break; - case 'Base_Object': + case 'Activitypub\Activity\Base_Object': return new Activity_Object( $data ); case 'json': return new Json( $data ); From 6707f8749635410460ba2779adeb8b1ca35bddb1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:36:50 +0100 Subject: [PATCH 100/116] Announce the full object! --- includes/transformer/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 94537c9b9..3c9582ec9 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,7 +146,7 @@ public function to_activity( $type ) { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) { + if ( in_array( $type, array( 'Like' ), true ) ) { $activity->set_object( $object->get_id() ); } From 243184b39fc075bbde15480084784af7a3180f9b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 12:39:07 +0100 Subject: [PATCH 101/116] fix indent --- tests/includes/class-test-dispatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index dbcf54168..aa759922c 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -59,7 +59,7 @@ public function test_maybe_add_inboxes_of_blog_user_not_update() { ->willReturnCallback( function ( $name ) { if ( 'get_to' === $name ) { - return array( 'https://www.w3.org/ns/activitystreams#Public' ); + return array( 'https://www.w3.org/ns/activitystreams#Public' ); } if ( 'get_cc' === $name ) { From 17d3b37faf688a34d65aa4fe69f73453c47ce7a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 15:54:05 +0100 Subject: [PATCH 102/116] Update includes/transformer/class-base.php Co-authored-by: Konstantin Obenland --- includes/transformer/class-base.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 3c9582ec9..c4ae115d8 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,7 +146,7 @@ public function to_activity( $type ) { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like' ), true ) ) { + if ( 'Like' === $type ) { $activity->set_object( $object->get_id() ); } From 0722b126d0407d889147a512c95cabdae208a18f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 15:57:06 +0100 Subject: [PATCH 103/116] add doc-block --- includes/functions.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index ec1da7a24..054750216 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1582,6 +1582,14 @@ function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content return false; } + /** + * Action triggered after an object has been added to the outbox. + * + * @param int $outbox_activity_id The ID of the outbox item. + * @param \Activitypub\Activity\Base_Object $activity_object The activity object. + * @param int $user_id The User-ID. + * @param string $content_visibility The visibility of the content. + */ \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity_object, $user_id, $content_visibility ); return $outbox_activity_id; From 14a372d52ede47a8e4850eaa97a13fb51c698277 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 22 Jan 2025 17:43:56 +0100 Subject: [PATCH 104/116] only boost content not profile updates --- includes/scheduler/class-post.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 26a8806a9..60c777b75 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -148,6 +148,11 @@ public static function send_announces( $outbox_activity_id, $activity_object, $a return; } + // Check if the object is an article, image, audio, video, event or document and ignore profile updates and other activities. + if ( ! in_array( $activity_object->get_type(), array( 'Note', 'Article', 'Image', 'Audio', 'Video', 'Event', 'Document' ), true ) ) { + return; + } + $transformer = Factory::get_transformer( $activity_object ); $activity = $transformer->to_activity( $activity_type ); From 70266cfa58ccae5e36bd978068fe8b8ae183e924 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 22 Jan 2025 12:55:13 -0600 Subject: [PATCH 105/116] Also handle `Delete` when bundling Blog Actor inboxes --- includes/class-dispatcher.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index fe0d35052..e7fb5e864 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -231,13 +231,13 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act if ( Actors::BLOG_USER_ID === $actor_id ) { return $inboxes; } - // Only if this is an Update. - if ( 'Update' !== $activity->get_type() ) { + // Only if this is an Update or Delete. Create handles its own Announce in dual user mode. + if ( ! in_array( $activity->get_type(), array( 'Update', 'Delete' ), true ) ) { return $inboxes; } $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); - + // array_unique is done in `send_activity_to_followers()`, no need here. return array_merge( $inboxes, $blog_inboxes ); } From 5b82940f37abd6581487ddee994b90ebcb7fcb7b Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:17:09 -0600 Subject: [PATCH 106/116] Update docs --- includes/class-dispatcher.php | 23 ++++++++++++----------- tests/includes/class-test-dispatcher.php | 9 +++++++-- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index e7fb5e864..9ee12df0f 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -7,6 +7,7 @@ namespace Activitypub; +use Activitypub\Activity\Activity; use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; use Activitypub\Transformer\Factory as Transformer_Factory; @@ -113,9 +114,9 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo /** * Default filter to add Inboxes of Followers. * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ @@ -132,9 +133,9 @@ public static function add_inboxes_of_follower( $inboxes, $actor_id, $activity ) /** * Default filter to add Inboxes of Mentioned Actors * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes. */ @@ -212,9 +213,9 @@ public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activi /** * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. * - * @param array $inboxes The list of Inboxes. - * @param int $actor_id The WordPress Actor-ID. - * @param array $activity The ActivityPub Activity. + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. * * @return array The filtered Inboxes */ @@ -244,8 +245,8 @@ public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $act /** * Check if passed Activity is public. * - * @param array $activity The Activity object as array. - * @param int $actor_id The Actor-ID. + * @param Activity $activity The Activity object. + * @param int $actor_id The Actor-ID. * * @return boolean True if public, false if not. */ diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index aa759922c..e63192091 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -10,16 +10,17 @@ use Activitypub\Collection\Followers; use Activitypub\Dispatcher; - /** * Test class for Activitypub Dispatcher. * - * @covers Activitypub\Dispatcher + * @coversDefaultClass Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); @@ -33,6 +34,8 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { /** * Test maybe_add_inboxes_of_blog_user when actor is blog user + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); @@ -46,6 +49,8 @@ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { /** * Test maybe_add_inboxes_of_blog_user when activity type is not Update + * + * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_not_update() { update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); From 6f87326820896eecb059e3a5fb1034d55e2a2ee6 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:18:52 -0600 Subject: [PATCH 107/116] Avoid activitypub_actor_mode bleeding into other tests --- tests/includes/class-test-dispatcher.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index e63192091..389cf89b3 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -16,6 +16,14 @@ * @coversDefaultClass Activitypub\Dispatcher */ class Test_Dispatcher extends WP_UnitTestCase { + /** + * Tear down the test case. + */ + public function tear_down() { + \delete_option( 'activitypub_actor_mode' ); + + parent::tear_down(); + } /** * Test maybe_add_inboxes_of_blog_user when actor mode is not ACTIVITYPUB_ACTOR_AND_BLOG_MODE @@ -23,7 +31,7 @@ class Test_Dispatcher extends WP_UnitTestCase { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); @@ -38,7 +46,7 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); @@ -53,7 +61,7 @@ public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { * @covers ::maybe_add_inboxes_of_blog_user */ public function test_maybe_add_inboxes_of_blog_user_not_update() { - update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class, array( '__call' ) ); From 8c778451c4a91fc8439d14ba9ad3d0c4fcd533d5 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:23:13 -0600 Subject: [PATCH 108/116] Fix comments tests --- tests/includes/scheduler/class-test-comment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/includes/scheduler/class-test-comment.php b/tests/includes/scheduler/class-test-comment.php index 62584ea87..f8eb440f9 100644 --- a/tests/includes/scheduler/class-test-comment.php +++ b/tests/includes/scheduler/class-test-comment.php @@ -166,7 +166,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', From 8332017a202012c4926d4ecb5670b095ab676873 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 14:24:20 -0600 Subject: [PATCH 109/116] Account for inheritance in Activity objects --- includes/transformer/class-factory.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index 08eb70c4b..946390200 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -94,12 +94,14 @@ public static function get_transformer( $data ) { return new User( $data ); } break; - case 'Activitypub\Activity\Base_Object': - return new Activity_Object( $data ); case 'json': return new Json( $data ); } + if ( $data instanceof \Activitypub\Activity\Base_Object ) { + return new Activity_Object( $data ); + } + return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } } From 3382d7ff13d08230d407328e7ce052cd097464ed Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 22 Jan 2025 15:15:25 -0600 Subject: [PATCH 110/116] Move hook to the right place --- includes/scheduler/class-post.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 60c777b75..e02bdbda0 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -30,6 +30,8 @@ public static function init() { \add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** @@ -49,8 +51,6 @@ public static function transition_attachment_status( $post_id ) { self::schedule_post_activity( 'trash', '', $post_id ); break; } - - \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** From e0e9e13ab3791c7bcfee9b72082d24e2763d1f12 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 23 Jan 2025 11:43:12 +0100 Subject: [PATCH 111/116] Outbox: Fix Query (#1200) * Outbox: Fix Query If `author` is null, it will return every activity for the blog and application Actor, but we only want to have the Actors activities. * Update includes/rest/class-outbox-controller.php Co-authored-by: Konstantin Obenland * fix user check! * fix phpcs * use constants * Add tests for new functionality * Update tests/includes/rest/class-test-outbox-controller.php --------- Co-authored-by: Konstantin Obenland --- includes/collection/class-outbox.php | 12 +- includes/rest/class-outbox-controller.php | 50 +++-- .../rest/class-test-outbox-controller.php | 212 +++++++++++++++++- 3 files changed, 239 insertions(+), 35 deletions(-) diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index d7cd69683..2fa6a47f4 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -27,14 +27,14 @@ class Outbox { */ public static function add( $activity_object, $activity_type, $user_id, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { // phpcs:ignore switch ( $user_id ) { - case -1: - $actor = 'application'; + case Actors::APPLICATION_USER_ID: + $actor_type = 'application'; break; - case 0: - $actor = 'blog'; + case Actors::BLOG_USER_ID: + $actor_type = 'blog'; break; default: - $actor = 'user'; + $actor_type = 'user'; break; } @@ -47,7 +47,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_status' => 'draft', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, - '_activitypub_activity_actor' => $actor, + '_activitypub_activity_actor' => $actor_type, 'activitypub_content_visibility' => $content_visibility, ), ); diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 9adbddbea..59f2cbd16 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -115,36 +115,52 @@ public function get_items( $request ) { */ $activity_types = apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); + switch ( $user_id ) { + case Actors::APPLICATION_USER_ID: + $actor_type = 'application'; + break; + case Actors::BLOG_USER_ID: + $actor_type = 'blog'; + break; + default: + $actor_type = 'user'; + break; + } + $args = array( 'posts_per_page' => $request->get_param( 'per_page' ), 'author' => $user_id > 0 ? $user_id : null, 'paged' => $page, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'any', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( - 'key' => '_activitypub_activity_type', - 'value' => $activity_types, - 'compare' => 'IN', - ), - array( - 'relation' => 'OR', - array( - 'key' => 'activitypub_content_visibility', - 'compare' => 'NOT EXISTS', - ), - array( - 'key' => 'activitypub_content_visibility', - 'value' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, - ), + 'key' => '_activitypub_activity_actor', + 'value' => $actor_type, ), ), ); - if ( current_user_can( 'activitypub' ) ) { - unset( $args['meta_query'] ); + if ( get_current_user_id() !== $user_id && ! current_user_can( 'activitypub' ) ) { + $args['meta_query'][] = array( + 'key' => '_activitypub_activity_type', + 'value' => $activity_types, + 'compare' => 'IN', + ); + + $args['meta_query'][] = array( + 'relation' => 'OR', + array( + 'key' => 'activitypub_content_visibility', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'activitypub_content_visibility', + 'value' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ); } /** diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 047ed6237..300bdbb58 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -16,6 +16,14 @@ * @coversDefaultClass \Activitypub\Rest\Outbox_Controller */ class Test_Outbox_Controller extends \Activitypub\Tests\Test_REST_Controller_Testcase { + + /** + * Test user ID. + * + * @var int + */ + public static $user_id; + /** * Test post IDs. * @@ -25,17 +33,20 @@ class Test_Outbox_Controller extends \Activitypub\Tests\Test_REST_Controller_Tes /** * Set up class test fixtures. - * - * @param \WP_UnitTest_Factory $factory WordPress unit test factory. */ - public static function wpSetUpBeforeClass( $factory ) { - self::$post_ids = $factory->post->create_many( 10 ); + public static function set_up_before_class() { + self::$user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + \get_user_by( 'ID', self::$user_id )->add_cap( 'activitypub' ); + + self::$post_ids = self::factory()->post->create_many( 10, array( 'post_author' => self::$user_id ) ); } /** * Clean up test fixtures. */ public static function wpTearDownAfterClass() { + \wp_delete_user( self::$user_id ); + foreach ( self::$post_ids as $post_id ) { \wp_delete_post( $post_id, true ); } @@ -79,7 +90,7 @@ public function test_validate_user_id() { * @covers ::get_items */ public function test_get_items() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $response = \rest_get_server()->dispatch( $request ); $this->assertEquals( 200, $response->get_status() ); @@ -91,7 +102,7 @@ public function test_get_items() { * @covers ::get_collection_schema */ public function test_get_collection_schema() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $schema = ( new Outbox_Controller() )->get_collection_schema(); @@ -106,7 +117,7 @@ public function test_get_collection_schema() { * @covers ::get_items */ public function test_get_items_pagination() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $request->set_param( 'page', 2 ); $request->set_param( 'per_page', 3 ); $response = \rest_get_server()->dispatch( $request ); @@ -135,7 +146,7 @@ public function test_get_items_pagination() { * @covers ::get_items */ public function test_get_items_response_structure() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $response = \rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -161,7 +172,7 @@ public function test_get_items_specific_user() { $post_id = self::factory()->post->create( array( 'post_author' => $user_id, - 'post_type' => 'ap_outbox', + 'post_type' => Outbox::POST_TYPE, 'post_status' => 'draft', 'post_title' => 'https://example.org/activity/1', 'post_content' => wp_json_encode( @@ -229,7 +240,7 @@ function () use ( &$post_called ) { } ); - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); \rest_get_server()->dispatch( $request ); $this->assertTrue( $filter_called, 'activitypub_rest_outbox_array filter was not called.' ); @@ -247,7 +258,7 @@ function () use ( &$post_called ) { * @covers ::get_items */ public function test_get_items_minimum_per_page() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $request->set_param( 'per_page', 1 ); $response = \rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -262,7 +273,7 @@ public function test_get_items_minimum_per_page() { * @covers ::get_items */ public function test_get_items_maximum_per_page() { - $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/0/outbox' ); + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/%s/outbox', ACTIVITYPUB_REST_NAMESPACE, self::$user_id ) ); $request->set_param( 'per_page', 100 ); $response = \rest_get_server()->dispatch( $request ); @@ -501,6 +512,183 @@ public function test_get_items_content_visibility( $visibility, $public_visible, \wp_delete_user( $user_id ); } + /** + * Test getting items with correct actor type filtering. + * + * @covers ::get_items + */ + public function test_get_items_actor_type_filtering() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + // Create a post with user actor type. + $user_post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/1', + 'post_content' => wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/1', + 'type' => 'Create', + 'actor' => 'https://example.org/user/' . $user_id, + 'object' => array( + 'id' => 'https://example.org/note/1', + 'type' => 'Note', + 'content' => 'Test content', + ), + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => 'Create', + '_activitypub_activity_actor' => 'user', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ) + ); + + // Create a post with blog actor type. + $blog_post_id = self::factory()->post->create( + array( + 'post_author' => 0, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/2', + 'post_content' => wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/2', + 'type' => 'Create', + 'actor' => 'https://example.org/blog', + 'object' => array( + 'id' => 'https://example.org/note/2', + 'type' => 'Note', + 'content' => 'Test content', + ), + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => 'Create', + '_activitypub_activity_actor' => 'blog', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ) + ); + + // Test user outbox only returns user actor type. + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $user_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 1, (int) $data['totalItems'] ); + $this->assertCount( 1, $data['orderedItems'] ); + $this->assertSame( 'https://example.org/activity/1', $data['orderedItems'][0]['object']['id'] ); + + // Test blog outbox only returns blog actor type. + $request = new \WP_REST_Request( 'GET', sprintf( '/%s/actors/0/outbox', ACTIVITYPUB_REST_NAMESPACE ) ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 1, (int) $data['totalItems'] ); + + \wp_delete_post( $user_post_id, true ); + \wp_delete_post( $blog_post_id, true ); + \wp_delete_user( $user_id ); + } + + /** + * Test meta query behavior for non-privileged users. + * + * @covers ::get_items + */ + public function test_get_items_meta_query_for_non_privileged_users() { + $author_id = self::factory()->user->create( array( 'role' => 'author' ) ); + $viewer_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + + // Create a public post. + $public_post_id = self::factory()->post->create( + array( + 'post_author' => $author_id, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/1', + 'post_content' => wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/1', + 'type' => 'Create', + 'actor' => 'https://example.org/user/' . $author_id, + 'object' => array( + 'id' => 'https://example.org/note/1', + 'type' => 'Note', + 'content' => 'Public content', + ), + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => 'Create', + '_activitypub_activity_actor' => 'user', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ) + ); + + // Create a private post. + $private_post_id = self::factory()->post->create( + array( + 'post_author' => $author_id, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'draft', + 'post_title' => 'https://example.org/activity/2', + 'post_content' => wp_json_encode( + array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/activity/2', + 'type' => 'Follow', + 'actor' => 'https://example.org/user/' . $author_id, + 'object' => 'https://example.org/user/123', + ) + ), + 'meta_input' => array( + '_activitypub_activity_type' => 'Follow', + '_activitypub_activity_actor' => 'user', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ), + ) + ); + + // Test as non-privileged user. + wp_set_current_user( $viewer_id ); + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $author_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 1, (int) $data['totalItems'] ); + $this->assertCount( 1, $data['orderedItems'] ); + $this->assertSame( 'https://example.org/activity/1', $data['orderedItems'][0]['object']['id'] ); + + // Test as privileged user. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/' . $author_id . '/outbox' ); + $response = \rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 2, (int) $data['totalItems'] ); + $this->assertCount( 2, $data['orderedItems'] ); + + \wp_delete_post( $public_post_id, true ); + \wp_delete_post( $private_post_id, true ); + \wp_delete_user( $author_id ); + \wp_delete_user( $viewer_id ); + \wp_delete_user( $admin_id ); + } + /** * Test get_item method. * From b1c89457ac9bdd8333c90128ae4e55166bdce26d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 23 Jan 2025 11:56:24 +0100 Subject: [PATCH 112/116] Fix typo --- CHANGELOG.md | 2 +- readme.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c56e2fbc3..1ed6365da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Untitled] +## [Unreleased] ### Changed diff --git a/readme.txt b/readme.txt index af290bc64..ab7ea665e 100644 --- a/readme.txt +++ b/readme.txt @@ -134,7 +134,6 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = - * Added: Outbox queue * Changed: Improved content negotiation and AUTHORIZED_FETCH support for third-party plugins From bc6e7ed145585a736dbfa31a63d2f675bc8c0056 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Thu, 23 Jan 2025 08:17:03 -0600 Subject: [PATCH 113/116] Outbox: Tests and fixes for Post Scheduler (#1196) * Outbox: Tests and fixes for Post Scheduler * Move extrafields post to Actor class * Add test coverage for remaining Actor methods * Move cap-manipulating tests to the end * Account for inheritance in Transformer Factory Reverts changes to `add_to_outbox()` * Remove outbox items after every test. * Use fully qualified class name * Split extra fields tests to not hit time limit * Adjust bog modes * debug * debug * debug * debug * Use Profile_update action. `wp_update_user` wasn't introduced until WP 6.3. * revert changes to add_to_outbox --- includes/scheduler/class-actor.php | 22 +- includes/scheduler/class-post.php | 20 +- includes/transformer/class-factory.php | 6 +- tests/includes/scheduler/class-test-actor.php | 241 ++++++++++++++++++ tests/includes/scheduler/class-test-post.php | 147 +++++++++++ 5 files changed, 413 insertions(+), 23 deletions(-) create mode 100644 tests/includes/scheduler/class-test-actor.php create mode 100644 tests/includes/scheduler/class-test-post.php diff --git a/includes/scheduler/class-actor.php b/includes/scheduler/class-actor.php index e210c2eea..006e2af63 100644 --- a/includes/scheduler/class-actor.php +++ b/includes/scheduler/class-actor.php @@ -8,6 +8,7 @@ namespace Activitypub\Scheduler; use Activitypub\Collection\Actors; +use Activitypub\Collection\Extra_Fields; use function Activitypub\add_to_outbox; use function Activitypub\is_user_type_disabled; @@ -31,10 +32,12 @@ public static function init() { // Profile updates for user options. if ( ! is_user_type_disabled( 'user' ) ) { - \add_action( 'wp_update_user', array( self::class, 'user_update' ) ); + \add_action( 'profile_update', array( self::class, 'user_update' ) ); \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 ); // @todo figure out a feasible way of updating the header image since it's not unique to any user. } + + \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); } /** @@ -89,6 +92,21 @@ public static function blog_user_update( $value = null ) { return $value; } + /** + * Schedule Activities. + * + * @param string $new_status New post status. + * @param string $old_status Old post status. + * @param \WP_Post $post Post object. + */ + public static function schedule_post_activity( $new_status, $old_status, $post ) { + if ( Extra_Fields::USER_POST_TYPE === $post->post_type ) { + self::schedule_profile_update( $post->post_author ); + } elseif ( Extra_Fields::BLOG_POST_TYPE === $post->post_type ) { + self::schedule_profile_update( Actors::BLOG_USER_ID ); + } + } + /** * Send a profile update to all followers. Gets hooked into all relevant options/meta etc. * @@ -97,6 +115,6 @@ public static function blog_user_update( $value = null ) { public static function schedule_profile_update( $user_id ) { $actor = Actors::get_by_id( $user_id ); - add_to_outbox( $actor->get_id(), 'Update', $user_id ); + add_to_outbox( $actor, 'Update', $user_id ); } } diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 14e1ac325..952ab3ffc 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -55,19 +55,7 @@ public static function transition_attachment_status( $post_id ) { * @param int|\WP_Post $post Post ID or post object. */ public static function schedule_post_activity( $new_status, $old_status, $post ) { - $post = get_post( $post ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - if ( 'ap_extrafield' === $post->post_type ) { - self::schedule_profile_update( $post->post_author ); - return; - } - - if ( 'ap_extrafield_blog' === $post->post_type ) { - self::schedule_profile_update( 0 ); + if ( is_post_disabled( $post ) ) { return; } @@ -76,12 +64,6 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) return; } - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - switch ( $new_status ) { case 'publish': $type = ( 'publish' === $old_status ) ? 'Update' : 'Create'; diff --git a/includes/transformer/class-factory.php b/includes/transformer/class-factory.php index a4662eca4..946390200 100644 --- a/includes/transformer/class-factory.php +++ b/includes/transformer/class-factory.php @@ -94,12 +94,14 @@ public static function get_transformer( $data ) { return new User( $data ); } break; - case 'Base_Object': - return new Activity_Object( $data ); case 'json': return new Json( $data ); } + if ( $data instanceof \Activitypub\Activity\Base_Object ) { + return new Activity_Object( $data ); + } + return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } } diff --git a/tests/includes/scheduler/class-test-actor.php b/tests/includes/scheduler/class-test-actor.php new file mode 100644 index 000000000..0f9236a77 --- /dev/null +++ b/tests/includes/scheduler/class-test-actor.php @@ -0,0 +1,241 @@ +user->create( + array( + 'role' => 'author', + 'display_name' => 'Test User', + 'meta_input' => array( + 'activitypub_description' => 'test description', + 'activitypub_header_image' => 'test header image', + 'description' => 'test description', + 'user_url' => 'https://example.org', + 'display_name' => 'Test Name', + ), + ) + ); + + // Add activitypub capability to the user. + \get_user_by( 'id', self::$user_id )->add_cap( 'activitypub' ); + + \add_filter( 'pre_schedule_event', '__return_false' ); + } + + /** + * Clean up test resources. + */ + public static function tear_down_after_class() { + \delete_option( 'activitypub_actor_mode' ); + \wp_delete_user( self::$user_id ); + \remove_filter( 'pre_schedule_event', '__return_false' ); + } + + /** + * Tear down. + */ + public function tear_down() { + $outbox_items = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => -1, + 'post_status' => 'any', + 'fields' => 'ids', + ) + ); + + foreach ( $outbox_items as $outbox_item ) { + \wp_delete_post( $outbox_item, true ); + } + } + + /** + * Data provider for user meta update scheduling. + * + * @return string[][] + */ + public function user_meta_provider() { + return array( + array( 'activitypub_description' ), + array( 'activitypub_header_image' ), + array( 'description' ), + array( 'user_url' ), + array( 'display_name' ), + ); + } + + /** + * Test user meta update scheduling. + * + * @dataProvider user_meta_provider + * @covers ::user_meta_update + * + * @param string $meta_key Meta key to test. + */ + public function test_user_meta_update( $meta_key ) { + \update_user_meta( self::$user_id, $meta_key, 'test value' ); + + $activitpub_id = Actors::get_by_id( self::$user_id )->get_id(); + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + } + + /** + * Test user update scheduling. + * + * @covers ::user_update + */ + public function test_user_update() { + self::factory()->user->update_object( self::$user_id, array( 'display_name' => 'Test Name' ) ); + + $activitpub_id = Actors::get_by_id( self::$user_id )->get_id(); + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + } + + /** + * Test blog user update scheduling. + * + * @covers ::blog_user_update + */ + public function test_blog_user_update() { + $test_value = 'test value'; + $result = \Activitypub\Scheduler\Actor::blog_user_update( $test_value ); + + $activitpub_id = Actors::get_by_id( Actors::BLOG_USER_ID )->get_id(); + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + $this->assertSame( $test_value, $result ); + } + + /** + * Test user update scheduling with non-publishing user. + * + * @covers ::user_update + */ + public function test_user_update_no_publish() { + $activitpub_id = Actors::get_by_id( self::$user_id )->get_id(); + + // Temporarily remove the activitypub capability. + \get_user_by( 'id', self::$user_id )->remove_cap( 'activitypub' ); + self::factory()->user->update_object( self::$user_id, array( 'display_name' => 'Test Name No Publish' ) ); + + $this->assertNull( $this->get_latest_outbox_item( $activitpub_id ) ); + + // Restore the activitypub capability. + \get_user_by( 'id', self::$user_id )->add_cap( 'activitypub' ); + } + + /** + * Test user meta update scheduling with non-publishing user. + * + * @covers ::user_meta_update + */ + public function test_user_meta_update_no_publish() { + $activitpub_id = Actors::get_by_id( self::$user_id )->get_id(); + + // Temporarily remove the activitypub capability. + \get_user_by( 'id', self::$user_id )->remove_cap( 'activitypub' ); + + \update_user_meta( self::$user_id, 'description', 'test value' ); + + $this->assertNull( $this->get_latest_outbox_item( $activitpub_id ) ); + + // Restore the activitypub capability. + \get_user_by( 'id', self::$user_id )->add_cap( 'activitypub' ); + } + + /** + * Test post activity scheduling for ActivityPub extra fields. + * + * @covers ::schedule_post_activity + */ + public function test_schedule_post_activity_extra_fields() { + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$user_id, + 'post_type' => Extra_Fields::USER_POST_TYPE, + ) + ); + $activitpub_id = Actors::get_by_id( self::$user_id )->get_id(); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + + \wp_delete_post( $post_id, true ); + } + + /** + * Test post activity scheduling for ActivityPub extra fields. + * + * @covers ::schedule_post_activity + */ + public function test_schedule_post_activity_extra_field_blog() { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + $blog_post_id = self::factory()->post->create( array( 'post_type' => Extra_Fields::BLOG_POST_TYPE ) ); + $activitpub_id = Actors::get_by_id( Actors::BLOG_USER_ID )->get_id(); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + + // Clean up. + \wp_delete_post( $blog_post_id, true ); + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + } + + /** + * Retrieve the latest Outbox item to compare against. + * + * @param string $title Title of the Outbox item. + * @return int|\WP_Post|null + */ + private function get_latest_outbox_item( $title = '' ) { + $outbox = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => 1, + 'post_status' => 'draft', + 'post_title' => $title, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return $outbox ? $outbox[0] : null; + } +} diff --git a/tests/includes/scheduler/class-test-post.php b/tests/includes/scheduler/class-test-post.php new file mode 100644 index 000000000..a5fa05eab --- /dev/null +++ b/tests/includes/scheduler/class-test-post.php @@ -0,0 +1,147 @@ +user->create( array( 'role' => 'author' ) ); + + // Add activitypub capability to the user. + \get_user_by( 'id', self::$user_id )->add_cap( 'activitypub' ); + + \add_filter( 'pre_schedule_event', '__return_false' ); + } + + /** + * Clean up test resources. + */ + public static function tear_down_after_class() { + \wp_delete_user( self::$user_id ); + \remove_filter( 'pre_schedule_event', '__return_false' ); + } + + /** + * Tear down. + */ + public function tear_down() { + $outbox_items = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => -1, + 'post_status' => 'any', + 'fields' => 'ids', + ) + ); + + foreach ( $outbox_items as $outbox_item ) { + \wp_delete_post( $outbox_item, true ); + } + } + + /** + * Test post activity scheduling for regular posts. + * + * @covers ::schedule_post_activity + */ + public function test_schedule_post_activity_regular_post() { + $post_id = self::factory()->post->create(); + $activitpub_id = \add_query_arg( 'p', $post_id, \trailingslashit( \home_url() ) ); + + $post = $this->get_latest_outbox_item( $activitpub_id ); + $this->assertSame( $activitpub_id, $post->post_title ); + + \wp_delete_post( $post_id, true ); + } + + /** + * Data provider for no activity tests. + * + * @return array[] Test parameters. + */ + public function no_activity_post_provider() { + return array( + 'password_protected' => array( + array( 'post_password' => 'test-password' ), + ), + 'unsupported_post_type' => array( + array( 'post_type' => 'nav_menu_item' ), + ), + 'disabled_post' => array( + array( + 'meta_input' => array( + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ), + ), + ), + ); + } + + /** + * Test post activity scheduling under various conditions. + * + * @dataProvider no_activity_post_provider + * + * @param array $args Post data for creating the test post. + */ + public function test_no_activity_scheduled( $args ) { + $post_id = self::factory()->post->create( $args ); + $activitpub_id = \add_query_arg( 'p', $post_id, \trailingslashit( \home_url() ) ); + + $this->assertNull( $this->get_latest_outbox_item( $activitpub_id ) ); + + \wp_delete_post( $post_id, true ); + } + + /** + * Retrieve the latest Outbox item to compare against. + * + * @param string $title Title of the Outbox item. + * @return int|\WP_Post|null + */ + private function get_latest_outbox_item( $title = '' ) { + $outbox = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'posts_per_page' => 1, + 'post_status' => 'draft', + 'post_title' => $title, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return $outbox ? $outbox[0] : null; + } +} From 9bcf121206aa49127adbe17e4182e39e5e6cbc62 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 23 Jan 2025 15:43:42 +0100 Subject: [PATCH 114/116] Update Dispatcher to use Outbox (#1186) * rename dispatcher * Simple dispatcher based on the Outbox-Collection This is a simple rewrite of the current dispatcher system, to use the Outbox instead of the Scheduler. This is a first draft and will be improved over time, to better handle: * Re-tries * Errors * Logging * Batch processing * update changelog * mark post as `publish` after federation id done * show only published activities * fix missing rename * use pending instead of draft * do not check for post_status * fix tests props @obenland * Send `Update`s to Blog Actor in dual mode * Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland * Update includes/class-dispatcher.php Co-authored-by: Konstantin Obenland * Update includes/rest/class-outbox-controller.php Co-authored-by: Konstantin Obenland * Check if Activity should be sent to followers * the unique check will be done `send_activity_to_followers` * fix tests * fix PHPCS * move scheduler behind action * Add `private` visibility * Add Announce activity * Announce the full object! * fix indent * Update includes/transformer/class-base.php Co-authored-by: Konstantin Obenland * add doc-block * only boost content not profile updates * Also handle `Delete` when bundling Blog Actor inboxes * Update docs * Avoid activitypub_actor_mode bleeding into other tests * Fix comments tests * Account for inheritance in Activity objects * Move hook to the right place * fix typo! * trigger scheduler * Fix tests --------- Co-authored-by: Matt Wiebe Co-authored-by: Konstantin Obenland --- CHANGELOG.md | 9 +- activitypub.php | 2 +- includes/class-activity-dispatcher.php | 348 ------------------ includes/class-activitypub.php | 2 +- includes/class-dispatcher.php | 278 ++++++++++++++ includes/class-scheduler.php | 20 + includes/collection/class-outbox.php | 2 +- includes/constants.php | 1 + includes/functions.php | 34 +- includes/rest/class-outbox-controller.php | 2 +- includes/scheduler/class-post.php | 56 +++ includes/transformer/class-base.php | 2 +- readme.txt | 1 + tests/includes/class-test-dispatcher.php | 93 +++++ .../includes/collection/class-test-outbox.php | 2 +- .../rest/class-test-outbox-controller.php | 11 +- tests/includes/scheduler/class-test-actor.php | 2 +- .../includes/scheduler/class-test-comment.php | 2 +- tests/includes/scheduler/class-test-post.php | 2 +- 19 files changed, 491 insertions(+), 378 deletions(-) delete mode 100644 includes/class-activity-dispatcher.php create mode 100644 includes/class-dispatcher.php create mode 100644 tests/includes/class-test-dispatcher.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed6365da..58f59ed15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Support for WPML post locale +### Added + +* Outbox queue + +### Changed + +* Rewrite the current dispatcher system, to use the Outbox instead of the Scheduler. + ### Removed * Built-in support for nodeinfo2. Use the [NodeInfo plugin](https://wordpress.org/plugins/nodeinfo/) instead. @@ -42,7 +50,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Outbox queue * Comment counts get updated when the plugin is activated/deactivated/deleted * Added a filter to make custom comment types manageable in WP.com Calypso diff --git a/activitypub.php b/activitypub.php index cb52d0c16..4446452bf 100644 --- a/activitypub.php +++ b/activitypub.php @@ -65,7 +65,7 @@ function rest_init() { function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php deleted file mode 100644 index 670a3a2f2..000000000 --- a/includes/class-activity-dispatcher.php +++ /dev/null @@ -1,348 +0,0 @@ -get_wp_user_id() ) && - ! is_user_disabled( Actors::BLOG_USER_ID ) - ) { - $transformer->change_wp_user_id( Actors::BLOG_USER_ID ); - } - - if ( null !== $user_id ) { - $transformer->change_wp_user_id( $user_id ); - } - - $user_id = $transformer->get_wp_user_id(); - - if ( is_user_disabled( $user_id ) ) { - return; - } - - $activity = $transformer->to_activity( $type ); - - self::send_activity_to_followers( $activity, $user_id, $wp_object ); - } - - /** - * Send Announces to followers and mentioned users. - * - * @param mixed $wp_object The ActivityPub Post. - * @param string $type The Activity-Type. - */ - public static function send_announce( $wp_object, $type ) { - if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) { - return; - } - - if ( is_user_disabled( Actors::BLOG_USER_ID ) ) { - return; - } - - $transformer = Factory::get_transformer( $wp_object ); - - if ( \is_wp_error( $transformer ) ) { - return; - } - - $user_id = Actors::BLOG_USER_ID; - $activity = $transformer->to_activity( $type ); - $user = Actors::get_by_id( Actors::BLOG_USER_ID ); - - $announce = new Activity(); - $announce->set_type( 'Announce' ); - $announce->set_object( $activity ); - $announce->set_actor( $user->get_id() ); - - self::send_activity_to_followers( $announce, $user_id, $wp_object ); - } - - /** - * Send a "Update" Activity when a user updates their profile. - * - * @param int $user_id The user ID to send an update for. - */ - public static function send_profile_update( $user_id ) { - $user = Actors::get_by_various( $user_id ); - - // Bail if that's not a good user. - if ( is_wp_error( $user ) ) { - return; - } - - // Build the update. - $activity = new Activity(); - $activity->set_type( 'Update' ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $user->get_id() ); - $activity->set_to( array( 'https://www.w3.org/ns/activitystreams#Public' ) ); - - // Send the update. - self::send_activity_to_followers( $activity, $user_id, $user ); - } - - /** - * Send an Activity to all followers and mentioned users. - * - * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. - */ - private static function send_activity_to_followers( $activity, $user_id, $wp_object ) { - /** - * Filters whether to send an Activity to followers. - * - * @param bool $send_activity_to_followers Whether to send the Activity to followers. - * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. - */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) { - return; - } - - /** - * Filters the list of inboxes to send the Activity to. - * - * @param array $inboxes The list of inboxes to send to. - * @param int $user_id The user ID. - * @param Activity $activity The ActivityPub Activity. - */ - $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity ); - $inboxes = array_unique( $inboxes ); - - if ( empty( $inboxes ) ) { - return; - } - - $json = $activity->to_json(); - - foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); - } - - set_wp_object_state( $wp_object, 'federated' ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Post. - * - * @param int $id The WordPress Post ID. - * @param string $type The Activity-Type. - */ - public static function send_post( $id, $type ) { - $post = get_post( $id ); - - if ( ! $post || is_post_disabled( $post ) ) { - return; - } - - /** - * Fires when an Activity is being sent for any object type. - * - * @param WP_Post $post The WordPress Post. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $post, $type ); - - /** - * Fires when a specific type of Activity is being sent. - * - * @param WP_Post $post The WordPress Post. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Comment. - * - * @param int $id The WordPress Comment ID. - * @param string $type The Activity-Type. - */ - public static function send_comment( $id, $type ) { - $comment = get_comment( $id ); - - if ( ! $comment ) { - return; - } - - /** - * Fires when an Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $comment, $type ); - - /** - * Fires when a specific type of Activity is being sent for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment ); - } - - /** - * Default filter to add Inboxes of Followers. - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * - * @return array The filtered Inboxes - */ - public static function add_inboxes_of_follower( $inboxes, $user_id ) { - $follower_inboxes = Followers::get_inboxes( $user_id ); - - return array_merge( $inboxes, $follower_inboxes ); - } - - /** - * Default filter to add Inboxes of Mentioned Actors - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * @param array $activity The ActivityPub Activity. - * - * @return array The filtered Inboxes. - */ - public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) { - $cc = $activity->get_cc() ?? array(); - $to = $activity->get_to() ?? array(); - - $audience = array_merge( $cc, $to ); - - // Remove "public placeholder" and "same domain" from the audience. - $audience = array_filter( - $audience, - function ( $actor ) { - return 'https://www.w3.org/ns/activitystreams#Public' !== $actor && ! is_same_domain( $actor ); - } - ); - - if ( $audience ) { - $mentioned_inboxes = Mention::get_inboxes( $audience ); - - return array_merge( $inboxes, $mentioned_inboxes ); - } - - return $inboxes; - } - - /** - * Default filter to add Inboxes of Posts that are set as `in-reply-to` - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * @param array $activity The ActivityPub Activity. - * - * @return array The filtered Inboxes - */ - public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) { - $in_reply_to = $activity->get_in_reply_to(); - - if ( ! $in_reply_to ) { - return $inboxes; - } - - if ( ! is_array( $in_reply_to ) ) { - $in_reply_to = array( $in_reply_to ); - } - - foreach ( $in_reply_to as $url ) { - $object = Http::get_remote_object( $url ); - - if ( - ! $object || - \is_wp_error( $object ) || - empty( $object['attributedTo'] ) - ) { - continue; - } - - $actor = object_to_uri( $object['attributedTo'] ); - $actor = Http::get_remote_object( $actor ); - - if ( ! $actor || \is_wp_error( $actor ) ) { - continue; - } - - if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) { - $inboxes[] = $actor['endpoints']['sharedInbox']; - } elseif ( ! empty( $actor['inbox'] ) ) { - $inboxes[] = $actor['inbox']; - } - } - - return $inboxes; - } -} diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index f0cb83166..c62a300d9 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -602,7 +602,7 @@ private static function register_post_types() { 'sanitize_callback' => function ( $value ) { $schema = array( 'type' => 'string', - 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ); diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php new file mode 100644 index 000000000..9ee12df0f --- /dev/null +++ b/includes/class-dispatcher.php @@ -0,0 +1,278 @@ +ID, '_activitypub_activity_actor', true ); + + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $outbox_item->post_author; + break; + } + + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + $transformer = Transformer_Factory::get_transformer( $outbox_item->post_content ); + $activity = $transformer->to_activity( $type ); + + self::send_activity_to_followers( $activity, $actor_id, $outbox_item ); + } + + /** + * Send an Activity to all followers and mentioned users. + * + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. + */ + private static function send_activity_to_followers( $activity, $actor_id, $outbox_item = null ) { + /** + * Filters whether to send an Activity to followers. + * + * @param bool $send_activity_to_followers Whether to send the Activity to followers. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. + */ + if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $actor_id, $outbox_item ) ) { + return; + } + + /** + * Filters the list of inboxes to send the Activity to. + * + * @param array $inboxes The list of inboxes to send to. + * @param int $actor_id The actor ID. + * @param Activity $activity The ActivityPub Activity. + */ + $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $actor_id, $activity ); + $inboxes = array_unique( $inboxes ); + + if ( empty( $inboxes ) ) { + return; + } + + $json = $activity->to_json(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $actor_id ); + } + + \wp_publish_post( $outbox_item ); + } + + /** + * Default filter to add Inboxes of Followers. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function add_inboxes_of_follower( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + + $follower_inboxes = Followers::get_inboxes( $actor_id ); + + return array_merge( $inboxes, $follower_inboxes ); + } + + /** + * Default filter to add Inboxes of Mentioned Actors + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes. + */ + public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) { + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + // Remove "public placeholder" and "same domain" from the audience. + $audience = array_filter( + $audience, + function ( $actor ) { + return 'https://www.w3.org/ns/activitystreams#Public' !== $actor && ! is_same_domain( $actor ); + } + ); + + if ( $audience ) { + $mentioned_inboxes = Mention::get_inboxes( $audience ); + + return array_merge( $inboxes, $mentioned_inboxes ); + } + + return $inboxes; + } + + /** + * Default filter to add Inboxes of Posts that are set as `in-reply-to` + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param array $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) { + $in_reply_to = $activity->get_in_reply_to(); + + if ( ! $in_reply_to ) { + return $inboxes; + } + + if ( ! is_array( $in_reply_to ) ) { + $in_reply_to = array( $in_reply_to ); + } + + foreach ( $in_reply_to as $url ) { + $object = Http::get_remote_object( $url ); + + if ( + ! $object || + \is_wp_error( $object ) || + empty( $object['attributedTo'] ) + ) { + continue; + } + + $actor = object_to_uri( $object['attributedTo'] ); + $actor = Http::get_remote_object( $actor ); + + if ( ! $actor || \is_wp_error( $actor ) ) { + continue; + } + + if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) { + $inboxes[] = $actor['endpoints']['sharedInbox']; + } elseif ( ! empty( $actor['inbox'] ) ) { + $inboxes[] = $actor['inbox']; + } + } + + return $inboxes; + } + + /** + * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { + if ( ! self::should_send_to_followers( $activity, $actor_id ) ) { + return $inboxes; + } + + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return $inboxes; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return $inboxes; + } + // Only if this is an Update or Delete. Create handles its own Announce in dual user mode. + if ( ! in_array( $activity->get_type(), array( 'Update', 'Delete' ), true ) ) { + return $inboxes; + } + + $blog_inboxes = Followers::get_inboxes( Actors::BLOG_USER_ID ); + // array_unique is done in `send_activity_to_followers()`, no need here. + return array_merge( $inboxes, $blog_inboxes ); + } + + /** + * Check if passed Activity is public. + * + * @param Activity $activity The Activity object. + * @param int $actor_id The Actor-ID. + * + * @return boolean True if public, false if not. + */ + protected static function should_send_to_followers( $activity, $actor_id ) { + // Check if follower endpoint is set. + $actor = Actors::get_by_id( $actor_id ); + + if ( ! $actor || is_wp_error( $actor ) ) { + return false; + } + + // Check if follower endpoint is set. + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + if ( + // Check if activity is public. + in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) || + // ...or check if follower endpoint is set. + in_array( $actor->get_followers(), $audience, true ) + ) { + return true; + } + + return false; + } +} diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 36ad1b809..cbcf5f96c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -28,6 +28,8 @@ public static function init() { // Follower Cleanups. \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) ); } /** @@ -138,4 +140,22 @@ public static function cleanup_followers() { } } } + + /** + * Schedule the outbox item for federation. + * + * @param int $id The ID of the outbox item. + */ + public static function schedule_outbox_activity_for_federation( $id ) { + $hook = 'activitypub_process_outbox'; + $args = array( $id ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + \wp_schedule_single_event( + \time() + 10, + $hook, + $args + ); + } + } } diff --git a/includes/collection/class-outbox.php b/includes/collection/class-outbox.php index 2fa6a47f4..87a8a0505 100644 --- a/includes/collection/class-outbox.php +++ b/includes/collection/class-outbox.php @@ -44,7 +44,7 @@ public static function add( $activity_object, $activity_type, $user_id, $content 'post_content' => $activity_object->to_json(), // ensure that user ID is not below 0. 'post_author' => \max( $user_id, 0 ), - 'post_status' => 'draft', + 'post_status' => 'pending', 'meta_input' => array( '_activitypub_activity_type' => $activity_type, '_activitypub_activity_actor' => $actor_type, diff --git a/includes/constants.php b/includes/constants.php index e12450205..98da5d4a4 100644 --- a/includes/constants.php +++ b/includes/constants.php @@ -71,4 +71,5 @@ // Post visibility constants. \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' ); \define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' ); diff --git a/includes/functions.php b/includes/functions.php index 380777e27..054750216 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1424,6 +1424,7 @@ function get_content_visibility( $post_id ) { $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; $options = array( ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, ); @@ -1554,43 +1555,42 @@ function is_self_ping( $id ) { * Add an object to the outbox. * * @param mixed $data The object to add to the outbox. - * @param string $type The type of the Activity. + * @param string $activity_type The type of the Activity. * @param integer $user_id The User-ID. * @param string $content_visibility The visibility of the content. * * @return boolean|int The ID of the outbox item or false on failure. */ -function add_to_outbox( $data, $type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { +function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { $transformer = Transformer_Factory::get_transformer( $data ); if ( ! $transformer || is_wp_error( $transformer ) ) { return false; } - $activity = $transformer->to_object(); + $activity_object = $transformer->to_object(); - if ( ! $activity || is_wp_error( $activity ) ) { + if ( ! $activity_object || \is_wp_error( $activity_object ) ) { return false; } set_wp_object_state( $data, 'federate' ); - $id = Outbox::add( $activity, $type, $user_id, $content_visibility ); + $outbox_activity_id = Outbox::add( $activity_object, $activity_type, $user_id, $content_visibility ); - if ( ! $id ) { + if ( ! $outbox_activity_id ) { return false; } - $hook = 'activitypub_process_outbox'; - $args = array( $id ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - \wp_schedule_single_event( - \time() + 10, - $hook, - $args - ); - } + /** + * Action triggered after an object has been added to the outbox. + * + * @param int $outbox_activity_id The ID of the outbox item. + * @param \Activitypub\Activity\Base_Object $activity_object The activity object. + * @param int $user_id The User-ID. + * @param string $content_visibility The visibility of the content. + */ + \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity_object, $user_id, $content_visibility ); - return $id; + return $outbox_activity_id; } diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 59f2cbd16..ac1b2dd22 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -187,7 +187,7 @@ public function get_items( $request ) { 'orderedItems' => array(), ); - update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + \update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); foreach ( $query_result as $outbox_item ) { $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); } diff --git a/includes/scheduler/class-post.php b/includes/scheduler/class-post.php index 952ab3ffc..69059f6b3 100644 --- a/includes/scheduler/class-post.php +++ b/includes/scheduler/class-post.php @@ -7,6 +7,11 @@ namespace Activitypub\Scheduler; +use Activitypub\Scheduler; +use Activitypub\Collection\Outbox; +use Activitypub\Collection\Actors; +use Activitypub\Transformer\Factory; + use function Activitypub\add_to_outbox; use function Activitypub\is_post_disabled; use function Activitypub\get_wp_object_state; @@ -26,6 +31,8 @@ public static function init() { \add_action( 'add_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'edit_attachment', array( self::class, 'transition_attachment_status' ) ); \add_action( 'delete_attachment', array( self::class, 'transition_attachment_status' ) ); + + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'send_announces' ), 10, 4 ); } /** @@ -92,4 +99,53 @@ public static function schedule_post_activity( $new_status, $old_status, $post ) // Add the post to the outbox. add_to_outbox( $post, $type, $post->post_author, $content_visibility ); } + + /** + * Send announces. + * + * @param int $outbox_activity_id The outbox activity ID. + * @param array $activity_object The activity object. + * @param int $actor_id The actor ID. + * @param int $content_visibility The content visibility. + */ + public static function send_announces( $outbox_activity_id, $activity_object, $actor_id, $content_visibility ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return; + } + + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return; + } + + // Only if the content is public or quiet public. + if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) { + return; + } + + $activity_type = \get_post_meta( $outbox_activity_id, '_activitypub_activity_type', true ); + + // Only if the activity is a Create, Update or Delete. + if ( ! in_array( $activity_type, array( 'Create', 'Update', 'Delete' ), true ) ) { + return; + } + + // Check if the object is an article, image, audio, video, event or document and ignore profile updates and other activities. + if ( ! in_array( $activity_object->get_type(), array( 'Note', 'Article', 'Image', 'Audio', 'Video', 'Event', 'Document' ), true ) ) { + return; + } + + $transformer = Factory::get_transformer( $activity_object ); + $activity = $transformer->to_activity( $activity_type ); + + $outbox_activity_id = Outbox::add( $activity, 'Announce', Actors::BLOG_USER_ID, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + + if ( ! $outbox_activity_id ) { + return false; + } + + // Schedule the outbox item for federation. + Scheduler::schedule_outbox_activity_for_federation( $outbox_activity_id ); + } } diff --git a/includes/transformer/class-base.php b/includes/transformer/class-base.php index 94537c9b9..c4ae115d8 100644 --- a/includes/transformer/class-base.php +++ b/includes/transformer/class-base.php @@ -146,7 +146,7 @@ public function to_activity( $type ) { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) { + if ( 'Like' === $type ) { $activity->set_object( $object->get_id() ); } diff --git a/readme.txt b/readme.txt index ab7ea665e..a47cf3af6 100644 --- a/readme.txt +++ b/readme.txt @@ -135,6 +135,7 @@ For reasons of data protection, it is not possible to see the followers of other = Unreleased = * Added: Outbox queue +* Changed: Rewrite the current dispatcher system, to use the Outbox instead of a Scheduler. * Changed: Improved content negotiation and AUTHORIZED_FETCH support for third-party plugins = 4.7.3 = diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php new file mode 100644 index 000000000..389cf89b3 --- /dev/null +++ b/tests/includes/class-test-dispatcher.php @@ -0,0 +1,93 @@ +createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when actor is blog user + * + * @covers ::maybe_add_inboxes_of_blog_user + */ + public function test_maybe_add_inboxes_of_blog_user_is_blog_user() { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, Actors::BLOG_USER_ID, $activity ); + $this->assertEquals( $inboxes, $result ); + } + + /** + * Test maybe_add_inboxes_of_blog_user when activity type is not Update + * + * @covers ::maybe_add_inboxes_of_blog_user + */ + public function test_maybe_add_inboxes_of_blog_user_not_update() { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + + $inboxes = array( 'https://example.com/inbox' ); + $activity = $this->createMock( Activity::class, array( '__call' ) ); + + // Mock the static method using reflection. + $activity->expects( $this->any() ) + ->method( '__call' ) + ->willReturnCallback( + function ( $name ) { + if ( 'get_to' === $name ) { + return array( 'https://www.w3.org/ns/activitystreams#Public' ); + } + + if ( 'get_cc' === $name ) { + return array(); + } + + if ( 'get_type' === $name ) { + return 'Create'; + } + + return null; + } + ); + + $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); + $this->assertEquals( $inboxes, $result ); + } +} diff --git a/tests/includes/collection/class-test-outbox.php b/tests/includes/collection/class-test-outbox.php index 653300589..57724696e 100644 --- a/tests/includes/collection/class-test-outbox.php +++ b/tests/includes/collection/class-test-outbox.php @@ -33,7 +33,7 @@ public function test_add( $data, $type, $user_id, $json ) { $post = get_post( $id ); $this->assertInstanceOf( 'WP_Post', $post ); - $this->assertEquals( 'draft', $post->post_status ); + $this->assertEquals( 'pending', $post->post_status ); $this->assertEquals( $json, $post->post_content ); $this->assertEquals( $type, get_post_meta( $id, '_activitypub_activity_type', true ) ); diff --git a/tests/includes/rest/class-test-outbox-controller.php b/tests/includes/rest/class-test-outbox-controller.php index 300bdbb58..c96984a43 100644 --- a/tests/includes/rest/class-test-outbox-controller.php +++ b/tests/includes/rest/class-test-outbox-controller.php @@ -173,8 +173,8 @@ public function test_get_items_specific_user() { array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', 'post_title' => 'https://example.org/activity/1', + 'post_status' => 'pending', 'post_content' => wp_json_encode( array( '@context' => array( 'https://www.w3.org/ns/activitystreams' ), @@ -344,7 +344,7 @@ public function test_get_items_activity_type( $type, $activity, $allowed ) { array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => "https://example.org/activity/{$type}", 'post_content' => \wp_json_encode( array( @@ -420,6 +420,11 @@ public function data_content_visibility() { 'public_visible' => false, 'private_visible' => true, ), + 'private' => array( + 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + 'public_visible' => false, + 'private_visible' => true, + ), 'local' => array( 'visibility' => \ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, 'public_visible' => false, @@ -453,7 +458,7 @@ public function test_get_items_content_visibility( $visibility, $public_visible, array( 'post_author' => $user_id, 'post_type' => Outbox::POST_TYPE, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => 'https://example.org/activity/1', 'post_content' => \wp_json_encode( array( diff --git a/tests/includes/scheduler/class-test-actor.php b/tests/includes/scheduler/class-test-actor.php index 0f9236a77..48501d0d5 100644 --- a/tests/includes/scheduler/class-test-actor.php +++ b/tests/includes/scheduler/class-test-actor.php @@ -229,7 +229,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', diff --git a/tests/includes/scheduler/class-test-comment.php b/tests/includes/scheduler/class-test-comment.php index 62584ea87..f8eb440f9 100644 --- a/tests/includes/scheduler/class-test-comment.php +++ b/tests/includes/scheduler/class-test-comment.php @@ -166,7 +166,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', diff --git a/tests/includes/scheduler/class-test-post.php b/tests/includes/scheduler/class-test-post.php index a5fa05eab..764621d25 100644 --- a/tests/includes/scheduler/class-test-post.php +++ b/tests/includes/scheduler/class-test-post.php @@ -135,7 +135,7 @@ private function get_latest_outbox_item( $title = '' ) { array( 'post_type' => Outbox::POST_TYPE, 'posts_per_page' => 1, - 'post_status' => 'draft', + 'post_status' => 'pending', 'post_title' => $title, 'orderby' => 'date', 'order' => 'DESC', From bb66680967fbc62ceba3f858b0a2c65b612c1685 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Thu, 23 Jan 2025 14:17:18 -0600 Subject: [PATCH 115/116] lighter logging --- includes/class-activitypub.php | 24 ------------------------ includes/class-dispatcher.php | 6 ------ 2 files changed, 30 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index c67162ba8..c9043d8ce 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -615,30 +615,6 @@ private static function register_post_types() { ) ); - register_post_meta( - Outbox::POST_TYPE, - 'activitypub_sent_json', - array( - 'type' => 'string', - 'description' => 'JSON activity for regular user', - 'single' => true, - 'sanitize_callback' => null, - 'show_in_rest' => false, - ) - ); - - register_post_meta( - Outbox::POST_TYPE, - 'activitypub_sent_inboxes', - array( - 'type' => 'array', - 'description' => 'List of inboxes for regular user', - 'single' => true, - 'sanitize_callback' => null, - 'show_in_rest' => false, - ) - ); - register_post_meta( Outbox::POST_TYPE, 'activitypub_send_log', diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 49b51a7f0..22d3b88e8 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -104,12 +104,6 @@ private static function send_activity_to_followers( $activity, $actor_id, $outbo $json = $activity->to_json(); - // We will store the json as generated by the transformer, even though it 's also in plaintext in $outbox_item->post_content - // This will also allow us to keep the logs below leaner. - \add_post_meta( $outbox_item->ID, 'activitypub_sent_json', $json ); - // This will allow error checking later, that all inboxes have been sent. - \add_post_meta( $outbox_item->ID, 'activitypub_sent_inboxes', $inboxes, true ); - foreach ( $inboxes as $inbox ) { $response = safe_remote_post( $inbox, $json, $actor_id ); $to_log = array( From 433a67f4234f1143dc6d6e8371deadb2f28ebc91 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Thu, 23 Jan 2025 14:22:36 -0600 Subject: [PATCH 116/116] cleanup merge mess --- includes/rest/class-outbox-controller.php | 1 + tests/includes/class-test-dispatcher.php | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/rest/class-outbox-controller.php b/includes/rest/class-outbox-controller.php index 2ba10b39b..ac1b2dd22 100644 --- a/includes/rest/class-outbox-controller.php +++ b/includes/rest/class-outbox-controller.php @@ -133,6 +133,7 @@ public function get_items( $request ) { 'paged' => $page, 'post_type' => Outbox::POST_TYPE, 'post_status' => 'any', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( diff --git a/tests/includes/class-test-dispatcher.php b/tests/includes/class-test-dispatcher.php index 75807a094..ff748e54c 100644 --- a/tests/includes/class-test-dispatcher.php +++ b/tests/includes/class-test-dispatcher.php @@ -36,7 +36,6 @@ public function test_maybe_add_inboxes_of_blog_user_wrong_mode() { $inboxes = array( 'https://example.com/inbox' ); $activity = $this->createMock( Activity::class ); $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); - $this->assertEquals( $inboxes, $result ); } @@ -88,7 +87,6 @@ function ( $name ) { ); $result = Dispatcher::maybe_add_inboxes_of_blog_user( $inboxes, 1, $activity ); - $this->assertEquals( $inboxes, $result ); } }