PluginProbe
ActivityPub / 8.3.0
ActivityPub v8.3.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-server.php

class-server.php in ActivityPub 8.3.0, at includes/rest/class-server.php

239 lines 7.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Server REST-Class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Rest;
9
10 /**
11 * ActivityPub Server REST-Class.
12 *
13 * @author Django Doucet
14 *
15 * @see https://www.w3.org/TR/activitypub/#security-verification
16 */
17 class Server {
18 /**
19 * Initialize the class, registering WordPress hooks.
20 */
21 public static function init() {
22 \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 );
23 \add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 );
24
25 \add_filter( 'rest_post_dispatch', array( self::class, 'filter_output' ), 10, 3 );
26 \add_filter( 'rest_post_dispatch', array( self::class, 'add_cors_headers' ), 10, 3 );
27 \add_filter( 'rest_allowed_cors_headers', array( self::class, 'allow_cors_headers' ), 10, 2 );
28 }
29
30 /**
31 * Callback function to validate incoming ActivityPub requests
32 *
33 * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
34 * Usually a WP_REST_Response or WP_Error.
35 * @param array $handler Route handler used for the request.
36 * @param \WP_REST_Request $request Request used to generate the response.
37 *
38 * @return mixed|\WP_Error The response, error, or modified response.
39 */
40 public static function validate_requests( $response, $handler, $request ) {
41 if ( 'HEAD' === $request->get_method() ) {
42 return $response;
43 }
44
45 $route = $request->get_route();
46
47 if (
48 \is_wp_error( $response ) ||
49 ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
50 ) {
51 return $response;
52 }
53
54 $params = $request->get_json_params();
55
56 // Type is required for ActivityPub requests, so it fail later in the process.
57 if ( ! isset( $params['type'] ) ) {
58 return $response;
59 }
60
61 if (
62 ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
63 in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
64 ) {
65 return new \WP_Error(
66 'activitypub_server_does_not_accept_incoming_interactions',
67 \__( 'This server does not accept incoming interactions.', 'activitypub' ),
68 // We have to use a 2XX status code here, because otherwise the response will be
69 // treated as an error and Mastodon might block this WordPress instance.
70 array( 'status' => 202 )
71 );
72 }
73
74 return $response;
75 }
76
77 /**
78 * Modify the parameter priority order for a REST API request.
79 *
80 * @param string[] $order Array of types to check, in order of priority.
81 * @param \WP_REST_Request $request The request object.
82 *
83 * @return string[] The modified order of types to check.
84 */
85 public static function request_parameter_order( $order, $request ) {
86 $route = $request->get_route();
87
88 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
89 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
90 return $order;
91 }
92
93 $method = $request->get_method();
94
95 if ( \WP_REST_Server::CREATABLE !== $method ) {
96 return $order;
97 }
98
99 return array(
100 'JSON',
101 'POST',
102 'URL',
103 'defaults',
104 );
105 }
106
107 /**
108 * Filters the REST API response to properly handle the ActivityPub error formatting.
109 *
110 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
111 *
112 * @param \WP_HTTP_Response $response Result to send to the client. Usually a `WP_REST_Response`.
113 * @param \WP_REST_Server $server Server instance.
114 * @param \WP_REST_Request $request Request used to generate the response.
115 *
116 * @return \WP_HTTP_Response The filtered response.
117 */
118 public static function filter_output( $response, $server, $request ) {
119 $route = $request->get_route();
120
121 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
122 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
123 return $response;
124 }
125
126 // Exclude OAuth endpoints - they have their own error format per RFC 6749.
127 if ( \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE . '/oauth' ) ) {
128 return $response;
129 }
130
131 // Only alter responses that return an error status code.
132 if ( $response->get_status() < 400 ) {
133 return $response;
134 }
135
136 $data = $response->get_data();
137
138 // Ensure that `$data` was already converted to a response.
139 if ( \is_wp_error( $data ) ) {
140 $response = \rest_convert_error_to_response( $data );
141 $data = $response->get_data();
142 }
143
144 $error = array(
145 'type' => 'about:blank',
146 'title' => $data['code'] ?? '',
147 'detail' => $data['message'] ?? '',
148 'status' => $response->get_status(),
149
150 /*
151 * Provides the unstructured error data.
152 *
153 * @see https://nodeinfo.diaspora.software/schema.html#metadata.
154 */
155 'metadata' => $data,
156 );
157
158 $response->set_data( $error );
159
160 return $response;
161 }
162
163 /**
164 * Add CORS headers to ActivityPub REST responses.
165 *
166 * @param \WP_REST_Response $response The REST response.
167 * @param \WP_REST_Server $server The REST server instance.
168 * @param \WP_REST_Request $request The request object.
169 *
170 * @return \WP_REST_Response The modified response.
171 */
172 public static function add_cors_headers( $response, $server, $request ) {
173 $route = $request->get_route();
174 $namespace = '/' . ACTIVITYPUB_REST_NAMESPACE;
175
176 // Only add CORS to ActivityPub endpoints, except the interactive OAuth authorize endpoint.
177 if ( ! \str_starts_with( $route, $namespace ) || \str_starts_with( $route, $namespace . '/oauth/authorize' ) ) {
178 return $response;
179 }
180
181 /*
182 * ActivityPub data is meant to be publicly readable by federation peers
183 * and browser-side clients. We do not enable credentialed cross-origin
184 * access: cookie auth would still be rejected by WordPress core's
185 * REST nonce check, and OAuth Bearer tokens travel in the
186 * Authorization header — which is permitted via Allow-Headers and
187 * does not require Allow-Credentials.
188 *
189 * Allow-Headers is contributed by core (which already lists `X-WP-Nonce`,
190 * `Authorization`, `Content-Type`, `Content-Disposition`, and `Content-MD5`)
191 * and extended for ActivityPub via the `rest_allowed_cors_headers` filter
192 * in self::allow_cors_headers().
193 */
194 $response->header( 'Access-Control-Allow-Origin', '*' );
195 $response->header( 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS' );
196
197 return $response;
198 }
199
200 /**
201 * Extend the CORS Allow-Headers list for ActivityPub REST endpoints.
202 *
203 * Adds the headers ActivityPub clients need on top of WordPress core's
204 * defaults: `Accept` for content negotiation and `Last-Event-ID` for
205 * Server-Sent Events resume.
206 *
207 * @since 8.3.0
208 *
209 * @param string[] $allow_headers Headers core currently permits in CORS requests.
210 * @param \WP_REST_Request $request The current REST request.
211 *
212 * @return string[] The (possibly extended) list of allowed headers.
213 */
214 public static function allow_cors_headers( $allow_headers, $request ) {
215 $route = $request->get_route();
216 $namespace = '/' . ACTIVITYPUB_REST_NAMESPACE;
217
218 if ( ! \str_starts_with( $route, $namespace ) || \str_starts_with( $route, $namespace . '/oauth/authorize' ) ) {
219 return $allow_headers;
220 }
221
222 return \array_values( \array_unique( \array_merge( (array) $allow_headers, array( 'Accept', 'Last-Event-ID' ) ) ) );
223 }
224
225 /**
226 * Send CORS headers directly via header().
227 *
228 * Use this for endpoints that bypass the REST response flow
229 * (e.g. SSE streams that call exit() instead of returning a WP_REST_Response).
230 *
231 * @since 8.1.0
232 */
233 public static function send_cors_headers() {
234 \header( 'Access-Control-Allow-Origin: *' );
235 \header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
236 \header( 'Access-Control-Allow-Headers: Authorization, X-WP-Nonce, Content-Disposition, Content-MD5, Content-Type, Accept, Last-Event-ID' );
237 }
238 }
239