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/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/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'; + } +}