PluginProbe
ActivityPub / 7.0.0
ActivityPub v7.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
activitypub / includes / rest / class-server.php

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

220 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 self::add_hooks();
27 }
28
29 /**
30 * Add sever hooks.
31 */
32 public static function add_hooks() {
33 \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 );
34 \add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 );
35
36 \add_filter( 'rest_post_dispatch', array( self::class, 'filter_output' ), 10, 3 );
37 }
38
39 /**
40 * Callback function to authorize an api request.
41 *
42 * The function is meant to be used as part of permission callbacks for rest api endpoints.
43 *
44 * It verifies the signature of POST, PUT, PATCH, and DELETE requests, as well as GET requests in secure mode.
45 * You can use the filter 'activitypub_defer_signature_verification' to defer the signature verification.
46 * HEAD requests are always bypassed.
47 *
48 * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
49 * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
50 *
51 * @param \WP_REST_Request $request The request object.
52 *
53 * @return bool|\WP_Error True if the request is authorized, WP_Error if not.
54 */
55 public static function verify_signature( $request ) {
56 if ( 'HEAD' === $request->get_method() ) {
57 return true;
58 }
59
60 /**
61 * Filter to defer signature verification.
62 *
63 * Skip signature verification for debugging purposes or to reduce load for
64 * certain Activity-Types, like "Delete".
65 *
66 * @param bool $defer Whether to defer signature verification.
67 * @param \WP_REST_Request $request The request used to generate the response.
68 *
69 * @return bool Whether to defer signature verification.
70 */
71 $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
72
73 if ( $defer ) {
74 return true;
75 }
76
77 // POST-Requests always have to be signed, GET-Requests only require a signature in secure mode.
78 if ( 'GET' !== $request->get_method() || use_authorized_fetch() ) {
79 $verified_request = Signature::verify_http_signature( $request );
80 if ( \is_wp_error( $verified_request ) ) {
81 return new \WP_Error(
82 'activitypub_signature_verification',
83 $verified_request->get_error_message(),
84 array( 'status' => 401 )
85 );
86 }
87 }
88
89 return true;
90 }
91
92 /**
93 * Callback function to validate incoming ActivityPub requests
94 *
95 * @param \WP_REST_Response|\WP_HTTP_Response|\WP_Error|mixed $response Result to send to the client.
96 * Usually a WP_REST_Response or WP_Error.
97 * @param array $handler Route handler used for the request.
98 * @param \WP_REST_Request $request Request used to generate the response.
99 *
100 * @return mixed|\WP_Error The response, error, or modified response.
101 */
102 public static function validate_requests( $response, $handler, $request ) {
103 if ( 'HEAD' === $request->get_method() ) {
104 return $response;
105 }
106
107 $route = $request->get_route();
108
109 if (
110 \is_wp_error( $response ) ||
111 ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
112 ) {
113 return $response;
114 }
115
116 $params = $request->get_json_params();
117
118 // Type is required for ActivityPub requests, so it fail later in the process.
119 if ( ! isset( $params['type'] ) ) {
120 return $response;
121 }
122
123 if (
124 ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
125 in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
126 ) {
127 return new \WP_Error(
128 'activitypub_server_does_not_accept_incoming_interactions',
129 \__( 'This server does not accept incoming interactions.', 'activitypub' ),
130 // We have to use a 2XX status code here, because otherwise the response will be
131 // treated as an error and Mastodon might block this WordPress instance.
132 array( 'status' => 202 )
133 );
134 }
135
136 return $response;
137 }
138
139 /**
140 * Modify the parameter priority order for a REST API request.
141 *
142 * @param string[] $order Array of types to check, in order of priority.
143 * @param \WP_REST_Request $request The request object.
144 *
145 * @return string[] The modified order of types to check.
146 */
147 public static function request_parameter_order( $order, $request ) {
148 $route = $request->get_route();
149
150 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
151 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
152 return $order;
153 }
154
155 $method = $request->get_method();
156
157 if ( \WP_REST_Server::CREATABLE !== $method ) {
158 return $order;
159 }
160
161 return array(
162 'JSON',
163 'POST',
164 'URL',
165 'defaults',
166 );
167 }
168
169 /**
170 * Filters the REST API response to properly handle the ActivityPub error formatting.
171 *
172 * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/c180/fep-c180.md
173 *
174 * @param \WP_HTTP_Response $response Result to send to the client. Usually a `WP_REST_Response`.
175 * @param \WP_REST_Server $server Server instance.
176 * @param \WP_REST_Request $request Request used to generate the response.
177 *
178 * @return \WP_HTTP_Response The filtered response.
179 */
180 public static function filter_output( $response, $server, $request ) {
181 $route = $request->get_route();
182
183 // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints.
184 if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) {
185 return $response;
186 }
187
188 // Only alter responses that return an error status code.
189 if ( $response->get_status() < 400 ) {
190 return $response;
191 }
192
193 $data = $response->get_data();
194
195 // Ensure that `$data` was already converted to a response.
196 if ( \is_wp_error( $data ) ) {
197 $response = \rest_convert_error_to_response( $data );
198 $data = $response->get_data();
199 }
200
201 $error = array(
202 'type' => 'about:blank',
203 'title' => $data['code'] ?? '',
204 'detail' => $data['message'] ?? '',
205 'status' => $response->get_status(),
206
207 /*
208 * Provides the unstructured error data.
209 *
210 * @see https://nodeinfo.diaspora.software/schema.html#metadata.
211 */
212 'metadata' => $data,
213 );
214
215 $response->set_data( $error );
216
217 return $response;
218 }
219 }
220