PluginProbe
ActivityPub / 8.0.2
ActivityPub v8.0.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
← All changes | includes/rest/class-server.php +57 -83 8.3.08.0.2 View file →
@@ -6,8 +6,12 @@
6 6 */
7 7
8 8 namespace Activitypub\Rest;
9 9
10 +use Activitypub\Signature;
11 +
12 +use function Activitypub\use_authorized_fetch;
13 +
10 14 /**
11 15 * ActivityPub Server REST-Class.
12 16 *
13 17 * @author Django Doucet
@@ -22,13 +26,64 @@
22 26 \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 );
23 27 \add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 );
24 28
25 29 \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 30 }
29 31
30 32 /**
33 + * Callback function to authorize an api request.
34 + *
35 + * The function is meant to be used as part of permission callbacks for rest api endpoints.
36 + *
37 + * It verifies the signature of POST, PUT, PATCH, and DELETE requests, as well as GET requests in secure mode.
38 + * You can use the filter 'activitypub_defer_signature_verification' to defer the signature verification.
39 + * HEAD requests are always bypassed.
40 + *
41 + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
42 + * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
43 + *
44 + * @param \WP_REST_Request $request The request object.
45 + *
46 + * @return bool|\WP_Error True if the request is authorized, WP_Error if not.
47 + */
48 + public static function verify_signature( $request ) {
49 + if ( 'HEAD' === $request->get_method() ) {
50 + return true;
51 + }
52 +
53 + /**
54 + * Filter to defer signature verification.
55 + *
56 + * Skip signature verification for debugging purposes or to reduce load for
57 + * certain Activity-Types, like "Delete".
58 + *
59 + * @param bool $defer Whether to defer signature verification.
60 + * @param \WP_REST_Request $request The request used to generate the response.
61 + *
62 + * @return bool Whether to defer signature verification.
63 + */
64 + $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
65 +
66 + if ( $defer ) {
67 + return true;
68 + }
69 +
70 + // POST-Requests always have to be signed, GET-Requests only require a signature in secure mode.
71 + if ( 'GET' !== $request->get_method() || use_authorized_fetch() ) {
72 + $verified_request = Signature::verify_http_signature( $request );
73 + if ( \is_wp_error( $verified_request ) ) {
74 + return new \WP_Error(
75 + 'activitypub_signature_verification',
76 + $verified_request->get_error_message(),
77 + array( 'status' => 401 )
78 + );
79 + }
80 + }
81 +
82 + return true;
83 + }
84 +
85 + /**
31 86 * Callback function to validate incoming ActivityPub requests
32 87 *
33 88 * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
34 89 * Usually a WP_REST_Response or WP_Error.
@@ -122,13 +177,8 @@
122 177 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
123 178 return $response;
124 179 }
125 180
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 181 // Only alter responses that return an error status code.
132 182 if ( $response->get_status() < 400 ) {
133 183 return $response;
134 184 }
@@ -157,82 +207,6 @@
157 207
158 208 $response->set_data( $error );
159 209
160 210 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 211 }
238 212 }