PluginProbe
ActivityPub / 9.0.0
ActivityPub v9.0.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / rest / class-proxy-controller.php

class-proxy-controller.php in ActivityPub 9.0.0, at includes/rest/class-proxy-controller.php

271 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Proxy Controller file.
4 *
5 * Implements the proxyUrl endpoint for C2S clients to fetch remote ActivityPub objects.
6 *
7 * @package Activitypub
8 * @see https://www.w3.org/wiki/ActivityPub/Primer/proxyUrl_endpoint
9 */
10
11 namespace Activitypub\Rest;
12
13 use Activitypub\Collection\Remote_Actors;
14 use Activitypub\Http;
15 use Activitypub\Webfinger;
16
17 use function Activitypub\is_actor;
18
19 /**
20 * Proxy Controller.
21 *
22 * Provides a bridge between C2S OAuth authentication and S2S HTTP Signature authentication.
23 * Allows C2S clients to fetch remote ActivityPub objects through their home server.
24 */
25 class Proxy_Controller extends \WP_REST_Controller {
26 use Event_Stream;
27 use Verification;
28
29 /**
30 * The namespace of this controller's route.
31 *
32 * @var string
33 */
34 protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
35
36 /**
37 * The base of this controller's route.
38 *
39 * @var string
40 */
41 protected $rest_base = 'proxy';
42
43 /**
44 * Register routes.
45 */
46 public function register_routes() {
47 \register_rest_route(
48 $this->namespace,
49 '/' . $this->rest_base,
50 array(
51 array(
52 'methods' => \WP_REST_Server::CREATABLE,
53 'callback' => array( $this, 'create_item' ),
54 'permission_callback' => array( $this, 'verify_authentication' ),
55 'args' => array(
56 'id' => array(
57 'description' => 'The remote ActivityPub object to fetch: an HTTPS URL or an acct identifier (`user@host`, `@user@host`, or `acct:user@host`).',
58 'type' => 'string',
59 'required' => true,
60 'sanitize_callback' => array( $this, 'sanitize_url' ),
61 'validate_callback' => array( $this, 'validate_url' ),
62 ),
63 ),
64 ),
65 'schema' => array( $this, 'get_item_schema' ),
66 )
67 );
68
69 \register_rest_route(
70 $this->namespace,
71 '/' . $this->rest_base . '/stream',
72 array(
73 array(
74 'methods' => \WP_REST_Server::READABLE,
75 'callback' => array( $this, 'get_stream' ),
76 'permission_callback' => array( $this, 'get_stream_permissions_check' ),
77 'args' => array(
78 'id' => array(
79 'description' => 'The remote actor identifier (URL or WebFinger acct) whose eventStream to proxy.',
80 'type' => 'string',
81 'required' => true,
82 'sanitize_callback' => array( $this, 'sanitize_url' ),
83 'validate_callback' => array( $this, 'validate_url' ),
84 ),
85 ),
86 ),
87 )
88 );
89 }
90
91 /**
92 * Sanitize the `id` parameter.
93 *
94 * Accepts either an HTTPS URL or an acct identifier (`user@host`,
95 * `@user@host`, or `acct:user@host`). Acct identifiers are returned
96 * as-is; URLs are run through `sanitize_url()`. Matches the dual-shape
97 * contract of `Remote_Actors::fetch_by_various()`.
98 *
99 * @see https://developer.wordpress.org/reference/functions/sanitize_url/
100 *
101 * @param string $url The urlencoded URL or acct identifier to sanitize.
102 * @return string The sanitized value.
103 */
104 public function sanitize_url( $url ) {
105 $decoded = \urldecode( $url );
106
107 if ( Webfinger::is_acct( $decoded ) ) {
108 return $decoded;
109 }
110
111 return \sanitize_url( $decoded );
112 }
113
114 /**
115 * Validate the `id` parameter.
116 *
117 * Accepts either an HTTPS URL (validated via `wp_http_validate_url()`,
118 * which blocks local/private IPs and restricts ports) or an acct
119 * identifier in any of the forms accepted by `Webfinger::is_acct()`:
120 * `user@host`, `@user@host`, or `acct:user@host`. Matches the
121 * dual-shape contract of `Remote_Actors::fetch_by_various()`.
122 *
123 * @see https://developer.wordpress.org/reference/functions/wp_http_validate_url/
124 *
125 * @param string $url The URL or acct identifier to validate.
126 * @return bool True if valid, false otherwise.
127 */
128 public function validate_url( $url ) {
129 $decoded_url = \urldecode( $url );
130
131 if ( Webfinger::is_acct( $decoded_url ) ) {
132 return true;
133 }
134
135 // Must be HTTPS.
136 if ( 'https' !== \wp_parse_url( $decoded_url, PHP_URL_SCHEME ) ) {
137 return false;
138 }
139
140 // Use WordPress built-in validation (blocks local IPs, restricts ports).
141 return (bool) \wp_http_validate_url( $decoded_url );
142 }
143
144 /**
145 * Fetch a remote ActivityPub object via the proxy.
146 *
147 * @see https://www.w3.org/wiki/ActivityPub/Primer/proxyUrl_endpoint
148 *
149 * @param \WP_REST_Request $request Full details about the request.
150 * @return \WP_REST_Response|\WP_Error Response object on success, WP_Error on failure.
151 */
152 public function create_item( $request ) {
153 // Rate-limit proxy requests (max 30 per minute per user).
154 $user_id = \get_current_user_id();
155 $transient_key = 'ap_proxy_' . $user_id;
156 $count = (int) \get_transient( $transient_key );
157
158 if ( $count >= 30 ) {
159 return new \WP_Error(
160 'activitypub_rate_limit',
161 \__( 'Too many proxy requests. Please try again later.', 'activitypub' ),
162 array( 'status' => 429 )
163 );
164 }
165
166 \set_transient( $transient_key, $count + 1, MINUTE_IN_SECONDS );
167
168 $url = $request->get_param( 'id' );
169
170 // Try to fetch as an actor first using Remote_Actors which handles caching.
171 $post = Remote_Actors::fetch_by_various( $url );
172
173 if ( ! \is_wp_error( $post ) ) {
174 $actor = Remote_Actors::get_actor( $post );
175
176 if ( ! \is_wp_error( $actor ) ) {
177 $response = new \WP_REST_Response( $actor->to_array(), 200 );
178 $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
179
180 return $response;
181 }
182 }
183
184 // Fall back to fetching as a generic object.
185 $object = Http::get_remote_object( $url );
186
187 if ( \is_wp_error( $object ) ) {
188 return new \WP_Error(
189 'activitypub_fetch_failed',
190 \__( 'Failed to fetch the remote object.', 'activitypub' ),
191 array( 'status' => 502 )
192 );
193 }
194
195 // If it's an actor, store it for future use.
196 if ( is_actor( $object ) ) {
197 Remote_Actors::upsert( $object );
198 }
199
200 $response = new \WP_REST_Response( $object, 200 );
201 $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
202
203 return $response;
204 }
205
206 /**
207 * Get the schema for the proxy endpoint.
208 *
209 * @return array Schema array.
210 */
211 public function get_item_schema() {
212 return array(
213 '$schema' => 'http://json-schema.org/draft-04/schema#',
214 'title' => 'proxy',
215 'type' => 'object',
216 'properties' => array(
217 'id' => array(
218 'description' => \__( 'The URI of the remote ActivityPub object.', 'activitypub' ),
219 'type' => 'string',
220 'format' => 'uri',
221 'context' => array( 'view' ),
222 ),
223 ),
224 );
225 }
226
227 /**
228 * Proxy a remote eventStream.
229 *
230 * Fetches the remote object to discover its eventStream URL,
231 * then opens a streaming connection and relays SSE events.
232 *
233 * @param \WP_REST_Request $request Full details about the request.
234 *
235 * @return \WP_Error|void WP_Error on failure, exits on success.
236 */
237 public function get_stream( $request ) {
238 $remote_id = $request->get_param( 'id' );
239
240 $object = Http::get_remote_object( $remote_id );
241
242 if ( \is_wp_error( $object ) ) {
243 return new \WP_Error(
244 'activitypub_proxy_fetch_failed',
245 \__( 'Failed to fetch the remote object.', 'activitypub' ),
246 array( 'status' => 502 )
247 );
248 }
249
250 $stream_url = isset( $object['eventStream'] ) ? $object['eventStream'] : null;
251
252 if ( ! $stream_url ) {
253 return new \WP_Error(
254 'activitypub_no_event_stream',
255 \__( 'The remote object does not advertise an eventStream.', 'activitypub' ),
256 array( 'status' => 404 )
257 );
258 }
259
260 if ( ! $this->validate_url( $stream_url ) ) {
261 return new \WP_Error(
262 'activitypub_invalid_event_stream',
263 \__( 'The remote eventStream URL is not valid.', 'activitypub' ),
264 array( 'status' => 400 )
265 );
266 }
267
268 $this->relay_remote_stream( $stream_url );
269 }
270 }
271