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-clients-controller.php

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

1,394 lines 52.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * MainWP REST Controller
4 *
5 * This class handles the REST API
6 *
7 * @package MainWP\Dashboard
8 */
9
10 use MainWP\Dashboard\MainWP_DB_Client;
11 use MainWP\Dashboard\MainWP_Client_Handler;
12 use MainWP\Dashboard\Module\CostTracker\Cost_Tracker_DB;
13 use MainWP\Dashboard\Module\CostTracker\Cost_Tracker_Rest_Api_Handle_V1;
14 use MainWP\Dashboard\MainWP_Utility;
15
16 // Exit if accessed directly.
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit;
19 }
20
21 /**
22 * Class MainWP_Rest_Clients_Controller
23 *
24 * @package MainWP\Dashboard
25 */
26 class MainWP_Rest_Clients_Controller extends MainWP_REST_Controller { //phpcs:ignore -- NOSONAR - multi methods.
27
28 // phpcs:disable Generic.Metrics.CyclomaticComplexity -- complexity.
29
30 /**
31 * Protected static variable to hold the single instance of the class.
32 *
33 * @var mixed Default null
34 */
35 private static $instance = null;
36
37
38 /**
39 * Endpoint namespace.
40 *
41 * @var string
42 */
43 protected $namespace = 'mainwp/v2';
44
45 /**
46 * Route base.
47 *
48 * @var string
49 */
50 protected $rest_base = 'clients';
51
52 /**
53 * Method instance()
54 *
55 * Create public static instance.
56 *
57 * @static
58 * @return static::$instance
59 */
60 public static function instance() {
61 if ( null === static::$instance ) {
62 static::$instance = new self();
63 }
64 return static::$instance;
65 }
66
67 /**
68 * Constructor.
69 */
70 public function __construct() {
71 add_filter( 'mainwp_rest_routes_clients_controller_filter_allowed_fields_by_context', array( $this, 'hook_filter_allowed_fields_by_context' ), 10, 2 );
72 add_filter( 'mainwp_rest_client_fields_object_query', array( $this, 'client_fields_custom_query_args' ), 10, 2 );
73 }
74
75 /**
76 * Get filter client.
77 *
78 * @param object $item item data.
79 * @param string $context context.
80 *
81 * @return object item in context.
82 */
83 public function hook_filter_allowed_fields_by_context( $item, $context = 'view' ) {
84 return $this->filter_response_data_by_allowed_fields( $item, $context );
85 }
86
87 /**
88 * Add custom query args.
89 *
90 * @param array $args Query args.
91 * @param WP_REST_Request $request Request object.
92 *
93 * @return array
94 */
95 public function client_fields_custom_query_args( $args, $request ) {
96 if ( ! empty( $request['client_id'] ) ) {
97 $args['client_id'] = $request['client_id'];
98 }
99
100 return $args;
101 }
102
103 /**
104 * Method register_routes()
105 *
106 * Creates the necessary endpoints for the api.
107 * Note, for a request to be successful the URL query parameters consumer_key and consumer_secret need to be set and correct.
108 */
109 public function register_routes() { // phpcs:ignore -- NOSONAR - complex.
110
111 // Retrieves all clients.
112 register_rest_route(
113 $this->namespace,
114 '/' . $this->rest_base,
115 array(
116 array(
117 'methods' => WP_REST_Server::READABLE,
118 'callback' => array( $this, 'get_items' ),
119 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
120 ),
121 )
122 );
123
124 // Retrieves the number of clients.
125 register_rest_route(
126 $this->namespace,
127 '/' . $this->rest_base . '/count',
128 array(
129 array(
130 'methods' => WP_REST_Server::READABLE,
131 'callback' => array( $this, 'count_items' ),
132 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
133 ),
134 )
135 );
136
137 // Adds new client.
138 register_rest_route(
139 $this->namespace,
140 '/' . $this->rest_base . '/add',
141 array(
142 array(
143 'methods' => WP_REST_Server::CREATABLE,
144 'callback' => array( $this, 'create_item' ),
145 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
146 'args' => $this->get_collection_params(),
147 ),
148 )
149 );
150
151 // Retrieves client by ID or Email.
152 register_rest_route(
153 $this->namespace,
154 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[\d]+)',
155 array(
156 array(
157 'methods' => WP_REST_Server::READABLE,
158 'callback' => array( $this, 'get_item' ),
159 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
160 ),
161 )
162 );
163
164 // Retrieves client by ID or Email.
165 register_rest_route(
166 $this->namespace,
167 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\.\_\%\+\-\@]+)',
168 array(
169 array(
170 'methods' => WP_REST_Server::READABLE,
171 'callback' => array( $this, 'get_item' ),
172 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
173 ),
174 )
175 );
176
177 // Edit client by ID or Email.
178 register_rest_route(
179 $this->namespace,
180 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/edit',
181 array(
182 array(
183 'methods' => WP_REST_Server::EDITABLE,
184 'callback' => array( $this, 'update_item' ),
185 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
186 ),
187 )
188 );
189
190 // Removes client by ID or Email from the Dashboard.
191 register_rest_route(
192 $this->namespace,
193 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/remove',
194 array(
195 array(
196 'methods' => WP_REST_Server::DELETABLE,
197 'callback' => array( $this, 'delete_item' ),
198 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
199 ),
200 )
201 );
202
203 // Retrieves all sites for a client by client ID or Email.
204 register_rest_route(
205 $this->namespace,
206 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/sites',
207 array(
208 array(
209 'methods' => WP_REST_Server::READABLE,
210 'callback' => array( $this, 'get_sites_client' ),
211 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
212 ),
213 )
214 );
215
216 // Retrieves all costs for a client by client ID or Email.
217 register_rest_route(
218 $this->namespace,
219 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/costs',
220 array(
221 array(
222 'methods' => WP_REST_Server::READABLE,
223 'callback' => array( $this, 'get_costs_client' ),
224 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
225 ),
226 )
227 );
228
229 // Retrieves the number of sites for a client by client ID or Email.
230 register_rest_route(
231 $this->namespace,
232 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/sites/count',
233 array(
234 array(
235 'methods' => WP_REST_Server::READABLE,
236 'callback' => array( $this, 'count_sites_client' ),
237 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
238 ),
239 )
240 );
241
242 // Suspends client by ID or Domain.
243 register_rest_route(
244 $this->namespace,
245 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/suspend',
246 array(
247 array(
248 'methods' => WP_REST_Server::EDITABLE,
249 'callback' => array( $this, 'suspend_client' ),
250 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
251 ),
252 )
253 );
254
255 // Unsuspends client by ID or Domain.
256 register_rest_route(
257 $this->namespace,
258 '/' . $this->rest_base . '/(?P<id_email>(?!fields$)[a-zA-Z0-9\_\-\.\@]+)/unsuspend',
259 array(
260 array(
261 'methods' => WP_REST_Server::EDITABLE,
262 'callback' => array( $this, 'unsuspend_client' ),
263 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
264 ),
265 )
266 );
267 register_rest_route(
268 $this->namespace,
269 '/' . $this->rest_base . '/batch',
270 array(
271 array(
272 'methods' => WP_REST_Server::EDITABLE,
273 'callback' => array( $this, 'batch_items' ),
274 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
275 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
276 ),
277 'schema' => array( $this, 'get_public_batch_schema' ),
278 )
279 );
280
281 // Client Fields.
282 register_rest_route(
283 $this->namespace,
284 '/' . $this->rest_base . '/fields',
285 array(
286 array(
287 'methods' => WP_REST_Server::READABLE,
288 'callback' => array( $this, 'client_fields' ),
289 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
290 'args' => $this->get_client_fields_allowed_fields(),
291 ),
292 )
293 );
294
295 register_rest_route(
296 $this->namespace,
297 '/' . $this->rest_base . '/fields/add',
298 array(
299 array(
300 'methods' => WP_REST_Server::CREATABLE,
301 'callback' => array( $this, 'create_client_fields' ),
302 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
303 'args' => $this->create_client_fields_allowed_fields(),
304 ),
305 )
306 );
307
308 register_rest_route(
309 $this->namespace,
310 '/' . $this->rest_base . '/fields/(?P<id_name>[^/]+)/edit',
311 array(
312 array(
313 'methods' => 'PUT, PATCH',
314 'callback' => array( $this, 'edit_client_fields' ),
315 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
316 'args' => $this->edit_client_fields_allowed_fields(),
317 ),
318 )
319 );
320 register_rest_route(
321 $this->namespace,
322 '/' . $this->rest_base . '/fields/(?P<id_name>[^/]+)/delete',
323 array(
324 array(
325 'methods' => WP_REST_Server::DELETABLE,
326 'callback' => array( $this, 'delete_client_fields' ),
327 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
328 ),
329 )
330 );
331 }
332
333 /**
334 * Get site by.
335 *
336 * @param WP_REST_Request $request Full details about the request.
337 *
338 * @return WP_Error|bool|Object Item.
339 */
340 public function get_request_item( $request ) {
341 $route = $request->get_route();
342
343 if ( MainWP_Utility::string_ends_by( $route, '/batch' ) ) {
344 $by = 'id';
345 $value = intval( $request['id'] );
346 } else {
347 $value = $request['id_email'];
348 $by = 'email';
349 if ( is_numeric( $value ) ) {
350 $by = 'id';
351 } else {
352 $value = wp_unslash( $value );
353 }
354 }
355 return $this->get_client_by( $by, $value );
356 }
357
358 /**
359 * Get the query params for collections.
360 *
361 * @return array
362 */
363 public function get_collection_params() {
364 $params = array();
365 $params['context']['default'] = 'view';
366 return $params;
367 }
368
369 /**
370 * Get formatted item object.
371 *
372 * @since 5.2
373 * @param array $data data object.
374 *
375 * @return array
376 */
377 protected function get_formatted_item_data( $data ) {
378 if ( ! empty( $data['created'] ) ) {
379 $data['created'] = mainwp_rest_prepare_date_response( $data['created'] );
380 }
381 if ( isset( $data['client_id'] ) && ! isset( $data['field_id'] ) ) {
382 $data['id'] = $data['client_id'];
383 unset( $data['client_id'] );
384 }
385 return $data;
386 }
387
388
389 /**
390 * Get Client by.
391 *
392 * @param WP_REST_Request $request Full details about the request.
393 * @return WP_Error|WP_REST_Response
394 */
395 public function get_item( $request ) {
396
397 $item = $this->get_request_item( $request );
398
399 if ( is_wp_error( $item ) ) {
400 return $item;
401 }
402
403 $resp_data = array(
404 'success' => $item ? 1 : 0,
405 'data' => $this->filter_response_data_by_allowed_fields( $item, 'simple_view' ),
406 );
407
408 return rest_ensure_response( $resp_data );
409 }
410
411
412 /**
413 * Get all Clients.
414 *
415 * @param WP_REST_Request $request Full details about the request.
416 * @return WP_Error|WP_REST_Response
417 */
418 public function get_items( $request ) { //phpcs:ignore -- NOSONAR - complex.
419
420 $args = $this->prepare_objects_query( $request );
421
422 $status = isset( $args['status'] ) ? wp_parse_list( $args['status'] ) : array();
423 $status = array_filter( array_map( 'trim', $status ) );
424
425 if ( empty( $status ) ) {
426 $status[] = 'all';
427 }
428
429 $all = is_array( $status ) && in_array( 'all', $status ) ? true : false;
430
431 $s = isset( $args['s'] ) ? trim( $args['s'] ) : '';
432 $includ = isset( $args['include'] ) ? $args['include'] : '';
433 $exclud = isset( $args['exclude'] ) ? $args['exclude'] : '';
434 $page = isset( $args['paged'] ) ? intval( $args['paged'] ) : false;
435 $per_page = isset( $args['items_per_page'] ) ? intval( $args['items_per_page'] ) : false;
436
437 $prepared_args = array(
438 's' => ! empty( $s ) ? $s : '',
439 'include' => ! empty( $includ ) ? $includ : array(),
440 'exclude' => ! empty( $exclud ) ? $exclud : array(),
441 'status' => $all ? array() : $status,
442 'custom_fields' => isset( $request['custom_fields'] ) && $request['custom_fields'] ? true : false,
443 'with_tags' => isset( $request['with_tags'] ) && $request['with_tags'] ? true : false,
444 'with_contacts' => isset( $request['with_contacts'] ) && $request['with_contacts'] ? true : false,
445 );
446
447 if ( false !== $page ) {
448 $prepared_args['page'] = $page;
449 }
450 if ( false !== $per_page ) {
451 $prepared_args['per_page'] = $per_page;
452 }
453
454 // get data.
455 $clients = MainWP_DB_Client::instance()->get_wp_clients( $prepared_args );
456 $data = array();
457 if ( is_array( $clients ) ) {
458 foreach ( $clients as $client ) {
459 $data[] = $this->filter_response_data_by_allowed_fields( $client );
460 }
461 }
462 return rest_ensure_response(
463 array(
464 'success' => 1,
465 'total' => is_array( $data ) ? count( $data ) : 0,
466 'data' => $data,
467 )
468 );
469 }
470
471 /**
472 * Count all Clients.
473 *
474 * @param WP_REST_Request $request Full details about the request.
475 * @return WP_Error|WP_REST_Response
476 */
477 public function count_items( $request ) {
478 $prepared_args = array(
479 'count_only' => true,
480 );
481 // get data.
482 $value = MainWP_DB_Client::instance()->get_wp_clients( $prepared_args );
483 return rest_ensure_response(
484 array(
485 'success' => 1,
486 'total' => $value,
487 )
488 );
489 }
490
491 /**
492 * Adds new client.
493 *
494 * @param WP_REST_Request $request Full details about the request.
495 * @return WP_Error|WP_REST_Response
496 */
497 public function create_item( $request ) {
498 $resp_data = array();
499 $resp_data['success'] = 0;
500 try {
501 $item = array_filter( $request->get_params() );
502 if ( isset( $item['selected_sites'] ) ) {
503 $item['selected_sites'] = array_unique( wp_parse_list( $item['selected_sites'] ) );
504 }
505 $result = MainWP_Client_Handler::rest_api_add_client( $item );
506 if ( is_array( $result ) && isset( $result['client_id'] ) ) {
507 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $result['client_id'], OBJECT, array( 'with_selected_sites' => true ) );
508 $resp_data['success'] = 1;
509 $resp_data['message'] = esc_html__( 'Client created Successfully.', 'mainwp' );
510 $resp_data['data'] = $this->filter_response_data_by_allowed_fields( $client, 'simple_view', array( 'selected_sites' ) );
511 } elseif ( is_array( $result ) && ! empty( $result['error'] ) ) {
512 $resp_data['error'] = wp_strip_all_tags( $result['error'] );
513 } else {
514 $resp_data['error'] = esc_html__( 'Add client failed. Please try again.', 'mainwp' );
515 }
516 } catch ( Exception $e ) {
517 $resp_data['error'] = wp_strip_all_tags( $e->getMessage() );
518 }
519 return rest_ensure_response( $resp_data );
520 }
521
522
523 /**
524 * Update new client.
525 *
526 * @param WP_REST_Request $request Full details about the request.
527 * @return WP_Error|WP_REST_Response
528 */
529 public function update_item( $request ) {
530
531 $item = $this->get_request_item( $request );
532
533 if ( is_wp_error( $item ) ) {
534 return $item;
535 }
536
537 $resp_data = array();
538 $resp_data['success'] = 0;
539 try {
540 $data = array_filter( $request->get_params() );
541 if ( is_array( $data ) ) {
542 $data['client_id'] = $item->client_id;
543 }
544 if ( isset( $data['selected_sites'] ) ) {
545 $data['selected_sites'] = array_unique( wp_parse_list( $data['selected_sites'] ) );
546 }
547 $result = MainWP_Client_Handler::rest_api_add_client( $data, true );
548 if ( is_array( $result ) && isset( $result['client_id'] ) ) {
549 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $result['client_id'], OBJECT, array( 'with_selected_sites' => true ) );
550 $resp_data['success'] = 1;
551 $resp_data['message'] = esc_html__( 'Client updated Successfully.', 'mainwp' );
552 $resp_data['data'] = $this->filter_response_data_by_allowed_fields( $client, 'simple_view', array( 'selected_sites' ) );
553 } elseif ( is_array( $result ) && ! empty( $result['error'] ) ) {
554 $resp_data['error'] = wp_strip_all_tags( $result['error'] );
555 } else {
556 $resp_data['error'] = esc_html__( 'Update client failed. Please try again.', 'mainwp' );
557 }
558 } catch ( Exception $e ) {
559 $resp_data['error'] = wp_strip_all_tags( $e->getMessage() );
560 }
561 return rest_ensure_response( $resp_data );
562 }
563
564
565 /**
566 * Delete client.
567 *
568 * @param WP_REST_Request $request Full details about the request.
569 * @return WP_Error|WP_REST_Response
570 */
571 public function delete_item( $request ) {
572 $item = $this->get_request_item( $request );
573 if ( is_wp_error( $item ) ) {
574 return $item;
575 }
576
577 $ret = MainWP_DB_Client::instance()->delete_client( $item->client_id );
578 $resp_data = array(
579 'success' => $ret ? 1 : 0,
580 'message' => $ret ? esc_html__( 'Client deleted successfully', 'mainwp' ) : esc_html__( 'Client deleted failed.', 'mainwp' ),
581 'data' => $this->filter_response_data_by_allowed_fields( $item, 'simple_view' ),
582 );
583 return rest_ensure_response( $resp_data );
584 }
585
586 /**
587 * Get sites of client.
588 *
589 * @param WP_REST_Request $request Full details about the request.
590 *
591 * @return WP_Error|WP_REST_Response
592 */
593 public function get_sites_client( $request ) {
594 $item = $this->get_request_item( $request );
595
596 if ( is_wp_error( $item ) ) {
597 return $item;
598 }
599
600 $data = array();
601 $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids(
602 $item->client_id,
603 array(
604 'selectgroups' => true,
605 'full_data' => true,
606 )
607 );
608 if ( $sites ) {
609 foreach ( $sites as $site ) {
610 $data[] = apply_filters( 'mainwp_rest_routes_sites_controller_filter_allowed_fields_by_context', $site );
611 }
612 }
613
614 $resp_data = array(
615 'success' => 1,
616 'total' => count( $data ),
617 'data' => $data,
618 );
619
620 return rest_ensure_response( $resp_data );
621 }
622
623 /**
624 * Get costs of client.
625 *
626 * @param WP_REST_Request $request Full details about the request.
627 *
628 * @return WP_Error|WP_REST_Response
629 */
630 public function get_costs_client( $request ) {
631 $item = $this->get_request_item( $request );
632 if ( is_wp_error( $item ) ) {
633 return $item;
634 }
635 $resp_data = array(
636 'success' => 1,
637 );
638
639 $client_costs = Cost_Tracker_DB::get_instance()->get_all_cost_trackers_by_clients( array( $item->client_id ) );
640
641 if ( is_array( $client_costs ) ) {
642 $client_costs = current( $client_costs ); // for current client.
643 }
644
645 if ( $client_costs ) {
646 $data = Cost_Tracker_Rest_Api_Handle_V1::instance()->prepare_api_costs_data( $client_costs );
647 $resp_data['total'] = is_array( $data ) ? count( $data ) : 0;
648 $resp_data['data'] = $data;
649 } else {
650 $resp_data['message'] = esc_html__( 'Costs not found.', 'mainwp' );
651 }
652 return rest_ensure_response( $resp_data );
653 }
654
655 /**
656 * Count sites of client.
657 *
658 * @param WP_REST_Request $request Full details about the request.
659 *
660 * @return WP_Error|WP_REST_Response
661 */
662 public function count_sites_client( $request ) {
663 $item = $this->get_request_item( $request );
664 if ( is_wp_error( $item ) ) {
665 return $item;
666 }
667 $resp_data = array(
668 'success' => 1,
669 );
670 $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $item->client_id );
671 $resp_data['total'] = is_array( $sites ) ? count( $sites ) : 0;
672 return rest_ensure_response( $resp_data );
673 }
674
675 /**
676 * Suspend client.
677 *
678 * @param WP_REST_Request $request Full details about the request.
679 *
680 * @return WP_Error|WP_REST_Response
681 */
682 public function suspend_client( $request ) {
683 $item = $this->get_request_item( $request );
684 if ( is_wp_error( $item ) ) {
685 return $item;
686 }
687
688 $params = array(
689 'client_id' => $item->client_id,
690 'suspended' => 1,
691 );
692
693 MainWP_DB_Client::instance()->update_client( $params );
694
695 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $item->client_id );
696
697 $resp_data = array(
698 'success' => 1,
699 'message' => esc_html__( 'Client suspended successfully.', 'mainwp' ),
700 'data' => $this->filter_response_data_by_allowed_fields( $client, 'simple_view' ),
701 );
702 return rest_ensure_response( $resp_data );
703 }
704
705 /**
706 * Unsuspend client.
707 *
708 * @param WP_REST_Request $request Full details about the request.
709 *
710 * @return WP_Error|WP_REST_Response
711 */
712 public function unsuspend_client( $request ) {
713 $item = $this->get_request_item( $request );
714 if ( is_wp_error( $item ) ) {
715 return $item;
716 }
717
718 $params = array(
719 'client_id' => $item->client_id,
720 'suspended' => 0,
721 );
722
723 MainWP_DB_Client::instance()->update_client( $params );
724
725 $client = MainWP_DB_Client::instance()->get_wp_client_by( 'client_id', $item->client_id );
726
727 $resp_data = array(
728 'success' => 1,
729 'message' => esc_html__( 'Client unsuspended successfully.', 'mainwp' ),
730 'data' => $this->filter_response_data_by_allowed_fields( $client, 'simple_view' ),
731 );
732 return rest_ensure_response( $resp_data );
733 }
734
735 /**
736 * Get client fields.
737 *
738 * @param WP_REST_Request $request Full details about the request.
739 *
740 * @uses MainWP_DB_Client::instance()->get_client_fields_by_params()
741 *
742 * @return WP_Error|WP_REST_Response
743 */
744 public function client_fields( $request ) { // phpcs:ignore -- NOSONAR - complex.
745 // pre_page shipped as the documented page size but was never read; keep it working through per_page.
746 if ( null === $request->get_param( 'per_page' ) && null !== $request->get_param( 'pre_page' ) ) {
747 $request->set_param( 'per_page', $request->get_param( 'pre_page' ) );
748 }
749
750 // Prepare query args.
751 $args = $this->prepare_objects_query( $request, 'client_fields' );
752 $page = ! empty( $args['paged'] ) ? (int) $args['paged'] : 1;
753 $per_page = ! empty( $args['items_per_page'] ) ? (int) $args['items_per_page'] : 20;
754 $params = array(
755 'exclude' => ! empty( $args['exclude'] ) ? $args['exclude'] : '',
756 'include' => ! empty( $args['include'] ) ? $args['include'] : '',
757 'search' => ! empty( $args['s'] ) ? $args['s'] : '',
758 );
759
760 // Get client fields.
761 $fields = MainWP_DB_Client::instance()->get_client_fields_by_params( $params );
762 if ( empty( $fields ) ) {
763 return rest_ensure_response(
764 array(
765 'success' => 0,
766 'message' => __( 'No client fields found.', 'mainwp' ),
767 )
768 );
769 }
770
771 $data = array();
772 foreach ( $fields as $field ) {
773 // A stored "0" is reported as the value it is, not as an empty one. field_id is cast
774 // because wpdb answers every column as a string and the schema declares an integer.
775 $record = array(
776 'field_id' => isset( $field->field_id ) ? (int) $field->field_id : 0,
777 'name' => isset( $field->field_name ) ? $field->field_name : '',
778 'description' => isset( $field->field_desc ) ? $field->field_desc : '',
779 );
780 $data[] = $this->filter_response_data_by_allowed_fields( $record, 'field_view' );
781 }
782
783 // Page navigation.
784 $total = count( $data );
785 $pages = (int) max( 1, ceil( $total / max( 1, $per_page ) ) );
786 if ( $page > $pages ) {
787 $page = $pages;
788 }
789
790 $offset = ( $page - 1 ) * $per_page;
791 $data = array_slice( $data, $offset, $per_page );
792
793 return rest_ensure_response(
794 array(
795 'success' => 1,
796 'total' => $total,
797 'pages' => $pages,
798 'data' => $data,
799 )
800 );
801 }
802
803 /**
804 * Create client fields.
805 *
806 * @param WP_REST_Request $request Full details about the request.
807 *
808 * @uses MainWP_DB_Client::instance()->add_client_field()
809 * @uses MainWP_DB_Client::instance()->get_wp_client_by()
810 *
811 * @return WP_Error|WP_REST_Response
812 */
813 public function create_client_fields( $request ) {
814 // Get request body.
815 $body = $this->get_request_body( $request );
816 if ( is_wp_error( $body ) ) {
817 return $body;
818 }
819
820 $invalid = $this->validate_client_field_values( $body );
821 if ( is_wp_error( $invalid ) ) {
822 return $invalid;
823 }
824
825 // Validate request body. "0" is a value the column stores, so a required value is one whose
826 // key was sent carrying something other than whitespace, not one that passes a falsy test.
827 // validate_client_field_values() has already refused a key sent as anything but a string.
828 foreach ( array( 'name', 'description' ) as $required ) {
829 if ( ! isset( $body[ $required ] ) || '' === trim( $body[ $required ] ) ) {
830 return new WP_Error(
831 'empty_name',
832 __( 'Name and description are required.', 'mainwp' ),
833 array( 'status' => 400 )
834 );
835 }
836 }
837
838 $client_id = 0;
839 $name = sanitize_text_field( wp_unslash( $body['name'] ) );
840 $desc = sanitize_text_field( wp_unslash( $body['description'] ) );
841
842 // field_name is varchar(191), so a longer name is truncated by wpdb before the unique index sees
843 // it and the duplicate lookup, which queries the untruncated name, would not recognise the refusal.
844 // A body read as raw JSON skips the registered maxLength, so the limit is enforced here as well.
845 if ( mb_strlen( $name ) > 191 ) {
846 return new WP_Error(
847 'invalid_field_value',
848 __( 'Name must be 191 characters or fewer.', 'mainwp' ),
849 array( 'status' => 400 )
850 );
851 }
852
853 // field_desc is varchar(255), so a longer description is either refused by the write or stored
854 // truncated depending on the server's strict mode, and neither answers what the caller sent.
855 // A body read as raw JSON skips the registered maxLength, so the limit is enforced here as well.
856 if ( mb_strlen( $desc ) > 255 ) {
857 return new WP_Error(
858 'invalid_field_value',
859 __( 'Description must be 255 characters or fewer.', 'mainwp' ),
860 array( 'status' => 400 )
861 );
862 }
863
864 $field = MainWP_DB_Client::instance()->add_client_field(
865 array(
866 'field_name' => $name,
867 'field_desc' => $desc,
868 'client_id' => $client_id,
869 )
870 );
871
872 if ( ! $field ) {
873 // add_client_field() returns false for a name it refuses to store as well as for a failed
874 // insert, so a name left empty by sanitizing or already taken stays a client error and
875 // everything else is a server failure.
876 $taken = $this->client_field_name_taken( $name, $client_id );
877 $caller_fault = ( '' === $name || $taken );
878
879 return new WP_Error(
880 'create_field_failed',
881 $caller_fault ? __( 'Create client field failed.', 'mainwp' ) : __( 'Creating the client field failed.', 'mainwp' ),
882 array( 'status' => $caller_fault ? 400 : 500 )
883 );
884 }
885
886 // Prepare response data. A stored "0" is reported as the value it is, not as an empty one.
887 // field_id is cast because wpdb answers every column as a string and the schema declares an integer.
888 $data = array(
889 'field_id' => isset( $field->field_id ) ? (int) $field->field_id : 0,
890 'name' => isset( $field->field_name ) ? $field->field_name : '',
891 'description' => isset( $field->field_desc ) ? $field->field_desc : '',
892 );
893
894 return rest_ensure_response(
895 array(
896 'success' => 1,
897 'message' => __( 'Client field created successfully.', 'mainwp' ),
898 'data' => $this->filter_response_data_by_allowed_fields( $data, 'field_edit' ),
899 )
900 );
901 }
902
903 /**
904 * Edit client fields.
905 *
906 * @param WP_REST_Request $request Full details about the request.
907 *
908 * @users MainWP_DB_Client::instance()->update_client_field()
909 *
910 * @return WP_Error|WP_REST_Response
911 */
912 public function edit_client_fields( $request ) {
913 // Get client field.
914 $field = $this->get_request_client_fields_item( $request );
915 if ( empty( $field ) ) {
916 return new WP_Error(
917 'invalid_field_id',
918 __( 'Invalid client field.', 'mainwp' ),
919 array( 'status' => 404 )
920 );
921 }
922
923 // Get request body.
924 $body = $this->get_request_body( $request );
925 if ( is_wp_error( $body ) ) {
926 return $body;
927 }
928
929 $invalid = $this->validate_client_field_values( $body );
930 if ( is_wp_error( $invalid ) ) {
931 return $invalid;
932 }
933
934 // Only a key left out, or sent with nothing but whitespace, keeps the stored value. The add
935 // route reads a value the same way, and sanitizing trims whitespace away, so a value tested
936 // untrimmed here would sanitize down to an empty string the DB layer refuses. "0" is a value
937 // the column stores, and a falsy test would drop it and answer that the edit succeeded.
938 $name = isset( $body['name'] ) && '' !== trim( $body['name'] ) ? sanitize_text_field( wp_unslash( $body['name'] ) ) : $field->field_name;
939 $desc = isset( $body['description'] ) && '' !== trim( $body['description'] ) ? sanitize_text_field( wp_unslash( $body['description'] ) ) : $field->field_desc;
940
941 // field_name is varchar(191), so a longer name is truncated by wpdb before the unique index sees
942 // it and the duplicate lookup, which queries the untruncated name, would not recognise the refusal.
943 // A body read as raw JSON skips the registered maxLength, so the limit is enforced here as well.
944 if ( mb_strlen( $name ) > 191 ) {
945 return new WP_Error(
946 'invalid_field_value',
947 __( 'Name must be 191 characters or fewer.', 'mainwp' ),
948 array( 'status' => 400 )
949 );
950 }
951
952 // field_desc is varchar(255), so a longer description is either refused by the write or stored
953 // truncated depending on the server's strict mode, and neither answers what the caller sent.
954 // A body read as raw JSON skips the registered maxLength, so the limit is enforced here as well.
955 if ( mb_strlen( $desc ) > 255 ) {
956 return new WP_Error(
957 'invalid_field_value',
958 __( 'Description must be 255 characters or fewer.', 'mainwp' ),
959 array( 'status' => 400 )
960 );
961 }
962
963 // $wpdb->update() reports no changed rows for an edit that stores the values already there, and
964 // the DB layer cannot tell that apart from a failure, so an edit that changes nothing is not run.
965 if ( $name !== $field->field_name || $desc !== $field->field_desc ) {
966 $updated = MainWP_DB_Client::instance()->update_client_field(
967 $field->field_id,
968 array(
969 'field_name' => $name,
970 'field_desc' => $desc,
971 )
972 );
973 if ( ! $updated ) {
974 // The DB layer refuses a name left empty by sanitizing, and the unique index on
975 // (client_id, field_name) refuses a rename onto a name another field of this client
976 // already owns. Both are the caller's fault; anything else is the write itself failing.
977 $owned = $this->client_field_name_taken( $name, $field->client_id, $field->field_id );
978 $caller_fault = ( '' === $name || $owned );
979
980 return new WP_Error(
981 'update_field_failed',
982 $caller_fault ? __( 'Field already exists, try different field name.', 'mainwp' ) : __( 'Updating the client field failed.', 'mainwp' ),
983 array( 'status' => $caller_fault ? 400 : 500 )
984 );
985 }
986 }
987
988 return rest_ensure_response(
989 array(
990 'success' => 1,
991 'message' => __( 'Client field updated successfully.', 'mainwp' ),
992 )
993 );
994 }
995
996 /**
997 * Delete client fields.
998 *
999 * @param WP_REST_Request $request Full details about the request.
1000 *
1001 * @users MainWP_DB_Client::instance()->delete_client_field_by()
1002 *
1003 * @return WP_Error|WP_REST_Response
1004 */
1005 public function delete_client_fields( $request ) {
1006 // Get client field.
1007 $field = $this->get_request_client_fields_item( $request );
1008 if ( empty( $field ) ) {
1009 return new WP_Error(
1010 'invalid_field_id',
1011 __( 'Invalid client field.', 'mainwp' ),
1012 );
1013 }
1014
1015 // Delete client field.
1016 $field_id = ! empty( $field->field_id ) ? $field->field_id : 0;
1017 $client_id = ! empty( $field->client_id ) ? $field->client_id : 0;
1018 $deleted = MainWP_DB_Client::instance()->delete_client_field_by( 'field_id', $field_id, $client_id );
1019
1020 if ( ! $deleted ) {
1021 return new WP_Error(
1022 'delete_field_failed',
1023 __( 'Delete client field failed.', 'mainwp' ),
1024 );
1025 }
1026
1027 return rest_ensure_response(
1028 array(
1029 'success' => 1,
1030 'message' => esc_html__( 'Client field deleted successfully.', 'mainwp' ),
1031 )
1032 );
1033 }
1034
1035 /**
1036 * Edit client fields allowed fields.
1037 *
1038 * @return array
1039 */
1040 public function edit_client_fields_allowed_fields() {
1041 // WordPress only runs the type check its own sanitizer carries, and a registered
1042 // sanitize_callback replaces it, so without an explicit validate_callback a JSON 42 or true
1043 // would reach the handler already cast to text by sanitize_text_field().
1044 return array(
1045 'name' => array(
1046 'required' => false,
1047 'type' => 'string',
1048 'maxLength' => 191,
1049 'sanitize_callback' => 'sanitize_text_field',
1050 'validate_callback' => 'rest_validate_request_arg',
1051 'description' => __( 'Field name.', 'mainwp' ),
1052 ),
1053 'description' => array(
1054 'required' => false,
1055 'type' => 'string',
1056 'maxLength' => 255,
1057 'sanitize_callback' => 'sanitize_text_field',
1058 'validate_callback' => 'rest_validate_request_arg',
1059 'description' => __( 'Field description.', 'mainwp' ),
1060 ),
1061 );
1062 }
1063
1064 /**
1065 * Create client fields allowed fields.
1066 *
1067 * @return array
1068 */
1069 public function create_client_fields_allowed_fields() {
1070 // See edit_client_fields_allowed_fields(): the registered sanitizer displaces the type check
1071 // WordPress would otherwise run, so the declared type needs its own validate_callback.
1072 return array(
1073 'name' => array(
1074 'required' => true,
1075 'type' => 'string',
1076 'maxLength' => 191,
1077 'sanitize_callback' => 'sanitize_text_field',
1078 'validate_callback' => 'rest_validate_request_arg',
1079 'description' => __( 'Field name.', 'mainwp' ),
1080 ),
1081 'description' => array(
1082 'required' => true,
1083 'type' => 'string',
1084 'maxLength' => 255,
1085 'sanitize_callback' => 'sanitize_text_field',
1086 'validate_callback' => 'rest_validate_request_arg',
1087 'description' => __( 'Field description.', 'mainwp' ),
1088 ),
1089 );
1090 }
1091
1092 /**
1093 * Get client fields allowed fields.
1094 *
1095 * @return array
1096 */
1097 public function get_client_fields_allowed_fields() {
1098 return array(
1099 'search' => array(
1100 'required' => false,
1101 'type' => 'string',
1102 'sanitize_callback' => 'sanitize_text_field',
1103 'description' => __( 'Search client field name.', 'mainwp' ),
1104 ),
1105 'include' => array(
1106 'required' => false,
1107 'type' => 'string',
1108 'sanitize_callback' => 'sanitize_text_field',
1109 'description' => __( 'Include client field IDs.', 'mainwp' ),
1110 ),
1111 'exclude' => array(
1112 'required' => false,
1113 'type' => 'string',
1114 'sanitize_callback' => 'sanitize_text_field',
1115 'description' => __( 'Exclude client field IDs.', 'mainwp' ),
1116 ),
1117 'page' => array(
1118 'required' => false,
1119 'type' => 'integer',
1120 'minimum' => 1,
1121 'sanitize_callback' => 'absint',
1122 'description' => __( 'Page number.', 'mainwp' ),
1123 ),
1124 'per_page' => array(
1125 'required' => false,
1126 'type' => 'integer',
1127 'sanitize_callback' => 'absint',
1128 'validate_callback' => 'rest_validate_request_arg',
1129 'minimum' => 1,
1130 'maximum' => 200,
1131 'description' => __( 'Number of client fields per page.', 'mainwp' ),
1132 ),
1133 'pre_page' => array(
1134 'required' => false,
1135 'type' => 'integer',
1136 'sanitize_callback' => 'absint',
1137 'validate_callback' => 'rest_validate_request_arg',
1138 'minimum' => 1,
1139 'maximum' => 200,
1140 'description' => __( 'Deprecated alias of per_page. Number of client fields per page.', 'mainwp' ),
1141 ),
1142 );
1143 }
1144
1145 /**
1146 * Get request body.
1147 *
1148 * @param WP_REST_Request $request Full details about the request.
1149 *
1150 * @return array|WP_Error Body payload, or an error when the request carries none.
1151 */
1152 private function get_request_body( $request ) {
1153 // Get request body from form-encoded senders.
1154 $body = $request->get_body_params();
1155 if ( ! empty( $body ) && is_array( $body ) ) {
1156 return $body;
1157 }
1158
1159 // Get request body from JSON senders.
1160 $body = $request->get_json_params();
1161 if ( ! empty( $body ) && is_array( $body ) ) {
1162 return $body;
1163 }
1164
1165 // Get request body from raw, for JSON sent without the JSON content type.
1166 $body = $request->get_body();
1167 if ( ! empty( $body ) && is_string( $body ) ) {
1168 $body = json_decode( $body, true );
1169 if ( is_array( $body ) && ! empty( $body ) ) {
1170 return $body;
1171 }
1172 }
1173
1174 return new WP_Error(
1175 'empty_body',
1176 __( 'Request body is empty.', 'mainwp' ),
1177 array( 'status' => 400 )
1178 );
1179 }
1180
1181 /**
1182 * Validate the client field text values carried by a request body.
1183 *
1184 * @param array $body Request body.
1185 *
1186 * @return WP_Error|true Error when a value cannot be stored as text.
1187 */
1188 private function validate_client_field_values( $body ) {
1189 foreach ( array( 'name', 'description' ) as $key ) {
1190 // A body read as raw JSON skips the registered sanitizers, so a value that is not already
1191 // text reaches sanitize_text_field() and is stored as a cast of itself or as an empty
1192 // string, wiping what it meant to set. A key sent as null counts as sent, not as omitted.
1193 if ( array_key_exists( $key, $body ) && ! is_string( $body[ $key ] ) ) {
1194 return new WP_Error(
1195 'invalid_field_value',
1196 __( 'Name and description must be text values.', 'mainwp' ),
1197 array( 'status' => 400 )
1198 );
1199 }
1200 }
1201
1202 return true;
1203 }
1204
1205 /**
1206 * Whether a client already has a field stored under the given name.
1207 *
1208 * @param string $name Field name as the handler sanitized it before the write.
1209 * @param int $client_id Client the field belongs to.
1210 * @param int $exclude_id Field id to skip, for a rename that keeps its own name.
1211 *
1212 * @uses MainWP_DB_Client::instance()->get_client_fields_by_params()
1213 *
1214 * @return bool
1215 */
1216 private function client_field_name_taken( $name, $client_id, $exclude_id = 0 ) {
1217 // A field_name lookup strips [ and ] from the value it queries with while the stored name keeps
1218 // them, and a PHP comparison would call two names that differ only in case distinct while the
1219 // column collation and the unique index call them the same. The match is left to the database
1220 // on the stored value so this answer agrees with the index that refused the write.
1221 $client_id = (int) $client_id;
1222 $fields = MainWP_DB_Client::instance()->get_client_fields_by_params(
1223 array(
1224 'client_id' => $client_id,
1225 'field_name_exact' => (string) $name,
1226 )
1227 );
1228
1229 if ( ! is_array( $fields ) ) {
1230 return false;
1231 }
1232
1233 foreach ( $fields as $existing ) {
1234 // Client 0 holds the general fields this route writes, and 0 is not a value the query filters
1235 // on, so the owner check happens here.
1236 if ( (int) $existing->client_id !== $client_id || (int) $existing->field_id === (int) $exclude_id ) {
1237 continue;
1238 }
1239
1240 return true;
1241 }
1242
1243 return false;
1244 }
1245
1246 /**
1247 * Get client field request id or name
1248 *
1249 * @param WP_REST_Request $request Full details about the request.
1250 *
1251 * @uses MainWP_DB_Client::instance()->get_client_fields_by()
1252 *
1253 * @return Object|bool Item.
1254 */
1255 private function get_request_client_fields_item( $request ) {
1256 $raw = $request->get_param( 'id_name' );
1257 $decoded = rawurldecode( (string) $raw );
1258 $field_value = trim( sanitize_text_field( wp_unslash( $decoded ) ) );
1259
1260 if ( '' === $field_value ) {
1261 return false;
1262 }
1263
1264 // Get client field by id. A digit-only segment is an id, so "0" and any other all-digit name is
1265 // addressable by its own id only, never by name.
1266 if ( ctype_digit( $field_value ) ) {
1267 return MainWP_DB_Client::instance()->get_client_fields_by( 'field_id', (int) $field_value );
1268 }
1269
1270 return MainWP_DB_Client::instance()->get_client_fields_by( 'field_name', $field_value );
1271 }
1272
1273 /**
1274 * Get the Tags schema, conforming to JSON Schema.
1275 *
1276 * @since 5.2
1277 * @return array
1278 */
1279 public function get_item_schema() {
1280 return array(
1281 '$schema' => 'http://json-schema.org/draft-04/schema#',
1282 'title' => 'clients',
1283 'type' => 'object',
1284 'properties' => array(
1285 'id' => array(
1286 'type' => 'integer',
1287 'description' => __( 'Client ID.', 'mainwp' ),
1288 'context' => array( 'view', 'edit', 'simple_view' ),
1289 ),
1290 'client_id' => array(
1291 'type' => 'integer',
1292 'description' => __( 'Client ID.', 'mainwp' ),
1293 'context' => array( 'view', 'edit', 'simple_view' ),
1294 ),
1295 'name' => array(
1296 'type' => 'string',
1297 'description' => __( 'Client name.', 'mainwp' ),
1298 'context' => array( 'view', 'edit', 'simple_view', 'field_view', 'field_edit' ),
1299 ),
1300 'address_1' => array(
1301 'type' => 'string',
1302 'description' => __( 'Address 1.', 'mainwp' ),
1303 'context' => array( 'view', 'edit', 'simple_view' ),
1304 ),
1305 'address_2' => array(
1306 'type' => 'string',
1307 'description' => __( 'Address 2.', 'mainwp' ),
1308 'context' => array( 'view', 'edit', 'simple_view' ),
1309 ),
1310 'city' => array(
1311 'type' => 'string',
1312 'description' => __( 'City.', 'mainwp' ),
1313 'context' => array( 'view', 'edit', 'simple_view' ),
1314 ),
1315 'zip' => array(
1316 'type' => 'string',
1317 'description' => __( 'Zip.', 'mainwp' ),
1318 'context' => array( 'view', 'edit', 'simple_view' ),
1319 ),
1320 'state' => array(
1321 'type' => 'string',
1322 'description' => __( 'State.', 'mainwp' ),
1323 'context' => array( 'view', 'edit', 'simple_view' ),
1324 ),
1325 'country' => array(
1326 'type' => 'string',
1327 'description' => __( 'Country.', 'mainwp' ),
1328 'context' => array( 'view', 'edit', 'simple_view' ),
1329 ),
1330 'note' => array(
1331 'type' => 'string',
1332 'description' => __( 'Note.', 'mainwp' ),
1333 'context' => array( 'view', 'edit' ),
1334 ),
1335 'client_email' => array(
1336 'type' => 'string',
1337 'description' => __( 'Email.', 'mainwp' ),
1338 'context' => array( 'view', 'edit', 'simple_view' ),
1339 ),
1340 'client_phone' => array(
1341 'type' => 'string',
1342 'description' => __( 'Phone.', 'mainwp' ),
1343 'context' => array( 'view', 'edit', 'simple_view' ),
1344 ),
1345 'client_facebook' => array(
1346 'type' => 'string',
1347 'description' => __( 'Facebook.', 'mainwp' ),
1348 'context' => array( 'view', 'edit' ),
1349 ),
1350 'client_twitter' => array(
1351 'type' => 'string',
1352 'description' => __( 'X.', 'mainwp' ),
1353 'context' => array( 'view', 'edit' ),
1354 ),
1355 'client_instagram' => array(
1356 'type' => 'string',
1357 'description' => __( 'Instagram.', 'mainwp' ),
1358 'context' => array( 'view', 'edit' ),
1359 ),
1360 'client_linkedin' => array(
1361 'type' => 'string',
1362 'description' => __( 'Linkedin.', 'mainwp' ),
1363 'context' => array( 'view', 'edit' ),
1364 ),
1365 'created' => array(
1366 'type' => 'integer',
1367 'description' => __( 'Created.', 'mainwp' ),
1368 'context' => array( 'view', 'edit', 'simple_view' ),
1369 ),
1370 'suspended' => array(
1371 'type' => 'integer',
1372 'description' => __( 'Suspended.', 'mainwp' ),
1373 'context' => array( 'view', 'edit', 'simple_view' ),
1374 ),
1375 'primary_contact_id' => array(
1376 'type' => 'integer',
1377 'description' => __( 'Primary contact id.', 'mainwp' ),
1378 'context' => array( 'view', 'edit' ),
1379 ),
1380 'field_id' => array(
1381 'type' => 'integer',
1382 'description' => __( 'Field ID.', 'mainwp' ),
1383 'context' => array( 'field_view', 'field_edit' ),
1384 ),
1385 'description' => array(
1386 'type' => 'string',
1387 'description' => __( 'Field description.', 'mainwp' ),
1388 'context' => array( 'field_view', 'field_edit' ),
1389 ),
1390 ),
1391 );
1392 }
1393 }
1394