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
mainwp / includes / rest-api / controller / version2 / class-mainwp-rest-controller.php

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

2,024 lines 75.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * REST Controller
4 *
5 * This class extend `WP_REST_Controller` in order to include /batch endpoint
6 * for almost all endpoints in MainWP REST API.
7 *
8 * It's required to follow "Controller Classes" guide before extending this class:
9 * <https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/>
10 *
11 * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS.
12 *
13 * ## Authentication Pattern
14 *
15 * All MainWP REST API v2 routes MUST be protected using the shared `get_rest_permissions_check()`
16 * method as their `permission_callback`. This method:
17 *
18 * 1. Calls `MainWP_REST_Authentication::is_valid_permissions()` to validate API key authentication
19 * 2. Returns `WP_Error` with 401 status if authentication fails or user is null
20 * 3. Returns `true` if the authenticated user has appropriate API key permissions
21 *
22 * Example route registration:
23 * ```php
24 * register_rest_route( $this->namespace, '/' . $this->rest_base, array(
25 * array(
26 * 'methods' => WP_REST_Server::READABLE,
27 * 'callback' => array( $this, 'get_items' ),
28 * 'permission_callback' => array( $this, 'get_rest_permissions_check' ), // REQUIRED
29 * ),
30 * ) );
31 * ```
32 *
33 * IMPORTANT: Do NOT use `__return_true` or skip `permission_callback` for protected endpoints.
34 * All endpoints intended for API key access must use `get_rest_permissions_check`.
35 *
36 * @class MainWP_REST_Controller
37 * @package MainWP\Dashboard
38 * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/
39 */
40
41 if ( ! defined( 'ABSPATH' ) ) {
42 exit;
43 }
44
45 use MainWP\Dashboard\MainWP_DB;
46 use MainWP\Dashboard\MainWP_DB_Client;
47 use MainWP\Dashboard\MainWP_Utility;
48 use MainWP\Dashboard\MainWP_System_Utility;
49 use MainWP\Dashboard\MainWP_Connect;
50 use MainWP\Dashboard\MainWP_Exception;
51 use MainWP\Dashboard\MainWP_Error_Helper;
52
53 /**
54 * Abstract Rest Controller Class
55 *
56 * @package MainWP\Dashboard
57 * @extends WP_REST_Controller
58 * @version 5.2
59 */
60 abstract class MainWP_REST_Controller extends WP_REST_Controller { //phpcs:ignore -- NOSONAR - maximumMethodThreshold.
61 // phpcs:disable Generic.Metrics.CyclomaticComplexity -- complexity.
62 /**
63 * Endpoint namespace.
64 *
65 * @var string
66 */
67 protected $namespace = 'mainwp/v2';
68
69 /**
70 * Route base.
71 *
72 * @var string
73 */
74 protected $rest_base = '';
75
76 /**
77 * Used to cache computed return fields.
78 *
79 * @var null|array
80 */
81 private $_fields = null; //phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
82
83 /**
84 * Used to verify if cached fields are for correct request object.
85 *
86 * @var null|WP_REST_Request
87 */
88 private $_request = null; //phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
89
90 /**
91 *
92 * Add the schema from additional fields to an schema array.
93 *
94 * The type of object is inferred from the passed schema.
95 *
96 * @param array $schema Schema array.
97 *
98 * @return array
99 */
100 protected function add_additional_fields_schema( $schema ) {
101 return $schema;
102 }
103
104 /**
105 * Get site item by id or domain.
106 *
107 * @param WP_REST_Request $request Full details about the request.
108 *
109 * @return WP_Error|Object Item.
110 */
111 public function get_site_item( $request ) {
112 $route = $request->get_route();
113 if ( MainWP_Utility::string_ends_by( $route, '/batch' ) ) {
114 $by = 'id';
115 $value = $request['id'];
116 } else {
117 $value = $request['id_domain'];
118 $by = 'domain';
119 if ( is_numeric( $value ) ) {
120 $by = 'id';
121 } else {
122 $value = urldecode( $value );
123 }
124 }
125 return $this->get_site_by( $by, $value );
126 }
127
128 /**
129 * Get site by.
130 *
131 * @param string $by Get by.
132 * @param mixed $value Site id or domain.
133 * @param array $args args.
134 *
135 * @return array|mixed Response data, ready for insertion into collection data.
136 */
137 public function get_client_by( $by, $value, $args = array() ) {
138 if ( 'id' === $by ) {
139 $_by = 'client_id';
140 } elseif ( 'email' === $by ) {
141 $_by = 'client_email';
142 }
143 $client = MainWP_DB_Client::instance()->get_wp_client_by( $_by, $value );
144 if ( empty( $client ) ) {
145 return $this->get_rest_data_error( $by, 'client' );
146 }
147 return $client;
148 }
149
150 /**
151 * Compatibility functions for WP 5.5, since custom types are not supported anymore.
152 * See @link https://core.trac.wordpress.org/changeset/48306
153 *
154 * @param string $method Optional. HTTP method of the request.
155 *
156 * @return array Endpoint arguments.
157 */
158 public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
159
160 $endpoint_args = $this->parent_get_endpoint_args_for_item_schema( $method );
161
162 if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) {
163 return $endpoint_args;
164 }
165
166 return $endpoint_args;
167 }
168
169 /**
170 * Retrieves an array of endpoint arguments from the item schema for the controller.
171 *
172 * @uses rest_get_endpoint_args_for_schema()
173 * @param string $method Optional. HTTP method of the request.
174 * @return array Endpoint arguments.
175 */
176 public function parent_get_endpoint_args_for_item_schema( $method = \WP_REST_Server::CREATABLE ) {
177 $schema = $this->get_item_schema();
178 $endpoint_args = rest_get_endpoint_args_for_schema( $schema, $method );
179 $endpoint_args = $this->remove_arg_options( $endpoint_args );
180 return $endpoint_args;
181 }
182
183 /**
184 * Recursive removal of arg_options.
185 *
186 * @param array $properties Schema properties.
187 */
188 protected function remove_arg_options( $properties ) {
189 return array_map(
190 function ( $property ) {
191 if ( isset( $property['properties'] ) ) {
192 $property['properties'] = $this->remove_arg_options( $property['properties'] );
193 } elseif ( isset( $property['items']['properties'] ) ) {
194 $property['items']['properties'] = $this->remove_arg_options( $property['items']['properties'] );
195 }
196 unset( $property['arg_options'] );
197 return $property;
198 },
199 (array) $properties
200 );
201 }
202
203 /**
204 * Get normalized rest base.
205 *
206 * @return string
207 */
208 protected function get_normalized_rest_base() {
209 return preg_replace( '/\(.*\)\//i', '', $this->rest_base );
210 }
211
212 /**
213 * Check batch limit.
214 *
215 * @param array $items Request items.
216 * @return bool|WP_Error
217 */
218 protected function check_batch_limit( $items ) { //phpcs:ignore -- NOSONAR - complex.
219 $limit = apply_filters( 'mainwp_rest_batch_items_limit', 100, $this->get_normalized_rest_base() );
220 $total = 0;
221
222 if ( ! empty( $items['create'] ) && is_countable( $items['create'] ) ) {
223 $total += count( $items['create'] );
224 }
225
226 if ( ! empty( $items['update'] ) && is_countable( $items['update'] ) ) {
227 $total += count( $items['update'] );
228 }
229
230 if ( ! empty( $items['delete'] ) && is_countable( $items['delete'] ) ) {
231 $total += count( $items['delete'] );
232 }
233
234 if ( ! empty( $items['sync'] ) && is_countable( $items['sync'] ) ) {
235 $total += count( $items['sync'] );
236 }
237
238 if ( ! empty( $items['reconnect'] ) && is_countable( $items['reconnect'] ) ) {
239 $total += count( $items['reconnect'] );
240 }
241
242 if ( ! empty( $items['disconnect'] ) && is_countable( $items['disconnect'] ) ) {
243 $total += count( $items['disconnect'] );
244 }
245
246 if ( ! empty( $items['suspend'] ) && is_countable( $items['suspend'] ) ) {
247 $total += count( $items['suspend'] );
248 }
249
250 if ( ! empty( $items['check'] ) && is_countable( $items['check'] ) ) {
251 $total += count( $items['check'] );
252 }
253
254 if ( ! empty( $items['remove'] ) && is_countable( $items['remove'] ) ) {
255 $total += count( $items['remove'] );
256 }
257
258 if ( ! empty( $items['security'] ) && is_countable( $items['security'] ) ) {
259 $total += count( $items['security'] );
260 }
261
262 if ( ! empty( $items['plugins'] ) && is_countable( $items['plugins'] ) ) {
263 $total += count( $items['plugins'] );
264 }
265
266 if ( ! empty( $items['themes'] ) && is_countable( $items['themes'] ) ) {
267 $total += count( $items['themes'] );
268 }
269
270 if ( ! empty( $items['non-mainwp-changes'] ) && is_countable( $items['non-mainwp-changes'] ) ) {
271 $total += count( $items['non-mainwp-changes'] );
272 }
273
274 if ( $total > $limit ) {
275 /* translators: %s: items limit */
276 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 ) );
277 }
278
279 return true;
280 }
281
282 /**
283 * Method get_validate_args_params().
284 *
285 * @param string $slug to validate args.
286 * @return array validate args info.
287 */
288 public function get_validate_args_params( $slug ) {
289
290 switch ( $slug ) {
291 case 'get_sites':
292 return array(
293 'status' => array( 'any', 'connected', 'disconnected', 'suspended', 'available_update' ),
294 );
295 case 'site_plugins':
296 case 'site_themes':
297 return array(
298 'status' => array( 'any', 'active', 'inactive' ),
299 );
300 case 'get_updates':
301 return array(
302 'type' => array( 'wp', 'plugins', 'themes', 'translations' ),
303 );
304 case 'get_costs':
305 return array(
306 'type' => array( 'any', 'subscription', 'lifetime' ),
307 );
308 default:
309 break;
310 }
311 return false;
312 }
313
314 /**
315 * Method validate_get_items_args().
316 *
317 * @param array $args args request.
318 * @param array $valid_args Valid args array.
319 *
320 * @return WP_Error|WP_REST_Response
321 */
322 public function validate_rest_args( $args, $valid_args ) { //phpcs:ignore -- NOSONAR - complex.
323
324 if ( ! is_array( $args ) ) {
325 return $args;
326 }
327
328 if ( empty( $valid_args ) || ! is_array( $valid_args ) ) {
329 return $args;
330 }
331
332 foreach ( $valid_args as $name => $valid_values ) {
333 if ( ! empty( $args[ $name ] ) ) {
334 $list_values = wp_parse_list( $args[ $name ] );
335 if ( ! empty( $list_values ) && is_array( $valid_values ) ) {
336 $filtered_values = array_map(
337 function ( $val ) use ( $valid_values ) {
338 return in_array( $val, $valid_values ) ? $val : '';
339 },
340 $list_values
341 );
342 $filtered_values = array_filter(
343 $filtered_values,
344 function ( $val ) {
345 return '' !== $val;
346 }
347 );
348 if ( empty( $filtered_values ) && ! empty( $list_values ) ) {
349 return new WP_Error(
350 'mainwp_rest_invalid_param',
351 /* translators: 1: argument name, 2: list of valid values */ sprintf( __( 'The %1$s argument should be: %2$s', 'mainwp' ), $name, implode( ',', $valid_values ) ),
352 array( 'status' => 400 )
353 );
354 }
355 }
356 }
357 }
358 return $args;
359 }
360
361 /**
362 * Prepare objects query.
363 *
364 * @since 5.2
365 * @param WP_REST_Request $request Full details about the request.
366 * @param string $type Object type.
367 *
368 * @return array
369 */
370 protected function prepare_objects_query( $request, $type = 'object' ) {
371 $args = array();
372 if ( ! empty( $request['offset'] ) ) {
373 $args['offset'] = $request['offset'];
374 }
375
376 if ( ! empty( $request['limit'] ) ) {
377 $args['limit'] = $request['limit'];
378 }
379
380 if ( ! empty( $request['order'] ) ) {
381 $args['order'] = $request['order'];
382 }
383
384 if ( ! empty( $request['orderby'] ) ) {
385 $args['orderby'] = $request['orderby'];
386 }
387
388 if ( ! empty( $request['page'] ) ) {
389 $args['paged'] = $request['page'];
390 } elseif ( ! empty( $request['paged'] ) ) {
391 $args['paged'] = $request['paged']; // compatible.
392 }
393
394 if ( ! empty( $request['per_page'] ) ) {
395 $args['items_per_page'] = $request['per_page'];
396 }
397
398 if ( ! empty( $request['slug'] ) ) {
399 $args['slug'] = $request['slug'];
400 }
401
402 if ( ! empty( $request['search'] ) ) {
403 $args['s'] = $request['search'];
404 }
405
406 if ( ! empty( $request['type'] ) ) {
407 $args['type'] = $request['type'];
408 }
409
410 if ( isset( $request['status'] ) ) {
411 $args['status'] = $request['status'];
412 }
413
414 if ( ! empty( $request['exclude'] ) ) {
415 $args['exclude'] = $request['exclude'];
416 }
417
418 if ( ! empty( $request['include'] ) ) {
419 $args['include'] = $request['include'];
420 }
421
422 if ( ! empty( $request['must_use'] ) ) {
423 $args['must_use'] = $request['must_use'];
424 }
425
426 if ( ! empty( $request['must_use'] ) ) {
427 $args['must_use'] = $request['must_use'];
428 }
429
430 $args['fields'] = $this->get_fields_for_response( $request );
431
432 /**
433 * Filter the query arguments for a request.
434 *
435 * Enables adding extra arguments or setting defaults for a post
436 * collection request.
437 *
438 * @param array $args Key value array of query var to query value.
439 * @param WP_REST_Request $request The request used.
440 */
441 $args = apply_filters( "mainwp_rest_{$type}_object_query", $args, $request );
442
443 return $args;
444 }
445
446 /**
447 * Get site by.
448 *
449 * @param string $by Get by.
450 * @param mixed $value Site id or domain.
451 * @param array $args args.
452 *
453 * @return array|mixed Response data, ready for insertion into collection data.
454 */
455 public function get_site_by( $by, $value, $args = array() ) {
456 $site = false;
457 $selectgroups = ! empty( $args['with_tags'] );
458
459 if ( 'id' === $by ) {
460 $site_id = intval( $value );
461 } elseif ( 'domain' === $by ) {
462 $site = MainWP_DB::instance()->get_websites_by_url( $value );
463 if ( empty( $site ) ) {
464 return $this->get_rest_data_error( 'domain', 'site' );
465 }
466 $site = current( $site );
467 $site_id = $site->id;
468 }
469
470 $site = MainWP_DB::instance()->get_website_by_id( $site_id, $selectgroups );
471 if ( empty( $site ) ) {
472 return $this->get_rest_data_error( 'id', 'site' );
473 }
474 return $site;
475 }
476
477 /**
478 * Get the args a batch create item is validated against.
479 *
480 * Mirrors the args the controller's own create route registers.
481 *
482 * @return array
483 */
484 public function get_batch_create_args() {
485 return $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE );
486 }
487
488 /**
489 * Get the args a batch update item is validated against.
490 *
491 * Mirrors the args the controller's own edit route registers.
492 *
493 * @return array
494 */
495 public function get_batch_update_args() {
496 $args = $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE );
497 $args['id'] = $this->get_batch_item_id_arg();
498 return $args;
499 }
500
501 /**
502 * Get the args a batch delete item is validated against.
503 *
504 * @return array
505 */
506 public function get_batch_delete_args() {
507 return array(
508 'id' => $this->get_batch_item_id_arg(),
509 'force' => array(
510 'type' => 'boolean',
511 'description' => __( 'Force deletion.', 'mainwp' ),
512 'sanitize_callback' => 'rest_parse_request_arg',
513 'validate_callback' => 'rest_validate_request_arg',
514 ),
515 );
516 }
517
518 /**
519 * Get the id arg a batch update or delete item is validated against.
520 *
521 * The sites item schema registers id with a wp_parse_id_list sanitizer for the
522 * collection filter, which would turn a batch item's scalar id into an array
523 * before get_site_item() reads it, so batch items get their own scalar id arg.
524 *
525 * @return array
526 */
527 protected function get_batch_item_id_arg() {
528 return array(
529 'type' => 'integer',
530 'required' => true,
531 'minimum' => 1,
532 'description' => __( 'Item ID.', 'mainwp' ),
533 'sanitize_callback' => 'rest_parse_request_arg',
534 'validate_callback' => 'rest_validate_request_arg',
535 );
536 }
537
538 /**
539 * Build the internal request for one batch item addressed by id.
540 *
541 * The raw value goes in uncast and is validated against the batch id arg: an
542 * (int) cast first would turn true, 5.9 or ["x"] into a valid-looking id and
543 * act on a different item.
544 *
545 * @param string $route Batch route the parent request came in on.
546 * @param mixed $id Raw id from the batch payload.
547 * @param array $query Extra query params (e.g. force for delete).
548 * @param array $args Args to validate against; defaults to the id arg alone.
549 * @return WP_REST_Request|WP_Error
550 */
551 protected function prepare_batch_id_request( $route, $id, $query = array(), $args = array() ) {
552 if ( empty( $args ) ) {
553 $args = array( 'id' => $this->get_batch_item_id_arg() );
554 }
555 $_item = new WP_REST_Request( 'DELETE', $route );
556 $_item->set_query_params( array_merge( array( 'id' => $id ), $query ) );
557 // Core only validates the registered args of a dispatched request, so a
558 // request assembled here has to run those checks itself.
559 $_item->set_attributes( array( 'args' => $args ) );
560 $valid = $_item->has_valid_params();
561 if ( ! is_wp_error( $valid ) ) {
562 $valid = $_item->sanitize_params();
563 }
564 return is_wp_error( $valid ) ? $valid : $_item;
565 }
566
567 /**
568 * Build the internal request for one batch update item.
569 *
570 * An update item has to carry its fields in an object: a scalar cannot be a body,
571 * and core only validates the registered args of a dispatched request, so a
572 * request assembled here has to run those checks itself.
573 *
574 * @param string $route Batch route the parent request came in on.
575 * @param mixed $item Raw item from the batch payload.
576 * @param array $args Args to validate against.
577 * @return WP_REST_Request|WP_Error
578 */
579 protected function prepare_batch_update_request( $route, $item, $args ) {
580 if ( ! is_array( $item ) ) {
581 return new WP_Error( 'rest_invalid_param', __( 'Batch update items must be objects carrying an item ID.', 'mainwp' ), array( 'status' => 400 ) );
582 }
583 $_item = new WP_REST_Request( 'PUT', $route );
584 $_item->set_body_params( $item );
585 $_item->set_attributes( array( 'args' => $args ) );
586 $valid = $_item->has_valid_params();
587 if ( ! is_wp_error( $valid ) ) {
588 $valid = $_item->sanitize_params();
589 }
590 return is_wp_error( $valid ) ? $valid : $_item;
591 }
592
593 /**
594 * Build the internal request for one batch create item.
595 *
596 * A create item has to carry its fields in an object: no create arg is required, so a
597 * scalar or a JSON list would pass validation and then insert a row built out of the
598 * schema defaults alone. An empty array stays allowed because json_decode() reports
599 * {} and [] as the same empty PHP array. Core only validates the registered args of a
600 * dispatched request, so a request assembled here has to run those checks itself.
601 *
602 * @param string $route Batch route the parent request came in on.
603 * @param mixed $item Raw item from the batch payload.
604 * @param array $args Args to validate against.
605 * @param array $defaults Schema defaults the item falls back on.
606 * @param array $query Query params of the batch request.
607 * @return WP_REST_Request|WP_Error
608 */
609 protected function prepare_batch_create_request( $route, $item, $args, $defaults, $query ) {
610 if ( ! is_array( $item ) || ( ! empty( $item ) && wp_is_numeric_array( $item ) ) ) {
611 return new WP_Error( 'rest_invalid_param', __( 'Batch create items must be objects.', 'mainwp' ), array( 'status' => 400 ) );
612 }
613 $_item = new WP_REST_Request( 'POST', $route );
614 $_item->set_default_params( $defaults );
615 $_item->set_body_params( $item );
616 $_item->set_query_params( $query );
617 $_item->set_attributes( array( 'args' => $args ) );
618 $valid = $_item->has_valid_params();
619 if ( ! is_wp_error( $valid ) ) {
620 $valid = $_item->sanitize_params();
621 }
622 return is_wp_error( $valid ) ? $valid : $_item;
623 }
624
625 /**
626 * Id to report for a batch item that failed before it could be resolved.
627 *
628 * @param mixed $id Raw id from the batch payload.
629 * @return int
630 */
631 protected function batch_error_id( $id ) {
632 return is_numeric( $id ) ? (int) $id : 0;
633 }
634
635 /**
636 * Bulk create, update and delete items.
637 *
638 * @param WP_REST_Request $request Full details about the request.
639 * @return array Of WP_Error or WP_REST_Response.
640 */
641 public function batch_items( $request ) { //phpcs:ignore -- NOSONAR complex function.
642 /**
643 * REST Server
644 *
645 * @var WP_REST_Server $wp_rest_server
646 */
647 global $wp_rest_server;
648
649 // Get the request params.
650 $items = array_filter( $request->get_params() );
651 $query = $request->get_query_params();
652 $response = array();
653
654 // Check batch limit.
655 $limit = $this->check_batch_limit( $items );
656 if ( is_wp_error( $limit ) ) {
657 return $limit;
658 }
659
660 if ( ! empty( $items['create'] ) ) {
661 foreach ( $items['create'] as $item ) {
662 // Default parameters.
663 $defaults = array();
664 $schema = $this->get_public_item_schema();
665 foreach ( $schema['properties'] as $arg => $options ) {
666 if ( isset( $options['default'] ) ) {
667 $defaults[ $arg ] = $options['default'];
668 }
669 }
670
671 $_item = $this->prepare_batch_create_request( $request->get_route(), $item, $this->get_batch_create_args(), $defaults, $query );
672 $_response = is_wp_error( $_item ) ? $_item : $this->create_item( $_item );
673
674 if ( is_wp_error( $_response ) ) {
675 $response['create'][] = array(
676 'id' => 0,
677 'error' => array(
678 'code' => $_response->get_error_code(),
679 'message' => $_response->get_error_message(),
680 'data' => $_response->get_error_data(),
681 ),
682 );
683 } else {
684 $response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
685 }
686 }
687 }
688
689 if ( ! empty( $items['update'] ) ) {
690 foreach ( $items['update'] as $item ) {
691 $_item = $this->prepare_batch_update_request( $request->get_route(), $item, $this->get_batch_update_args() );
692 $_response = is_wp_error( $_item ) ? $_item : $this->update_item( $_item );
693
694 if ( is_wp_error( $_response ) ) {
695 $response['update'][] = array(
696 'id' => is_array( $item ) ? $this->batch_error_id( $item['id'] ?? 0 ) : 0,
697 'error' => array(
698 'code' => $_response->get_error_code(),
699 'message' => $_response->get_error_message(),
700 'data' => $_response->get_error_data(),
701 ),
702 );
703 } else {
704 $response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
705 }
706 }
707 }
708
709 if ( ! empty( $items['delete'] ) ) {
710 foreach ( $items['delete'] as $id ) {
711 $_item = $this->prepare_batch_id_request( $request->get_route(), $id, array( 'force' => true ), $this->get_batch_delete_args() );
712 $_response = is_wp_error( $_item ) ? $_item : $this->delete_item( $_item );
713
714 if ( is_wp_error( $_response ) ) {
715 $response['delete'][] = array(
716 'id' => $this->batch_error_id( $id ),
717 'error' => array(
718 'code' => $_response->get_error_code(),
719 'message' => $_response->get_error_message(),
720 'data' => $_response->get_error_data(),
721 ),
722 );
723 } else {
724 $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
725 }
726 }
727 }
728
729 $route = $request->get_route();
730 if ( MainWP_Utility::string_ends_by( $route, '/sites/batch' ) ) {
731 if ( ! empty( $items['sync'] ) ) {
732 foreach ( $items['sync'] as $id ) {
733 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
734 $_response = is_wp_error( $_item ) ? $_item : $this->sync_item( $_item );
735
736 if ( is_wp_error( $_response ) ) {
737 $response['sync'][] = array(
738 'id' => $this->batch_error_id( $id ),
739 'error' => array(
740 'code' => $_response->get_error_code(),
741 'message' => $_response->get_error_message(),
742 'data' => $_response->get_error_data(),
743 ),
744 );
745 } else {
746 $response['sync'][] = $wp_rest_server->response_to_data( $_response, '' );
747 }
748 }
749 }
750
751 if ( ! empty( $items['reconnect'] ) ) {
752 foreach ( $items['reconnect'] as $id ) {
753 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
754 $_response = is_wp_error( $_item ) ? $_item : $this->reconnect_item( $_item );
755
756 if ( is_wp_error( $_response ) ) {
757 $response['reconnect'][] = array(
758 'id' => $this->batch_error_id( $id ),
759 'error' => array(
760 'code' => $_response->get_error_code(),
761 'message' => $_response->get_error_message(),
762 'data' => $_response->get_error_data(),
763 ),
764 );
765 } else {
766 $response['reconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
767 }
768 }
769 }
770
771 if ( ! empty( $items['disconnect'] ) ) {
772 foreach ( $items['disconnect'] as $id ) {
773 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
774 $_response = is_wp_error( $_item ) ? $_item : $this->disconnect_site( $_item );
775
776 if ( is_wp_error( $_response ) ) {
777 $response['disconnect'][] = array(
778 'id' => $this->batch_error_id( $id ),
779 'error' => array(
780 'code' => $_response->get_error_code(),
781 'message' => $_response->get_error_message(),
782 'data' => $_response->get_error_data(),
783 ),
784 );
785 } else {
786 $response['disconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
787 }
788 }
789 }
790
791 if ( ! empty( $items['suspend'] ) ) {
792 foreach ( $items['suspend'] as $id ) {
793 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
794 $_response = is_wp_error( $_item ) ? $_item : $this->suspend_item( $_item );
795
796 if ( is_wp_error( $_response ) ) {
797 $response['suspend'][] = array(
798 'id' => $this->batch_error_id( $id ),
799 'error' => array(
800 'code' => $_response->get_error_code(),
801 'message' => $_response->get_error_message(),
802 'data' => $_response->get_error_data(),
803 ),
804 );
805 } else {
806 $response['suspend'][] = $wp_rest_server->response_to_data( $_response, '' );
807 }
808 }
809 }
810
811 if ( ! empty( $items['check'] ) ) {
812 foreach ( $items['check'] as $id ) {
813 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
814 $_response = is_wp_error( $_item ) ? $_item : $this->check_item( $_item );
815
816 if ( is_wp_error( $_response ) ) {
817 $response['check'][] = array(
818 'id' => $this->batch_error_id( $id ),
819 'error' => array(
820 'code' => $_response->get_error_code(),
821 'message' => $_response->get_error_message(),
822 'data' => $_response->get_error_data(),
823 ),
824 );
825 } else {
826 $response['check'][] = $wp_rest_server->response_to_data( $_response, '' );
827 }
828 }
829 }
830
831 if ( ! empty( $items['remove'] ) ) {
832 foreach ( $items['remove'] as $id ) {
833 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
834 $_response = is_wp_error( $_item ) ? $_item : $this->delete_item( $_item );
835
836 if ( is_wp_error( $_response ) ) {
837 $response['remove'][] = array(
838 'id' => $this->batch_error_id( $id ),
839 'error' => array(
840 'code' => $_response->get_error_code(),
841 'message' => $_response->get_error_message(),
842 'data' => $_response->get_error_data(),
843 ),
844 );
845 } else {
846 $response['remove'][] = $wp_rest_server->response_to_data( $_response, '' );
847 }
848 }
849 }
850
851 if ( ! empty( $items['security'] ) ) {
852 foreach ( $items['security'] as $id ) {
853 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
854 $_response = is_wp_error( $_item ) ? $_item : $this->security_item( $_item );
855
856 if ( is_wp_error( $_response ) ) {
857 $response['security'][] = array(
858 'id' => $this->batch_error_id( $id ),
859 'error' => array(
860 'code' => $_response->get_error_code(),
861 'message' => $_response->get_error_message(),
862 'data' => $_response->get_error_data(),
863 ),
864 );
865 } else {
866 $response['security'][] = $wp_rest_server->response_to_data( $_response, '' );
867 }
868 }
869 }
870
871 if ( ! empty( $items['plugins'] ) ) {
872 foreach ( $items['plugins'] as $id ) {
873 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
874 $_response = is_wp_error( $_item ) ? $_item : $this->get_site_plugins( $_item );
875
876 if ( is_wp_error( $_response ) ) {
877 $response['plugins'][] = array(
878 'id' => $this->batch_error_id( $id ),
879 'error' => array(
880 'code' => $_response->get_error_code(),
881 'message' => $_response->get_error_message(),
882 'data' => $_response->get_error_data(),
883 ),
884 );
885 } else {
886 $response['plugins'][] = $wp_rest_server->response_to_data( $_response, '' );
887 }
888 }
889 }
890
891 if ( ! empty( $items['themes'] ) ) {
892 foreach ( $items['themes'] as $id ) {
893 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
894 $_response = is_wp_error( $_item ) ? $_item : $this->get_site_themes( $_item );
895
896 if ( is_wp_error( $_response ) ) {
897 $response['themes'][] = array(
898 'id' => $this->batch_error_id( $id ),
899 'error' => array(
900 'code' => $_response->get_error_code(),
901 'message' => $_response->get_error_message(),
902 'data' => $_response->get_error_data(),
903 ),
904 );
905 } else {
906 $response['themes'][] = $wp_rest_server->response_to_data( $_response, '' );
907 }
908 }
909 }
910
911 if ( ! empty( $items['non-mainwp-changes'] ) ) {
912 foreach ( $items['non-mainwp-changes'] as $id ) {
913 $_item = $this->prepare_batch_id_request( $request->get_route(), $id );
914 $_response = is_wp_error( $_item ) ? $_item : $this->get_non_mainwp_changes_of_site( $_item );
915
916 if ( is_wp_error( $_response ) ) {
917 $response['non-mainwp-changes'][] = array(
918 'id' => $this->batch_error_id( $id ),
919 'error' => array(
920 'code' => $_response->get_error_code(),
921 'message' => $_response->get_error_message(),
922 'data' => $_response->get_error_data(),
923 ),
924 );
925 } else {
926 $response['non-mainwp-changes'][] = $wp_rest_server->response_to_data( $_response, '' );
927 }
928 }
929 }
930 }
931
932 return $response;
933 }
934
935 /**
936 * Validate a text value for a text based setting.
937 *
938 * @since 5.2
939 * @param string $value Value.
940 * @param array $setting Setting.
941 * @return string
942 */
943 public function validate_setting_text_field( $value, $setting ) {
944 $value = is_null( $value ) ? '' : $value;
945 return wp_kses_post( trim( stripslashes( $value ) ) );
946 }
947
948 /**
949 * Validate select based settings.
950 *
951 * @since 5.2
952 * @param string $value Value.
953 * @param array $setting Setting.
954 * @return string|WP_Error
955 */
956 public function validate_setting_select_field( $value, $setting ) {
957 if ( array_key_exists( $value, $setting['options'] ) ) {
958 return $value;
959 } else {
960 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
961 }
962 }
963
964 /**
965 * Validate multiselect based settings.
966 *
967 * @since 5.2
968 * @param array $values Values.
969 * @param array $setting Setting.
970 * @return array|WP_Error
971 */
972 public function validate_setting_multiselect_field( $values, $setting ) {
973 if ( empty( $values ) ) {
974 return array();
975 }
976
977 if ( ! is_array( $values ) ) {
978 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
979 }
980
981 $final_values = array();
982 foreach ( $values as $value ) {
983 if ( array_key_exists( $value, $setting['options'] ) ) {
984 $final_values[] = $value;
985 }
986 }
987
988 return $final_values;
989 }
990
991 /**
992 * Validate image_width based settings.
993 *
994 * @since 5.2
995 * @param array $values Values.
996 * @param array $setting Setting.
997 * @return string|WP_Error
998 */
999 public function validate_setting_image_width_field( $values, $setting ) {
1000 if ( ! is_array( $values ) ) {
1001 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
1002 }
1003
1004 $current = $setting['value'];
1005 if ( isset( $values['width'] ) ) {
1006 $current['width'] = intval( $values['width'] );
1007 }
1008 if ( isset( $values['height'] ) ) {
1009 $current['height'] = intval( $values['height'] );
1010 }
1011 if ( isset( $values['crop'] ) ) {
1012 $current['crop'] = (bool) $values['crop'];
1013 }
1014 return $current;
1015 }
1016
1017 /**
1018 * Validate radio based settings.
1019 *
1020 * @since 5.2
1021 * @param string $value Value.
1022 * @param array $setting Setting.
1023 * @return string|WP_Error
1024 */
1025 public function validate_setting_radio_field( $value, $setting ) {
1026 return $this->validate_setting_select_field( $value, $setting );
1027 }
1028
1029 /**
1030 * Validate checkbox based settings.
1031 *
1032 * @since 5.2
1033 * @param string $value Value.
1034 * @param array $setting Setting.
1035 * @return string|WP_Error
1036 */
1037 public function validate_setting_checkbox_field( $value, $setting ) {
1038 if ( in_array( $value, array( 'yes', 'no' ) ) ) {
1039 return $value;
1040 } elseif ( empty( $value ) ) {
1041 return isset( $setting['default'] ) ? $setting['default'] : 'no';
1042 } else {
1043 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
1044 }
1045 }
1046
1047 /**
1048 * Validate textarea based settings.
1049 *
1050 * @since 5.2
1051 * @param string $value Value.
1052 * @param array $setting Setting.
1053 * @return string
1054 */
1055 public function validate_setting_textarea_field( $value, $setting ) {
1056 $value = is_null( $value ) ? '' : $value;
1057 return wp_kses(
1058 trim( stripslashes( $value ) ),
1059 array_merge(
1060 array(
1061 'iframe' => array(
1062 'src' => true,
1063 'style' => true,
1064 'id' => true,
1065 'class' => true,
1066 ),
1067 ),
1068 wp_kses_allowed_html( 'post' )
1069 )
1070 );
1071 }
1072
1073
1074 /**
1075 * Get the batch schema, conforming to JSON Schema.
1076 *
1077 * @return array
1078 */
1079 public function get_public_batch_schema() {
1080 return array(
1081 '$schema' => 'http://json-schema.org/draft-04/schema#',
1082 'title' => 'batch',
1083 'type' => 'object',
1084 'properties' => array(
1085 'create' => array(
1086 'description' => __( 'List of created resources.', 'mainwp' ),
1087 'type' => 'array',
1088 'context' => array( 'view', 'edit' ),
1089 'items' => array(
1090 'type' => 'object',
1091 ),
1092 ),
1093 'update' => array(
1094 'description' => __( 'List of updated resources.', 'mainwp' ),
1095 'type' => 'array',
1096 'context' => array( 'view', 'edit' ),
1097 'items' => array(
1098 'type' => 'object',
1099 ),
1100 ),
1101 'delete' => array(
1102 'description' => __( 'List of delete resources.', 'mainwp' ),
1103 'type' => 'array',
1104 'context' => array( 'view', 'edit' ),
1105 'items' => array(
1106 'type' => 'integer',
1107 ),
1108 ),
1109 ),
1110 );
1111 }
1112
1113
1114 /**
1115 * Get formatted item data, not including orders count nor total spent.
1116 * This method is needed because v3 API doesn't return those two fields.
1117 *
1118 * @internal This method could disappear or have its name or signature changed in future releases.
1119 *
1120 * @param object $obj data instance.
1121 * @return array
1122 */
1123 protected function get_formatted_item_data_core( $obj ) {
1124 return array();
1125 }
1126
1127 /**
1128 * Get formatted item data, not including orders count nor total spent.
1129 * This method is needed because v3 API doesn't return those two fields.
1130 *
1131 * @internal This method could disappear or have its name or signature changed in future releases.
1132 *
1133 * @param array $data data instance.
1134 * @return array
1135 */
1136 protected function get_pre_formatted_item_data( $data ) {
1137 return $data;
1138 }
1139
1140
1141 /**
1142 * Get formatted item data, not including orders count nor total spent.
1143 * This method is needed because v3 API doesn't return those two fields.
1144 *
1145 * @internal This method could disappear or have its name or signature changed in future releases.
1146 *
1147 * @param array $data data instance.
1148 * @return array
1149 */
1150 protected function get_formatted_item_data( $data ) { //phpcs:ignore -- NOSONAR - compatible.
1151 return $data;
1152 }
1153
1154 /**
1155 * Gets an array of fields to be included on the response.
1156 *
1157 * Included fields are based on item schema and `_fields=` request argument.
1158 * Updated from WordPress 5.3, included into this class to support old versions.
1159 *
1160 * @since 5.2
1161 * @param WP_REST_Request $request Full details about the request.
1162 * @return array Fields to be included in the response.
1163 */
1164 public function get_fields_for_response( $request ) { //phpcs:ignore -- NOSONAR - complex.
1165 // From xdebug profiling, this method could take upto 25% of request time in index calls.
1166 // Cache it and make sure _fields was cached on current request object!
1167 if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) {
1168 return $this->_fields;
1169 }
1170 $this->_request = $request;
1171
1172 $schema = $this->get_item_schema();
1173 $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
1174
1175 // Exclude fields that specify a different context than the request context.
1176 $context = isset( $request['context'] ) ? $request['context'] : 'view';
1177 if ( $context ) {
1178 foreach ( $properties as $name => $options ) {
1179 if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
1180 unset( $properties[ $name ] );
1181 }
1182 }
1183 }
1184
1185 $fields = array_keys( $properties );
1186
1187 if ( ! isset( $request['_fields'] ) ) {
1188 $this->_fields = $fields;
1189 return $fields;
1190 }
1191 $requested_fields = wp_parse_list( $request['_fields'] );
1192 if ( empty( $requested_fields ) ) {
1193 $this->_fields = $fields;
1194 return $fields;
1195 }
1196 // Trim off outside whitespace from the comma delimited list.
1197 $requested_fields = array_map( 'trim', $requested_fields );
1198 // Always persist 'id', because it can be needed for add_additional_fields_to_object().
1199 if ( in_array( 'id', $fields, true ) ) {
1200 $requested_fields[] = 'id';
1201 }
1202 // Return the list of all requested fields which appear in the schema.
1203 $this->_fields = array_reduce(
1204 $requested_fields,
1205 function ( $response_fields, $field ) use ( $fields ) {
1206 if ( in_array( $field, $fields, true ) ) {
1207 $response_fields[] = $field;
1208 return $response_fields;
1209 }
1210 // Check for nested fields if $field is not a direct match.
1211 $nested_fields = explode( '.', $field );
1212 // A nested field is included so long as its top-level property.
1213 // is present in the schema.
1214 if ( in_array( $nested_fields[0], $fields, true ) ) {
1215 $response_fields[] = $field;
1216 }
1217 return $response_fields;
1218 },
1219 array()
1220 );
1221 return $this->_fields;
1222 }
1223
1224 /**
1225 * Returns the full item schema.
1226 *
1227 * @return array
1228 */
1229 public function get_item_schema() {
1230 return array(
1231 '$schema' => 'http://json-schema.org/draft-04/schema#',
1232 'title' => $this->title,
1233 'type' => 'object',
1234 'properties' => $this->get_properties(),
1235 );
1236 }
1237
1238
1239 /**
1240 * Returns the full item response.
1241 *
1242 * @param mixed $item Item to get response for.
1243 * @return array|stdClass
1244 */
1245 public function get_item_response( $item ) {
1246 return array();
1247 }
1248
1249 /**
1250 * Return schema properties.
1251 *
1252 * @return array
1253 */
1254 public function get_properties() {
1255 return array();
1256 }
1257
1258 /**
1259 * Get route error.
1260 *
1261 * @param string $type_slug Type slug.
1262 * @param string $type 'object'.
1263 *
1264 * @return WP_Error
1265 */
1266 public function get_rest_data_error( $type_slug, $type = 'object' ) {
1267 if ( empty( $type ) && is_string( $type_slug ) ) {
1268 $type = $type_slug;
1269 }
1270 switch ( $type_slug ) {
1271 case 'id':
1272 return new WP_Error( "mainwp_rest_invalid_{$type}_id", __( 'Invalid or not found ID.', 'mainwp' ), array( 'status' => 404 ) );
1273 case 'domain':
1274 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid or not found domain.', 'mainwp' ), array( 'status' => 404 ) );
1275 case 'email':
1276 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid or not found email.', 'mainwp' ), array( 'status' => 404 ) );
1277 default:
1278 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid data.', 'mainwp' ), array( 'status' => 500 ) );
1279 }
1280 }
1281
1282 /**
1283 * Gets an array of fields to be included on the response.
1284 *
1285 * Included fields are based on item schema and `_fields=` request argument.
1286 * Updated from WordPress 5.3, included into this class to support old versions.
1287 *
1288 * @param array $item data.
1289 * @param string $context fields to filter.
1290 * @param array $addition_fields addition_fields to be included in the response.
1291 *
1292 * @return array addition_fields Fields to be included in the response.
1293 */
1294 public function filter_response_data_by_allowed_fields( $item, $context = 'view', $addition_fields = array() ) { //phpcs:ignore -- NOSONAR - complex.
1295 $data = $this->filter_response_by_context( $item, $context );
1296 $fields = $this->get_allowed_fields_by_context( $context );
1297
1298 if ( ! empty( $addition_fields ) && is_array( $addition_fields ) ) {
1299 $fields = array_values( array_unique( array_merge( $fields, $addition_fields ) ) );
1300 }
1301
1302 if ( is_array( $fields ) && ! empty( $fields ) ) {
1303 $_data = array();
1304 foreach ( $fields as $field ) {
1305 if ( is_array( $data ) ) {
1306 if ( isset( $data[ $field ] ) ) {
1307 $_data[ $field ] = $data[ $field ];
1308 } else {
1309 $_data[ $field ] = '';
1310 }
1311 } elseif ( is_object( $data ) ) {
1312 if ( property_exists( $data, $field ) ) {
1313 $_data[ $field ] = $data->{$field};
1314 } else {
1315 $_data[ $field ] = '';
1316 }
1317 }
1318 }
1319 $_data = $this->get_formatted_item_data( $_data );
1320 return $_data;
1321 }
1322 return $data;
1323 }
1324
1325 /**
1326 * Gets an array of fields to be included on the response.
1327 *
1328 * Included fields are based on item schema and `_fields=` request argument.
1329 * Updated from WordPress 5.3, included into this class to support old versions.
1330 *
1331 * @since 5.2
1332 * @param string $context context.
1333 * @return array Fields to be included in the response.
1334 */
1335 public function get_allowed_fields_by_context( $context ) {
1336 $schema = $this->get_item_schema();
1337 $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
1338 if ( $context ) {
1339 foreach ( $properties as $name => $options ) {
1340 if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
1341 unset( $properties[ $name ] );
1342 }
1343 }
1344 }
1345 return array_keys( $properties );
1346 }
1347
1348 /**
1349 * Check rest permissions callback.
1350 *
1351 * @param WP_REST_Request $request Full details about the request.
1352 *
1353 * @return bool
1354 */
1355 public function get_rest_permissions_check( $request ) {
1356 $valid = \MainWP_REST_Authentication::get_instance()->is_valid_permissions( $request );
1357 if ( is_wp_error( $valid ) ) {
1358 return $valid;
1359 }
1360 return true;
1361 }
1362
1363 /**
1364 * Method get_rest_api_user().
1365 *
1366 * @return mixed
1367 */
1368 public function get_rest_api_user() {
1369 return \MainWP_REST_Authentication::get_instance()->get_rest_valid_user();
1370 }
1371
1372 /**
1373 * Get the query params for collections of attachments.
1374 *
1375 * @return array
1376 */
1377 public function get_collection_params() {
1378 $params = array();
1379 $params['context'] = $this->get_context_param();
1380 $params['context']['default'] = 'view';
1381
1382 $params['page'] = array(
1383 'description' => __( 'Current page of the collection.', 'mainwp' ),
1384 'type' => 'integer',
1385 'default' => 1,
1386 'sanitize_callback' => 'absint',
1387 'validate_callback' => 'rest_validate_request_arg',
1388 'minimum' => 1,
1389 );
1390 $params['per_page'] = array(
1391 'description' => __( 'Maximum number of items to be returned in result set.', 'mainwp' ),
1392 'type' => 'integer',
1393 'default' => 10,
1394 'minimum' => 1,
1395 'maximum' => 100,
1396 'sanitize_callback' => 'absint',
1397 'validate_callback' => 'rest_validate_request_arg',
1398 );
1399 $params['search'] = array(
1400 'description' => __( 'Limit results to those matching a string.', 'mainwp' ),
1401 'type' => 'string',
1402 'sanitize_callback' => 'sanitize_text_field',
1403 'validate_callback' => 'rest_validate_request_arg',
1404 );
1405
1406 $params['slug'] = array(
1407 'default' => '',
1408 'description' => __( 'Slugs.', 'mainwp' ),
1409 'type' => 'array',
1410 'items' => array(
1411 'type' => 'string',
1412 ),
1413 'sanitize_callback' => 'wp_parse_list',
1414 'validate_callback' => 'rest_validate_request_arg',
1415 );
1416
1417 $params['status'] = array(
1418 'default' => '',
1419 'description' => __( 'Status.', 'mainwp' ),
1420 'type' => array( 'string' ),
1421 'sanitize_callback' => 'wp_parse_list',
1422 'validate_callback' => 'rest_validate_request_arg',
1423 );
1424
1425 $params['exclude'] = array(
1426 'description' => __( 'Exclude IDs.', 'mainwp' ),
1427 'type' => 'array',
1428 'items' => array(
1429 'type' => 'integer',
1430 ),
1431 'sanitize_callback' => 'wp_parse_id_list',
1432 );
1433 $params['include'] = array(
1434 'description' => __( 'Include IDs.', 'mainwp' ),
1435 'type' => 'array',
1436 'items' => array(
1437 'type' => 'integer',
1438 ),
1439 'sanitize_callback' => 'wp_parse_id_list',
1440 );
1441
1442 /**
1443 * Filter collection parameters for the controller.
1444 *
1445 * @param array $query_params JSON Schema-formatted collection parameters.
1446 * @param object This object.
1447 */
1448 return apply_filters( 'mainwp_rest_collection_params', $params, $this );
1449 }
1450
1451 /**
1452 * Method sanitize_request_slugs().
1453 *
1454 * @param array $slugs slugs.
1455 * @return array
1456 */
1457 public function sanitize_request_slugs( $slugs ) {
1458 if ( ! empty( $slugs ) ) {
1459 $slugs = array_map( 'sanitize_text_field', array_map( 'urldecode', $slugs ) );
1460 }
1461 return $slugs;
1462 }
1463
1464 /**
1465 * Prepare post or page data.
1466 *
1467 * @param array $new_post New post data.
1468 * @param array $post_custom Post custom data.
1469 * @param array $post_featured_image Post featured image data.
1470 * @param array $featured_image_data Featured image data.
1471 * @param array $post_gallery_images Post gallery images data.
1472 * @param array $post_category Post category data.
1473 *
1474 * @return array
1475 */
1476 public function prepare_post_page_data( $new_post, $post_custom, $post_featured_image, $featured_image_data, $post_gallery_images = '', $post_category = '' ) {
1477 return array(
1478 'new_post' => base64_encode( wp_json_encode( $new_post ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1479 'post_custom' => base64_encode( wp_json_encode( $post_custom ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1480 'post_featured_image' => ( null !== $post_featured_image ) ? base64_encode( $post_featured_image ) : null, // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1481 'featured_image_data' => ( null !== $featured_image_data ) ? base64_encode( wp_json_encode( $featured_image_data ) ) : null, // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1482 'mainwp_upload_dir' => base64_encode( wp_json_encode( wp_upload_dir() ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1483 'post_gallery_images' => base64_encode( wp_json_encode( $post_gallery_images ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1484 'post_category' => base64_encode( wp_json_encode( $post_category ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1485 );
1486 }
1487
1488 /**
1489 * Decode post or page data.
1490 *
1491 * @param array $data Data.
1492 *
1493 * @return array
1494 */
1495 public function decode_post_page_data( $data ) {
1496 // Decode base64 json.
1497 $decode_base64_json = function ( $value, $default_value = '', $type = 'array' ) {
1498 if ( empty( $value ) ) {
1499 return $default_value;
1500 }
1501 $decoded = base64_decode( $value, true ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1502 if ( false === $decoded ) {
1503 return $default_value;
1504 }
1505
1506 if ( 'string' === $type ) {
1507 return ! empty( $decoded ) ? rawurldecode( $decoded ) : $default_value;
1508 }
1509
1510 $json = json_decode( $decoded, true );
1511 return is_array( $json ) ? $json : $default_value;
1512 };
1513 // Safely decode page data.
1514 return array(
1515 'new_post' => $decode_base64_json( $data['new_post'] ?? '' ),
1516 'post_custom' => $decode_base64_json( $data['post_custom'] ?? '' ),
1517 'post_featured_image' => $decode_base64_json( $data['post_featured_image'] ?? '', '', 'string' ),
1518 'featured_image_data' => $decode_base64_json( $data['featured_image_data'] ?? '' ),
1519 'mainwp_upload_dir' => $decode_base64_json( $data['mainwp_upload_dir'] ?? '' ),
1520 'child_upload_dir' => $decode_base64_json( $data['child_upload_dir'] ?? '' ),
1521 'post_gallery_images' => $decode_base64_json( $data['post_gallery_images'] ?? '' ),
1522 'post_category' => $decode_base64_json( $data['post_category'] ?? '', '', 'string' ),
1523 );
1524 }
1525
1526 /**
1527 * Sanitize field.
1528 *
1529 * @param mixed $value Value to sanitize.
1530 *
1531 * @return mixed
1532 */
1533 public function sanitize_field( $value ) {
1534 if ( null === $value || '' === $value ) {
1535 return '';
1536 }
1537 // trim() raises a TypeError on arrays and objects under PHP 8, and a body value is free to be either.
1538 if ( ! is_scalar( $value ) ) {
1539 return '';
1540 }
1541 return sanitize_text_field( trim( $value ) );
1542 }
1543
1544 /**
1545 * Read the request body as an associative array.
1546 *
1547 * The published routes accept either a JSON or a form-encoded body; clients
1548 * send both, so JSON params are tried first and form params second.
1549 *
1550 * @param WP_REST_Request $request Request object.
1551 * @return array|WP_Error Body data, or an empty_body error when nothing usable was sent.
1552 */
1553 protected function get_request_body_data( $request ) {
1554 $body = $request->get_json_params();
1555
1556 // Only the two parsed bags count: a raw-body decode would hand the
1557 // handler values that never went through the registered-arg validation.
1558 if ( ! is_array( $body ) || empty( $body ) ) {
1559 $body = $request->get_body_params();
1560 }
1561
1562 if ( ! is_array( $body ) || empty( $body ) ) {
1563 return new WP_Error(
1564 'empty_body',
1565 __( 'Request body is empty.', 'mainwp' ),
1566 array( 'status' => 400 )
1567 );
1568 }
1569
1570 return $body;
1571 }
1572
1573 /**
1574 * Make enum sanitizer.
1575 *
1576 * @param array $allowed Allowed values.
1577 * @param string $type Type to coerce to.
1578 *
1579 * @return callable
1580 */
1581 public function make_enum_sanitizer( array $allowed, string $type = 'int' ) { // phpcs:ignore -- NOSONAR - complex.
1582 $allowed_norm = array_map( fn( $v ) => $this->coerce_type( $v, $type ), $allowed );
1583
1584 return function ( $value, $request, $param ) use ( $allowed_norm, $type, $allowed ) { // phpcs:ignore -- NOSONAR
1585 if ( null === $value || '' === $value ) {
1586 return $value;
1587 }
1588
1589 if ( 'array' === $type ) {
1590 if ( ! is_array( $value ) ) {
1591 $value = $this->sanitize_field( $value );
1592 if ( is_string( $value ) && strpos( $value, ',' ) !== false ) {
1593 $value = array_map( 'trim', explode( ',', $value ) );
1594 } else {
1595 $value = array( $value );
1596 }
1597 }
1598
1599 $sanitized = array();
1600 foreach ( $value as $item ) {
1601 $item = $this->sanitize_field( $item );
1602 if ( in_array( $item, $allowed, true ) ) {
1603 $sanitized[] = $item;
1604 } else {
1605 return new WP_Error(
1606 "invalid_{$param}",
1607 sprintf(
1608 /* translators: 1: field name, 2: allowed list */
1609 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ), // NOSONAR.
1610 esc_html( $param ),
1611 esc_html( implode( ', ', $allowed ) )
1612 ),
1613 );
1614 }
1615 }
1616 return $sanitized;
1617 }
1618
1619 // Standard sanitization for non-array types.
1620 // sanitize_field() trims, which fatals on an array or object, so treat those as a plain enum mismatch.
1621 if ( is_scalar( $value ) ) {
1622 $value = $this->sanitize_field( $value );
1623 $v = $this->coerce_type( $value, $type );
1624
1625 if ( in_array( $v, $allowed_norm, true ) ) {
1626 return $v;
1627 }
1628 }
1629
1630 return new WP_Error(
1631 "invalid_{$param}",
1632 sprintf(
1633 /* translators: 1: field name, 2: allowed list */
1634 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1635 esc_html( $param ),
1636 esc_html( implode( ', ', $allowed_norm ) )
1637 ),
1638 );
1639 };
1640 }
1641
1642 /**
1643 * Coerce value to type.
1644 *
1645 * @param mixed $value Value to coerce.
1646 * @param string $type Type to coerce to.
1647 *
1648 * @return mixed
1649 */
1650 public function coerce_type( $value, string $type ) { // phpcs:ignore -- NOSONAR
1651 switch ( $type ) {
1652 case 'int':
1653 return (int) $value;
1654 case 'bool':
1655 return (bool) $value;
1656 case 'array':
1657 return (array) $value;
1658 case 'string':
1659 default:
1660 return (string) $value;
1661 }
1662 }
1663
1664 /**
1665 * Make enum validator.
1666 *
1667 * @param array $allowed Allowed values.
1668 * @param string $type Type to coerce to.
1669 *
1670 * @return callable
1671 */
1672 public function make_enum_validator( array $allowed, string $type = 'int' ) { // phpcs:ignore -- NOSONAR
1673 $allowed_norm = array_map( fn( $v ) => $this->coerce_type( $v, $type ), $allowed );
1674
1675 return function ( $value, $request, $param ) use ( $allowed_norm, $type, $allowed ) { // phpcs:ignore -- NOSONAR
1676 if ( null === $value || '' === $value ) {
1677 return true;
1678 }
1679
1680 if ( 'array' === $type ) {
1681 if ( ! is_array( $value ) ) {
1682 $value = $this->sanitize_field( $value );
1683 if ( is_string( $value ) && strpos( $value, ',' ) !== false ) {
1684 $value = array_map( 'trim', explode( ',', $value ) );
1685 } else {
1686 $value = array( $value );
1687 }
1688 }
1689
1690 // Validate each element in the array.
1691 foreach ( $value as $item ) {
1692 $item = $this->sanitize_field( $item );
1693 if ( ! in_array( $item, $allowed, true ) ) {
1694 return new WP_Error(
1695 "invalid_{$param}",
1696 sprintf(
1697 /* translators: 1: field name, 2: allowed list */
1698 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1699 esc_html( $param ),
1700 esc_html( implode( ', ', $allowed ) )
1701 ),
1702 );
1703 }
1704 }
1705 return true;
1706 }
1707
1708 // Standard validation for non-array types.
1709 // sanitize_field() trims, which fatals on an array or object, so treat those as a plain enum mismatch.
1710 if ( is_scalar( $value ) ) {
1711 $value = $this->sanitize_field( $value );
1712 $v = $this->coerce_type( $value, $type );
1713
1714 if ( in_array( $v, $allowed_norm, true ) ) {
1715 return true;
1716 }
1717 }
1718
1719 return new WP_Error(
1720 "invalid_{$param}",
1721 sprintf(
1722 /* translators: 1: field name, 2: allowed list */
1723 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1724 esc_html( $param ),
1725 esc_html( implode( ', ', $allowed_norm ) )
1726 ),
1727 );
1728 };
1729 }
1730
1731 /**
1732 * Pages or Posts search handler for REST API.
1733 *
1734 * @param mixed $data Search data from child site.
1735 * @param object $website Child site object.
1736 * @param mixed $output Output object to store results.
1737 * @param array $params Request parameters.
1738 *
1739 * @return void
1740 */
1741 public static function posts_pages_search_handler( $data, $website, &$output, $params = array() ) {
1742 if ( ! isset( $output->errors ) ) {
1743 $output->errors = array();
1744 }
1745
1746 if ( preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) > 0 ) {
1747 $result = $results[1];
1748 $pages = MainWP\Dashboard\MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1749
1750 if ( is_array( $pages ) && isset( $pages['error'] ) ) {
1751 $output->errors[ $website->id ] = esc_html( $pages['error'] );
1752 return;
1753 }
1754
1755 $output->results[ $website->id ] = $pages;
1756 }
1757 }
1758
1759 /**
1760 * Get post/page id and remote edit data.
1761 *
1762 * @param object $website Website.
1763 * @param WP_REST_Request $request Full details about the request.
1764 * @param string $type Post type: 'page' or 'post' (or any CPT if child supports).
1765 *
1766 * @return array|WP_Error
1767 */
1768 protected function get_request_post_page_id( $website, $request, $type = 'page' ) { // phpcs:ignore -- NOSONAR
1769 // Param names.
1770 $type = ( 'post' === $type ) ? 'post' : 'page';
1771 $type_id_param = ( 'page' === $type ) ? 'id_page' : 'id_post';
1772 $id = $request->get_param( $type_id_param );
1773
1774 if ( empty( $id ) ) {
1775 return new WP_Error(
1776 'post_id_not_found',
1777 ( 'page' === $type ) ? __( 'Page id not found.', 'mainwp' ) : __( 'Post id not found.', 'mainwp' )
1778 );
1779 }
1780
1781 // Fetch remote edit data.
1782 try {
1783 $information = MainWP_Connect::fetch_url_authed(
1784 $website,
1785 'post_action',
1786 array(
1787 'action' => 'get_edit',
1788 'id' => $id,
1789 'post_type' => $type,
1790 )
1791 );
1792 } catch ( MainWP_Exception $e ) {
1793 return new WP_Error( 'get_post_error', MainWP_Error_Helper::get_error_message( $e ) );
1794 }
1795
1796 // Validate response.
1797 if ( empty( $information['status'] ) || 'SUCCESS' !== $information['status'] || empty( $information['my_post'] ) ) {
1798 return new WP_Error(
1799 'post_not_exist',
1800 ( 'page' === $type ) ? __( 'Page not exist.', 'mainwp' ) : __( 'Post not exist.', 'mainwp' )
1801 );
1802 }
1803
1804 return array(
1805 'data' => $information,
1806 'post_id' => $id,
1807 'post_type' => $type,
1808 );
1809 }
1810
1811 /**
1812 * Validate site ids.
1813 *
1814 * @param string $value Site id.
1815 * @param WP_REST_Request $request Request object.
1816 *
1817 * @return bool|WP_Error
1818 */
1819 public function validate_site_ids( $value, $request ) {
1820 if ( empty( $value ) ) {
1821 return true;
1822 }
1823 $value = $this->sanitize_field( $value );
1824 $site_ids = explode( ',', $value );
1825 foreach ( $site_ids as $site_id ) {
1826 $db_site = \MainWP\Dashboard\MainWP_DB::instance()->get_website_by_id( trim( $site_id ) );
1827 if ( ! $db_site ) {
1828 return new WP_Error(
1829 'invalid_site',
1830 sprintf(
1831 /* translators: %s: site ID */
1832 __( 'Invalid site ID: %s.', 'mainwp' ),
1833 esc_html( $site_id )
1834 ),
1835 );
1836 }
1837 }
1838 return true;
1839 }
1840
1841 /**
1842 * Sanitize text field to array.
1843 *
1844 * @param string $value Client id.
1845 *
1846 * @return string|array
1847 */
1848 public function sanitize_text_field_to_array( $value ) {
1849 if ( empty( $value ) ) {
1850 return '';
1851 }
1852 $value = $this->sanitize_field( $value );
1853 return array_map( 'trim', explode( ',', $value ) );
1854 }
1855
1856 /**
1857 * Validate clients.
1858 *
1859 * @param string $value Client id.
1860 * @param WP_REST_Request $request Request object.
1861 *
1862 * @return bool|WP_Error
1863 */
1864 public function validate_clients( $value, $request ) {
1865 if ( empty( $value ) ) {
1866 return true;
1867 }
1868 $value = $this->sanitize_field( $value );
1869 $clients = array_map( 'trim', explode( ',', $value ) );
1870
1871 foreach ( $clients as $client ) {
1872 $db_client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client );
1873 if ( ! $db_client ) {
1874 return new WP_Error(
1875 'invalid_client',
1876 sprintf(
1877 /* translators: %s: client ID */
1878 __( 'Invalid client ID: %s.', 'mainwp' ),
1879 esc_html( $client )
1880 ),
1881 );
1882 }
1883 }
1884 return true;
1885 }
1886 /**
1887 * Sanitize groups text field.
1888 *
1889 * @param string $value Group name.
1890 *
1891 * @return string
1892 */
1893 public function sanitize_groups_text_field( $value ) {
1894 if ( empty( $value ) ) {
1895 return '';
1896 }
1897 $value = $this->sanitize_field( $value );
1898 $group_ids = array();
1899 $groups = array_map( 'trim', explode( ',', $value ) );
1900 foreach ( $groups as $group ) {
1901 $db_group = \MainWP\Dashboard\MainWP_DB_Common::instance()->get_group_by_name( $group );
1902 if ( ! $db_group ) {
1903 return new WP_Error(
1904 'invalid_group',
1905 sprintf(
1906 /* translators: %s: group name */
1907 __( 'Invalid Group: %s.', 'mainwp' ),
1908 esc_html( $group )
1909 ),
1910 );
1911 }
1912 $group_ids[] = $db_group->id;
1913 }
1914 return $group_ids;
1915 }
1916
1917 /**
1918 * Get group ids from group names.
1919 *
1920 * @param string $value Group name.
1921 * @param mixed $request Request object.
1922 * @return bool|WP_Error True if valid, WP_Error otherwise.
1923 */
1924 public function validate_groups( $value, $request ) {
1925 if ( empty( $value ) ) {
1926 return true;
1927 }
1928 $value = $this->sanitize_field( $value );
1929 $groups = array_map( 'trim', explode( ',', $value ) );
1930 foreach ( $groups as $group ) {
1931 $db_group = \MainWP\Dashboard\MainWP_DB_Common::instance()->get_group_by_name( $group );
1932 if ( ! $db_group ) {
1933 return new WP_Error(
1934 'invalid_group',
1935 sprintf(
1936 /* translators: %s: group name */
1937 __( 'Invalid Group: %s.', 'mainwp' ),
1938 esc_html( $group )
1939 ),
1940 );
1941 }
1942 }
1943 return true;
1944 }
1945
1946 /**
1947 * Get websites by filter.
1948 *
1949 * @param array $sites Sites.
1950 * @param array $groups Groups.
1951 * @param array $clients Clients.
1952 *
1953 * @return array
1954 */
1955 protected function get_db_websites_by_filter( $sites, $groups, $clients ) { // phpcs:ignore -- NOSONAR - complex.
1956 $utility = MainWP_Utility::instance();
1957 $system_utility = new MainWP_System_Utility();
1958 $db = MainWP_DB::instance();
1959 $data_fields = $system_utility->get_default_map_site_fields();
1960 $data_fields[] = 'users';
1961 $data_fields[] = 'verify_certificate'; // Add verify_certificate to the list of fields to fetch from the database.
1962
1963 // Default result.
1964 $website_url = array();
1965 $db_websites = array();
1966
1967 if ( ! empty( $sites ) && is_array( $sites ) ) {
1968 foreach ( $sites as $v ) {
1969 if ( $utility->ctype_digit( $v ) ) {
1970 $website = $db->get_website_by_id( $v );
1971 if ( $website && empty( $website->sync_errors ) && ! $system_utility->is_suspended_site( $website ) ) {
1972 $db_websites[ $website->id ] = $utility->map_site(
1973 $website,
1974 $data_fields
1975 );
1976 $website_url[ $website->id ] = $website->url;
1977 }
1978 }
1979 }
1980 }
1981 if ( ! empty( $groups ) && is_array( $groups ) ) {
1982 foreach ( $groups as $v ) {
1983 if ( $utility->ctype_digit( $v ) ) {
1984 $websites = $db->query( $db->get_sql_websites_by_group_id( $v ) );
1985 while ( $websites && ( $website = $db->fetch_object( $websites ) ) ) {
1986 if ( ! empty( $website->sync_errors ) || $system_utility->is_suspended_site( $website ) ) {
1987 continue;
1988 }
1989 $db_websites[ $website->id ] = $utility->map_site(
1990 $website,
1991 $data_fields
1992 );
1993 $website_url[ $website->id ] = $website->url;
1994 }
1995 $db->free_result( $websites );
1996 }
1997 }
1998 }
1999
2000 if ( ! empty( $clients ) && is_array( $clients ) ) {
2001 $websites = MainWP_DB_Client::instance()->get_websites_by_client_ids(
2002 $clients,
2003 array(
2004 'select_data' => $data_fields,
2005 )
2006 );
2007 if ( $websites ) {
2008 foreach ( $websites as $website ) {
2009 if ( ! empty( $website->sync_errors ) || $system_utility->is_suspended_site( $website ) ) {
2010 continue;
2011 }
2012 $db_websites[ $website->id ] = $utility->map_site(
2013 $website,
2014 $data_fields
2015 );
2016 $website_url[ $website->id ] = $website->url;
2017 }
2018 }
2019 }
2020
2021 return compact( 'db_websites', 'website_url' );
2022 }
2023 }
2024