Skip to content

Commit

Permalink
Interaction: Use Rest Controller structure (#1149)
Browse files Browse the repository at this point in the history
* Rename interaction endpoint file

* Update interaction controller endpoint

* Remove schema argument

This endpoint doesn't have a schema.

* Keep namespace import

Props @pfefferle

* Go back to using wp_die()

For requests that only accept json it'll return a json error response.

Props @pfefferle

* Add docs around this behavior.

---------

Co-authored-by: Matthias Pfefferle <[email protected]>
  • Loading branch information
obenland and pfefferle authored Jan 15, 2025
1 parent 9bcf0d5 commit 79e8b9b
Show file tree
Hide file tree
Showing 3 changed files with 268 additions and 43 deletions.
2 changes: 1 addition & 1 deletion activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
<?php
/**
* ActivityPub Interaction REST-Class file.
* ActivityPub Interaction Controller file.
*
* @package Activitypub
*/

namespace Activitypub\Rest;

use WP_REST_Response;
use Activitypub\Http;

/**
* Interaction class.
* Interaction Controller.
*/
class Interaction {
class Interaction_Controller extends \WP_REST_Controller {
/**
* Initialize the class, registering WordPress hooks.
* The namespace of this controller's route.
*
* @var string
*/
public static function init() {
self::register_routes();
}
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;

/**
* Register routes
* The base of this controller's route.
*
* @var string
*/
public static function register_routes() {
protected $rest_base = 'interactions';

/**
* Register routes.
*/
public function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/interactions',
$this->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,
),
),
),
Expand All @@ -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,
)
);
}

Expand Down Expand Up @@ -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 ) ) );
}
}
220 changes: 220 additions & 0 deletions tests/includes/rest/class-test-interaction-controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<?php
/**
* Interaction REST API endpoint test file.
*
* @package Activitypub
*/

namespace Activitypub\Tests\Rest;

/**
* Tests for Interaction REST API endpoint.
*
* @coversDefaultClass \Activitypub\Rest\Interaction_Controller
*/
class Test_Interaction_Controller extends \Activitypub\Tests\Test_REST_Controller_Testcase {

/**
* Tear down.
*/
public function tear_down() {
\remove_all_filters( 'activitypub_interactions_follow_url' );
\remove_all_filters( 'activitypub_interactions_reply_url' );

parent::tear_down();
}

/**
* Test route registration.
*
* @covers ::register_routes
*/
public function test_register_routes() {
$routes = rest_get_server()->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';
}
}

0 comments on commit 79e8b9b

Please sign in to comment.