PluginProbe
ActivityPub / 8.2.1
ActivityPub v8.2.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-server.php

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

209 lines 6.5 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 }
28
29 /**
30 * Callback function to validate incoming ActivityPub requests
31 *
32 * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
33 * Usually a WP_REST_Response or WP_Error.
34 * @param array $handler Route handler used for the request.
35 * @param \WP_REST_Request $request Request used to generate the response.
36 *
37 * @return mixed|\WP_Error The response, error, or modified response.
38 */
39 public static function validate_requests( $response, $handler, $request ) {
40 if ( 'HEAD' === $request->get_method() ) {
41 return $response;
42 }
43
44 $route = $request->get_route();
45
46 if (
47 \is_wp_error( $response ) ||
48 ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
49 ) {
50 return $response;
51 }
52
53 $params = $request->get_json_params();
54
55 // Type is required for ActivityPub requests, so it fail later in the process.
56 if ( ! isset( $params['type'] ) ) {
57 return $response;
58 }
59
60 if (
61 ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
62 in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
63 ) {
64 return new \WP_Error(
65 'activitypub_server_does_not_accept_incoming_interactions',
66 \__( 'This server does not accept incoming interactions.', 'activitypub' ),
67 // We have to use a 2XX status code here, because otherwise the response will be
68 // treated as an error and Mastodon might block this WordPress instance.
69 array( 'status' => 202 )
70 );
71 }
72
73 return $response;
74 }
75
76 /**
77 * Modify the parameter priority order for a REST API request.
78 *
79 * @param string[] $order Array of types to check, in order of priority.
80 * @param \WP_REST_Request $request The request object.
81 *
82 * @return string[] The modified order of types to check.
83 */
84 public static function request_parameter_order( $order, $request ) {
85 $route = $request->get_route();
86
87 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
88 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
89 return $order;
90 }
91
92 $method = $request->get_method();
93
94 if ( \WP_REST_Server::CREATABLE !== $method ) {
95 return $order;
96 }
97
98 return array(
99 'JSON',
100 'POST',
101 'URL',
102 'defaults',
103 );
104 }
105
106 /**
107 * Filters the REST API response to properly handle the ActivityPub error formatting.
108 *
109 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
110 *
111 * @param \WP_HTTP_Response $response Result to send to the client. Usually a `WP_REST_Response`.
112 * @param \WP_REST_Server $server Server instance.
113 * @param \WP_REST_Request $request Request used to generate the response.
114 *
115 * @return \WP_HTTP_Response The filtered response.
116 */
117 public static function filter_output( $response, $server, $request ) {
118 $route = $request->get_route();
119
120 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
121 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
122 return $response;
123 }
124
125 // Exclude OAuth endpoints - they have their own error format per RFC 6749.
126 if ( \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE . '/oauth' ) ) {
127 return $response;
128 }
129
130 // Only alter responses that return an error status code.
131 if ( $response->get_status() < 400 ) {
132 return $response;
133 }
134
135 $data = $response->get_data();
136
137 // Ensure that `$data` was already converted to a response.
138 if ( \is_wp_error( $data ) ) {
139 $response = \rest_convert_error_to_response( $data );
140 $data = $response->get_data();
141 }
142
143 $error = array(
144 'type' => 'about:blank',
145 'title' => $data['code'] ?? '',
146 'detail' => $data['message'] ?? '',
147 'status' => $response->get_status(),
148
149 /*
150 * Provides the unstructured error data.
151 *
152 * @see https://nodeinfo.diaspora.software/schema.html#metadata.
153 */
154 'metadata' => $data,
155 );
156
157 $response->set_data( $error );
158
159 return $response;
160 }
161
162 /**
163 * Add CORS headers to ActivityPub REST responses.
164 *
165 * @param \WP_REST_Response $response The REST response.
166 * @param \WP_REST_Server $server The REST server instance.
167 * @param \WP_REST_Request $request The request object.
168 *
169 * @return \WP_REST_Response The modified response.
170 */
171 public static function add_cors_headers( $response, $server, $request ) {
172 $route = $request->get_route();
173 $namespace = '/' . ACTIVITYPUB_REST_NAMESPACE;
174
175 // Only add CORS to ActivityPub endpoints, except the interactive OAuth authorize endpoint.
176 if ( ! \str_starts_with( $route, $namespace ) || \str_starts_with( $route, $namespace . '/oauth/authorize' ) ) {
177 return $response;
178 }
179
180 /*
181 * ActivityPub data is meant to be publicly readable by federation peers
182 * and browser-side clients. We do not enable credentialed cross-origin
183 * access: cookie auth would still be rejected by WordPress core's
184 * REST nonce check, and OAuth Bearer tokens travel in the
185 * Authorization header — which is permitted via Allow-Headers and
186 * does not require Allow-Credentials.
187 */
188 $response->header( 'Access-Control-Allow-Origin', '*' );
189 $response->header( 'Access-Control-Allow-Methods', 'GET, POST, OPTIONS' );
190 $response->header( 'Access-Control-Allow-Headers', 'Accept, Content-Type, Authorization, Last-Event-ID' );
191
192 return $response;
193 }
194
195 /**
196 * Send CORS headers directly via header().
197 *
198 * Use this for endpoints that bypass the REST response flow
199 * (e.g. SSE streams that call exit() instead of returning a WP_REST_Response).
200 *
201 * @since 8.1.0
202 */
203 public static function send_cors_headers() {
204 \header( 'Access-Control-Allow-Origin: *' );
205 \header( 'Access-Control-Allow-Methods: GET, POST, OPTIONS' );
206 \header( 'Access-Control-Allow-Headers: Accept, Content-Type, Authorization, Last-Event-ID' );
207 }
208 }
209