PluginProbe
ActivityPub / 3.2.2
ActivityPub v3.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
← All changes | includes/rest/class-server.php +79 -160 8.2.03.2.2 View file →
@@ -1,15 +1,15 @@
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_Error;
6 +use WP_REST_Response;
7 +use Activitypub\Signature;
8 +use Activitypub\Model\Application;
9 9
10 10 /**
11 - * ActivityPub Server REST-Class.
11 + * ActivityPub Server REST-Class
12 12 *
13 13 * @author Django Doucet
14 14 *
15 15 * @see https://www.w3.org/TR/activitypub/#security-verification
@@ -15,194 +15,113 @@
15 15 * @see https://www.w3.org/TR/activitypub/#security-verification
16 16 */
17 17 class Server {
18 18 /**
19 - * Initialize the class, registering WordPress hooks.
19 + * Initialize the class, registering WordPress hooks
20 20 */
21 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 );
22 + self::register_routes();
24 23
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 );
24 + \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
27 25 }
28 26
29 27 /**
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.
28 + * Register routes
38 29 */
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;
30 + public static function register_routes() {
31 + \register_rest_route(
32 + ACTIVITYPUB_REST_NAMESPACE,
33 + '/application',
34 + array(
35 + array(
36 + 'methods' => \WP_REST_Server::READABLE,
37 + 'callback' => array( self::class, 'application_actor' ),
38 + 'permission_callback' => '__return_true',
39 + ),
40 + )
41 + );
74 42 }
75 43
76 44 /**
77 - * Modify the parameter priority order for a REST API request.
45 + * Render Application actor profile
78 46 *
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.
47 + * @return WP_REST_Response The JSON profile of the Application Actor.
83 48 */
84 - public static function request_parameter_order( $order, $request ) {
85 - $route = $request->get_route();
49 + public static function application_actor() {
50 + $user = new Application();
86 51
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 - }
52 + $json = $user->to_array();
91 53
92 - $method = $request->get_method();
54 + $rest_response = new WP_REST_Response( $json, 200 );
55 + $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
93 56
94 - if ( \WP_REST_Server::CREATABLE !== $method ) {
95 - return $order;
96 - }
97 -
98 - return array(
99 - 'JSON',
100 - 'POST',
101 - 'URL',
102 - 'defaults',
103 - );
57 + return $rest_response;
104 58 }
105 59
106 60 /**
107 - * Filters the REST API response to properly handle the ActivityPub error formatting.
61 + * Callback function to authorize each api requests
108 62 *
109 - * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
63 + * @see WP_REST_Request
110 64 *
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.
65 + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
66 + * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
114 67 *
115 - * @return \WP_HTTP_Response The filtered 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.
72 + *
73 + * @return mixed|WP_Error The response, error, or modified response.
116 74 */
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 ) ) {
75 + public static function authorize_activitypub_requests( $response, $handler, $request ) {
76 + if ( 'HEAD' === $request->get_method() ) {
122 77 return $response;
123 78 }
124 79
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 - }
80 + $route = $request->get_route();
129 81
130 - // Only alter responses that return an error status code.
131 - if ( $response->get_status() < 400 ) {
82 + // check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
83 + if (
84 + ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
85 + \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
86 + \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) ||
87 + \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'application' )
88 + ) {
132 89 return $response;
133 90 }
134 91
135 - $data = $response->get_data();
92 + /**
93 + * Filter to defer signature verification
94 + *
95 + * Skip signature verification for debugging purposes or to reduce load for
96 + * certain Activity-Types, like "Delete".
97 + *
98 + * @param bool $defer Whether to defer signature verification.
99 + * @param WP_REST_Request $request The request used to generate the response.
100 + *
101 + * @return bool Whether to defer signature verification.
102 + */
103 + $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
136 104
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();
105 + if ( $defer ) {
106 + return $response;
141 107 }
142 108
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;
109 + if (
110 + // POST-Requests are always signed
111 + 'GET' !== $request->get_method() ||
112 + // GET-Requests only require a signature in secure mode
113 + ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH )
114 + ) {
115 + $verified_request = Signature::verify_http_signature( $request );
116 + if ( \is_wp_error( $verified_request ) ) {
117 + return new WP_Error(
118 + 'activitypub_signature_verification',
119 + $verified_request->get_error_message(),
120 + array( 'status' => 401 )
121 + );
122 + }
178 123 }
179 124
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 125 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 126 }
208 127 }