diff --git a/activitypub.php b/activitypub.php index c812cc49f..22a23ec49 100644 --- a/activitypub.php +++ b/activitypub.php @@ -47,8 +47,8 @@ function rest_init() { Rest\Comment::init(); Rest\Server::init(); Rest\Collection::init(); - Rest\Interaction::init(); Rest\Post::init(); + ( new Rest\Interaction_Controller() )->register_routes(); ( new Rest\Application_Controller() )->register_routes(); ( new Rest\Webfinger_Controller() )->register_routes(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index d6f8b0727..77ade14e1 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -98,7 +98,7 @@ public static function remove_follower( $user_id, $actor ) { * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * - * @return Follower|null The Follower object or null + * @return \Activitypub\Activity\Base_Object|WP_Error|null The Follower object or null */ public static function get_follower( $user_id, $actor ) { global $wpdb; @@ -266,7 +266,7 @@ public static function count_followers( $user_id ) { } /** - * Returns all Inboxes for a Users Followers. + * Returns all Inboxes for an Actor's Followers. * * @param int $user_id The ID of the WordPress User. * diff --git a/includes/handler/class-update.php b/includes/handler/class-update.php index 983c68d42..e09eeb85a 100644 --- a/includes/handler/class-update.php +++ b/includes/handler/class-update.php @@ -26,9 +26,9 @@ public static function init() { } /** - * Handle "Update" requests + * Handle "Update" requests. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function handle_update( $activity ) { $object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : ''; @@ -75,7 +75,7 @@ public static function handle_update( $activity ) { /** * Update an Interaction. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function update_interaction( $activity ) { $commentdata = Interactions::update_comment( $activity ); @@ -102,7 +102,7 @@ public static function update_interaction( $activity ) { /** * Update an Actor. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function update_actor( $activity ) { // Update cache. diff --git a/includes/rest/class-interaction.php b/includes/rest/class-interaction-controller.php similarity index 53% rename from includes/rest/class-interaction.php rename to includes/rest/class-interaction-controller.php index 2eca4bf2e..f9620e3a4 100644 --- a/includes/rest/class-interaction.php +++ b/includes/rest/class-interaction-controller.php @@ -1,43 +1,50 @@ namespace, + '/' . $this->rest_base, array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), + 'callback' => array( $this, 'get_item' ), 'permission_callback' => '__return_true', 'args' => array( 'uri' => array( - 'type' => 'string', - 'required' => true, - 'sanitize_callback' => 'esc_url', + 'description' => 'The URI of the object to interact with.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, ), ), ), @@ -46,27 +53,26 @@ public static function register_routes() { } /** - * Handle GET request. + * Retrieves the interaction URL for a given URI. * * @param \WP_REST_Request $request The request object. * - * @return WP_REST_Response Redirect to the editor or die. + * @return \WP_REST_Response Response object on success, dies on failure. */ - public static function get( $request ) { + public function get_item( $request ) { $uri = $request->get_param( 'uri' ); $redirect_url = null; $object = Http::get_remote_object( $uri ); - if ( - \is_wp_error( $object ) || - ! isset( $object['type'] ) - ) { + if ( \is_wp_error( $object ) || ! isset( $object['type'] ) ) { + // Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109. \wp_die( - \esc_html__( - 'The URL is not supported!', - 'activitypub' - ), - 400 + esc_html__( 'The URL is not supported!', 'activitypub' ), + '', + array( + 'response' => 400, + 'back_link' => true, + ) ); } @@ -104,31 +110,30 @@ public static function get( $request ) { } /** - * Filter the redirect URL. + * Filters the redirect URL. + * + * This filter runs after the type-specific filters and allows for final modifications + * to the interaction URL regardless of the object type. * * @param string $redirect_url The URL to redirect to. * @param string $uri The URI of the object. - * @param array $object The object. + * @param array $object The object being interacted with. */ $redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object ); // Check if hook is implemented. if ( ! $redirect_url ) { + // Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109. \wp_die( - esc_html__( - 'This Interaction type is not supported yet!', - 'activitypub' - ), - 400 + esc_html__( 'This Interaction type is not supported yet!', 'activitypub' ), + '', + array( + 'response' => 400, + 'back_link' => true, + ) ); } - return new WP_REST_Response( - null, - 302, - array( - 'Location' => \esc_url( $redirect_url ), - ) - ); + return new \WP_REST_Response( null, 302, array( 'Location' => \esc_url( $redirect_url ) ) ); } } diff --git a/tests/includes/class-test-activity-dispatcher.php b/tests/includes/class-test-activity-dispatcher.php index 1ecda7971..09a3628b3 100644 --- a/tests/includes/class-test-activity-dispatcher.php +++ b/tests/includes/class-test-activity-dispatcher.php @@ -17,11 +17,11 @@ class Test_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HTTP { /** - * Users. + * Actors. * - * @var array[] $users + * @var array[] */ - public static $users = array( + public static $actors = array( 'username@example.org' => array( 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', @@ -109,7 +109,7 @@ public function test_dispatch_mentions() { ) ); - self::$users['https://example.com/alex'] = array( + self::$actors['https://example.com/alex'] = array( 'id' => 'https://example.com/alex', 'url' => 'https://example.com/alex', 'inbox' => 'https://example.com/alex/inbox', @@ -300,10 +300,10 @@ function ( $disabled, $user_id ) { * @return array|bool */ public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { - if ( isset( self::$users[ $actor ] ) ) { - return self::$users[ $actor ]; + if ( isset( self::$actors[ $actor ] ) ) { + return self::$actors[ $actor ]; } - foreach ( self::$users as $data ) { + foreach ( self::$actors as $data ) { if ( $data['url'] === $actor ) { return $data; } diff --git a/tests/includes/class-test-mention.php b/tests/includes/class-test-mention.php index 65917a22d..badc7d635 100644 --- a/tests/includes/class-test-mention.php +++ b/tests/includes/class-test-mention.php @@ -17,11 +17,11 @@ class Test_Mention extends \WP_UnitTestCase { /** - * Users. + * Actors. * - * @var array + * @var array[] */ - public static $users = array( + public static $actors = array( 'username@example.org' => array( 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', @@ -133,8 +133,8 @@ public function pre_http_request( $response, $parsed_args, $url ) { public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { $actor = ltrim( $actor, '@' ); - if ( isset( self::$users[ $actor ] ) ) { - return self::$users[ $actor ]; + if ( isset( self::$actors[ $actor ] ) ) { + return self::$actors[ $actor ]; } return $pre; diff --git a/tests/includes/collection/class-test-followers.php b/tests/includes/collection/class-test-followers.php index 4eab7196b..c2cd8656f 100644 --- a/tests/includes/collection/class-test-followers.php +++ b/tests/includes/collection/class-test-followers.php @@ -17,11 +17,11 @@ class Test_Followers extends \WP_UnitTestCase { /** - * Users. + * Actors. * * @var array[] */ - public static $users = array( + public static $actors = array( 'username@example.org' => array( 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', @@ -399,8 +399,8 @@ public function test_migration_followers( $followers, $expected_count ) { add_filter( 'pre_get_remote_metadata_by_actor', function ( $pre, $actor ) { - if ( isset( self::$users[ $actor ] ) ) { - return self::$users[ $actor ]; + if ( isset( self::$actors[ $actor ] ) ) { + return self::$actors[ $actor ]; } return $pre; }, @@ -549,10 +549,10 @@ public function test_get_all_followers() { * @return array */ public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { - if ( isset( self::$users[ $actor ] ) ) { - return self::$users[ $actor ]; + if ( isset( self::$actors[ $actor ] ) ) { + return self::$actors[ $actor ]; } - foreach ( self::$users as $data ) { + foreach ( self::$actors as $data ) { if ( $data['url'] === $actor ) { return $data; } diff --git a/tests/includes/rest/class-test-interaction-controller.php b/tests/includes/rest/class-test-interaction-controller.php new file mode 100644 index 000000000..e7cde7ac7 --- /dev/null +++ b/tests/includes/rest/class-test-interaction-controller.php @@ -0,0 +1,220 @@ +get_routes(); + $this->assertArrayHasKey( '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions', $routes ); + } + + /** + * Test get_item with invalid URI. + * + * @covers ::get_item + */ + public function test_get_item_invalid_uri() { + $this->expectException( \WPDieException::class ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'invalid-uri' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 400, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 'activitypub_invalid_object', $data['code'] ); + } + + /** + * Test get_item with Note object type. + * + * @covers ::get_item + */ + public function test_get_item() { + \add_filter( + 'pre_http_request', + function () { + return array( + 'response' => array( 'code' => 200 ), + 'body' => wp_json_encode( + array( + 'type' => 'Note', + 'url' => 'https://example.org/note', + ) + ), + ); + } + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'https://example.org/note' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 302, $response->get_status() ); + $this->assertArrayHasKey( 'Location', $response->get_headers() ); + $this->assertStringContainsString( 'post-new.php?in_reply_to=', $response->get_headers()['Location'] ); + } + + /** + * Test get_item with custom follow URL filter. + * + * @covers ::get_item + */ + public function test_get_item_custom_follow_url() { + \add_filter( + 'pre_http_request', + function () { + return array( + 'response' => array( 'code' => 200 ), + 'body' => wp_json_encode( + array( + 'type' => 'Person', + 'url' => 'https://example.org/person', + ) + ), + ); + } + ); + + \add_filter( 'activitypub_interactions_follow_url', array( $this, 'follow_or_reply_url' ) ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'https://example.org/person' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 302, $response->get_status() ); + $this->assertArrayHasKey( 'Location', $response->get_headers() ); + $this->assertEquals( 'https://custom-follow-or-reply-url.com', $response->get_headers()['Location'] ); + } + + /** + * Test get_item with custom reply URL filter. + * + * @covers ::get_item + */ + public function test_get_item_custom_reply_url() { + \add_filter( + 'pre_http_request', + function () { + return array( + 'response' => array( 'code' => 200 ), + 'body' => wp_json_encode( + array( + 'type' => 'Note', + 'url' => 'https://example.org/note', + ) + ), + ); + } + ); + + \add_filter( 'activitypub_interactions_reply_url', array( $this, 'follow_or_reply_url' ) ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'https://example.org/note' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 302, $response->get_status() ); + $this->assertArrayHasKey( 'Location', $response->get_headers() ); + $this->assertEquals( 'https://custom-follow-or-reply-url.com', $response->get_headers()['Location'] ); + } + + /** + * Test get_item with WP_Error response from get_remote_object. + * + * @covers ::get_item + */ + public function test_get_item_wp_error() { + $this->expectException( \WPDieException::class ); + + \add_filter( + 'pre_http_request', + function () { + return new \WP_Error( 'http_request_failed', 'Connection failed.' ); + } + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'https://example.org/person' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 400, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 'activitypub_invalid_object', $data['code'] ); + $this->assertEquals( 'The URL is not supported!', $data['message'] ); + } + + /** + * Test get_item with invalid object without type. + * + * @covers ::get_item + */ + public function test_get_item_invalid_object() { + $this->expectException( \WPDieException::class ); + + \add_filter( + 'pre_http_request', + function () { + return array( + 'response' => array( 'code' => 200 ), + 'body' => wp_json_encode( + array( + 'url' => 'https://example.org/invalid', + ) + ), + ); + } + ); + + $request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' ); + $request->set_param( 'uri', 'https://example.org/invalid' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 400, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( 'activitypub_invalid_object', $data['code'] ); + $this->assertEquals( 'The URL is not supported!', $data['message'] ); + } + + /** + * Test get_item_schema method. + * + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } + + /** + * Returns a valid follow URL. + */ + public function follow_or_reply_url() { + return 'https://custom-follow-or-reply-url.com'; + } +}