PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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 +160 -76 2.1.17.7.0 View file →
@@ -1,15 +1,19 @@
1 1 <?php
2 +/**
3 + * Server REST-Class file.
4 + *
5 + * @package Activitypub
6 + */
7 +
2 8 namespace Activitypub\Rest;
3 9
4 -use stdClass;
5 -use WP_Error;
6 -use WP_REST_Response;
7 10 use Activitypub\Signature;
8 -use Activitypub\Model\Application_User;
9 11
12 +use function Activitypub\use_authorized_fetch;
13 +
10 14 /**
11 - * ActivityPub Server REST-Class
15 + * ActivityPub Server REST-Class.
12 16 *
13 17 * @author Django Doucet
14 18 *
15 19 * @see https://www.w3.org/TR/activitypub/#security-verification
@@ -15,85 +19,46 @@
15 19 * @see https://www.w3.org/TR/activitypub/#security-verification
16 20 */
17 21 class Server {
18 22 /**
19 - * Initialize the class, registering WordPress hooks
23 + * Initialize the class, registering WordPress hooks.
20 24 */
21 25 public static function init() {
22 - self::register_routes();
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 );
23 28
24 - \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
29 + \add_filter( 'rest_post_dispatch', array( self::class, 'filter_output' ), 10, 3 );
25 30 }
26 31
27 32 /**
28 - * Register routes
29 - */
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 - );
42 - }
43 -
44 - /**
45 - * Render Application actor profile
33 + * Callback function to authorize an api request.
46 34 *
47 - * @return WP_REST_Response The JSON profile of the Application Actor.
48 - */
49 - public static function application_actor() {
50 - $user = new Application_User();
51 -
52 - $json = $user->to_array();
53 -
54 - $rest_response = new WP_REST_Response( $json, 200 );
55 - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
56 -
57 - return $rest_response;
58 - }
59 -
60 - /**
61 - * Callback function to authorize each api requests
35 + * The function is meant to be used as part of permission callbacks for rest api endpoints.
62 36 *
63 - * @see WP_REST_Request
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.
64 40 *
65 - * @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
66 - * Usually a WP_REST_Response or WP_Error.
67 - * @param array $handler Route handler used for the request.
68 - * @param WP_REST_Request $request Request used to generate the response.
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
69 43 *
70 - * @return mixed|WP_Error The response, error, or modified response.
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.
71 47 */
72 - public static function authorize_activitypub_requests( $response, $handler, $request ) {
48 + public static function verify_signature( $request ) {
73 49 if ( 'HEAD' === $request->get_method() ) {
74 - return $response;
50 + return true;
75 51 }
76 52
77 - $route = $request->get_route();
78 -
79 - // check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
80 - if (
81 - ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
82 - \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
83 - \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' )
84 - ) {
85 - return $response;
86 - }
87 -
88 53 /**
89 - * Filter to defer signature verification
54 + * Filter to defer signature verification.
90 55 *
91 56 * Skip signature verification for debugging purposes or to reduce load for
92 57 * certain Activity-Types, like "Delete".
93 58 *
94 - * @param bool $defer Whether to defer signature verification.
95 - * @param WP_REST_Request $request The request used to generate the response.
59 + * @param bool $defer Whether to defer signature verification.
60 + * @param \WP_REST_Request $request The request used to generate the response.
96 61 *
97 62 * @return bool Whether to defer signature verification.
98 63 */
99 64 $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
@@ -98,31 +63,150 @@
98 63 */
99 64 $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
100 65
101 66 if ( $defer ) {
102 - return $response;
67 + return true;
103 68 }
104 69
105 - // POST-Requets are always signed
106 - if ( 'GET' !== $request->get_method() ) {
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() ) {
107 72 $verified_request = Signature::verify_http_signature( $request );
108 73 if ( \is_wp_error( $verified_request ) ) {
109 - return new WP_Error(
74 + return new \WP_Error(
110 75 'activitypub_signature_verification',
111 76 $verified_request->get_error_message(),
112 77 array( 'status' => 401 )
113 78 );
114 79 }
115 - } elseif ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) { // GET-Requests are only signed in secure mode
116 - $verified_request = Signature::verify_http_signature( $request );
117 - if ( \is_wp_error( $verified_request ) ) {
118 - return new WP_Error(
119 - 'activitypub_signature_verification',
120 - $verified_request->get_error_message(),
121 - array( 'status' => 401 )
122 - );
123 - }
124 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 );
125 209
126 210 return $response;
127 211 }
128 212 }