PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.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 5.2.2 5.3 All 153 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.1, at includes/rest-api/controller/version2/class-mainwp-rest-controller.php

1,957 lines 69.6 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 * Bulk create, update and delete items.
479 *
480 * @param WP_REST_Request $request Full details about the request.
481 * @return array Of WP_Error or WP_REST_Response.
482 */
483 public function batch_items( $request ) { //phpcs:ignore -- NOSONAR complex function.
484 /**
485 * REST Server
486 *
487 * @var WP_REST_Server $wp_rest_server
488 */
489 global $wp_rest_server;
490
491 // Get the request params.
492 $items = array_filter( $request->get_params() );
493 $query = $request->get_query_params();
494 $response = array();
495
496 // Check batch limit.
497 $limit = $this->check_batch_limit( $items );
498 if ( is_wp_error( $limit ) ) {
499 return $limit;
500 }
501
502 if ( ! empty( $items['create'] ) ) {
503 foreach ( $items['create'] as $item ) {
504 $_item = new WP_REST_Request( 'POST', $request->get_route() );
505
506 // Default parameters.
507 $defaults = array();
508 $schema = $this->get_public_item_schema();
509 foreach ( $schema['properties'] as $arg => $options ) {
510 if ( isset( $options['default'] ) ) {
511 $defaults[ $arg ] = $options['default'];
512 }
513 }
514 $_item->set_default_params( $defaults );
515
516 // Set request parameters.
517 $_item->set_body_params( $item );
518
519 // Set query (GET) parameters.
520 $_item->set_query_params( $query );
521
522 $_response = $this->create_item( $_item );
523
524 if ( is_wp_error( $_response ) ) {
525 $response['create'][] = array(
526 'id' => 0,
527 'error' => array(
528 'code' => $_response->get_error_code(),
529 'message' => $_response->get_error_message(),
530 'data' => $_response->get_error_data(),
531 ),
532 );
533 } else {
534 $response['create'][] = $wp_rest_server->response_to_data( $_response, '' );
535 }
536 }
537 }
538
539 if ( ! empty( $items['update'] ) ) {
540 foreach ( $items['update'] as $item ) {
541 $_item = new WP_REST_Request( 'PUT', $request->get_route() );
542 $_item->set_body_params( $item );
543 $_response = $this->update_item( $_item );
544
545 if ( is_wp_error( $_response ) ) {
546 $response['update'][] = array(
547 'id' => $item['id'],
548 'error' => array(
549 'code' => $_response->get_error_code(),
550 'message' => $_response->get_error_message(),
551 'data' => $_response->get_error_data(),
552 ),
553 );
554 } else {
555 $response['update'][] = $wp_rest_server->response_to_data( $_response, '' );
556 }
557 }
558 }
559
560 if ( ! empty( $items['delete'] ) ) {
561 foreach ( $items['delete'] as $id ) {
562 $id = (int) $id;
563
564 if ( 0 === $id ) {
565 continue;
566 }
567
568 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
569 $_item->set_query_params(
570 array(
571 'id' => $id,
572 'force' => true,
573 )
574 );
575 $_response = $this->delete_item( $_item );
576
577 if ( is_wp_error( $_response ) ) {
578 $response['delete'][] = array(
579 'id' => $id,
580 'error' => array(
581 'code' => $_response->get_error_code(),
582 'message' => $_response->get_error_message(),
583 'data' => $_response->get_error_data(),
584 ),
585 );
586 } else {
587 $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' );
588 }
589 }
590 }
591
592 $route = $request->get_route();
593 if ( MainWP_Utility::string_ends_by( $route, '/sites/batch' ) ) {
594 if ( ! empty( $items['sync'] ) ) {
595 foreach ( $items['sync'] as $id ) {
596 $id = (int) $id;
597
598 if ( 0 === $id ) {
599 continue;
600 }
601
602 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
603 $_item->set_query_params(
604 array(
605 'id' => $id,
606 )
607 );
608 $_response = $this->sync_item( $_item );
609
610 if ( is_wp_error( $_response ) ) {
611 $response['sync'][] = array(
612 'id' => $id,
613 'error' => array(
614 'code' => $_response->get_error_code(),
615 'message' => $_response->get_error_message(),
616 'data' => $_response->get_error_data(),
617 ),
618 );
619 } else {
620 $response['sync'][] = $wp_rest_server->response_to_data( $_response, '' );
621 }
622 }
623 }
624
625 if ( ! empty( $items['reconnect'] ) ) {
626 foreach ( $items['reconnect'] as $id ) {
627 $id = (int) $id;
628
629 if ( 0 === $id ) {
630 continue;
631 }
632
633 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
634 $_item->set_query_params(
635 array(
636 'id' => $id,
637 )
638 );
639 $_response = $this->reconnect_item( $_item );
640
641 if ( is_wp_error( $_response ) ) {
642 $response['reconnect'][] = array(
643 'id' => $id,
644 'error' => array(
645 'code' => $_response->get_error_code(),
646 'message' => $_response->get_error_message(),
647 'data' => $_response->get_error_data(),
648 ),
649 );
650 } else {
651 $response['reconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
652 }
653 }
654 }
655
656 if ( ! empty( $items['disconnect'] ) ) {
657 foreach ( $items['disconnect'] as $id ) {
658 $id = (int) $id;
659
660 if ( 0 === $id ) {
661 continue;
662 }
663
664 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
665 $_item->set_query_params(
666 array(
667 'id' => $id,
668 )
669 );
670 $_response = $this->disconnect_site( $_item );
671
672 if ( is_wp_error( $_response ) ) {
673 $response['disconnect'][] = array(
674 'id' => $id,
675 'error' => array(
676 'code' => $_response->get_error_code(),
677 'message' => $_response->get_error_message(),
678 'data' => $_response->get_error_data(),
679 ),
680 );
681 } else {
682 $response['disconnect'][] = $wp_rest_server->response_to_data( $_response, '' );
683 }
684 }
685 }
686
687 if ( ! empty( $items['suspend'] ) ) {
688 foreach ( $items['suspend'] as $id ) {
689 $id = (int) $id;
690
691 if ( 0 === $id ) {
692 continue;
693 }
694
695 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
696 $_item->set_query_params(
697 array(
698 'id' => $id,
699 )
700 );
701 $_response = $this->suspend_item( $_item );
702
703 if ( is_wp_error( $_response ) ) {
704 $response['suspend'][] = array(
705 'id' => $id,
706 'error' => array(
707 'code' => $_response->get_error_code(),
708 'message' => $_response->get_error_message(),
709 'data' => $_response->get_error_data(),
710 ),
711 );
712 } else {
713 $response['suspend'][] = $wp_rest_server->response_to_data( $_response, '' );
714 }
715 }
716 }
717
718 if ( ! empty( $items['check'] ) ) {
719 foreach ( $items['check'] as $id ) {
720 $id = (int) $id;
721
722 if ( 0 === $id ) {
723 continue;
724 }
725
726 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
727 $_item->set_query_params(
728 array(
729 'id' => $id,
730 )
731 );
732 $_response = $this->check_item( $_item );
733
734 if ( is_wp_error( $_response ) ) {
735 $response['check'][] = array(
736 'id' => $id,
737 'error' => array(
738 'code' => $_response->get_error_code(),
739 'message' => $_response->get_error_message(),
740 'data' => $_response->get_error_data(),
741 ),
742 );
743 } else {
744 $response['check'][] = $wp_rest_server->response_to_data( $_response, '' );
745 }
746 }
747 }
748
749 if ( ! empty( $items['remove'] ) ) {
750 foreach ( $items['remove'] as $id ) {
751 $id = (int) $id;
752
753 if ( 0 === $id ) {
754 continue;
755 }
756
757 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
758 $_item->set_query_params(
759 array(
760 'id' => $id,
761 )
762 );
763 $_response = $this->delete_item( $_item );
764
765 if ( is_wp_error( $_response ) ) {
766 $response['remove'][] = array(
767 'id' => $id,
768 'error' => array(
769 'code' => $_response->get_error_code(),
770 'message' => $_response->get_error_message(),
771 'data' => $_response->get_error_data(),
772 ),
773 );
774 } else {
775 $response['remove'][] = $wp_rest_server->response_to_data( $_response, '' );
776 }
777 }
778 }
779
780 if ( ! empty( $items['security'] ) ) {
781 foreach ( $items['security'] as $id ) {
782 $id = (int) $id;
783
784 if ( 0 === $id ) {
785 continue;
786 }
787
788 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
789 $_item->set_query_params(
790 array(
791 'id' => $id,
792 )
793 );
794 $_response = $this->security_item( $_item );
795
796 if ( is_wp_error( $_response ) ) {
797 $response['security'][] = array(
798 '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['security'][] = $wp_rest_server->response_to_data( $_response, '' );
807 }
808 }
809 }
810
811 if ( ! empty( $items['plugins'] ) ) {
812 foreach ( $items['plugins'] as $id ) {
813 $id = (int) $id;
814
815 if ( 0 === $id ) {
816 continue;
817 }
818
819 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
820 $_item->set_query_params(
821 array(
822 'id' => $id,
823 )
824 );
825 $_response = $this->get_site_plugins( $_item );
826
827 if ( is_wp_error( $_response ) ) {
828 $response['plugins'][] = array(
829 'id' => $id,
830 'error' => array(
831 'code' => $_response->get_error_code(),
832 'message' => $_response->get_error_message(),
833 'data' => $_response->get_error_data(),
834 ),
835 );
836 } else {
837 $response['plugins'][] = $wp_rest_server->response_to_data( $_response, '' );
838 }
839 }
840 }
841
842 if ( ! empty( $items['themes'] ) ) {
843 foreach ( $items['themes'] as $id ) {
844 $id = (int) $id;
845
846 if ( 0 === $id ) {
847 continue;
848 }
849
850 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
851 $_item->set_query_params(
852 array(
853 'id' => $id,
854 )
855 );
856 $_response = $this->get_site_themes( $_item );
857
858 if ( is_wp_error( $_response ) ) {
859 $response['themes'][] = array(
860 'id' => $id,
861 'error' => array(
862 'code' => $_response->get_error_code(),
863 'message' => $_response->get_error_message(),
864 'data' => $_response->get_error_data(),
865 ),
866 );
867 } else {
868 $response['themes'][] = $wp_rest_server->response_to_data( $_response, '' );
869 }
870 }
871 }
872
873 if ( ! empty( $items['non-mainwp-changes'] ) ) {
874 foreach ( $items['non-mainwp-changes'] as $id ) {
875 $id = (int) $id;
876
877 if ( 0 === $id ) {
878 continue;
879 }
880
881 $_item = new WP_REST_Request( 'DELETE', $request->get_route() );
882 $_item->set_query_params(
883 array(
884 'id' => $id,
885 )
886 );
887 $_response = $this->get_non_mainwp_changes_of_site( $_item );
888
889 if ( is_wp_error( $_response ) ) {
890 $response['non-mainwp-changes'][] = array(
891 'id' => $id,
892 'error' => array(
893 'code' => $_response->get_error_code(),
894 'message' => $_response->get_error_message(),
895 'data' => $_response->get_error_data(),
896 ),
897 );
898 } else {
899 $response['non-mainwp-changes'][] = $wp_rest_server->response_to_data( $_response, '' );
900 }
901 }
902 }
903 }
904
905 return $response;
906 }
907
908 /**
909 * Validate a text value for a text based setting.
910 *
911 * @since 5.2
912 * @param string $value Value.
913 * @param array $setting Setting.
914 * @return string
915 */
916 public function validate_setting_text_field( $value, $setting ) {
917 $value = is_null( $value ) ? '' : $value;
918 return wp_kses_post( trim( stripslashes( $value ) ) );
919 }
920
921 /**
922 * Validate select based settings.
923 *
924 * @since 5.2
925 * @param string $value Value.
926 * @param array $setting Setting.
927 * @return string|WP_Error
928 */
929 public function validate_setting_select_field( $value, $setting ) {
930 if ( array_key_exists( $value, $setting['options'] ) ) {
931 return $value;
932 } else {
933 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
934 }
935 }
936
937 /**
938 * Validate multiselect based settings.
939 *
940 * @since 5.2
941 * @param array $values Values.
942 * @param array $setting Setting.
943 * @return array|WP_Error
944 */
945 public function validate_setting_multiselect_field( $values, $setting ) {
946 if ( empty( $values ) ) {
947 return array();
948 }
949
950 if ( ! is_array( $values ) ) {
951 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
952 }
953
954 $final_values = array();
955 foreach ( $values as $value ) {
956 if ( array_key_exists( $value, $setting['options'] ) ) {
957 $final_values[] = $value;
958 }
959 }
960
961 return $final_values;
962 }
963
964 /**
965 * Validate image_width based settings.
966 *
967 * @since 5.2
968 * @param array $values Values.
969 * @param array $setting Setting.
970 * @return string|WP_Error
971 */
972 public function validate_setting_image_width_field( $values, $setting ) {
973 if ( ! is_array( $values ) ) {
974 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
975 }
976
977 $current = $setting['value'];
978 if ( isset( $values['width'] ) ) {
979 $current['width'] = intval( $values['width'] );
980 }
981 if ( isset( $values['height'] ) ) {
982 $current['height'] = intval( $values['height'] );
983 }
984 if ( isset( $values['crop'] ) ) {
985 $current['crop'] = (bool) $values['crop'];
986 }
987 return $current;
988 }
989
990 /**
991 * Validate radio based settings.
992 *
993 * @since 5.2
994 * @param string $value Value.
995 * @param array $setting Setting.
996 * @return string|WP_Error
997 */
998 public function validate_setting_radio_field( $value, $setting ) {
999 return $this->validate_setting_select_field( $value, $setting );
1000 }
1001
1002 /**
1003 * Validate checkbox based settings.
1004 *
1005 * @since 5.2
1006 * @param string $value Value.
1007 * @param array $setting Setting.
1008 * @return string|WP_Error
1009 */
1010 public function validate_setting_checkbox_field( $value, $setting ) {
1011 if ( in_array( $value, array( 'yes', 'no' ) ) ) {
1012 return $value;
1013 } elseif ( empty( $value ) ) {
1014 return isset( $setting['default'] ) ? $setting['default'] : 'no';
1015 } else {
1016 return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'mainwp' ), array( 'status' => 400 ) );
1017 }
1018 }
1019
1020 /**
1021 * Validate textarea based settings.
1022 *
1023 * @since 5.2
1024 * @param string $value Value.
1025 * @param array $setting Setting.
1026 * @return string
1027 */
1028 public function validate_setting_textarea_field( $value, $setting ) {
1029 $value = is_null( $value ) ? '' : $value;
1030 return wp_kses(
1031 trim( stripslashes( $value ) ),
1032 array_merge(
1033 array(
1034 'iframe' => array(
1035 'src' => true,
1036 'style' => true,
1037 'id' => true,
1038 'class' => true,
1039 ),
1040 ),
1041 wp_kses_allowed_html( 'post' )
1042 )
1043 );
1044 }
1045
1046
1047 /**
1048 * Get the batch schema, conforming to JSON Schema.
1049 *
1050 * @return array
1051 */
1052 public function get_public_batch_schema() {
1053 return array(
1054 '$schema' => 'http://json-schema.org/draft-04/schema#',
1055 'title' => 'batch',
1056 'type' => 'object',
1057 'properties' => array(
1058 'create' => array(
1059 'description' => __( 'List of created resources.', 'mainwp' ),
1060 'type' => 'array',
1061 'context' => array( 'view', 'edit' ),
1062 'items' => array(
1063 'type' => 'object',
1064 ),
1065 ),
1066 'update' => array(
1067 'description' => __( 'List of updated resources.', 'mainwp' ),
1068 'type' => 'array',
1069 'context' => array( 'view', 'edit' ),
1070 'items' => array(
1071 'type' => 'object',
1072 ),
1073 ),
1074 'delete' => array(
1075 'description' => __( 'List of delete resources.', 'mainwp' ),
1076 'type' => 'array',
1077 'context' => array( 'view', 'edit' ),
1078 'items' => array(
1079 'type' => 'integer',
1080 ),
1081 ),
1082 ),
1083 );
1084 }
1085
1086
1087 /**
1088 * Get formatted item data, not including orders count nor total spent.
1089 * This method is needed because v3 API doesn't return those two fields.
1090 *
1091 * @internal This method could disappear or have its name or signature changed in future releases.
1092 *
1093 * @param object $obj data instance.
1094 * @return array
1095 */
1096 protected function get_formatted_item_data_core( $obj ) {
1097 return array();
1098 }
1099
1100 /**
1101 * Get formatted item data, not including orders count nor total spent.
1102 * This method is needed because v3 API doesn't return those two fields.
1103 *
1104 * @internal This method could disappear or have its name or signature changed in future releases.
1105 *
1106 * @param array $data data instance.
1107 * @return array
1108 */
1109 protected function get_pre_formatted_item_data( $data ) {
1110 return $data;
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 array $data data instance.
1121 * @return array
1122 */
1123 protected function get_formatted_item_data( $data ) { //phpcs:ignore -- NOSONAR - compatible.
1124 return $data;
1125 }
1126
1127 /**
1128 * Gets an array of fields to be included on the response.
1129 *
1130 * Included fields are based on item schema and `_fields=` request argument.
1131 * Updated from WordPress 5.3, included into this class to support old versions.
1132 *
1133 * @since 5.2
1134 * @param WP_REST_Request $request Full details about the request.
1135 * @return array Fields to be included in the response.
1136 */
1137 public function get_fields_for_response( $request ) { //phpcs:ignore -- NOSONAR - complex.
1138 // From xdebug profiling, this method could take upto 25% of request time in index calls.
1139 // Cache it and make sure _fields was cached on current request object!
1140 if ( isset( $this->_fields ) && is_array( $this->_fields ) && $request === $this->_request ) {
1141 return $this->_fields;
1142 }
1143 $this->_request = $request;
1144
1145 $schema = $this->get_item_schema();
1146 $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
1147
1148 // Exclude fields that specify a different context than the request context.
1149 $context = isset( $request['context'] ) ? $request['context'] : 'view';
1150 if ( $context ) {
1151 foreach ( $properties as $name => $options ) {
1152 if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
1153 unset( $properties[ $name ] );
1154 }
1155 }
1156 }
1157
1158 $fields = array_keys( $properties );
1159
1160 if ( ! isset( $request['_fields'] ) ) {
1161 $this->_fields = $fields;
1162 return $fields;
1163 }
1164 $requested_fields = wp_parse_list( $request['_fields'] );
1165 if ( empty( $requested_fields ) ) {
1166 $this->_fields = $fields;
1167 return $fields;
1168 }
1169 // Trim off outside whitespace from the comma delimited list.
1170 $requested_fields = array_map( 'trim', $requested_fields );
1171 // Always persist 'id', because it can be needed for add_additional_fields_to_object().
1172 if ( in_array( 'id', $fields, true ) ) {
1173 $requested_fields[] = 'id';
1174 }
1175 // Return the list of all requested fields which appear in the schema.
1176 $this->_fields = array_reduce(
1177 $requested_fields,
1178 function ( $response_fields, $field ) use ( $fields ) {
1179 if ( in_array( $field, $fields, true ) ) {
1180 $response_fields[] = $field;
1181 return $response_fields;
1182 }
1183 // Check for nested fields if $field is not a direct match.
1184 $nested_fields = explode( '.', $field );
1185 // A nested field is included so long as its top-level property.
1186 // is present in the schema.
1187 if ( in_array( $nested_fields[0], $fields, true ) ) {
1188 $response_fields[] = $field;
1189 }
1190 return $response_fields;
1191 },
1192 array()
1193 );
1194 return $this->_fields;
1195 }
1196
1197 /**
1198 * Returns the full item schema.
1199 *
1200 * @return array
1201 */
1202 public function get_item_schema() {
1203 return array(
1204 '$schema' => 'http://json-schema.org/draft-04/schema#',
1205 'title' => $this->title,
1206 'type' => 'object',
1207 'properties' => $this->get_properties(),
1208 );
1209 }
1210
1211
1212 /**
1213 * Returns the full item response.
1214 *
1215 * @param mixed $item Item to get response for.
1216 * @return array|stdClass
1217 */
1218 public function get_item_response( $item ) {
1219 return array();
1220 }
1221
1222 /**
1223 * Return schema properties.
1224 *
1225 * @return array
1226 */
1227 public function get_properties() {
1228 return array();
1229 }
1230
1231 /**
1232 * Get route error.
1233 *
1234 * @param string $type_slug Type slug.
1235 * @param string $type 'object'.
1236 *
1237 * @return WP_Error
1238 */
1239 public function get_rest_data_error( $type_slug, $type = 'object' ) {
1240 if ( empty( $type ) && is_string( $type_slug ) ) {
1241 $type = $type_slug;
1242 }
1243 switch ( $type_slug ) {
1244 case 'id':
1245 return new WP_Error( "mainwp_rest_invalid_{$type}_id", __( 'Invalid or not found ID.', 'mainwp' ), array( 'status' => 404 ) );
1246 case 'domain':
1247 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid or not found domain.', 'mainwp' ), array( 'status' => 404 ) );
1248 case 'email':
1249 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid or not found email.', 'mainwp' ), array( 'status' => 404 ) );
1250 default:
1251 return new WP_Error( "mainwp_rest_invalid_{$type}_data", __( 'Invalid data.', 'mainwp' ), array( 'status' => 500 ) );
1252 }
1253 }
1254
1255 /**
1256 * Gets an array of fields to be included on the response.
1257 *
1258 * Included fields are based on item schema and `_fields=` request argument.
1259 * Updated from WordPress 5.3, included into this class to support old versions.
1260 *
1261 * @param array $item data.
1262 * @param string $context fields to filter.
1263 * @param array $addition_fields addition_fields to be included in the response.
1264 *
1265 * @return array addition_fields Fields to be included in the response.
1266 */
1267 public function filter_response_data_by_allowed_fields( $item, $context = 'view', $addition_fields = array() ) { //phpcs:ignore -- NOSONAR - complex.
1268 $data = $this->filter_response_by_context( $item, $context );
1269 $fields = $this->get_allowed_fields_by_context( $context );
1270
1271 if ( ! empty( $addition_fields ) && is_array( $addition_fields ) ) {
1272 $fields = array_values( array_unique( array_merge( $fields, $addition_fields ) ) );
1273 }
1274
1275 if ( is_array( $fields ) && ! empty( $fields ) ) {
1276 $_data = array();
1277 foreach ( $fields as $field ) {
1278 if ( is_array( $data ) ) {
1279 if ( isset( $data[ $field ] ) ) {
1280 $_data[ $field ] = $data[ $field ];
1281 } else {
1282 $_data[ $field ] = '';
1283 }
1284 } elseif ( is_object( $data ) ) {
1285 if ( property_exists( $data, $field ) ) {
1286 $_data[ $field ] = $data->{$field};
1287 } else {
1288 $_data[ $field ] = '';
1289 }
1290 }
1291 }
1292 $_data = $this->get_formatted_item_data( $_data );
1293 return $_data;
1294 }
1295 return $data;
1296 }
1297
1298 /**
1299 * Gets an array of fields to be included on the response.
1300 *
1301 * Included fields are based on item schema and `_fields=` request argument.
1302 * Updated from WordPress 5.3, included into this class to support old versions.
1303 *
1304 * @since 5.2
1305 * @param string $context context.
1306 * @return array Fields to be included in the response.
1307 */
1308 public function get_allowed_fields_by_context( $context ) {
1309 $schema = $this->get_item_schema();
1310 $properties = isset( $schema['properties'] ) ? $schema['properties'] : array();
1311 if ( $context ) {
1312 foreach ( $properties as $name => $options ) {
1313 if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) {
1314 unset( $properties[ $name ] );
1315 }
1316 }
1317 }
1318 return array_keys( $properties );
1319 }
1320
1321 /**
1322 * Check rest permissions callback.
1323 *
1324 * @param WP_REST_Request $request Full details about the request.
1325 *
1326 * @return bool
1327 */
1328 public function get_rest_permissions_check( $request ) {
1329 $valid = \MainWP_REST_Authentication::get_instance()->is_valid_permissions( $request );
1330 if ( is_wp_error( $valid ) ) {
1331 return $valid;
1332 }
1333 return true;
1334 }
1335
1336 /**
1337 * Method get_rest_api_user().
1338 *
1339 * @return mixed
1340 */
1341 public function get_rest_api_user() {
1342 return \MainWP_REST_Authentication::get_instance()->get_rest_valid_user();
1343 }
1344
1345 /**
1346 * Get the query params for collections of attachments.
1347 *
1348 * @return array
1349 */
1350 public function get_collection_params() {
1351 $params = array();
1352 $params['context'] = $this->get_context_param();
1353 $params['context']['default'] = 'view';
1354
1355 $params['page'] = array(
1356 'description' => __( 'Current page of the collection.', 'mainwp' ),
1357 'type' => 'integer',
1358 'default' => 1,
1359 'sanitize_callback' => 'absint',
1360 'validate_callback' => 'rest_validate_request_arg',
1361 'minimum' => 1,
1362 );
1363 $params['per_page'] = array(
1364 'description' => __( 'Maximum number of items to be returned in result set.', 'mainwp' ),
1365 'type' => 'integer',
1366 'default' => 10,
1367 'minimum' => 1,
1368 'maximum' => 100,
1369 'sanitize_callback' => 'absint',
1370 'validate_callback' => 'rest_validate_request_arg',
1371 );
1372 $params['search'] = array(
1373 'description' => __( 'Limit results to those matching a string.', 'mainwp' ),
1374 'type' => 'string',
1375 'sanitize_callback' => 'sanitize_text_field',
1376 'validate_callback' => 'rest_validate_request_arg',
1377 );
1378
1379 $params['slug'] = array(
1380 'default' => '',
1381 'description' => __( 'Slugs.', 'mainwp' ),
1382 'type' => 'array',
1383 'items' => array(
1384 'type' => 'string',
1385 ),
1386 'sanitize_callback' => 'wp_parse_list',
1387 'validate_callback' => 'rest_validate_request_arg',
1388 );
1389
1390 $params['status'] = array(
1391 'default' => '',
1392 'description' => __( 'Status.', 'mainwp' ),
1393 'type' => array( 'string' ),
1394 'sanitize_callback' => 'wp_parse_list',
1395 'validate_callback' => 'rest_validate_request_arg',
1396 );
1397
1398 $params['exclude'] = array(
1399 'description' => __( 'Exclude IDs.', 'mainwp' ),
1400 'type' => 'array',
1401 'items' => array(
1402 'type' => 'integer',
1403 ),
1404 'sanitize_callback' => 'wp_parse_id_list',
1405 );
1406 $params['include'] = array(
1407 'description' => __( 'Include IDs.', 'mainwp' ),
1408 'type' => 'array',
1409 'items' => array(
1410 'type' => 'integer',
1411 ),
1412 'sanitize_callback' => 'wp_parse_id_list',
1413 );
1414
1415 /**
1416 * Filter collection parameters for the controller.
1417 *
1418 * @param array $query_params JSON Schema-formatted collection parameters.
1419 * @param object This object.
1420 */
1421 return apply_filters( 'mainwp_rest_collection_params', $params, $this );
1422 }
1423
1424 /**
1425 * Method sanitize_request_slugs().
1426 *
1427 * @param array $slugs slugs.
1428 * @return array
1429 */
1430 public function sanitize_request_slugs( $slugs ) {
1431 if ( ! empty( $slugs ) ) {
1432 $slugs = array_map( 'sanitize_text_field', array_map( 'urldecode', $slugs ) );
1433 }
1434 return $slugs;
1435 }
1436
1437 /**
1438 * Prepare post or page data.
1439 *
1440 * @param array $new_post New post data.
1441 * @param array $post_custom Post custom data.
1442 * @param array $post_featured_image Post featured image data.
1443 * @param array $featured_image_data Featured image data.
1444 * @param array $post_gallery_images Post gallery images data.
1445 * @param array $post_category Post category data.
1446 *
1447 * @return array
1448 */
1449 public function prepare_post_page_data( $new_post, $post_custom, $post_featured_image, $featured_image_data, $post_gallery_images = '', $post_category = '' ) {
1450 return array(
1451 'new_post' => base64_encode( wp_json_encode( $new_post ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1452 'post_custom' => base64_encode( wp_json_encode( $post_custom ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1453 '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.
1454 '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.
1455 'mainwp_upload_dir' => base64_encode( wp_json_encode( wp_upload_dir() ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1456 'post_gallery_images' => base64_encode( wp_json_encode( $post_gallery_images ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1457 'post_category' => base64_encode( wp_json_encode( $post_category ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1458 );
1459 }
1460
1461 /**
1462 * Decode post or page data.
1463 *
1464 * @param array $data Data.
1465 *
1466 * @return array
1467 */
1468 public function decode_post_page_data( $data ) {
1469 // Decode base64 json.
1470 $decode_base64_json = function ( $value, $default_value = '', $type = 'array' ) {
1471 if ( empty( $value ) ) {
1472 return $default_value;
1473 }
1474 $decoded = base64_decode( $value, true ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1475 if ( false === $decoded ) {
1476 return $default_value;
1477 }
1478
1479 if ( 'string' === $type ) {
1480 return ! empty( $decoded ) ? rawurldecode( $decoded ) : $default_value;
1481 }
1482
1483 $json = json_decode( $decoded, true );
1484 return is_array( $json ) ? $json : $default_value;
1485 };
1486 // Safely decode page data.
1487 return array(
1488 'new_post' => $decode_base64_json( $data['new_post'] ?? '' ),
1489 'post_custom' => $decode_base64_json( $data['post_custom'] ?? '' ),
1490 'post_featured_image' => $decode_base64_json( $data['post_featured_image'] ?? '', '', 'string' ),
1491 'featured_image_data' => $decode_base64_json( $data['featured_image_data'] ?? '' ),
1492 'mainwp_upload_dir' => $decode_base64_json( $data['mainwp_upload_dir'] ?? '' ),
1493 'child_upload_dir' => $decode_base64_json( $data['child_upload_dir'] ?? '' ),
1494 'post_gallery_images' => $decode_base64_json( $data['post_gallery_images'] ?? '' ),
1495 'post_category' => $decode_base64_json( $data['post_category'] ?? '', '', 'string' ),
1496 );
1497 }
1498
1499 /**
1500 * Sanitize field.
1501 *
1502 * @param mixed $value Value to sanitize.
1503 *
1504 * @return mixed
1505 */
1506 public function sanitize_field( $value ) {
1507 if ( null === $value || '' === $value ) {
1508 return '';
1509 }
1510 return sanitize_text_field( wp_unslash( trim( $value ) ) );
1511 }
1512
1513 /**
1514 * Make enum sanitizer.
1515 *
1516 * @param array $allowed Allowed values.
1517 * @param string $type Type to coerce to.
1518 *
1519 * @return callable
1520 */
1521 public function make_enum_sanitizer( array $allowed, string $type = 'int' ) { // phpcs:ignore -- NOSONAR - complex.
1522 $allowed_norm = array_map( fn( $v ) => $this->coerce_type( $v, $type ), $allowed );
1523
1524 return function ( $value, $request, $param ) use ( $allowed_norm, $type, $allowed ) { // phpcs:ignore -- NOSONAR
1525 if ( null === $value || '' === $value ) {
1526 return $value;
1527 }
1528
1529 if ( 'array' === $type ) {
1530 if ( ! is_array( $value ) ) {
1531 $value = $this->sanitize_field( $value );
1532 if ( is_string( $value ) && strpos( $value, ',' ) !== false ) {
1533 $value = array_map( 'trim', explode( ',', $value ) );
1534 } else {
1535 $value = array( $value );
1536 }
1537 }
1538
1539 $sanitized = array();
1540 foreach ( $value as $item ) {
1541 $item = $this->sanitize_field( $item );
1542 if ( in_array( $item, $allowed, true ) ) {
1543 $sanitized[] = $item;
1544 } else {
1545 return new WP_Error(
1546 "invalid_{$param}",
1547 sprintf(
1548 /* translators: 1: field name, 2: allowed list */
1549 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ), // NOSONAR.
1550 esc_html( $param ),
1551 esc_html( implode( ', ', $allowed ) )
1552 ),
1553 );
1554 }
1555 }
1556 return $sanitized;
1557 }
1558
1559 // Standard sanitization for non-array types.
1560 $value = $this->sanitize_field( $value );
1561 $v = $this->coerce_type( $value, $type );
1562
1563 if ( in_array( $v, $allowed_norm, true ) ) {
1564 return $v;
1565 }
1566
1567 return new WP_Error(
1568 "invalid_{$param}",
1569 sprintf(
1570 /* translators: 1: field name, 2: allowed list */
1571 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1572 esc_html( $param ),
1573 esc_html( implode( ', ', $allowed_norm ) )
1574 ),
1575 );
1576 };
1577 }
1578
1579 /**
1580 * Coerce value to type.
1581 *
1582 * @param mixed $value Value to coerce.
1583 * @param string $type Type to coerce to.
1584 *
1585 * @return mixed
1586 */
1587 public function coerce_type( $value, string $type ) { // phpcs:ignore -- NOSONAR
1588 switch ( $type ) {
1589 case 'int':
1590 return (int) $value;
1591 case 'bool':
1592 return (bool) $value;
1593 case 'array':
1594 return (array) $value;
1595 case 'string':
1596 default:
1597 return (string) $value;
1598 }
1599 }
1600
1601 /**
1602 * Make enum validator.
1603 *
1604 * @param array $allowed Allowed values.
1605 * @param string $type Type to coerce to.
1606 *
1607 * @return callable
1608 */
1609 public function make_enum_validator( array $allowed, string $type = 'int' ) { // phpcs:ignore -- NOSONAR
1610 $allowed_norm = array_map( fn( $v ) => $this->coerce_type( $v, $type ), $allowed );
1611
1612 return function ( $value, $request, $param ) use ( $allowed_norm, $type, $allowed ) { // phpcs:ignore -- NOSONAR
1613 if ( null === $value || '' === $value ) {
1614 return true;
1615 }
1616
1617 if ( 'array' === $type ) {
1618 if ( ! is_array( $value ) ) {
1619 $value = $this->sanitize_field( $value );
1620 if ( is_string( $value ) && strpos( $value, ',' ) !== false ) {
1621 $value = array_map( 'trim', explode( ',', $value ) );
1622 } else {
1623 $value = array( $value );
1624 }
1625 }
1626
1627 // Validate each element in the array.
1628 foreach ( $value as $item ) {
1629 $item = $this->sanitize_field( $item );
1630 if ( ! in_array( $item, $allowed, true ) ) {
1631 return new WP_Error(
1632 "invalid_{$param}",
1633 sprintf(
1634 /* translators: 1: field name, 2: allowed list */
1635 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1636 esc_html( $param ),
1637 esc_html( implode( ', ', $allowed ) )
1638 ),
1639 );
1640 }
1641 }
1642 return true;
1643 }
1644
1645 // Standard validation for non-array types.
1646 $value = $this->sanitize_field( $value );
1647 $v = $this->coerce_type( $value, $type );
1648
1649 if ( in_array( $v, $allowed_norm, true ) ) {
1650 return true;
1651 }
1652
1653 return new WP_Error(
1654 "invalid_{$param}",
1655 sprintf(
1656 /* translators: 1: field name, 2: allowed list */
1657 __( 'Invalid %1$s. Allowed values: %2$s.', 'mainwp' ),
1658 esc_html( $param ),
1659 esc_html( implode( ', ', $allowed_norm ) )
1660 ),
1661 );
1662 };
1663 }
1664
1665 /**
1666 * Pages or Posts search handler for REST API.
1667 *
1668 * @param mixed $data Search data from child site.
1669 * @param object $website Child site object.
1670 * @param mixed $output Output object to store results.
1671 * @param array $params Request parameters.
1672 *
1673 * @return void
1674 */
1675 public static function posts_pages_search_handler( $data, $website, &$output, $params = array() ) {
1676 if ( ! isset( $output->errors ) ) {
1677 $output->errors = array();
1678 }
1679
1680 if ( preg_match( '/<mainwp>(.*)<\/mainwp>/', $data, $results ) > 0 ) {
1681 $result = $results[1];
1682 $pages = MainWP\Dashboard\MainWP_System_Utility::get_child_response( base64_decode( $result ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1683
1684 if ( is_array( $pages ) && isset( $pages['error'] ) ) {
1685 $output->errors[ $website->id ] = esc_html( $pages['error'] );
1686 return;
1687 }
1688
1689 $output->results[ $website->id ] = $pages;
1690 }
1691 }
1692
1693 /**
1694 * Get post/page id and remote edit data.
1695 *
1696 * @param object $website Website.
1697 * @param WP_REST_Request $request Full details about the request.
1698 * @param string $type Post type: 'page' or 'post' (or any CPT if child supports).
1699 *
1700 * @return array|WP_Error
1701 */
1702 protected function get_request_post_page_id( $website, $request, $type = 'page' ) { // phpcs:ignore -- NOSONAR
1703 // Param names.
1704 $type = ( 'post' === $type ) ? 'post' : 'page';
1705 $type_id_param = ( 'page' === $type ) ? 'id_page' : 'id_post';
1706 $id = $request->get_param( $type_id_param );
1707
1708 if ( empty( $id ) ) {
1709 return new WP_Error(
1710 'post_id_not_found',
1711 ( 'page' === $type ) ? __( 'Page id not found.', 'mainwp' ) : __( 'Post id not found.', 'mainwp' )
1712 );
1713 }
1714
1715 // Fetch remote edit data.
1716 try {
1717 $information = MainWP_Connect::fetch_url_authed(
1718 $website,
1719 'post_action',
1720 array(
1721 'action' => 'get_edit',
1722 'id' => $id,
1723 'post_type' => $type,
1724 )
1725 );
1726 } catch ( MainWP_Exception $e ) {
1727 return new WP_Error( 'get_post_error', MainWP_Error_Helper::get_error_message( $e ) );
1728 }
1729
1730 // Validate response.
1731 if ( empty( $information['status'] ) || 'SUCCESS' !== $information['status'] || empty( $information['my_post'] ) ) {
1732 return new WP_Error(
1733 'post_not_exist',
1734 ( 'page' === $type ) ? __( 'Page not exist.', 'mainwp' ) : __( 'Post not exist.', 'mainwp' )
1735 );
1736 }
1737
1738 return array(
1739 'data' => $information,
1740 'post_id' => $id,
1741 'post_type' => $type,
1742 );
1743 }
1744
1745 /**
1746 * Validate site ids.
1747 *
1748 * @param string $value Site id.
1749 * @param WP_REST_Request $request Request object.
1750 *
1751 * @return bool|WP_Error
1752 */
1753 public function validate_site_ids( $value, $request ) {
1754 if ( empty( $value ) ) {
1755 return true;
1756 }
1757 $value = $this->sanitize_field( $value );
1758 $site_ids = explode( ',', $value );
1759 foreach ( $site_ids as $site_id ) {
1760 $db_site = \MainWP\Dashboard\MainWP_DB::instance()->get_website_by_id( trim( $site_id ) );
1761 if ( ! $db_site ) {
1762 return new WP_Error(
1763 'invalid_site',
1764 sprintf(
1765 /* translators: %s: site ID */
1766 __( 'Invalid site ID: %s.', 'mainwp' ),
1767 esc_html( $site_id )
1768 ),
1769 );
1770 }
1771 }
1772 return true;
1773 }
1774
1775 /**
1776 * Sanitize text field to array.
1777 *
1778 * @param string $value Client id.
1779 *
1780 * @return string|array
1781 */
1782 public function sanitize_text_field_to_array( $value ) {
1783 if ( empty( $value ) ) {
1784 return '';
1785 }
1786 $value = $this->sanitize_field( $value );
1787 return array_map( 'trim', explode( ',', $value ) );
1788 }
1789
1790 /**
1791 * Validate clients.
1792 *
1793 * @param string $value Client id.
1794 * @param WP_REST_Request $request Request object.
1795 *
1796 * @return bool|WP_Error
1797 */
1798 public function validate_clients( $value, $request ) {
1799 if ( empty( $value ) ) {
1800 return true;
1801 }
1802 $value = $this->sanitize_field( $value );
1803 $clients = array_map( 'trim', explode( ',', $value ) );
1804
1805 foreach ( $clients as $client ) {
1806 $db_client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $client );
1807 if ( ! $db_client ) {
1808 return new WP_Error(
1809 'invalid_client',
1810 sprintf(
1811 /* translators: %s: client ID */
1812 __( 'Invalid client ID: %s.', 'mainwp' ),
1813 esc_html( $client )
1814 ),
1815 );
1816 }
1817 }
1818 return true;
1819 }
1820 /**
1821 * Sanitize groups text field.
1822 *
1823 * @param string $value Group name.
1824 *
1825 * @return string
1826 */
1827 public function sanitize_groups_text_field( $value ) {
1828 if ( empty( $value ) ) {
1829 return '';
1830 }
1831 $value = $this->sanitize_field( $value );
1832 $group_ids = array();
1833 $groups = array_map( 'trim', explode( ',', $value ) );
1834 foreach ( $groups as $group ) {
1835 $db_group = \MainWP\Dashboard\MainWP_DB_Common::instance()->get_group_by_name( $group );
1836 if ( ! $db_group ) {
1837 return new WP_Error(
1838 'invalid_group',
1839 sprintf(
1840 /* translators: %s: group name */
1841 __( 'Invalid Group: %s.', 'mainwp' ),
1842 esc_html( $group )
1843 ),
1844 );
1845 }
1846 $group_ids[] = $db_group->id;
1847 }
1848 return $group_ids;
1849 }
1850
1851 /**
1852 * Get group ids from group names.
1853 *
1854 * @param string $value Group name.
1855 * @param mixed $request Request object.
1856 * @return bool|WP_Error True if valid, WP_Error otherwise.
1857 */
1858 public function validate_groups( $value, $request ) {
1859 if ( empty( $value ) ) {
1860 return true;
1861 }
1862 $value = $this->sanitize_field( $value );
1863 $groups = array_map( 'trim', explode( ',', $value ) );
1864 foreach ( $groups as $group ) {
1865 $db_group = \MainWP\Dashboard\MainWP_DB_Common::instance()->get_group_by_name( $group );
1866 if ( ! $db_group ) {
1867 return new WP_Error(
1868 'invalid_group',
1869 sprintf(
1870 /* translators: %s: group name */
1871 __( 'Invalid Group: %s.', 'mainwp' ),
1872 esc_html( $group )
1873 ),
1874 );
1875 }
1876 }
1877 return true;
1878 }
1879
1880 /**
1881 * Get websites by filter.
1882 *
1883 * @param array $sites Sites.
1884 * @param array $groups Groups.
1885 * @param array $clients Clients.
1886 *
1887 * @return array
1888 */
1889 protected function get_db_websites_by_filter( $sites, $groups, $clients ) { // phpcs:ignore -- NOSONAR - complex.
1890 $utility = MainWP_Utility::instance();
1891 $system_utility = new MainWP_System_Utility();
1892 $db = MainWP_DB::instance();
1893 $data_fields = $system_utility->get_default_map_site_fields();
1894 $data_fields[] = 'users';
1895
1896 // Default result.
1897 $website_url = array();
1898 $db_websites = array();
1899
1900 if ( ! empty( $sites ) && is_array( $sites ) ) {
1901 foreach ( $sites as $v ) {
1902 if ( $utility->ctype_digit( $v ) ) {
1903 $website = $db->get_website_by_id( $v );
1904 if ( $website && empty( $website->sync_errors ) && ! $system_utility->is_suspended_site( $website ) ) {
1905 $db_websites[ $website->id ] = $utility->map_site(
1906 $website,
1907 $data_fields
1908 );
1909 $website_url[ $website->id ] = $website->url;
1910 }
1911 }
1912 }
1913 }
1914 if ( ! empty( $groups ) && is_array( $groups ) ) {
1915 foreach ( $groups as $v ) {
1916 if ( $utility->ctype_digit( $v ) ) {
1917 $websites = $db->query( $db->get_sql_websites_by_group_id( $v ) );
1918 while ( $websites && ( $website = $db->fetch_object( $websites ) ) ) {
1919 if ( ! empty( $website->sync_errors ) || $system_utility->is_suspended_site( $website ) ) {
1920 continue;
1921 }
1922 $db_websites[ $website->id ] = $utility->map_site(
1923 $website,
1924 $data_fields
1925 );
1926 $website_url[ $website->id ] = $website->url;
1927 }
1928 $db->free_result( $websites );
1929 }
1930 }
1931 }
1932
1933 if ( ! empty( $clients ) && is_array( $clients ) ) {
1934 $websites = MainWP_DB_Client::instance()->get_websites_by_client_ids(
1935 $clients,
1936 array(
1937 'select_data' => $data_fields,
1938 )
1939 );
1940 if ( $websites ) {
1941 foreach ( $websites as $website ) {
1942 if ( ! empty( $website->sync_errors ) || $system_utility->is_suspended_site( $website ) ) {
1943 continue;
1944 }
1945 $db_websites[ $website->id ] = $utility->map_site(
1946 $website,
1947 $data_fields
1948 );
1949 $website_url[ $website->id ] = $website->url;
1950 }
1951 }
1952 }
1953
1954 return compact( 'db_websites', 'website_url' );
1955 }
1956 }
1957