PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.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 7.8.0, at includes/rest/class-server.php

213 lines 6.4 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 use Activitypub\Signature;
11
12 use function Activitypub\use_authorized_fetch;
13
14 /**
15 * ActivityPub Server REST-Class.
16 *
17 * @author Django Doucet
18 *
19 * @see https://www.w3.org/TR/activitypub/#security-verification
20 */
21 class Server {
22 /**
23 * Initialize the class, registering WordPress hooks.
24 */
25 public static function init() {
26 \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 );
27 \add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 );
28
29 \add_filter( 'rest_post_dispatch', array( self::class, 'filter_output' ), 10, 3 );
30 }
31
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 /**
86 * Callback function to validate incoming ActivityPub requests
87 *
88 * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
89 * Usually a WP_REST_Response or WP_Error.
90 * @param array $handler Route handler used for the request.
91 * @param \WP_REST_Request $request Request used to generate the response.
92 *
93 * @return mixed|\WP_Error The response, error, or modified response.
94 */
95 public static function validate_requests( $response, $handler, $request ) {
96 if ( 'HEAD' === $request->get_method() ) {
97 return $response;
98 }
99
100 $route = $request->get_route();
101
102 if (
103 \is_wp_error( $response ) ||
104 ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
105 ) {
106 return $response;
107 }
108
109 $params = $request->get_json_params();
110
111 // Type is required for ActivityPub requests, so it fail later in the process.
112 if ( ! isset( $params['type'] ) ) {
113 return $response;
114 }
115
116 if (
117 ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
118 in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
119 ) {
120 return new \WP_Error(
121 'activitypub_server_does_not_accept_incoming_interactions',
122 \__( 'This server does not accept incoming interactions.', 'activitypub' ),
123 // We have to use a 2XX status code here, because otherwise the response will be
124 // treated as an error and Mastodon might block this WordPress instance.
125 array( 'status' => 202 )
126 );
127 }
128
129 return $response;
130 }
131
132 /**
133 * Modify the parameter priority order for a REST API request.
134 *
135 * @param string[] $order Array of types to check, in order of priority.
136 * @param \WP_REST_Request $request The request object.
137 *
138 * @return string[] The modified order of types to check.
139 */
140 public static function request_parameter_order( $order, $request ) {
141 $route = $request->get_route();
142
143 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
144 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
145 return $order;
146 }
147
148 $method = $request->get_method();
149
150 if ( \WP_REST_Server::CREATABLE !== $method ) {
151 return $order;
152 }
153
154 return array(
155 'JSON',
156 'POST',
157 'URL',
158 'defaults',
159 );
160 }
161
162 /**
163 * Filters the REST API response to properly handle the ActivityPub error formatting.
164 *
165 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
166 *
167 * @param \WP_HTTP_Response $response Result to send to the client. Usually a `WP_REST_Response`.
168 * @param \WP_REST_Server $server Server instance.
169 * @param \WP_REST_Request $request Request used to generate the response.
170 *
171 * @return \WP_HTTP_Response The filtered response.
172 */
173 public static function filter_output( $response, $server, $request ) {
174 $route = $request->get_route();
175
176 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
177 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
178 return $response;
179 }
180
181 // Only alter responses that return an error status code.
182 if ( $response->get_status() < 400 ) {
183 return $response;
184 }
185
186 $data = $response->get_data();
187
188 // Ensure that `$data` was already converted to a response.
189 if ( \is_wp_error( $data ) ) {
190 $response = \rest_convert_error_to_response( $data );
191 $data = $response->get_data();
192 }
193
194 $error = array(
195 'type' => 'about:blank',
196 'title' => $data['code'] ?? '',
197 'detail' => $data['message'] ?? '',
198 'status' => $response->get_status(),
199
200 /*
201 * Provides the unstructured error data.
202 *
203 * @see https://nodeinfo.diaspora.software/schema.html#metadata.
204 */
205 'metadata' => $data,
206 );
207
208 $response->set_data( $error );
209
210 return $response;
211 }
212 }
213