PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
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
mainwp / includes / rest-api / controller / version2 / class-mainwp-rest-global-batch-controller.php

class-mainwp-rest-global-batch-controller.php in MainWP Dashboard: Self-hosted WordPress Management for Agencies 6.2.2, at includes/rest-api/controller/version2/class-mainwp-rest-global-batch-controller.php

663 lines 29.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP REST Controller
4 *
5 * This class handles the REST API
6 *
7 * @package MainWP\Dashboard
8 */
9
10 // Exit if accessed directly.
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Class MainWP_Rest_Global_Batch_Controller
17 *
18 * @package MainWP\Dashboard
19 */
20 class MainWP_Rest_Global_Batch_Controller extends MainWP_REST_Controller{ //phpcs:ignore -- NOSONAR - multi methods.
21
22 /**
23 * Protected static variable to hold the single instance of the class.
24 *
25 * @var mixed Default null
26 */
27 private static $instance = null;
28
29 /**
30 * Endpoint namespace.
31 *
32 * @var string
33 */
34 protected $namespace = 'mainwp/v2';
35
36 /**
37 * Route base.
38 *
39 * @var string
40 */
41 protected $rest_base = 'batch';
42
43 /**
44 * Controller names handled by the batch endpoint.
45 *
46 * @var array
47 */
48 protected $controller_names = array( 'sites', 'clients', 'tags' );
49
50 /**
51 * Actions the dispatch below reads per group, each marked with the shape it reads its items in:
52 * an 'item' action passes every item on as a request body, an 'id' action casts every item to an int.
53 *
54 * @var array
55 */
56 const GROUP_ACTIONS = array(
57 'sites' => array(
58 'create' => 'item',
59 'sync' => 'id',
60 'reconnect' => 'id',
61 'disconnect' => 'id',
62 'suspend' => 'id',
63 'check' => 'id',
64 'remove' => 'id',
65 'security' => 'id',
66 'plugins' => 'id',
67 'themes' => 'id',
68 'non-mainwp-changes' => 'id',
69 ),
70 'clients' => array(
71 'create' => 'item',
72 ),
73 'tags' => array(
74 'create' => 'item',
75 ),
76 );
77
78 /**
79 * Method instance()
80 *
81 * Create public static instance.
82 *
83 * @static
84 * @return static::$instance
85 */
86 public static function instance() {
87 if ( null === static::$instance ) {
88 static::$instance = new self();
89 }
90 return static::$instance;
91 }
92
93 /**
94 * Method register_routes()
95 *
96 * Creates the necessary endpoints for the api.
97 * Note, for a request to be successful the URL query parameters consumer_key and consumer_secret need to be set and correct.
98 */
99 public function register_routes() { // phpcs:ignore -- NOSONAR - complex.
100 register_rest_route(
101 $this->namespace,
102 '/' . $this->rest_base,
103 array(
104 array(
105 'methods' => WP_REST_Server::EDITABLE,
106 'callback' => array( $this, 'batch_items' ),
107 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
108 ),
109 )
110 );
111 }
112
113 /**
114 * Resolve a batch group's controller.
115 *
116 * Module controllers register under a namespace key of their own
117 * (the Cost Tracker uses mainwp/v2/costs), so a miss on the shared
118 * namespace is retried under the group's key before it is treated
119 * as an unregistered group.
120 *
121 * @param string $con_name Group name.
122 * @return MainWP_REST_Controller|false
123 */
124 private function get_batch_controller( $con_name ) {
125 $server = MainWP_Rest_Server::instance();
126 $controller = $server->get_rest_controller( $this->namespace, $con_name );
127 if ( false === $controller ) {
128 $controller = $server->get_rest_controller( $this->namespace . '/' . $con_name, $con_name );
129 }
130 return $controller;
131 }
132
133 /**
134 * Bulk create, update and delete items.
135 *
136 * @param WP_REST_Request $request Full details about the request.
137 * @return array Of WP_Error or WP_REST_Response.
138 */
139 public function batch_items( $request ) { //phpcs:ignore -- NOSONAR complex function.
140 /**
141 * REST Server
142 *
143 * @var WP_REST_Server $wp_rest_server
144 */
145 global $wp_rest_server;
146
147 // Get the request params.
148 $all_params = $request->get_params();
149 $items = array_filter( $all_params );
150 $query = $request->get_query_params();
151 $response = array();
152
153 $body_groups = $request->get_json_params();
154 if ( empty( $body_groups ) || ! is_array( $body_groups ) ) {
155 $body_groups = (array) $request->get_body_params();
156 }
157
158 // Groups the batch endpoint cannot dispatch (updates has no batch-capable create handler,
159 // costs has no controller at all, anything else is unknown) are reported once each instead
160 // of being dropped without a word or failing per item with a 405 from the core stub.
161 //
162 // The names come from the same merged params the limit check and the dispatch read, so a
163 // group sent only in the query string is answered rather than counted in silence. A group is
164 // always an array there, so scalar params (per_page, the auth keys) name none. The body is
165 // read on top of that because a falsy group is filtered out of the items, and because for an
166 // unsupported name the value does not matter, so a scalar body group is reported too.
167 // Whichever source it came from, an underscore parameter is one WordPress reserves for the
168 // REST server itself (_fields, _embed, _locale) and never a group the caller named.
169 $groups = array();
170 foreach ( $items as $param_name => $param_value ) {
171 if ( is_array( $param_value ) && 0 !== strpos( (string) $param_name, '_' ) ) {
172 $groups[ $param_name ] = true;
173 }
174 }
175 foreach ( array_keys( $body_groups ) as $body_group_name ) {
176 if ( 0 !== strpos( (string) $body_group_name, '_' ) ) {
177 $groups[ $body_group_name ] = true;
178 }
179 }
180
181 foreach ( array_keys( $groups ) as $group_name ) {
182 if ( ! in_array( $group_name, $this->controller_names, true ) ) {
183 $response[ $group_name ] = array(
184 'error' => array(
185 'code' => 'rest_batch_group_not_supported',
186 /* translators: %s: batch group name */
187 'message' => sprintf( __( 'The %s group is not supported by the batch endpoint.', 'mainwp' ), $group_name ),
188 'data' => array( 'status' => 400 ),
189 ),
190 );
191 }
192 }
193
194 // Shapes are checked against the items the dispatch below reads, not against the body, so a
195 // group sent as a query parameter cannot skip the check. The group is dropped from the items
196 // as well as reported, so nothing in it is dispatched.
197 foreach ( $this->controller_names as $group_name ) {
198 if ( isset( $items[ $group_name ] ) ) {
199 $group_items = $items[ $group_name ];
200 } elseif ( array_key_exists( $group_name, $body_groups ) ) {
201 // A falsy group is filtered out of the items, so it is read back from the body to be
202 // reported rather than passed over.
203 $group_items = $body_groups[ $group_name ];
204 } elseif ( array_key_exists( $group_name, $all_params ) ) {
205 // A falsy group sent only in the query string is in neither of the above, and
206 // skipping it answered the whole request with an empty success.
207 $group_items = $all_params[ $group_name ];
208 } else {
209 continue;
210 }
211
212 if ( ! $this->is_dispatchable_group( $group_name, $group_items ) ) {
213 $response[ $group_name ] = array(
214 'error' => array(
215 'code' => 'rest_invalid_param',
216 /* translators: %s: batch group name */
217 'message' => sprintf( __( 'The %s group must be an object of supported action arrays.', 'mainwp' ), $group_name ),
218 'data' => array( 'status' => 400 ),
219 ),
220 );
221 unset( $items[ $group_name ] );
222 }
223 }
224
225 // Counted after the malformed groups are dropped: nothing in them is dispatched, so their items
226 // must not be what pushes a request over the cap and hides the per-group error behind a 413.
227 $limit = $this->check_batch_limit( $items );
228 if ( is_wp_error( $limit ) ) {
229 return $limit;
230 }
231
232 foreach ( $this->controller_names as $con_name ) {
233
234 $controller_obj = $this->get_batch_controller( $con_name );
235
236 if ( false === $controller_obj ) {
237 continue;
238 }
239
240 if ( ! empty( $items[ $con_name ]['create'] ) ) {
241 foreach ( $items[ $con_name ]['create'] as $item ) {
242 // Default parameters.
243 $defaults = array();
244 $schema = $controller_obj->get_public_item_schema();
245
246 foreach ( $schema['properties'] as $arg => $options ) {
247 if ( isset( $options['default'] ) ) {
248 $defaults[ $arg ] = $options['default'];
249 }
250 }
251
252 $_item = $this->prepare_batch_create_request( $request->get_route(), $item, $controller_obj->get_batch_create_args(), $defaults, $query );
253 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->create_item( $_item );
254
255 if ( is_wp_error( $_response ) ) {
256 $response[ $con_name ]['create'][] = array(
257 'id' => 0,
258 'error' => array(
259 'code' => $_response->get_error_code(),
260 'message' => $_response->get_error_message(),
261 'data' => $_response->get_error_data(),
262 ),
263 );
264 } else {
265 $response[ $con_name ]['create'][] = $wp_rest_server->response_to_data( $_response, '' );
266 }
267 }
268 }
269
270 // check_batch_limit() counts update and delete items for every group, so they
271 // are dispatched here the same way the per-controller batch route dispatches them.
272 if ( ! empty( $items[ $con_name ]['update'] ) ) {
273 foreach ( $items[ $con_name ]['update'] as $item ) {
274 $_item = $this->prepare_batch_update_request( $request->get_route(), $item, $controller_obj->get_batch_update_args() );
275 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->update_item( $_item );
276
277 if ( is_wp_error( $_response ) ) {
278 $response[ $con_name ]['update'][] = array(
279 'id' => is_array( $item ) ? $this->batch_error_id( $item['id'] ?? 0 ) : 0,
280 'error' => array(
281 'code' => $_response->get_error_code(),
282 'message' => $_response->get_error_message(),
283 'data' => $_response->get_error_data(),
284 ),
285 );
286 } else {
287 $response[ $con_name ]['update'][] = $wp_rest_server->response_to_data( $_response, '' );
288 }
289 }
290 }
291
292 if ( ! empty( $items[ $con_name ]['delete'] ) ) {
293 foreach ( $items[ $con_name ]['delete'] as $id ) {
294 $_item = $this->prepare_batch_id_request( $request->get_route(), $id, array( 'force' => true ), $controller_obj->get_batch_delete_args() );
295 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->delete_item( $_item );
296
297 if ( is_wp_error( $_response ) ) {
298 $response[ $con_name ]['delete'][] = array(
299 'id' => $this->batch_error_id( $id ),
300 'error' => array(
301 'code' => $_response->get_error_code(),
302 'message' => $_response->get_error_message(),
303 'data' => $_response->get_error_data(),
304 ),
305 );
306 } else {
307 $response[ $con_name ]['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
308 }
309 }
310 }
311 }
312
313 $con_name = 'sites';
314
315 $controller_obj = MainWP_Rest_Server::instance()->get_rest_controller( $this->namespace, $con_name );
316
317 if ( $controller_obj ) {
318 if ( ! empty( $items[ $con_name ]['sync'] ) ) {
319 foreach ( $items[ $con_name ]['sync'] as $id ) {
320 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
321 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->sync_item( $_item );
322
323 if ( is_wp_error( $_response ) ) {
324 $response[ $con_name ]['sync'][] = array(
325 'id' => $this->batch_error_id( $id ),
326 'error' => array(
327 'code' => $_response->get_error_code(),
328 'message' => $_response->get_error_message(),
329 'data' => $_response->get_error_data(),
330 ),
331 );
332 } else {
333 $response[ $con_name ]['sync'][] = $wp_rest_server->response_to_data( $_response, '' );
334 }
335 }
336 }
337
338 if ( ! empty( $items[ $con_name ]['reconnect'] ) ) {
339 foreach ( $items[ $con_name ]['reconnect'] as $id ) {
340 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
341 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->reconnect_item( $_item );
342
343 if ( is_wp_error( $_response ) ) {
344 $response[ $con_name ]['reconnect'][] = array(
345 'id' => $this->batch_error_id( $id ),
346 'error' => array(
347 'code' => $_response->get_error_code(),
348 'message' => $_response->get_error_message(),
349 'data' => $_response->get_error_data(),
350 ),
351 );
352 } else {
353 $response[ $con_name ]['reconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
354 }
355 }
356 }
357
358 if ( ! empty( $items[ $con_name ]['disconnect'] ) ) {
359 foreach ( $items[ $con_name ]['disconnect'] as $id ) {
360 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
361 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->disconnect_site( $_item );
362
363 if ( is_wp_error( $_response ) ) {
364 $response[ $con_name ]['disconnect'][] = array(
365 'id' => $this->batch_error_id( $id ),
366 'error' => array(
367 'code' => $_response->get_error_code(),
368 'message' => $_response->get_error_message(),
369 'data' => $_response->get_error_data(),
370 ),
371 );
372 } else {
373 $response[ $con_name ]['disconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
374 }
375 }
376 }
377
378 if ( ! empty( $items[ $con_name ]['suspend'] ) ) {
379 foreach ( $items[ $con_name ]['suspend'] as $id ) {
380 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
381 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->suspend_item( $_item );
382
383 if ( is_wp_error( $_response ) ) {
384 $response[ $con_name ]['suspend'][] = array(
385 'id' => $this->batch_error_id( $id ),
386 'error' => array(
387 'code' => $_response->get_error_code(),
388 'message' => $_response->get_error_message(),
389 'data' => $_response->get_error_data(),
390 ),
391 );
392 } else {
393 $response[ $con_name ]['suspend'][] = $wp_rest_server->response_to_data( $_response, '' );
394 }
395 }
396 }
397
398 if ( ! empty( $items[ $con_name ]['check'] ) ) {
399 foreach ( $items[ $con_name ]['check'] as $id ) {
400 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
401 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->check_item( $_item );
402
403 if ( is_wp_error( $_response ) ) {
404 $response[ $con_name ]['check'][] = array(
405 'id' => $this->batch_error_id( $id ),
406 'error' => array(
407 'code' => $_response->get_error_code(),
408 'message' => $_response->get_error_message(),
409 'data' => $_response->get_error_data(),
410 ),
411 );
412 } else {
413 $response[ $con_name ]['check'][] = $wp_rest_server->response_to_data( $_response, '' );
414 }
415 }
416 }
417
418 if ( ! empty( $items[ $con_name ]['remove'] ) ) {
419 foreach ( $items[ $con_name ]['remove'] as $id ) {
420 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
421 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->delete_item( $_item );
422
423 if ( is_wp_error( $_response ) ) {
424 $response[ $con_name ]['remove'][] = array(
425 'id' => $this->batch_error_id( $id ),
426 'error' => array(
427 'code' => $_response->get_error_code(),
428 'message' => $_response->get_error_message(),
429 'data' => $_response->get_error_data(),
430 ),
431 );
432 } else {
433 $response[ $con_name ]['remove'][] = $wp_rest_server->response_to_data( $_response, '' );
434 }
435 }
436 }
437
438 if ( ! empty( $items[ $con_name ]['security'] ) ) {
439 foreach ( $items[ $con_name ]['security'] as $id ) {
440 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
441 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->security_item( $_item );
442
443 if ( is_wp_error( $_response ) ) {
444 $response[ $con_name ]['security'][] = array(
445 'id' => $this->batch_error_id( $id ),
446 'error' => array(
447 'code' => $_response->get_error_code(),
448 'message' => $_response->get_error_message(),
449 'data' => $_response->get_error_data(),
450 ),
451 );
452 } else {
453 $response[ $con_name ]['security'][] = $wp_rest_server->response_to_data( $_response, '' );
454 }
455 }
456 }
457
458 if ( ! empty( $items[ $con_name ]['plugins'] ) ) {
459 foreach ( $items[ $con_name ]['plugins'] as $id ) {
460 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
461 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->get_site_plugins( $_item );
462
463 if ( is_wp_error( $_response ) ) {
464 $response[ $con_name ]['plugins'][] = array(
465 'id' => $this->batch_error_id( $id ),
466 'error' => array(
467 'code' => $_response->get_error_code(),
468 'message' => $_response->get_error_message(),
469 'data' => $_response->get_error_data(),
470 ),
471 );
472 } else {
473 $response[ $con_name ]['plugins'][] = $wp_rest_server->response_to_data( $_response, '' );
474 }
475 }
476 }
477
478 if ( ! empty( $items[ $con_name ]['themes'] ) ) {
479 foreach ( $items[ $con_name ]['themes'] as $id ) {
480 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
481 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->get_site_themes( $_item );
482
483 if ( is_wp_error( $_response ) ) {
484 $response[ $con_name ]['themes'][] = array(
485 'id' => $this->batch_error_id( $id ),
486 'error' => array(
487 'code' => $_response->get_error_code(),
488 'message' => $_response->get_error_message(),
489 'data' => $_response->get_error_data(),
490 ),
491 );
492 } else {
493 $response[ $con_name ]['themes'][] = $wp_rest_server->response_to_data( $_response, '' );
494 }
495 }
496 }
497
498 if ( ! empty( $items[ $con_name ]['non-mainwp-changes'] ) ) {
499 foreach ( $items[ $con_name ]['non-mainwp-changes'] as $id ) {
500 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
501 $_response = is_wp_error( $_item ) ? $_item : $controller_obj->get_non_mainwp_changes_of_site( $_item );
502
503 if ( is_wp_error( $_response ) ) {
504 $response[ $con_name ]['non-mainwp-changes'][] = array(
505 'id' => $this->batch_error_id( $id ),
506 'error' => array(
507 'code' => $_response->get_error_code(),
508 'message' => $_response->get_error_message(),
509 'data' => $_response->get_error_data(),
510 ),
511 );
512 } else {
513 $response[ $con_name ]['non-mainwp-changes'][] = $wp_rest_server->response_to_data( $_response, '' );
514 }
515 }
516 }
517 }
518
519 return $response;
520 }
521
522
523 /**
524 * Check that a group carries only what the dispatch below can read.
525 *
526 * The dispatch indexes the group by action name and then walks each action list, so a scalar in
527 * either place would reach a foreach over something that is not a list, a JSON list carries none
528 * of the names it indexes by, and an action it does not read is silently dropped. Items are
529 * checked too: a create item is passed on as a request body and an id item is cast to an int, so
530 * neither can be an arbitrary value.
531 *
532 * @param string $group_name Group name.
533 * @param mixed $group_items Group value taken from the request.
534 * @return bool
535 */
536 private function is_dispatchable_group( $group_name, $group_items ) {
537 if ( ! is_array( $group_items ) ) {
538 return false;
539 }
540
541 // An empty group dispatches nothing and is not the caller getting the shape wrong. It is
542 // taken out of the list test as well, which range() cannot answer for a count of zero.
543 if ( array() === $group_items ) {
544 return true;
545 }
546
547 if ( array_keys( $group_items ) === range( 0, count( $group_items ) - 1 ) ) {
548 return false;
549 }
550
551 $actions = isset( self::GROUP_ACTIONS[ $group_name ] ) ? self::GROUP_ACTIONS[ $group_name ] : array();
552
553 foreach ( $group_items as $action_name => $action_items ) {
554 if ( ! isset( $actions[ $action_name ] ) || ! is_array( $action_items ) ) {
555 return false;
556 }
557
558 // The dispatch walks an action as a list, so an object of named items would be
559 // dispatched by its values with the names dropped and nothing said about the shape. An
560 // empty action dispatches nothing, and range() cannot answer for a count of zero.
561 if ( array() !== $action_items && array_keys( $action_items ) !== range( 0, count( $action_items ) - 1 ) ) {
562 return false;
563 }
564
565 foreach ( $action_items as $item ) {
566 if ( 'item' === $actions[ $action_name ] ) {
567 if ( ! is_array( $item ) ) {
568 return false;
569 }
570 } elseif ( ! ( is_int( $item ) && $item > 0 ) && ! ( is_string( $item ) && ctype_digit( $item ) && (int) $item > 0 && ltrim( $item, '0' ) === (string) (int) $item ) ) {
571 // A fractional or exponent string is numeric, and the (int) cast below would read
572 // it as a whole id the caller never asked for, so only digits are accepted. Zero
573 // is refused with them: every site action skips an id that casts to 0, so the
574 // batch would answer with neither an operation nor an error for that item. A digit
575 // string past PHP_INT_MAX casts to PHP_INT_MAX, a different id than the one sent.
576 return false;
577 }
578 }
579 }
580
581 return true;
582 }
583
584 /**
585 * Check batch limit.
586 *
587 * @param array $items Request items.
588 * @return bool|WP_Error
589 */
590 protected function check_batch_limit( $items ) { //phpcs:ignore -- NOSONAR complex function.
591 $limit = apply_filters( 'mainwp_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
592 $total = 0;
593
594 // The updates and costs groups are rejected as a whole by batch_items(), but their items
595 // still count toward the cap so an oversized request is refused before anything else is
596 // dispatched.
597 $count_names = array_merge( $this->controller_names, array( 'updates', 'costs' ) );
598
599 foreach ( $count_names as $con_name ) {
600 if ( ! empty( $items[ $con_name ] ) && is_countable( $items[ $con_name ] ) ) {
601 if ( ! empty( $items[ $con_name ]['create'] ) && is_countable( $items[ $con_name ]['create'] ) ) {
602 $total += count( $items[ $con_name ]['create'] );
603 }
604
605 if ( ! empty( $items[ $con_name ]['update'] ) && is_countable( $items[ $con_name ]['update'] ) ) {
606 $total += count( $items[ $con_name ]['update'] );
607 }
608
609 if ( ! empty( $items[ $con_name ]['delete'] ) && is_countable( $items[ $con_name ]['delete'] ) ) {
610 $total += count( $items[ $con_name ]['delete'] );
611 }
612 }
613 }
614
615 if ( ! empty( $items['sites']['sync'] ) && is_countable( $items['sites']['sync'] ) ) {
616 $total += count( $items['sites']['sync'] );
617 }
618
619 if ( ! empty( $items['sites']['reconnect'] ) && is_countable( $items['sites']['reconnect'] ) ) {
620 $total += count( $items['sites']['reconnect'] );
621 }
622
623 if ( ! empty( $items['sites']['disconnect'] ) && is_countable( $items['sites']['disconnect'] ) ) {
624 $total += count( $items['sites']['disconnect'] );
625 }
626
627 if ( ! empty( $items['sites']['suspend'] ) && is_countable( $items['sites']['suspend'] ) ) {
628 $total += count( $items['sites']['suspend'] );
629 }
630
631 if ( ! empty( $items['sites']['check'] ) && is_countable( $items['sites']['check'] ) ) {
632 $total += count( $items['sites']['check'] );
633 }
634
635 if ( ! empty( $items['sites']['remove'] ) && is_countable( $items['sites']['remove'] ) ) {
636 $total += count( $items['sites']['remove'] );
637 }
638
639 if ( ! empty( $items['sites']['security'] ) && is_countable( $items['sites']['security'] ) ) {
640 $total += count( $items['sites']['security'] );
641 }
642
643 if ( ! empty( $items['sites']['plugins'] ) && is_countable( $items['sites']['plugins'] ) ) {
644 $total += count( $items['sites']['plugins'] );
645 }
646
647 if ( ! empty( $items['sites']['themes'] ) && is_countable( $items['sites']['themes'] ) ) {
648 $total += count( $items['sites']['themes'] );
649 }
650
651 if ( ! empty( $items['sites']['non-mainwp-changes'] ) && is_countable( $items['sites']['non-mainwp-changes'] ) ) {
652 $total += count( $items['sites']['non-mainwp-changes'] );
653 }
654
655 if ( $total > $limit ) {
656 /* translators: %s: items limit */
657 return new WP_Error( 'mainwp_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'mainwp' ), $limit ), array( 'status' => 413 ) );
658 }
659
660 return true;
661 }
662 }
663