Skip to content

Commit 79e8b9b

Browse files
obenlandpfefferle
andauthored
Interaction: Use Rest Controller structure (#1149)
* 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]>
1 parent 9bcf0d5 commit 79e8b9b

File tree

3 files changed

+268
-43
lines changed

3 files changed

+268
-43
lines changed

activitypub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ function rest_init() {
4747
Rest\Comment::init();
4848
Rest\Server::init();
4949
Rest\Collection::init();
50-
Rest\Interaction::init();
5150
Rest\Post::init();
51+
( new Rest\Interaction_Controller() )->register_routes();
5252
( new Rest\Application_Controller() )->register_routes();
5353
( new Rest\Webfinger_Controller() )->register_routes();
5454

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,50 @@
11
<?php
22
/**
3-
* ActivityPub Interaction REST-Class file.
3+
* ActivityPub Interaction Controller file.
44
*
55
* @package Activitypub
66
*/
77

88
namespace Activitypub\Rest;
99

10-
use WP_REST_Response;
1110
use Activitypub\Http;
1211

1312
/**
14-
* Interaction class.
13+
* Interaction Controller.
1514
*/
16-
class Interaction {
15+
class Interaction_Controller extends \WP_REST_Controller {
1716
/**
18-
* Initialize the class, registering WordPress hooks.
17+
* The namespace of this controller's route.
18+
*
19+
* @var string
1920
*/
20-
public static function init() {
21-
self::register_routes();
22-
}
21+
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
2322

2423
/**
25-
* Register routes
24+
* The base of this controller's route.
25+
*
26+
* @var string
2627
*/
27-
public static function register_routes() {
28+
protected $rest_base = 'interactions';
29+
30+
/**
31+
* Register routes.
32+
*/
33+
public function register_routes() {
2834
\register_rest_route(
29-
ACTIVITYPUB_REST_NAMESPACE,
30-
'/interactions',
35+
$this->namespace,
36+
'/' . $this->rest_base,
3137
array(
3238
array(
3339
'methods' => \WP_REST_Server::READABLE,
34-
'callback' => array( self::class, 'get' ),
40+
'callback' => array( $this, 'get_item' ),
3541
'permission_callback' => '__return_true',
3642
'args' => array(
3743
'uri' => array(
38-
'type' => 'string',
39-
'required' => true,
40-
'sanitize_callback' => 'esc_url',
44+
'description' => 'The URI of the object to interact with.',
45+
'type' => 'string',
46+
'format' => 'uri',
47+
'required' => true,
4148
),
4249
),
4350
),
@@ -46,27 +53,26 @@ public static function register_routes() {
4653
}
4754

4855
/**
49-
* Handle GET request.
56+
* Retrieves the interaction URL for a given URI.
5057
*
5158
* @param \WP_REST_Request $request The request object.
5259
*
53-
* @return WP_REST_Response Redirect to the editor or die.
60+
* @return \WP_REST_Response Response object on success, dies on failure.
5461
*/
55-
public static function get( $request ) {
62+
public function get_item( $request ) {
5663
$uri = $request->get_param( 'uri' );
5764
$redirect_url = null;
5865
$object = Http::get_remote_object( $uri );
5966

60-
if (
61-
\is_wp_error( $object ) ||
62-
! isset( $object['type'] )
63-
) {
67+
if ( \is_wp_error( $object ) || ! isset( $object['type'] ) ) {
68+
// Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109.
6469
\wp_die(
65-
\esc_html__(
66-
'The URL is not supported!',
67-
'activitypub'
68-
),
69-
400
70+
esc_html__( 'The URL is not supported!', 'activitypub' ),
71+
'',
72+
array(
73+
'response' => 400,
74+
'back_link' => true,
75+
)
7076
);
7177
}
7278

@@ -104,31 +110,30 @@ public static function get( $request ) {
104110
}
105111

106112
/**
107-
* Filter the redirect URL.
113+
* Filters the redirect URL.
114+
*
115+
* This filter runs after the type-specific filters and allows for final modifications
116+
* to the interaction URL regardless of the object type.
108117
*
109118
* @param string $redirect_url The URL to redirect to.
110119
* @param string $uri The URI of the object.
111-
* @param array $object The object.
120+
* @param array $object The object being interacted with.
112121
*/
113122
$redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object );
114123

115124
// Check if hook is implemented.
116125
if ( ! $redirect_url ) {
126+
// Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109.
117127
\wp_die(
118-
esc_html__(
119-
'This Interaction type is not supported yet!',
120-
'activitypub'
121-
),
122-
400
128+
esc_html__( 'This Interaction type is not supported yet!', 'activitypub' ),
129+
'',
130+
array(
131+
'response' => 400,
132+
'back_link' => true,
133+
)
123134
);
124135
}
125136

126-
return new WP_REST_Response(
127-
null,
128-
302,
129-
array(
130-
'Location' => \esc_url( $redirect_url ),
131-
)
132-
);
137+
return new \WP_REST_Response( null, 302, array( 'Location' => \esc_url( $redirect_url ) ) );
133138
}
134139
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
/**
3+
* Interaction REST API endpoint test file.
4+
*
5+
* @package Activitypub
6+
*/
7+
8+
namespace Activitypub\Tests\Rest;
9+
10+
/**
11+
* Tests for Interaction REST API endpoint.
12+
*
13+
* @coversDefaultClass \Activitypub\Rest\Interaction_Controller
14+
*/
15+
class Test_Interaction_Controller extends \Activitypub\Tests\Test_REST_Controller_Testcase {
16+
17+
/**
18+
* Tear down.
19+
*/
20+
public function tear_down() {
21+
\remove_all_filters( 'activitypub_interactions_follow_url' );
22+
\remove_all_filters( 'activitypub_interactions_reply_url' );
23+
24+
parent::tear_down();
25+
}
26+
27+
/**
28+
* Test route registration.
29+
*
30+
* @covers ::register_routes
31+
*/
32+
public function test_register_routes() {
33+
$routes = rest_get_server()->get_routes();
34+
$this->assertArrayHasKey( '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions', $routes );
35+
}
36+
37+
/**
38+
* Test get_item with invalid URI.
39+
*
40+
* @covers ::get_item
41+
*/
42+
public function test_get_item_invalid_uri() {
43+
$this->expectException( \WPDieException::class );
44+
45+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
46+
$request->set_param( 'uri', 'invalid-uri' );
47+
$response = rest_get_server()->dispatch( $request );
48+
49+
$this->assertEquals( 400, $response->get_status() );
50+
$data = $response->get_data();
51+
$this->assertEquals( 'activitypub_invalid_object', $data['code'] );
52+
}
53+
54+
/**
55+
* Test get_item with Note object type.
56+
*
57+
* @covers ::get_item
58+
*/
59+
public function test_get_item() {
60+
\add_filter(
61+
'pre_http_request',
62+
function () {
63+
return array(
64+
'response' => array( 'code' => 200 ),
65+
'body' => wp_json_encode(
66+
array(
67+
'type' => 'Note',
68+
'url' => 'https://example.org/note',
69+
)
70+
),
71+
);
72+
}
73+
);
74+
75+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
76+
$request->set_param( 'uri', 'https://example.org/note' );
77+
$response = rest_get_server()->dispatch( $request );
78+
79+
$this->assertEquals( 302, $response->get_status() );
80+
$this->assertArrayHasKey( 'Location', $response->get_headers() );
81+
$this->assertStringContainsString( 'post-new.php?in_reply_to=', $response->get_headers()['Location'] );
82+
}
83+
84+
/**
85+
* Test get_item with custom follow URL filter.
86+
*
87+
* @covers ::get_item
88+
*/
89+
public function test_get_item_custom_follow_url() {
90+
\add_filter(
91+
'pre_http_request',
92+
function () {
93+
return array(
94+
'response' => array( 'code' => 200 ),
95+
'body' => wp_json_encode(
96+
array(
97+
'type' => 'Person',
98+
'url' => 'https://example.org/person',
99+
)
100+
),
101+
);
102+
}
103+
);
104+
105+
\add_filter( 'activitypub_interactions_follow_url', array( $this, 'follow_or_reply_url' ) );
106+
107+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
108+
$request->set_param( 'uri', 'https://example.org/person' );
109+
$response = rest_get_server()->dispatch( $request );
110+
111+
$this->assertEquals( 302, $response->get_status() );
112+
$this->assertArrayHasKey( 'Location', $response->get_headers() );
113+
$this->assertEquals( 'https://custom-follow-or-reply-url.com', $response->get_headers()['Location'] );
114+
}
115+
116+
/**
117+
* Test get_item with custom reply URL filter.
118+
*
119+
* @covers ::get_item
120+
*/
121+
public function test_get_item_custom_reply_url() {
122+
\add_filter(
123+
'pre_http_request',
124+
function () {
125+
return array(
126+
'response' => array( 'code' => 200 ),
127+
'body' => wp_json_encode(
128+
array(
129+
'type' => 'Note',
130+
'url' => 'https://example.org/note',
131+
)
132+
),
133+
);
134+
}
135+
);
136+
137+
\add_filter( 'activitypub_interactions_reply_url', array( $this, 'follow_or_reply_url' ) );
138+
139+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
140+
$request->set_param( 'uri', 'https://example.org/note' );
141+
$response = rest_get_server()->dispatch( $request );
142+
143+
$this->assertEquals( 302, $response->get_status() );
144+
$this->assertArrayHasKey( 'Location', $response->get_headers() );
145+
$this->assertEquals( 'https://custom-follow-or-reply-url.com', $response->get_headers()['Location'] );
146+
}
147+
148+
/**
149+
* Test get_item with WP_Error response from get_remote_object.
150+
*
151+
* @covers ::get_item
152+
*/
153+
public function test_get_item_wp_error() {
154+
$this->expectException( \WPDieException::class );
155+
156+
\add_filter(
157+
'pre_http_request',
158+
function () {
159+
return new \WP_Error( 'http_request_failed', 'Connection failed.' );
160+
}
161+
);
162+
163+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
164+
$request->set_param( 'uri', 'https://example.org/person' );
165+
$response = rest_get_server()->dispatch( $request );
166+
167+
$this->assertEquals( 400, $response->get_status() );
168+
$data = $response->get_data();
169+
$this->assertEquals( 'activitypub_invalid_object', $data['code'] );
170+
$this->assertEquals( 'The URL is not supported!', $data['message'] );
171+
}
172+
173+
/**
174+
* Test get_item with invalid object without type.
175+
*
176+
* @covers ::get_item
177+
*/
178+
public function test_get_item_invalid_object() {
179+
$this->expectException( \WPDieException::class );
180+
181+
\add_filter(
182+
'pre_http_request',
183+
function () {
184+
return array(
185+
'response' => array( 'code' => 200 ),
186+
'body' => wp_json_encode(
187+
array(
188+
'url' => 'https://example.org/invalid',
189+
)
190+
),
191+
);
192+
}
193+
);
194+
195+
$request = new \WP_REST_Request( 'GET', '/' . ACTIVITYPUB_REST_NAMESPACE . '/interactions' );
196+
$request->set_param( 'uri', 'https://example.org/invalid' );
197+
$response = rest_get_server()->dispatch( $request );
198+
199+
$this->assertEquals( 400, $response->get_status() );
200+
$data = $response->get_data();
201+
$this->assertEquals( 'activitypub_invalid_object', $data['code'] );
202+
$this->assertEquals( 'The URL is not supported!', $data['message'] );
203+
}
204+
205+
/**
206+
* Test get_item_schema method.
207+
*
208+
* @doesNotPerformAssertions
209+
*/
210+
public function test_get_item_schema() {
211+
// Controller does not implement get_item_schema().
212+
}
213+
214+
/**
215+
* Returns a valid follow URL.
216+
*/
217+
public function follow_or_reply_url() {
218+
return 'https://custom-follow-or-reply-url.com';
219+
}
220+
}

0 commit comments

Comments
 (0)