PluginProbe
ActivityPub / 9.2.2
ActivityPub v9.2.2
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-actors-controller.php

class-actors-controller.php in ActivityPub 9.2.2, at includes/rest/class-actors-controller.php

429 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ActivityPub Actors REST-Class
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Rest;
9
10 use Activitypub\Collection\Actors as Actor_Collection;
11 use Activitypub\Webfinger;
12
13 use function Activitypub\get_client_ip;
14
15 /**
16 * ActivityPub Actors REST-Class.
17 *
18 * @author Matthias Pfefferle
19 *
20 * @see https://www.w3.org/TR/activitypub/#followers
21 */
22 class Actors_Controller extends \WP_REST_Controller {
23 use Verification;
24
25 /**
26 * The namespace of this controller's route.
27 *
28 * @var string
29 */
30 protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
31
32 /**
33 * The base of this controller's route.
34 *
35 * @var string
36 */
37 protected $rest_base = '(?:users|actors)\/(?P<user_id>[-]?\d+)';
38
39 /**
40 * Register routes.
41 */
42 public function register_routes() {
43 \register_rest_route(
44 $this->namespace,
45 '/' . $this->rest_base,
46 array(
47 'args' => array(
48 'user_id' => array(
49 'description' => 'The ID of the actor.',
50 'type' => 'integer',
51 'required' => true,
52 'validate_callback' => array( $this, 'validate_user_id' ),
53 ),
54 ),
55 array(
56 'methods' => \WP_REST_Server::READABLE,
57 'callback' => array( $this, 'get_item' ),
58 'permission_callback' => array( $this, 'verify_signature' ),
59 ),
60 'schema' => array( $this, 'get_public_item_schema' ),
61 )
62 );
63
64 \register_rest_route(
65 $this->namespace,
66 '/' . $this->rest_base . '/remote-follow',
67 array(
68 'args' => array(
69 'user_id' => array(
70 'description' => 'The ID of the actor.',
71 'type' => 'integer',
72 'required' => true,
73 'validate_callback' => array( $this, 'validate_user_id' ),
74 ),
75 ),
76 array(
77 'methods' => \WP_REST_Server::READABLE,
78 'callback' => array( $this, 'get_remote_follow_item' ),
79 'permission_callback' => '__return_true',
80 'args' => array(
81 'resource' => array(
82 'description' => 'The resource to follow.',
83 'type' => 'string',
84 'required' => true,
85 ),
86 ),
87 ),
88 )
89 );
90 }
91
92 /**
93 * Retrieves a single actor.
94 *
95 * @param \WP_REST_Request $request Full details about the request.
96 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
97 */
98 public function get_item( $request ) {
99 $user_id = $request->get_param( 'user_id' );
100 $user = Actor_Collection::get_by_id( $user_id );
101
102 /**
103 * Action triggered prior to the ActivityPub profile being created and sent to the client.
104 */
105 \do_action( 'activitypub_rest_users_pre' );
106
107 $data = $user->to_array();
108
109 $response = \rest_ensure_response( $data );
110 $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
111 $response->header( 'Link', \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ) );
112
113 return $response;
114 }
115
116 /**
117 * Retrieves the remote follow endpoint.
118 *
119 * @param \WP_REST_Request $request Full details about the request.
120 * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
121 */
122 public function get_remote_follow_item( $request ) {
123 /*
124 * This endpoint is unauthenticated and triggers an outbound WebFinger request to a
125 * user-supplied host, so throttle it per IP (max 10 per minute) to limit its use as
126 * a blind SSRF / request-amplification vector. Fail closed when no IP is available.
127 */
128 $ip = get_client_ip();
129 if ( '' === $ip ) {
130 return self::rate_limit_response();
131 }
132
133 $transient_key = 'ap_remote_follow_' . \md5( $ip );
134 $count = (int) \get_transient( $transient_key );
135 if ( $count >= 10 ) {
136 return self::rate_limit_response();
137 }
138 \set_transient( $transient_key, $count + 1, MINUTE_IN_SECONDS );
139
140 $resource = $request->get_param( 'resource' );
141 $user_id = $request->get_param( 'user_id' );
142 $user = Actor_Collection::get_by_id( $user_id );
143
144 $template = Webfinger::get_remote_follow_endpoint( $resource );
145
146 if ( \is_wp_error( $template ) ) {
147 return $template;
148 }
149
150 $resource = $user->get_webfinger();
151 $url = \str_replace( '{uri}', $resource, $template );
152
153 return \rest_ensure_response(
154 array(
155 'url' => $url,
156 'template' => $template,
157 )
158 );
159 }
160
161 /**
162 * Build a 429 rate-limit response for the remote-follow endpoint.
163 *
164 * @return \WP_REST_Response The rate-limit response.
165 */
166 private static function rate_limit_response() {
167 return new \WP_REST_Response(
168 array(
169 'code' => 'activitypub_rate_limited',
170 'message' => \__( 'Too many requests. Please try again later.', 'activitypub' ),
171 'data' => array( 'status' => 429 ),
172 ),
173 429,
174 // RFC 6585 §4: send Retry-After so clients can back off.
175 array( 'Retry-After' => (string) MINUTE_IN_SECONDS )
176 );
177 }
178
179 /**
180 * Retrieves the actor schema, conforming to JSON Schema.
181 *
182 * @return array Item schema data.
183 */
184 public function get_item_schema() {
185 if ( $this->schema ) {
186 return $this->add_additional_fields_schema( $this->schema );
187 }
188
189 $this->schema = array(
190 '$schema' => 'http://json-schema.org/draft-04/schema#',
191 'title' => 'actor',
192 'type' => 'object',
193 'properties' => array(
194 '@context' => array(
195 'description' => 'The JSON-LD context for the response.',
196 'type' => array( 'array', 'object' ),
197 'readonly' => true,
198 ),
199 'id' => array(
200 'description' => 'The unique identifier for the actor.',
201 'type' => 'string',
202 'format' => 'uri',
203 'readonly' => true,
204 ),
205 'type' => array(
206 'description' => 'The type of the actor.',
207 'type' => 'string',
208 'enum' => array( 'Person', 'Service', 'Organization', 'Application', 'Group' ),
209 'readonly' => true,
210 ),
211 'attachment' => array(
212 'description' => 'Additional information attached to the actor.',
213 'type' => 'array',
214 'items' => array(
215 'type' => 'object',
216 'properties' => array(
217 'type' => array(
218 'type' => 'string',
219 'enum' => array( 'PropertyValue', 'Link' ),
220 ),
221 'name' => array(
222 'type' => 'string',
223 ),
224 'value' => array(
225 'type' => 'string',
226 ),
227 'href' => array(
228 'type' => 'string',
229 'format' => 'uri',
230 ),
231 'rel' => array(
232 'type' => 'array',
233 'items' => array(
234 'type' => 'string',
235 ),
236 ),
237 ),
238 ),
239 'readonly' => true,
240 ),
241 'name' => array(
242 'description' => 'The display name of the actor.',
243 'type' => 'string',
244 'readonly' => true,
245 ),
246 'icon' => array(
247 'description' => 'The icon/avatar of the actor.',
248 'type' => 'object',
249 'properties' => array(
250 'type' => array(
251 'type' => 'string',
252 ),
253 'url' => array(
254 'type' => 'string',
255 'format' => 'uri',
256 ),
257 ),
258 'readonly' => true,
259 ),
260 'published' => array(
261 'description' => 'The date the actor was published.',
262 'type' => 'string',
263 'format' => 'date-time',
264 'readonly' => true,
265 ),
266 'summary' => array(
267 'description' => 'A summary about the actor.',
268 'type' => 'string',
269 'readonly' => true,
270 ),
271 'tag' => array(
272 'description' => 'Tags associated with the actor.',
273 'type' => 'array',
274 'items' => array(
275 'type' => 'object',
276 'properties' => array(
277 'type' => array(
278 'type' => 'string',
279 ),
280 'href' => array(
281 'type' => 'string',
282 'format' => 'uri',
283 ),
284 'name' => array(
285 'type' => 'string',
286 ),
287 ),
288 ),
289 'readonly' => true,
290 ),
291 'url' => array(
292 'description' => 'The URL to the actor\'s profile page.',
293 'type' => 'string',
294 'format' => 'uri',
295 'readonly' => true,
296 ),
297 'inbox' => array(
298 'description' => 'The inbox endpoint for the actor.',
299 'type' => 'string',
300 'format' => 'uri',
301 'readonly' => true,
302 ),
303 'outbox' => array(
304 'description' => 'The outbox endpoint for the actor.',
305 'type' => 'string',
306 'format' => 'uri',
307 'readonly' => true,
308 ),
309 'following' => array(
310 'description' => 'The following endpoint for the actor.',
311 'type' => 'string',
312 'format' => 'uri',
313 'readonly' => true,
314 ),
315 'followers' => array(
316 'description' => 'The followers endpoint for the actor.',
317 'type' => 'string',
318 'format' => 'uri',
319 'readonly' => true,
320 ),
321 'streams' => array(
322 'description' => 'The streams associated with the actor.',
323 'type' => 'array',
324 'readonly' => true,
325 ),
326 'preferredUsername' => array(
327 'description' => 'The preferred username of the actor.',
328 'type' => 'string',
329 'readonly' => true,
330 ),
331 'publicKey' => array(
332 'description' => 'The public key information for the actor.',
333 'type' => 'object',
334 'properties' => array(
335 'id' => array(
336 'type' => 'string',
337 'format' => 'uri',
338 ),
339 'owner' => array(
340 'type' => 'string',
341 'format' => 'uri',
342 ),
343 'publicKeyPem' => array(
344 'type' => 'string',
345 ),
346 ),
347 'readonly' => true,
348 ),
349 'manuallyApprovesFollowers' => array(
350 'description' => 'Whether the actor manually approves followers.',
351 'type' => 'boolean',
352 'readonly' => true,
353 ),
354 'attributionDomains' => array(
355 'description' => 'The attribution domains for the actor.',
356 'type' => 'array',
357 'items' => array(
358 'type' => 'string',
359 ),
360 'readonly' => true,
361 ),
362 'featured' => array(
363 'description' => 'The featured collection endpoint for the actor.',
364 'type' => 'string',
365 'format' => 'uri',
366 'readonly' => true,
367 ),
368 'indexable' => array(
369 'description' => 'Whether the actor is indexable.',
370 'type' => 'boolean',
371 'readonly' => true,
372 ),
373 'webfinger' => array(
374 'description' => 'The webfinger identifier for the actor.',
375 'type' => 'string',
376 'readonly' => true,
377 ),
378 'discoverable' => array(
379 'description' => 'Whether the actor is discoverable.',
380 'type' => 'boolean',
381 'readonly' => true,
382 ),
383 'generator' => array(
384 'description' => 'The generator of the object.',
385 'type' => 'object',
386 'properties' => array(
387 'type' => array(
388 'type' => 'string',
389 ),
390 'implements' => array(
391 'type' => 'array',
392 'items' => array(
393 'type' => 'object',
394 'properties' => array(
395 'href' => array(
396 'type' => 'string',
397 'format' => 'uri',
398 ),
399 'name' => array(
400 'type' => 'string',
401 ),
402 ),
403 ),
404 ),
405 ),
406 'readonly' => true,
407 ),
408 ),
409 );
410
411 return $this->add_additional_fields_schema( $this->schema );
412 }
413
414 /**
415 * Validates the user_id parameter.
416 *
417 * @param mixed $user_id The user_id parameter.
418 * @return bool|\WP_Error True if the user_id is valid, WP_Error otherwise.
419 */
420 public function validate_user_id( $user_id ) {
421 $user = Actor_Collection::get_by_id( $user_id );
422 if ( \is_wp_error( $user ) ) {
423 return $user;
424 }
425
426 return true;
427 }
428 }
429