PluginProbe
ActivityPub / 9.1.0
ActivityPub v9.1.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.1.0, at includes/rest/class-proxy-controller.php

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