PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | includes/abilities/class-mainwp-abilities.php +180 -0 6.1.4 → 6.2.1 View file →
@@ -19,8 +19,22 @@
19 19 */
20 20 class MainWP_Abilities {
21 21
22 22 /**
23 + * Maximum encoded GET input size.
24 + *
25 + * @var int
26 + */
27 + private const MAX_GET_INPUT_BYTES = 8192;
28 +
29 + /**
30 + * Maximum DELETE JSON body size.
31 + *
32 + * @var int
33 + */
34 + private const MAX_DELETE_INPUT_BYTES = 1048576;
35 +
36 + /**
23 37 * Initialize the Abilities integration.
24 38 *
25 39 * Cron handlers are always initialized to process any previously queued jobs,
26 40 * even if the Abilities API is later disabled. The cron handler constructor
@@ -39,8 +53,17 @@
39 53 if ( ! function_exists( 'wp_register_ability' ) ) {
40 54 return;
41 55 }
42 56
57 + if ( false === has_filter( 'rest_request_before_callbacks', array( static::class, 'normalize_rest_input_transport' ) ) ) {
58 + add_filter(
59 + 'rest_request_before_callbacks',
60 + array( static::class, 'normalize_rest_input_transport' ),
61 + 5,
62 + 3
63 + );
64 + }
65 +
43 66 // Hook into MainWP REST authentication to include Abilities API routes.
44 67 // This allows consumer_key/consumer_secret authentication to work for abilities.
45 68 add_filter(
46 69 'mainwp_rest_is_request_to_rest_api',
@@ -76,8 +99,165 @@
76 99 // Abilities API has its own permission_callback that checks current_user_can()
77 100 // which works with both MainWP API keys (via MainWP_REST_Authentication) and
78 101 // WordPress Application Passwords (via native WP REST auth).
79 102 return $is_mainwp_api;
103 + }
104 +
105 + /**
106 + * Restore typed JSON values before the Abilities API validates a request.
107 + *
108 + * WordPress reads GET and DELETE ability input only from query parameters.
109 + * Query parsing cannot distinguish JSON null from strings without a JSON
110 + * carrier, and credentials must not be placed in DELETE URLs. This adapter
111 + * is limited to MainWP ability run routes and leaves the legacy bracket-style
112 + * input query parameter unchanged when no JSON carrier is present.
113 + *
114 + * @param mixed $response Result to send to the client, or null.
115 + * @param array $handler Matched REST route handler.
116 + * @param \WP_REST_Request $request Current REST request.
117 + * @return mixed
118 + */
119 + public static function normalize_rest_input_transport( $response, array $handler, \WP_REST_Request $request ) {
120 + unset( $handler );
121 +
122 + if ( null !== $response ) {
123 + return $response;
124 + }
125 +
126 + if ( ! preg_match( '#^/wp-abilities/v1/abilities/mainwp/[a-z0-9-]+/run$#', $request->get_route() ) ) {
127 + return $response;
128 + }
129 +
130 + $method = strtoupper( $request->get_method() );
131 + if ( 'GET' === $method ) {
132 + return self::normalize_get_input_transport( $response, $request );
133 + }
134 +
135 + if ( 'DELETE' === $method ) {
136 + return self::normalize_delete_input_transport( $response, $request );
137 + }
138 +
139 + return $response;
140 + }
141 +
142 + /**
143 + * Normalize the GET input_json query carrier.
144 + *
145 + * @param mixed $response Result to send to the client, or null.
146 + * @param \WP_REST_Request $request Current REST request.
147 + * @return mixed
148 + */
149 + private static function normalize_get_input_transport( $response, \WP_REST_Request $request ) {
150 + $query = $request->get_query_params();
151 + if ( ! array_key_exists( 'input_json', $query ) ) {
152 + return $response;
153 + }
154 +
155 + if ( array_key_exists( 'input', $query ) ) {
156 + return self::invalid_input_transport();
157 + }
158 +
159 + $input = self::decode_input_object( $query['input_json'], self::MAX_GET_INPUT_BYTES );
160 + if ( is_wp_error( $input ) ) {
161 + return $input;
162 + }
163 +
164 + unset( $query['input_json'] );
165 + $query['input'] = $input;
166 + $request->set_query_params( $query );
167 +
168 + return $response;
169 + }
170 +
171 + /**
172 + * Normalize the DELETE application/json body carrier.
173 + *
174 + * @param mixed $response Result to send to the client, or null.
175 + * @param \WP_REST_Request $request Current REST request.
176 + * @return mixed
177 + */
178 + private static function normalize_delete_input_transport( $response, \WP_REST_Request $request ) {
179 + $body = $request->get_body();
180 + $query = $request->get_query_params();
181 +
182 + if ( '' === trim( $body ) ) {
183 + if ( array_key_exists( 'input_json', $query ) ) {
184 + return self::invalid_input_transport();
185 + }
186 + return $response;
187 + }
188 +
189 + if ( array_key_exists( 'input', $query ) || array_key_exists( 'input_json', $query ) ) {
190 + return self::invalid_input_transport();
191 + }
192 +
193 + $content_type = $request->get_content_type();
194 + if ( ! is_array( $content_type ) || 'application/json' !== $content_type['value'] ) {
195 + return self::invalid_input_transport();
196 + }
197 +
198 + if ( strlen( $body ) > self::MAX_DELETE_INPUT_BYTES ) {
199 + return self::invalid_input_transport();
200 + }
201 +
202 + $shape = json_decode( $body, false, 32 );
203 + $decoded = json_decode( $body, true, 32 );
204 + if (
205 + JSON_ERROR_NONE !== json_last_error()
206 + || ! $shape instanceof \stdClass
207 + || array( 'input' ) !== array_keys( get_object_vars( $shape ) )
208 + || ! $shape->input instanceof \stdClass
209 + || ! is_array( $decoded )
210 + ) {
211 + return self::invalid_input_transport();
212 + }
213 +
214 + $input = $decoded['input'];
215 + if ( ! is_array( $input ) ) {
216 + return self::invalid_input_transport();
217 + }
218 +
219 + $query['input'] = $input;
220 + $request->set_query_params( $query );
221 +
222 + return $response;
223 + }
224 +
225 + /**
226 + * Decode a bounded JSON object without coercing scalar values.
227 + *
228 + * @param mixed $raw Raw JSON value.
229 + * @param int $max_bytes Maximum encoded size.
230 + * @return array|\WP_Error
231 + */
232 + private static function decode_input_object( $raw, int $max_bytes ) {
233 + if ( ! is_string( $raw ) || strlen( $raw ) > $max_bytes ) {
234 + return self::invalid_input_transport();
235 + }
236 +
237 + $decoded = json_decode( $raw, true, 32 );
238 + if (
239 + JSON_ERROR_NONE !== json_last_error()
240 + || ! is_array( $decoded )
241 + || '{' !== substr( ltrim( $raw ), 0, 1 )
242 + ) {
243 + return self::invalid_input_transport();
244 + }
245 +
246 + return $decoded;
247 + }
248 +
249 + /**
250 + * Return the closed, non-reflective transport error.
251 + *
252 + * @return \WP_Error
253 + */
254 + private static function invalid_input_transport(): \WP_Error {
255 + return new \WP_Error(
256 + 'mainwp_abilities_invalid_input_transport',
257 + __( 'The ability input transport is invalid.', 'mainwp' ),
258 + array( 'status' => 400 )
259 + );
80 260 }
81 261
82 262 /**
83 263 * Register MainWP ability categories.