PluginProbe
ActivityPub / 1.0.0
ActivityPub v1.0.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
← All changes | includes/rest/class-server.php +61 -195 8.3.01.0.0 View file →
@@ -1,15 +1,14 @@
1 1 <?php
2 -/**
3 - * Server REST-Class file.
4 - *
5 - * @package Activitypub
6 - */
2 +namespace Activitypub\Rest;
7 3
8 -namespace Activitypub\Rest;
4 +use stdClass;
5 +use WP_REST_Response;
6 +use Activitypub\Signature;
7 +use Activitypub\Model\Application_User;
9 8
10 9 /**
11 - * ActivityPub Server REST-Class.
10 + * ActivityPub Server REST-Class
12 11 *
13 12 * @author Django Doucet
14 13 *
15 14 * @see https://www.w3.org/TR/activitypub/#security-verification
@@ -15,224 +14,91 @@
15 14 * @see https://www.w3.org/TR/activitypub/#security-verification
16 15 */
17 16 class Server {
18 17 /**
19 - * Initialize the class, registering WordPress hooks.
18 + * Initialize the class, registering WordPress hooks
20 19 */
21 20 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 );
21 + \add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
22 + \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
28 23 }
29 24
30 25 /**
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.
26 + * Register routes
39 27 */
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;
28 + public static function register_routes() {
29 + \register_rest_route(
30 + ACTIVITYPUB_REST_NAMESPACE,
31 + '/application',
32 + array(
33 + array(
34 + 'methods' => \WP_REST_Server::READABLE,
35 + 'callback' => array( self::class, 'application_actor' ),
36 + 'permission_callback' => '__return_true',
37 + ),
38 + )
39 + );
75 40 }
76 41
77 42 /**
78 - * Modify the parameter priority order for a REST API request.
43 + * Render Application actor profile
79 44 *
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.
45 + * @return WP_REST_Response The JSON profile of the Application Actor.
84 46 */
85 - public static function request_parameter_order( $order, $request ) {
86 - $route = $request->get_route();
47 + public static function application_actor() {
48 + $user = new Application_User();
87 49
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 - }
50 + $user->set_context(
51 + \Activitypub\Activity\Activity::CONTEXT
52 + );
92 53
93 - $method = $request->get_method();
54 + $json = $user->to_array();
94 55
95 - if ( \WP_REST_Server::CREATABLE !== $method ) {
96 - return $order;
97 - }
56 + $response = new WP_REST_Response( $json, 200 );
98 57
99 - return array(
100 - 'JSON',
101 - 'POST',
102 - 'URL',
103 - 'defaults',
104 - );
58 + $response->header( 'Content-Type', 'application/activity+json' );
59 +
60 + return $response;
105 61 }
106 62
107 63 /**
108 - * Filters the REST API response to properly handle the ActivityPub error formatting.
64 + * Callback function to authorize each api requests
109 65 *
110 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
66 + * @see WP_REST_Request
111 67 *
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.
68 + * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
69 + * Usually a WP_REST_Response or WP_Error.
70 + * @param array $handler Route handler used for the request.
71 + * @param WP_REST_Request $request Request used to generate the response.
115 72 *
116 - * @return \WP_HTTP_Response The filtered response.
73 + * @return mixed|WP_Error The response, error, or modified response.
117 74 */
118 - public static function filter_output( $response, $server, $request ) {
75 + public static function authorize_activitypub_requests( $response, $handler, $request ) {
119 76 $route = $request->get_route();
120 77
121 - // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
122 - if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
78 + // check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
79 + if (
80 + ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
81 + \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
82 + \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' )
83 + ) {
123 84 return $response;
124 85 }
125 86
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;
87 + // POST-Requets are always signed
88 + if ( 'post' === \strtolower( $request->get_method() ) ) {
89 + $verified_request = Signature::verify_http_signature( $request );
90 + if ( \is_wp_error( $verified_request ) ) {
91 + return $verified_request;
92 + }
93 + } elseif ( 'get' === \strtolower( $request->get_method() ) ) { // GET-Requests are only signed in secure mode
94 + if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
95 + $verified_request = Signature::verify_http_signature( $request );
96 + if ( \is_wp_error( $verified_request ) ) {
97 + return $verified_request;
98 + }
99 + }
129 100 }
130 101
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 102 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 103 }
238 104 }