PluginProbe
ActivityPub / 8.1.1
ActivityPub v8.1.1
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 8.1.1, at includes/rest/class-proxy-controller.php

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