diff --git a/includes/class-dispatcher.php b/includes/class-dispatcher.php index 95ca26c66..afc398bba 100644 --- a/includes/class-dispatcher.php +++ b/includes/class-dispatcher.php @@ -67,11 +67,7 @@ public static function process_outbox( $id ) { $activity->set_id( $outbox_item->guid ); // Pre-fill the Activity with data (for example cc and to). $activity->from_json( $outbox_item->post_content ); - - // If the activity doesn't have an actor, set the actor to the post author. - if ( ! $activity->get_actor() ) { - $activity->set_actor( Actors::get_by_id( $outbox_item->post_author )->get_id() ); - } + $activity->set_actor( Actors::get_by_id( $outbox_item->post_author )->get_id() ); // Use simple Object (only ID-URI) for Like and Announce. if ( 'Like' === $type ) { diff --git a/includes/functions.php b/includes/functions.php index 90110e5f7..d24d0fb1b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -9,6 +9,7 @@ use WP_Error; use Activitypub\Activity\Activity; +use Activitypub\Activity\Base_Object; use Activitypub\Collection\Actors; use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; @@ -1600,3 +1601,62 @@ function add_to_outbox( $data, $activity_type = 'Create', $user_id = 0, $content return $outbox_activity_id; } + +/** + * Check if an object is an Activity. + * + * @param array|object $data The object to check. + * + * @see https://www.w3.org/ns/activitystreams#activities + * + * @return boolean True if the object is an Activity, false otherwise. + */ +function is_activity( $data ) { + /** + * Filters the activity types. + * + * @param array $types The activity types. + */ + $types = apply_filters( + 'activitypub_activity_types', + array( + 'Accept', + 'Add', + 'Announce', + 'Arrive', + 'Block', + 'Create', + 'Delete', + 'Dislike', + 'Follow', + 'Flag', + 'Ignore', + 'Invite', + 'Join', + 'Leave', + 'Like', + 'Listen', + 'Move', + 'Offer', + 'Read', + 'Reject', + 'Remove', + 'TentativeAccept', + 'TentativeReject', + 'Travel', + 'Undo', + 'Update', + 'View', + ) + ); + + if ( is_array( $data ) && isset( $data['type'] ) ) { + return in_array( $data['type'], $types, true ); + } + + if ( is_object( $data ) && $data instanceof Base_Object ) { + return in_array( $data->get_type(), $types, true ); + } + + return false; +} diff --git a/includes/handler/class-follow.php b/includes/handler/class-follow.php index f3e3d5487..4dd4761fc 100644 --- a/includes/handler/class-follow.php +++ b/includes/handler/class-follow.php @@ -13,6 +13,8 @@ use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; +use function Activitypub\add_to_outbox; + /** * Handle Follow requests. */ @@ -28,7 +30,7 @@ public static function init() { \add_action( 'activitypub_followers_post_follow', - array( self::class, 'send_follow_response' ), + array( self::class, 'queue_accept' ), 10, 4 ); @@ -83,7 +85,7 @@ public static function handle_follow( $activity ) { * @param int $user_id The ID of the WordPress User. * @param \Activitypub\Model\Follower $follower The Follower object. */ - public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) { + public static function queue_accept( $actor, $activity_object, $user_id, $follower ) { if ( \is_wp_error( $follower ) ) { // Impossible to send a "Reject" because we can not get the Remote-Inbox. return; @@ -102,21 +104,9 @@ public static function send_follow_response( $actor, $activity_object, $user_id, ) ); - $user = Actors::get_by_id( $user_id ); - - // Get inbox. - $inbox = $follower->get_shared_inbox(); - - // Send "Accept" activity. - $activity = new Activity(); - $activity->set_type( 'Accept' ); - $activity->set_object( $activity_object ); - $activity->set_actor( $user->get_id() ); - $activity->set_to( $actor ); - $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() ); - - $activity = $activity->to_json(); + // Send response only to the Follower. + $activity_object['to'] = $actor; - Http::post( $inbox, $activity, $user_id ); + add_to_outbox( $activity_object, 'Accept', $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } } diff --git a/includes/transformer/class-json.php b/includes/transformer/class-json.php index 6b7c0288e..46dd8fffb 100644 --- a/includes/transformer/class-json.php +++ b/includes/transformer/class-json.php @@ -9,6 +9,8 @@ use Activitypub\Activity\Base_Object; +use function Activitypub\is_activity; + /** * String Transformer Class file. */ @@ -22,10 +24,17 @@ class Json extends Activity_Object { public function __construct( $item ) { $object = new Base_Object(); + // Check if the item is an Activity or an Object. + if ( is_activity( $item ) ) { + $class = '\Activitypub\Activity\Activity'; + } else { + $class = '\Activitypub\Activity\Base_Object'; + } + if ( is_array( $item ) ) { - $object = Base_Object::init_from_array( $item ); + $object = $class::init_from_array( $item ); } elseif ( is_string( $item ) ) { - $object = Base_Object::init_from_json( $item ); + $object = $class::init_from_json( $item ); } parent::__construct( $object ); diff --git a/tests/includes/class-test-functions.php b/tests/includes/class-test-functions.php index 480ade24f..4069fbd88 100644 --- a/tests/includes/class-test-functions.php +++ b/tests/includes/class-test-functions.php @@ -216,4 +216,87 @@ public function object_to_uri_provider() { ), ); } + + /** + * Test is_activity with array input. + * + * @covers ::is_activity + * + * @dataProvider is_activity_data + * + * @param mixed $activity The activity object. + * @param bool $expected The expected result. + */ + public function test_is_activity( $activity, $expected ) { + $this->assertEquals( $expected, \Activitypub\is_activity( $activity ) ); + } + + /** + * Data provider for test_is_activity. + * + * @return array[] + */ + public function is_activity_data() { + // Test Activity object. + $create = new \Activitypub\Activity\Activity(); + $create->set_type( 'Create' ); + + // Test Base_Object. + $note = new \Activitypub\Activity\Base_Object(); + $note->set_type( 'Note' ); + + return array( + array( array( 'type' => 'Create' ), true ), + array( array( 'type' => 'Update' ), true ), + array( array( 'type' => 'Delete' ), true ), + array( array( 'type' => 'Follow' ), true ), + array( array( 'type' => 'Accept' ), true ), + array( array( 'type' => 'Reject' ), true ), + array( array( 'type' => 'Add' ), true ), + array( array( 'type' => 'Remove' ), true ), + array( array( 'type' => 'Like' ), true ), + array( array( 'type' => 'Announce' ), true ), + array( array( 'type' => 'Undo' ), true ), + array( array( 'type' => 'Note' ), false ), + array( array( 'type' => 'Article' ), false ), + array( array( 'type' => 'Person' ), false ), + array( array( 'type' => 'Image' ), false ), + array( array( 'type' => 'Video' ), false ), + array( array( 'type' => 'Audio' ), false ), + array( array( 'type' => '' ), false ), + array( array( 'type' => null ), false ), + array( array(), false ), + array( $create, true ), + array( $note, false ), + array( 'string', false ), + array( 123, false ), + array( true, false ), + array( false, false ), + array( null, false ), + array( new \stdClass(), false ), + ); + } + + /** + * Test is_activity with invalid input. + * + * @covers ::is_activity + */ + public function test_is_activity_with_invalid_input() { + $invalid_inputs = array( + 'string', + 123, + true, + false, + null, + new \stdClass(), + ); + + foreach ( $invalid_inputs as $input ) { + $this->assertFalse( + \Activitypub\is_activity( $input ), + sprintf( 'Input of type %s should be invalid', gettype( $input ) ) + ); + } + } } diff --git a/tests/includes/handler/class-test-follow.php b/tests/includes/handler/class-test-follow.php new file mode 100644 index 000000000..068f8c21e --- /dev/null +++ b/tests/includes/handler/class-test-follow.php @@ -0,0 +1,124 @@ +user->create( + array( + 'role' => 'author', + ) + ); + } + + /** + * Clean up after tests. + */ + public static function wpTearDownAfterClass() { + wp_delete_user( self::$user_id ); + } + + /** + * Test queue_accept method. + * + * @covers ::queue_accept + */ + public function test_queue_accept() { + $actor = 'https://example.com/actor'; + $activity_object = array( + 'id' => 'https://example.com/activity/123', + 'type' => 'Follow', + 'actor' => $actor, + 'object' => 'https://example.com/user/1', + ); + + // Test with WP_Error follower - should not create outbox entry. + $wp_error = new \WP_Error( 'test_error', 'Test Error' ); + Follow::queue_accept( $actor, $activity_object, self::$user_id, $wp_error ); + + $outbox_posts = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'author' => self::$user_id, + 'post_status' => 'pending', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_activitypub_activity_actor', + 'value' => 'user', + ), + ), + ) + ); + $this->assertEmpty( $outbox_posts, 'No outbox entry should be created for WP_Error follower' ); + + // Test with valid follower. + $follower = new Follower(); + $follower->set_actor( $actor ); + $follower->set_type( 'Person' ); + $follower->set_inbox( 'https://example.com/inbox' ); + + Follow::queue_accept( $actor, $activity_object, self::$user_id, $follower ); + + $outbox_posts = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'author' => self::$user_id, + 'post_status' => 'pending', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_activitypub_activity_actor', + 'value' => 'user', + ), + ), + ) + ); + + $this->assertCount( 1, $outbox_posts, 'One outbox entry should be created' ); + + $outbox_post = $outbox_posts[0]; + $activity_type = \get_post_meta( $outbox_post->ID, '_activitypub_activity_type', true ); + $activity_json = \json_decode( $outbox_post->post_content, true ); + $visibility = \get_post_meta( $outbox_post->ID, 'activitypub_content_visibility', true ); + + // Verify outbox entry. + $this->assertEquals( 'Accept', $activity_type ); + $this->assertEquals( ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, $visibility ); + + $this->assertEquals( 'Follow', $activity_json['type'] ); + $this->assertEquals( 'https://example.com/user/1', $activity_json['object'] ); + $this->assertEquals( array( $actor ), $activity_json['to'] ); + $this->assertEquals( $actor, $activity_json['actor'] ); + + // Clean up. + wp_delete_post( $outbox_post->ID, true ); + } +}