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

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

1,966 lines 70.6 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;
11 use MainWP\Dashboard\MainWP_DB_Client;
12 use MainWP\Dashboard\MainWP_Utility;
13 use MainWP\Dashboard\MainWP_System_Utility;
14 use MainWP\Dashboard\MainWP_Connect;
15 use MainWP\Dashboard\MainWP_Bulk_Add;
16 use MainWP\Dashboard\MainWP_Exception;
17 use MainWP\Dashboard\MainWP_Error_Helper;
18
19 // Exit if accessed directly.
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24
25 /**
26 * Class MainWP_Rest_Users_Controller
27 *
28 * @package MainWP\Dashboard
29 */
30 class MainWP_Rest_Users_Controller extends MainWP_REST_Controller { //phpcs:ignore -- NOSONAR - multi methods.
31
32 // phpcs:disable Generic.Metrics.CyclomaticComplexity -- complexity.
33
34 /**
35 * Protected static variable to hold the single instance of the class.
36 *
37 * @var mixed Default null
38 */
39 private static $instance = null;
40
41 /**
42 * Route base.
43 *
44 * @var string
45 */
46 protected $rest_base = 'users';
47
48 /**
49 * Database instance.
50 *
51 * @var MainWP_DB
52 */
53 private $db = null;
54
55 /**
56 * Constructor.
57 */
58 public function __construct() {
59 $this->db = MainWP_DB::instance();
60 add_filter( 'mainwp_rest_users_fields_object_query', array( $this, 'users_fields_custom_query_args' ), 10, 2 );
61 }
62
63 /**
64 * Method instance()
65 *
66 * Create public static instance.
67 *
68 * @static
69 * @return static::$instance
70 */
71 public static function instance() {
72 if ( null === static::$instance ) {
73 static::$instance = new self();
74 }
75 return static::$instance;
76 }
77
78 /**
79 * Method register_routes()
80 *
81 * Creates the necessary endpoints for the api.
82 * Note, for a request to be successful the URL query parameters consumer_key and consumer_secret need to be set and correct.
83 */
84 public function register_routes() { // phpcs:ignore -- NOSONAR - complex.
85 register_rest_route(
86 $this->namespace,
87 '/' . $this->rest_base,
88 array(
89 array(
90 'methods' => WP_REST_Server::READABLE,
91 'callback' => array( $this, 'get_users' ),
92 'args' => $this->get_users_fields_allowed_fields(),
93 'validate_callback' => array( $this, 'users_validate_filter_params' ),
94 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
95 ),
96 )
97 );
98 register_rest_route(
99 $this->namespace,
100 '/' . $this->rest_base . '/create',
101 array(
102 array(
103 'methods' => WP_REST_Server::CREATABLE,
104 'callback' => array( $this, 'create_user' ),
105 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
106 'validate_callback' => array( $this, 'users_validate_filter_params' ),
107 'args' => $this->create_users_fields_allowed_fields(),
108 ),
109 )
110 );
111 register_rest_route(
112 $this->namespace,
113 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/(?P<user_id>[\d]+)/edit',
114 array(
115 array(
116 'methods' => 'PUT, PATCH',
117 'callback' => array( $this, 'edit_user' ),
118 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
119 'validate_callback' => array( $this, 'edit_validate_filter_params' ),
120 'args' => $this->edit_users_fields_allowed_fields(),
121 ),
122 )
123 );
124 register_rest_route(
125 $this->namespace,
126 '/' . $this->rest_base . '/(?P<id_domain>(\d+|[A-Za-z0-9-\.]*[A-Za-z0-9-]{1,63}\.[A-Za-z]{2,6}))/(?P<user_id>[\d]+)/delete',
127 array(
128 array(
129 'methods' => WP_REST_Server::DELETABLE,
130 'callback' => array( $this, 'delete_user' ),
131 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
132 'args' => array_merge(
133 $this->get_users_fields_allowed_fields(),
134 $this->edit_users_fields_allowed_fields()
135 ),
136 ),
137 )
138 );
139 register_rest_route(
140 $this->namespace,
141 '/' . $this->rest_base . '/update-admin-password',
142 array(
143 array(
144 'methods' => 'PUT, PATCH',
145 'callback' => array( $this, 'update_admin_password' ),
146 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
147 'validate_callback' => array( $this, 'users_validate_filter_params' ),
148 'args' => $this->update_admin_password_fields_allowed_fields(),
149 ),
150 )
151 );
152 register_rest_route(
153 $this->namespace,
154 '/' . $this->rest_base . '/import',
155 array(
156 array(
157 'methods' => WP_REST_Server::CREATABLE,
158 'callback' => array( $this, 'import_users' ),
159 'permission_callback' => array( $this, 'get_rest_permissions_check' ),
160 'args' => $this->import_users_fields_allowed_fields(),
161 ),
162 )
163 );
164 }
165
166 /**
167 * Add custom query args for pages.
168 *
169 * @param array $args Query args.
170 * @param WP_REST_Request $request Request object.
171 *
172 * @return array
173 */
174 public function users_fields_custom_query_args( $args, $request ) {
175 if ( ! empty( $request['clients'] ) ) {
176 $args['clients'] = $request['clients'];
177 }
178 if ( ! empty( $request['groups'] ) ) {
179 $args['groups'] = $request['groups'];
180 }
181 if ( ! empty( $request['websites'] ) ) {
182 $args['websites'] = $request['websites'];
183 }
184 if ( ! empty( $request['roles'] ) ) {
185 $args['roles'] = $request['roles'];
186 }
187 return $args;
188 }
189
190 /**
191 * Get all general settings.
192 *
193 * @param WP_REST_Request $request Full details about the request.
194 *
195 * @return WP_Error|WP_REST_Response
196 */
197 public function get_users( $request ) { // phpcs:ignore -- NOSONAR - complex.
198 // Prepare query parameters.
199 $args = $this->prepare_objects_query( $request, 'users_fields' );
200
201 // Get target websites.
202 $websites_data = $this->get_websites_for_users_query( $args );
203 if ( is_wp_error( $websites_data ) ) {
204 return $websites_data;
205 }
206
207 // Prepare search parameters.
208 $post_data = $this->prepare_users_search_data( $args );
209
210 // Fetch users from child sites.
211 $output = $this->fetch_users_from_sites( $websites_data['db_websites'], $post_data );
212
213 // Handle fetch errors.
214 $error_response = $this->handle_fetch_errors( $output );
215 if ( is_wp_error( $error_response ) ) {
216 return $error_response;
217 }
218
219 // Process and format results.
220 $results = $this->process_users_results( $output->results, $websites_data['website_url'] );
221
222 return rest_ensure_response(
223 array(
224 'success' => 1,
225 'data' => $results,
226 )
227 );
228 }
229
230 /**
231 * Create user.
232 *
233 * @param WP_REST_Request $request Full details about the request.
234 *
235 * @return WP_Error|WP_REST_Response
236 */
237 public function create_user( $request ) { // phpcs:ignore -- NOSONAR - complex.
238 // Validate request.
239 $validation_error = $this->validate_user_request( $request );
240 if ( is_wp_error( $validation_error ) ) {
241 return $validation_error;
242 }
243
244 // Get request body.
245 $body = $this->get_request_body( $request );
246 if ( is_wp_error( $body ) ) {
247 return $body;
248 }
249
250 // Get target websites.
251 $db_websites = $this->get_target_websites( $body );
252 if ( is_wp_error( $db_websites ) ) {
253 return $db_websites;
254 }
255
256 // Prepare user data.
257 $user_to_add = $this->prepare_user_data( $body );
258 $post_data = $this->prepare_post_data( $user_to_add, $body );
259
260 // Execute user creation on child sites.
261 $output = $this->execute_user_creation( $db_websites, $post_data );
262
263 // Process and return results.
264 return $this->process_creation_results( $output, $db_websites, $user_to_add );
265 }
266
267 /**
268 * Edit user.
269 *
270 * @param WP_REST_Request $request Full details about the request.
271 *
272 * @return WP_Error|WP_REST_Response
273 */
274 public function edit_user( $request ) { // phpcs:ignore -- NOSONAR - complex.
275 // Get website.
276 $website = $this->get_request_website_by_id_domain_item( $request );
277 if ( is_wp_error( $website ) ) {
278 return $website;
279 }
280
281 // Get user information.
282 $user_info = $this->get_request_user_by_id( $website, $request );
283 if ( is_wp_error( $user_info ) ) {
284 return $user_info;
285 }
286
287 // Validate request.
288 $validation_error = $this->validate_user_request( $request );
289 if ( is_wp_error( $validation_error ) ) {
290 return $validation_error;
291 }
292
293 // Get request body.
294 $body = $this->get_request_body( $request );
295 if ( is_wp_error( $body ) ) {
296 return $body;
297 }
298
299 // Prepare user update data.
300 $user_data = $this->prepare_edit_user_data( $body, $user_info );
301
302 // Execute user update.
303 $result = $this->execute_user_update( $website, $user_data, $request );
304 if ( is_wp_error( $result ) ) {
305 return $result;
306 }
307
308 return rest_ensure_response(
309 array(
310 'success' => 1,
311 'message' => esc_html__( 'User updated successfully.', 'mainwp' ),
312 'data' => array(
313 'website_id' => $website->id,
314 'website_url' => $website->url,
315 'user_id' => $request->get_param( 'user_id' ),
316 'updated_data' => $user_data,
317 ),
318 )
319 );
320 }
321
322 /**
323 * Delete user.
324 *
325 * @param WP_REST_Request $request Full details about the request.
326 *
327 * @return WP_Error|WP_REST_Response
328 */
329 public function delete_user( $request ) { // phpcs:ignore -- NOSONAR - complex.
330 // Get website.
331 $website = $this->get_request_website_by_id_domain_item( $request );
332 if ( is_wp_error( $website ) ) {
333 return $website;
334 }
335
336 // Get user information.
337 $user_info = $this->get_request_user_by_id( $website, $request );
338 if ( is_wp_error( $user_info ) ) {
339 return $user_info;
340 }
341
342 // Execute user deletion.
343 $result = $this->execute_user_deletion( $website, $request );
344 if ( is_wp_error( $result ) ) {
345 return $result;
346 }
347
348 return rest_ensure_response(
349 array(
350 'success' => 1,
351 'message' => esc_html__( 'User deleted successfully.', 'mainwp' ),
352 )
353 );
354 }
355
356 /**
357 * Update admin password.
358 *
359 * @param WP_REST_Request $request Full details about the request.
360 *
361 * @return WP_Error|WP_REST_Response
362 */
363 public function update_admin_password( $request ) { // phpcs:ignore -- NOSONAR - complex.
364 // Validate request.
365 $validation_error = $this->validate_user_request( $request );
366 if ( is_wp_error( $validation_error ) ) {
367 return $validation_error;
368 }
369
370 // Get request body.
371 $body = $this->get_request_body( $request );
372 if ( is_wp_error( $body ) ) {
373 return $body;
374 }
375
376 // Get target websites.
377 $db_websites = $this->get_target_websites( $body );
378 if ( is_wp_error( $db_websites ) ) {
379 return $db_websites;
380 }
381
382 $pass_complexity = apply_filters( 'mainwp_new_user_password_complexity', '24' );
383 $password = ! empty( $body['password'] ) ? $body['password'] : wp_generate_password( $pass_complexity );
384
385 $output = $this->execute_update_admin_password( $db_websites, $password, $request );
386 if ( is_wp_error( $output ) ) {
387 return $output;
388 }
389
390 return $this->process_update_admin_password_results( $output, $db_websites );
391 }
392
393 /**
394 * Import users from CSV file.
395 *
396 * @param WP_REST_Request $request Full details about the request.
397 *
398 * @return WP_Error|WP_REST_Response
399 */
400 public function import_users( $request ) { // phpcs:ignore -- NOSONAR - complex.
401 $files = $request->get_file_params();
402
403 // Validate request.
404 $file = $this->validation_import_users_request( $files, $request );
405 if ( is_wp_error( $file ) ) {
406 return $file;
407 }
408
409 // Parse CSV file.
410 $csv_data = $this->parse_csv_file( $file['tmp_name'], $request->get_param( 'has_header' ) );
411 if ( is_wp_error( $csv_data ) ) {
412 return $csv_data;
413 }
414
415 // Validate and process users.
416 $results = $this->process_csv_users( $csv_data );
417
418 return rest_ensure_response( $results );
419 }
420
421 /**
422 * Validate import users request.
423 *
424 * @param array $files Array of uploaded files.
425 * @param WP_REST_Request $request Full details about the request.
426 *
427 * @return array|WP_Error Array of file data or WP_Error on failure.
428 */
429 private function validation_import_users_request( $files, $request ) { // phpcs:ignore -- NOSONAR - complex.
430 $uploaded = isset( $files['csv_file'] ) ? $files['csv_file'] : null;
431 if ( empty( $uploaded ) ) {
432 $ct = $request->get_header( 'content-type' );
433 $msg = __( 'No CSV file uploaded.', 'mainwp' );
434 if ( $ct && false !== stripos( $ct, 'multipart/form-data' ) ) {
435 $msg .= ' ' . __( 'If you manually set Content-Type: multipart/form-data, remove it and let the client add the boundary automatically.', 'mainwp' );
436 }
437 return new WP_Error( 'no_file_uploaded', $msg, array( 'status' => 400 ) );
438 }
439
440 $file = $uploaded;
441
442 // Validate file type.
443 $file_type = wp_check_filetype( $file['name'] );
444 if ( ! in_array( $file_type['ext'], array( 'csv', 'txt' ), true ) ) {
445 return new WP_Error(
446 'invalid_file_type',
447 __( 'Invalid file type. Only CSV files are allowed.', 'mainwp' ),
448 array( 'status' => 400 )
449 );
450 }
451
452 // Check for upload errors.
453 if ( UPLOAD_ERR_OK !== $file['error'] ) {
454 return new WP_Error(
455 'upload_error',
456 __( 'File upload error.', 'mainwp' ),
457 array( 'status' => 400 )
458 );
459 }
460 return $file;
461 }
462
463 /**
464 * Parse CSV file and extract user data.
465 *
466 * @param string $file_path Path to CSV file.
467 * @param bool $has_header Whether CSV has header row.
468 * @return array|WP_Error Array of user data or WP_Error on failure.
469 */
470 private function parse_csv_file( $file_path, $has_header = true ) { // phpcs:ignore -- NOSONAR - complex.
471 if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
472 return new WP_Error(
473 'file_not_readable',
474 __( 'CSV file is not readable.', 'mainwp' )
475 );
476 }
477
478 MainWP_System_Utility::get_wp_file_system();
479 global $wp_filesystem;
480
481 $content = $wp_filesystem->get_contents( $file_path );
482 if ( empty( $content ) ) {
483 return new WP_Error(
484 'empty_file',
485 __( 'CSV file is empty.', 'mainwp' )
486 );
487 }
488
489 // Parse CSV content.
490 $lines = explode( "\n", str_replace( "\r\n", "\n", $content ) );
491 $lines = array_filter( array_map( 'trim', $lines ) );
492
493 if ( empty( $lines ) ) {
494 return new WP_Error(
495 'no_data',
496 __( 'No data found in CSV file.', 'mainwp' )
497 );
498 }
499
500 // Remove header if present.
501 if ( $has_header ) {
502 array_shift( $lines );
503 }
504
505 $users_data = array();
506 $line_number = $has_header ? 2 : 1;
507
508 foreach ( $lines as $line ) {
509 if ( empty( $line ) ) {
510 continue;
511 }
512
513 $items = str_getcsv( $line );
514 if ( count( $items ) < 10 ) {
515 return new WP_Error(
516 'invalid_csv_format',
517 sprintf(
518 /* translators: %d: line number */
519 __( 'Invalid CSV format at line %d. Expected 10 columns.', 'mainwp' ),
520 $line_number
521 )
522 );
523 }
524
525 $users_data[] = array(
526 'line_number' => $line_number,
527 'username' => sanitize_text_field( $items[0] ),
528 'email' => sanitize_email( $items[1] ),
529 'first_name' => sanitize_text_field( $items[2] ),
530 'last_name' => sanitize_text_field( $items[3] ),
531 'user_url' => esc_url_raw( $items[4] ),
532 'password' => $items[5], // Don't sanitize password.
533 'send_password' => filter_var( $items[6], FILTER_VALIDATE_BOOLEAN ),
534 'role' => sanitize_text_field( strtolower( $items[7] ) ),
535 'select_sites' => sanitize_text_field( $items[8] ),
536 'select_groups' => sanitize_text_field( $items[9] ),
537 );
538
539 ++$line_number;
540 }
541
542 return $users_data;
543 }
544
545 /**
546 * Process CSV users data and create users on child sites.
547 *
548 * @param array $users_data Array of user data from CSV.
549 * @return array Results array with success and failed users.
550 */
551 private function process_csv_users( $users_data ) { // phpcs:ignore -- NOSONAR - complex.
552 $results = array(
553 'total' => count( $users_data ),
554 'success_count' => 0,
555 'failed_count' => 0,
556 'success_users' => array(),
557 'failed_users' => array(),
558 );
559
560 foreach ( $users_data as $user_data ) {
561 // Validate user data.
562 $validation_error = $this->validate_csv_user_data( $user_data );
563 if ( is_wp_error( $validation_error ) ) {
564 ++$results['failed_count'];
565 $results['failed_users'][] = array(
566 'line_number' => $user_data['line_number'],
567 'username' => $user_data['username'],
568 'email' => $user_data['email'],
569 'error' => $validation_error->get_error_message(),
570 );
571 continue;
572 }
573
574 // Get target websites for this user.
575 $db_websites = $this->get_websites_for_csv_user( $user_data );
576 if ( is_wp_error( $db_websites ) ) {
577 ++$results['failed_count'];
578 $results['failed_users'][] = array(
579 'line_number' => $user_data['line_number'],
580 'username' => $user_data['username'],
581 'email' => $user_data['email'],
582 'error' => $db_websites->get_error_message(),
583 );
584 continue;
585 }
586
587 if ( empty( $db_websites ) ) {
588 ++$results['failed_count'];
589 $results['failed_users'][] = array(
590 'line_number' => $user_data['line_number'],
591 'username' => $user_data['username'],
592 'email' => $user_data['email'],
593 'error' => __( 'No valid websites found for this user.', 'mainwp' ),
594 );
595 continue;
596 }
597
598 // Prepare user data for creation.
599 $user_to_add = $this->prepare_csv_user_for_creation( $user_data );
600 $post_data = array(
601 'new_user' => base64_encode( wp_json_encode( $user_to_add ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions
602 'send_password' => $user_data['send_password'] ? 1 : 0,
603 );
604
605 // Execute user creation.
606 $output = $this->execute_user_creation( $db_websites, $post_data );
607
608 // Process results for this user.
609 $user_result = $this->process_single_csv_user_result( $output, $db_websites, $user_data );
610
611 if ( $user_result['success'] ) {
612 ++$results['success_count'];
613 $results['success_users'][] = $user_result['data'];
614 } else {
615 ++$results['failed_count'];
616 $results['failed_users'][] = $user_result['data'];
617 }
618 }
619
620 return $results;
621 }
622
623 /**
624 * Validate CSV user data.
625 *
626 * @param array $user_data User data from CSV.
627 * @return true|WP_Error True on success, WP_Error on failure.
628 */
629 private function validate_csv_user_data( $user_data ) { // phpcs:ignore -- NOSONAR - complex.
630 // Validate required fields.
631 if ( empty( $user_data['username'] ) ) {
632 return new WP_Error(
633 'missing_username',
634 __( 'Username is required.', 'mainwp' )
635 );
636 }
637
638 if ( empty( $user_data['email'] ) ) {
639 return new WP_Error(
640 'missing_email',
641 __( 'Email is required.', 'mainwp' )
642 );
643 }
644
645 // Validate email format.
646 if ( ! is_email( $user_data['email'] ) ) {
647 return new WP_Error(
648 'invalid_email',
649 __( 'Invalid email format.', 'mainwp' )
650 );
651 }
652
653 // Validate role.
654 $valid_roles = array( 'administrator', 'subscriber', 'editor', 'author', 'contributor' );
655 if ( ! empty( $user_data['role'] ) && ! in_array( $user_data['role'], $valid_roles, true ) ) {
656 return new WP_Error(
657 'invalid_role',
658 sprintf(
659 /* translators: %s: allowed roles */
660 __( 'Invalid role. Allowed roles: %s', 'mainwp' ),
661 implode( ', ', $valid_roles )
662 )
663 );
664 }
665
666 // Validate that at least one of select_sites or select_groups is provided.
667 if ( empty( $user_data['select_sites'] ) && empty( $user_data['select_groups'] ) ) {
668 return new WP_Error(
669 'no_target_sites',
670 __( 'Either select_sites or select_groups must be provided.', 'mainwp' )
671 );
672 }
673
674 return true;
675 }
676
677 /**
678 * Get websites for CSV user based on select_sites and select_groups.
679 *
680 * @param array $user_data User data from CSV.
681 * @return array|WP_Error Array of websites or WP_Error on failure.
682 */
683 private function get_websites_for_csv_user( $user_data ) { // phpcs:ignore -- NOSONAR - complex.
684 $utility = MainWP_Utility::instance();
685 $system_utility = new MainWP_System_Utility();
686 $data_fields = $system_utility->get_default_map_site_fields();
687 $db_websites = array();
688
689 if ( ! empty( $user_data['select_sites'] ) ) {
690 $site_urls = array_filter( array_map( 'trim', explode( ';', $user_data['select_sites'] ) ) );
691
692 foreach ( $site_urls as $url ) {
693 $websites = $this->db->get_websites_by_url( $url );
694 if ( ! empty( $websites ) ) {
695 foreach ( $websites as $website ) {
696 if ( '' !== $website->sync_errors || $system_utility->is_suspended_site( $website ) ) {
697 continue;
698 }
699 $db_websites[ $website->id ] = $utility->map_site( $website, $data_fields );
700 }
701 }
702 }
703 }
704
705 if ( ! empty( $user_data['select_groups'] ) ) {
706 $group_names = array_filter( array_map( 'trim', explode( ';', $user_data['select_groups'] ) ) );
707
708 foreach ( $group_names as $group_name ) {
709 $group = \MainWP\Dashboard\MainWP_DB_Common::instance()->get_group_by_name( $group_name );
710 if ( ! $group ) {
711 continue;
712 }
713
714 $websites = $this->db->query( $this->db->get_sql_websites_by_group_id( $group->id ) );
715 while ( $websites && ( $website = $this->db->fetch_object( $websites ) ) ) {
716 if ( '' !== $website->sync_errors || $system_utility->is_suspended_site( $website ) ) {
717 continue;
718 }
719 $db_websites[ $website->id ] = $utility->map_site( $website, $data_fields );
720 }
721 $this->db->free_result( $websites );
722 }
723 }
724
725 return $db_websites;
726 }
727
728 /**
729 * Prepare CSV user data for creation.
730 *
731 * @param array $user_data User data from CSV.
732 * @return array Prepared user data.
733 */
734 private function prepare_csv_user_for_creation( $user_data ) {
735 $user_to_add = array(
736 'user_login' => $user_data['username'],
737 'email' => $user_data['email'],
738 'first_name' => $user_data['first_name'],
739 'last_name' => $user_data['last_name'],
740 'url' => $user_data['user_url'],
741 'role' => ! empty( $user_data['role'] ) ? $user_data['role'] : 'subscriber',
742 );
743
744 // Add password if provided.
745 if ( ! empty( $user_data['password'] ) ) {
746 $user_to_add['user_pass'] = $user_data['password'];
747 }
748
749 return $user_to_add;
750 }
751
752 /**
753 * Process result for a single CSV user.
754 *
755 * @param \stdClass $output Output from user creation.
756 * @param array $db_websites Array of websites.
757 * @param array $user_data Original user data from CSV.
758 * @return array Result array with success flag and data.
759 */
760 private function process_single_csv_user_result( $output, $db_websites, $user_data ) { // phpcs:ignore -- NOSONAR - complex.
761 $success_sites = array();
762 $failed_sites = array();
763
764 foreach ( $db_websites as $site_id => $website ) {
765 if ( isset( $output->ok[ $site_id ] ) && 1 === (int) $output->ok[ $site_id ] ) {
766 $success_sites[] = array(
767 'id' => $site_id,
768 'name' => isset( $website->name ) ? $website->name : '',
769 'url' => isset( $website->url ) ? $website->url : '',
770 );
771 } else {
772 $error_message = isset( $output->errors[ $site_id ] ) ? $output->errors[ $site_id ] : __( 'Unknown error occurred.', 'mainwp' );
773 $failed_sites[] = array(
774 'id' => $site_id,
775 'name' => isset( $website->name ) ? $website->name : '',
776 'url' => isset( $website->url ) ? $website->url : '',
777 'message' => $error_message,
778 );
779 }
780 }
781
782 $success = ! empty( $success_sites );
783
784 return array(
785 'success' => $success,
786 'data' => array(
787 'line_number' => $user_data['line_number'],
788 'username' => $user_data['username'],
789 'email' => $user_data['email'],
790 'success_sites' => $success_sites,
791 'failed_sites' => $failed_sites,
792 'total_sites' => count( $db_websites ),
793 'success_count' => count( $success_sites ),
794 'failed_count' => count( $failed_sites ),
795 ),
796 );
797 }
798
799 /**
800 * Process update admin password results.
801 *
802 * @param stdClass $output Output object with ok and errors arrays.
803 * @param array $db_websites Array of websites.
804 * @return WP_REST_Response Response object with success and failed websites.
805 */
806 private function process_update_admin_password_results( $output, $db_websites ) { // phpcs:ignore -- NOSONAR - complex.
807 $success_websites = array();
808 $failed_websites = array();
809
810 foreach ( $db_websites as $site_id => $website ) {
811 if ( isset( $output->ok[ $site_id ] ) && 1 === (int) $output->ok[ $site_id ] ) {
812 $success_websites[] = array(
813 'id' => $site_id,
814 'name' => isset( $website->name ) ? $website->name : '',
815 'url' => isset( $website->url ) ? $website->url : '',
816 );
817 } else {
818 $error_message = isset( $output->errors[ $site_id ] ) ? $output->errors[ $site_id ] : __( 'Unknown error occurred.', 'mainwp' );
819 $failed_websites[] = array(
820 'id' => $site_id,
821 'name' => isset( $website->name ) ? $website->name : '',
822 'url' => isset( $website->url ) ? $website->url : '',
823 'message' => $error_message,
824 );
825 }
826 }
827
828 $total_websites = count( $db_websites );
829 $success_count = count( $success_websites );
830 $failed_count = count( $failed_websites );
831
832 return rest_ensure_response(
833 array(
834 'success' => true,
835 'message' => sprintf(
836 /* translators: 1: success count, 2: total count */
837 __( 'Admin password updated on %1$d of %2$d websites.', 'mainwp' ),
838 $success_count,
839 $total_websites
840 ),
841 'total_websites' => $total_websites,
842 'success_count' => $success_count,
843 'failed_count' => $failed_count,
844 'success_websites' => $success_websites,
845 'failed_websites' => $failed_websites,
846 )
847 );
848 }
849
850 /**
851 * Summary of execute_update_admin_password
852 *
853 * @param mixed $db_websites Array of websites.
854 * @param mixed $password New password.
855 * @param mixed $request Request object.
856 * @return stdClass|WP_Error
857 */
858 private function execute_update_admin_password( $db_websites, $password, $request ) { // phpcs:ignore -- NOSONAR - complex.
859 $post_data = array( 'new_password' => base64_encode( $password ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
860 $output = new \stdClass();
861 $output->ok = array();
862 $output->errors = array();
863 try {
864 MainWP_Connect::fetch_urls_authed(
865 $db_websites,
866 'newadminpassword',
867 $post_data,
868 array(
869 MainWP_Bulk_Add::get_class_name(),
870 'posting_bulk_handler',
871 ),
872 $output
873 );
874
875 return $output;
876 } catch ( MainWP_Exception $e ) {
877 return new WP_Error( 'update_admin_password_error', MainWP_Error_Helper::get_error_message( $e ) );
878 }
879 }
880
881 /**
882 * Execute user deletion on child site.
883 *
884 * @param object $website Website object.
885 * @param WP_REST_Request $request Full details about the request.
886 * @return true|WP_Error True on success, WP_Error on failure.
887 */
888 private function execute_user_deletion( $website, $request ) { // phpcs:ignore -- NOSONAR - complex.
889 // Check if the user is the same user currently connecting to mainwp.
890 $user_id = (int) $request->get_param( 'user_id' );
891 if ( (int) $website->userid === $user_id ) {
892 return new WP_Error( 'user_not_found', __( 'This user is used for our secure link, it can not be deleted.', 'mainwp' ) );
893 }
894 try {
895
896 /**
897 * Action: mainwp_before_user_action
898 *
899 * Fires before user edit/delete/update_user/update_password actions.
900 *
901 * @since 4.1
902 */
903 do_action( 'mainwp_before_user_action', 'delete', $user_id, '', '', 1, $website );
904
905 $information = MainWP_Connect::fetch_url_authed(
906 $website,
907 'user_action',
908 array(
909 'action' => 'delete',
910 'id' => $user_id,
911 'extra' => '',
912 'user_pass' => '',
913 'optimize' => 1,
914 )
915 );
916
917 if ( is_array( $information ) && isset( $information['status'] ) && ( 'SUCCESS' === $information['status'] ) ) {
918 $data = isset( $information['other_data']['users_data'] ) ? $information['other_data']['users_data'] : array(); // user actions data.
919
920 /**
921 * Fires immediately after user action.
922 *
923 * @since 4.5.1.1
924 */
925 do_action( 'mainwp_user_action', $website, 'delete', $data, '', 1 );
926
927 return true;
928 }
929 return new WP_Error( 'user_deletion_error', __( 'User deletion failed.', 'mainwp' ) );
930 } catch ( MainWP_Exception $e ) {
931 return new WP_Error( 'user_deletion_error', MainWP_Error_Helper::get_error_message( $e ) );
932 }
933 }
934 /**
935 * Prepare user data for update.
936 *
937 * @param array $body Request body data.
938 * @param array $user_info Current user information.
939 * @return array User data array.
940 */
941 private function prepare_edit_user_data( $body, $user_info ) { // phpcs:ignore -- NOSONAR - complex.
942 $user_data = array();
943
944 // Map editable fields.
945 $field_mapping = array(
946 'password' => 'user_pass',
947 'email' => 'email',
948 'role' => 'role',
949 'first_name' => 'first_name',
950 'last_name' => 'last_name',
951 'user_url' => 'url',
952 'nickname' => 'nickname',
953 'display_name' => 'display_name',
954 'description' => 'description',
955 );
956
957 foreach ( $field_mapping as $request_field => $user_field ) {
958 if ( isset( $body[ $request_field ] ) && '' !== $body[ $request_field ] ) {
959 $user_data[ $user_field ] = $body[ $request_field ];
960 }
961 }
962
963 //
964 // Set default role if not provided.
965 if ( empty( $user_data['role'] ) ) {
966 $user_data['role'] = 'donotupdate';
967 }
968
969 // Handle password update.
970 if ( ! empty( $user_data['user_pass'] ) ) {
971 $pass = '';
972 if ( function_exists( '\mb_convert_encoding' ) ) {
973 $pass = \mb_convert_encoding( $user_data['user_pass'], 'ISO-8859-1', 'UTF-8' );
974 } else {
975 // phpcs:disable Generic.PHP.DeprecatedFunctions.Deprecated
976 $pass = utf8_decode( $user_data['user_pass'] ); // to compatible.
977 // phpcs:enable Generic.PHP.DeprecatedFunctions.Deprecated
978 }
979
980 if ( ! empty( $pass ) ) {
981 $user_data['pass1'] = $pass;
982 $user_data['pass2'] = $pass;
983 }
984 }
985
986 return $user_data;
987 }
988
989 /**
990 * Execute user update on child site.
991 *
992 * @param object $website Website object.
993 * @param array $user_data User data to update.
994 * @param WP_REST_Request $request Full details about the request.
995 * @return true|WP_Error True on success, WP_Error on failure.
996 */
997 private function execute_user_update( $website, $user_data, $request ) { // phpcs:ignore -- NOSONAR - complex.
998 $user_id = (int) $request->get_param( 'user_id' );
999 $user_pass = isset( $user_data['user_pass'] ) ? $user_data['user_pass'] : '';
1000
1001 // Check if the user is the same user currently connecting to mainwp.
1002 if ( ! empty( $user_data['role'] ) && (int) $user_id === (int) $website->userid ) {
1003 return new WP_Error( 'user_update_error', __( 'This user is used for our secure link, it can not be changed.', 'mainwp' ) );
1004 }
1005 /**
1006 * Action: mainwp_before_user_action
1007 *
1008 * Fires before user update.
1009 *
1010 * @since 4.1
1011 */
1012 do_action( 'mainwp_before_user_action', 'update_user', $user_id, $user_data, $user_pass, 1, $website );
1013
1014 try {
1015 $information = MainWP_Connect::fetch_url_authed(
1016 $website,
1017 'user_action',
1018 array(
1019 'action' => 'update_user',
1020 'id' => $user_id,
1021 'extra' => $user_data,
1022 'user_pass' => $user_pass,
1023 'optimize' => 1,
1024 )
1025 );
1026
1027 if ( is_array( $information ) && isset( $information['error'] ) ) {
1028 return new WP_Error(
1029 'user_update_error',
1030 esc_html( $information['error'] )
1031 );
1032 }
1033
1034 if ( ! isset( $information['status'] ) || 'SUCCESS' !== $information['status'] ) {
1035 return new WP_Error(
1036 'user_update_failed',
1037 esc_html__( 'User update failed. Unexpected error.', 'mainwp' )
1038 );
1039 }
1040
1041 // Update cached user data if optimize is enabled.
1042 if ( isset( $information['users'] ) ) {
1043 $data = isset( $information['other_data']['users_data'] ) ? $information['other_data']['users_data'] : array(); // user actions data.
1044
1045 /**
1046 * Fires immediately after user action.
1047 *
1048 * @since 4.5.1.1
1049 */
1050 do_action( 'mainwp_user_action', $website, 'update_user', $data, $user_data, 1 );
1051
1052 $website_values['users'] = wp_json_encode( $information['users'] );
1053 MainWP_DB::instance()->update_website_values( $website->id, $website_values );
1054 }
1055
1056 return true;
1057 } catch ( MainWP_Exception $e ) {
1058 return new WP_Error(
1059 'user_update_exception',
1060 MainWP_Error_Helper::get_error_message( $e )
1061 );
1062 }
1063 }
1064
1065 /**
1066 * Map user data from child site to standard format.
1067 *
1068 * @param array $user_data Raw user data from child site.
1069 * @param int $website_id Website ID.
1070 * @return array Mapped user data.
1071 */
1072 private function map_user_data( $user_data, $website_id ) {
1073 return array(
1074 'id' => $user_data['id'] ?? '',
1075 'website_id' => $website_id,
1076 'username' => $user_data['login'] ?? '',
1077 'email' => $user_data['email'] ?? '',
1078 'role' => $user_data['role'] ?? '',
1079 'posts' => $user_data['post_count'] ?? 0,
1080 'name' => $user_data['nicename'] ?? '',
1081 );
1082 }
1083
1084
1085 /**
1086 * Get websites for users query.
1087 *
1088 * @param array $args Query arguments.
1089 * @return array|WP_Error Array with db_websites and website_url or WP_Error on failure.
1090 */
1091 private function get_websites_for_users_query( $args ) {
1092 $clients = $args['clients'] ?? '';
1093 $groups = $args['groups'] ?? '';
1094 $sites = $args['websites'] ?? '';
1095
1096 $filter_db_websites = $this->get_db_websites_by_filter( $sites, $groups, $clients );
1097
1098 if ( empty( $filter_db_websites ) || ! is_array( $filter_db_websites ) ) {
1099 return new WP_Error(
1100 'no_website_found',
1101 __( 'No website found.', 'mainwp' ) // NOSONAR.
1102 );
1103 }
1104
1105 $db_websites = $filter_db_websites['db_websites'] ?? array();
1106 $website_url = $filter_db_websites['website_url'] ?? array();
1107
1108 if ( empty( $db_websites ) ) {
1109 return new WP_Error(
1110 'no_website_found',
1111 __( 'No website found.', 'mainwp' ) // NOSONAR.
1112 );
1113 }
1114
1115 return array(
1116 'db_websites' => $db_websites,
1117 'website_url' => $website_url,
1118 );
1119 }
1120
1121 /**
1122 * Prepare users search data.
1123 *
1124 * @param array $args Query arguments.
1125 * @return array Search data array.
1126 */
1127 private function prepare_users_search_data( $args ) {
1128 $search = $args['s'] ?? '';
1129 $roles = $args['roles'] ?? '';
1130
1131 return array(
1132 'role' => is_array( $roles ) ? implode( ',', $roles ) : $roles,
1133 'search' => '*' . trim( $search ) . '*',
1134 'search_columns' => 'user_login,display_name,user_email',
1135 );
1136 }
1137
1138 /**
1139 * Fetch users from child sites.
1140 *
1141 * @param array $db_websites Array of websites.
1142 * @param array $post_data Search parameters.
1143 * @return \stdClass|WP_Error Output object with results and errors.
1144 */
1145 private function fetch_users_from_sites( $db_websites, $post_data ) {
1146 try {
1147 $output = new \stdClass();
1148 $output->results = array();
1149 $output->errors = array();
1150
1151 MainWP_Connect::fetch_urls_authed(
1152 $db_websites,
1153 'search_users',
1154 $post_data,
1155 array(
1156 'MainWP_REST_Controller',
1157 'posts_pages_search_handler',
1158 ),
1159 $output
1160 );
1161
1162 return $output;
1163 } catch ( MainWP_Exception $e ) {
1164 return new WP_Error( 'fetch_users_error', MainWP_Error_Helper::get_error_message( $e ) );
1165 }
1166 }
1167
1168 /**
1169 * Handle fetch errors.
1170 *
1171 * @param \stdClass $output Output from fetch operation.
1172 * @return true|WP_Error True on success, WP_Error on failure.
1173 */
1174 private function handle_fetch_errors( $output ) {
1175 if ( empty( $output->results ) ) {
1176 if ( ! empty( $output->errors ) ) {
1177 return new WP_Error(
1178 'users_fetch_error',
1179 __( 'Error fetching users from child sites.', 'mainwp' ),
1180 array( 'errors' => $output->errors )
1181 );
1182 }
1183 return new WP_Error(
1184 'no_users_found',
1185 __( 'No users found.', 'mainwp' )
1186 );
1187 }
1188 return true;
1189 }
1190
1191 /**
1192 * Process users results and format for response.
1193 *
1194 * @param array $results Raw results from child sites.
1195 * @param array $website_url Website URL mapping.
1196 * @return array Formatted results array.
1197 */
1198 private function process_users_results( $results, $website_url ) {
1199 $formatted_results = array();
1200
1201 foreach ( $results as $website_id => $users ) {
1202 if ( ! isset( $website_url[ $website_id ] ) ) {
1203 continue;
1204 }
1205
1206 foreach ( $users as $user_index => $user_data ) {
1207 $user = $this->map_user_data( $user_data, $website_id );
1208 $formatted_results[ $website_url[ $website_id ] ][ $user_index ] = $this->filter_response_data_by_allowed_fields( $user, 'view' );
1209 }
1210 }
1211
1212 return $formatted_results;
1213 }
1214
1215 /**
1216 * Validate create user request.
1217 *
1218 * @param WP_REST_Request $request Full details about the request.
1219 * @return true|WP_Error True on success, WP_Error on failure.
1220 */
1221 private function validate_user_request( $request ) {
1222 $content_type = $this->validate_content_type( $request );
1223 if ( is_wp_error( $content_type ) ) {
1224 return $content_type;
1225 }
1226 return true;
1227 }
1228
1229 /**
1230 * Get target websites from request body.
1231 *
1232 * @param array $body Request body data.
1233 * @return array|WP_Error Array of websites or WP_Error on failure.
1234 */
1235 private function get_target_websites( $body ) {
1236 $clients = $body['clients'] ?? '';
1237 $groups = $body['groups'] ?? '';
1238 $sites = $body['websites'] ?? '';
1239
1240 $filter_db_websites = $this->get_db_websites_by_filter( $sites, $groups, $clients );
1241
1242 if ( empty( $filter_db_websites ) || ! is_array( $filter_db_websites ) ) {
1243 return new WP_Error(
1244 'no_website_found',
1245 __( 'No website found.', 'mainwp' )
1246 );
1247 }
1248
1249 $db_websites = $filter_db_websites['db_websites'] ?? array();
1250
1251 if ( empty( $db_websites ) ) {
1252 return new WP_Error(
1253 'no_website_found',
1254 __( 'No website found.', 'mainwp' )
1255 );
1256 }
1257
1258 return $db_websites;
1259 }
1260
1261 /**
1262 * Prepare user data from request body.
1263 *
1264 * @param array $body Request body data.
1265 * @return array User data array.
1266 */
1267 private function prepare_user_data( $body ) {
1268 $pass_complexity = apply_filters( 'mainwp_new_user_password_complexity', '24' );
1269 $password = ! empty( $body['password'] ) ? $body['password'] : wp_generate_password( $pass_complexity );
1270
1271 return array(
1272 'user_pass' => $password,
1273 'user_email' => $body['email'] ?? '',
1274 'user_login' => $body['username'] ?? '',
1275 'user_url' => $body['user_url'] ?? '',
1276 'first_name' => $body['first_name'] ?? '',
1277 'last_name' => $body['last_name'] ?? '',
1278 'role' => $body['role'] ?? 'subscriber',
1279 );
1280 }
1281
1282 /**
1283 * Prepare post data for API request.
1284 *
1285 * @param array $user_to_add User data.
1286 * @param array $body Request body data.
1287 * @return array Post data array.
1288 */
1289 private function prepare_post_data( $user_to_add, $body ) {
1290 return array(
1291 'new_user' => base64_encode( wp_json_encode( $user_to_add ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible.
1292 'send_password' => isset( $body['send_password'] ) ? intval( $body['send_password'] ) : 0,
1293 );
1294 }
1295
1296 /**
1297 * Execute user creation on child sites.
1298 *
1299 * @param array $db_websites Array of websites.
1300 * @param array $post_data Post data for API request.
1301 * @return \stdClass|WP_Error Output object with results.
1302 */
1303 private function execute_user_creation( $db_websites, $post_data ) {
1304 try {
1305 $output = new \stdClass();
1306 $output->ok = array();
1307 $output->errors = array();
1308
1309 /**
1310 * Action: mainwp_before_user_create
1311 *
1312 * Fires before user create.
1313 *
1314 * @since 4.1
1315 */
1316 do_action( 'mainwp_before_user_create', $post_data, $db_websites );
1317
1318 MainWP_Connect::fetch_urls_authed(
1319 $db_websites,
1320 'newuser',
1321 $post_data,
1322 array(
1323 MainWP_Bulk_Add::get_class_name(),
1324 'posting_bulk_handler',
1325 ),
1326 $output
1327 );
1328
1329 /**
1330 * Action: mainwp_after_user_create
1331 *
1332 * Fires after user create.
1333 *
1334 * @since 4.1
1335 */
1336 do_action( 'mainwp_after_user_create', $output, $post_data, $db_websites );
1337
1338 return $output;
1339 } catch ( MainWP_Exception $e ) {
1340 return new WP_Error( 'create_user_error', MainWP_Error_Helper::get_error_message( $e ) );
1341 }
1342 }
1343
1344 /**
1345 * Process creation results and prepare response.
1346 *
1347 * @param \stdClass $output Output from user creation.
1348 * @param array $db_websites Array of websites.
1349 * @param array $user_to_add User data.
1350 * @return WP_REST_Response Response object.
1351 */
1352 private function process_creation_results( $output, $db_websites, $user_to_add ) {
1353 $created_sites = array();
1354 $failed_sites = array();
1355
1356 foreach ( $db_websites as $website ) {
1357 if ( isset( $output->ok[ $website->id ] ) && 1 === (int) $output->ok[ $website->id ] ) {
1358 $created_sites[ $website->id ] = array(
1359 'url' => $website->url,
1360 'message' => esc_html__( 'User created successfully.', 'mainwp' ),
1361 );
1362 } elseif ( isset( $output->errors[ $website->id ] ) ) {
1363 $failed_sites[ $website->id ] = array(
1364 'url' => $website->url,
1365 'message' => $output->errors[ $website->id ],
1366 );
1367 }
1368 }
1369
1370 // Count results.
1371 $total_success = count( $created_sites );
1372 $total_failed = count( $failed_sites );
1373 $total_sites = count( $db_websites );
1374
1375 $message = $this->build_creation_message( $total_success, $total_failed, $total_sites );
1376 return rest_ensure_response(
1377 array(
1378 'success' => 1,
1379 'message' => $message,
1380 'data' => array(
1381 'user_info' => $user_to_add,
1382 'total_sites' => $total_sites,
1383 'total_success' => $total_success,
1384 'total_failed' => $total_failed,
1385 'created_sites' => $created_sites,
1386 'failed_sites' => $failed_sites,
1387 ),
1388 )
1389 );
1390 }
1391
1392 /**
1393 * Build creation message based on results.
1394 *
1395 * @param int $total_success Total successful creations.
1396 * @param int $total_failed Total failed creations.
1397 * @param int $total_sites Total sites processed.
1398 * @return string Message string.
1399 */
1400 private function build_creation_message( $total_success, $total_failed, $total_sites ) {
1401 if ( 0 === $total_failed ) {
1402 return sprintf(
1403 /* translators: %d: number of websites */
1404 esc_html__( 'User created successfully on all %d website(s).', 'mainwp' ),
1405 $total_success
1406 );
1407 } elseif ( 0 === $total_success ) {
1408 return sprintf(
1409 /* translators: %d: number of websites */
1410 esc_html__( 'User creation failed on all %d website(s).', 'mainwp' ),
1411 $total_failed
1412 );
1413 } else {
1414 return sprintf(
1415 /* translators: 1: number of successful websites, 2: number of failed websites, 3: total websites */
1416 esc_html__( 'User created on %1$d of %3$d website(s). %2$d failed.', 'mainwp' ),
1417 $total_success,
1418 $total_failed,
1419 $total_sites
1420 );
1421 }
1422 }
1423
1424 /**
1425 * Users Validate filter params.
1426 *
1427 * @param WP_REST_Request $request Request object.
1428 *
1429 * @return bool|WP_Error
1430 */
1431 /**
1432 * Validate filter params.
1433 *
1434 * @param WP_REST_Request $request Request object.
1435 *
1436 * @return bool|WP_Error
1437 */
1438 public function users_validate_filter_params( $request ) {
1439 $clients = $request->get_param( 'clients' );
1440 $groups = $request->get_param( 'groups' );
1441 $websites = $request->get_param( 'websites' );
1442 $filled_count = count( array_filter( array( $clients, $groups, $websites ) ) );
1443 if ( $filled_count > 1 ) {
1444 return new WP_Error(
1445 'invalid_filter_params',
1446 __( 'Only one of clients, groups, or websites can be specified.', 'mainwp' ),
1447 );
1448 }
1449 return true;
1450 }
1451
1452 /**
1453 * Validate edit user request parameters.
1454 *
1455 * @param WP_REST_Request $request Request object.
1456 * @return bool|WP_Error True on success, WP_Error on failure.
1457 */
1458 public function edit_validate_filter_params( $request ) {
1459 // Define editable fields.
1460 $editable_fields = array(
1461 'password',
1462 'role',
1463 'first_name',
1464 'last_name',
1465 'user_url',
1466 'nickname',
1467 'display_name',
1468 'email',
1469 'description',
1470 );
1471
1472 // Check if at least one editable field is provided and not empty.
1473 $has_field = false;
1474 foreach ( $editable_fields as $field ) {
1475 $value = $request->get_param( $field );
1476 if ( ! empty( $value ) || ( isset( $value ) && '' !== $value ) ) {
1477 $has_field = true;
1478 break;
1479 }
1480 }
1481
1482 if ( ! $has_field ) {
1483 return new WP_Error(
1484 'missing_required_fields',
1485 __( 'At least one field must be provided to update the user.', 'mainwp' ),
1486 );
1487 }
1488
1489 return true;
1490 }
1491
1492 /**
1493 * Get users fields allowed fields.
1494 *
1495 * @return array
1496 */
1497 public function get_users_fields_allowed_fields() {
1498 $roles = array( 'administrator', 'subscriber', 'editor', 'author', 'contributor' );
1499 return array(
1500 'clients' => array(
1501 'required' => false,
1502 'type' => 'string',
1503 'description' => __( 'Search by clients.', 'mainwp' ),
1504 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1505 'validate_callback' => array( $this, 'validate_clients' ),
1506 ),
1507 'groups' => array(
1508 'required' => false,
1509 'type' => 'string',
1510 'description' => __( 'Search by tags.', 'mainwp' ),
1511 'sanitize_callback' => array( $this, 'sanitize_groups_text_field' ),
1512 'validate_callback' => array( $this, 'validate_groups' ),
1513 ),
1514 'websites' => array(
1515 'required' => false,
1516 'type' => 'string',
1517 'description' => __( 'Websites child site field IDs.', 'mainwp' ),
1518 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1519 'validate_callback' => array( $this, 'validate_site_ids' ),
1520 ),
1521 'search' => array(
1522 'required' => false,
1523 'type' => 'string',
1524 'sanitize_callback' => 'sanitize_text_field',
1525 'description' => __( 'Search by user name.', 'mainwp' ),
1526 ),
1527 'roles' => array(
1528 'required' => false,
1529 'type' => 'array',
1530 'sanitize_callback' => $this->make_enum_sanitizer( $roles, 'array' ),
1531 'validate_callback' => $this->make_enum_validator( $roles, 'array' ),
1532 'description' => __( 'Search by roles.', 'mainwp' ),
1533 ),
1534 );
1535 }
1536
1537 /**
1538 * Create users fields allowed fields.
1539 *
1540 * @return array
1541 */
1542 public function create_users_fields_allowed_fields() {
1543
1544 return array_merge(
1545 $this->create_edit_users_fields_allowed_fields(),
1546 array(
1547 'username' => array(
1548 'required' => true,
1549 'type' => 'string',
1550 'sanitize_callback' => 'sanitize_text_field',
1551 'description' => __( 'Username.', 'mainwp' ),
1552 ),
1553 'email' => array(
1554 'required' => true,
1555 'type' => 'string',
1556 'sanitize_callback' => 'sanitize_email',
1557 'description' => __( 'Email.', 'mainwp' ),
1558 ),
1559 'send_password' => array(
1560 'required' => false,
1561 'type' => 'boolean',
1562 'sanitize_callback' => 'rest_sanitize_boolean',
1563 'description' => __( 'Send password.', 'mainwp' ),
1564 ),
1565 'clients' => array(
1566 'required' => false,
1567 'type' => 'string',
1568 'description' => __( 'Search by clients.', 'mainwp' ),
1569 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1570 'validate_callback' => array( $this, 'validate_clients' ),
1571 ),
1572 'groups' => array(
1573 'required' => false,
1574 'type' => 'string',
1575 'description' => __( 'Search by tags.', 'mainwp' ),
1576 'sanitize_callback' => array( $this, 'sanitize_groups_text_field' ),
1577 'validate_callback' => array( $this, 'validate_groups' ),
1578 ),
1579 'websites' => array(
1580 'required' => false,
1581 'type' => 'string',
1582 'description' => __( 'Websites child site field IDs.', 'mainwp' ),
1583 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1584 'validate_callback' => array( $this, 'validate_site_ids' ),
1585 ),
1586 )
1587 );
1588 }
1589
1590 /**
1591 * Edit users fields allowed fields.
1592 *
1593 * @return array
1594 */
1595 public function edit_users_fields_allowed_fields() {
1596 return array_merge(
1597 $this->allowed_user_id_field(),
1598 $this->allowed_id_domain_field(),
1599 $this->create_edit_users_fields_allowed_fields(),
1600 array(
1601 'nickname' => array(
1602 'required' => false,
1603 'type' => 'string',
1604 'sanitize_callback' => 'sanitize_text_field',
1605 'description' => __( 'Nickname.', 'mainwp' ),
1606 ),
1607 'display_name' => array(
1608 'required' => false,
1609 'type' => 'string',
1610 'sanitize_callback' => 'sanitize_text_field',
1611 'description' => __( 'Display name.', 'mainwp' ),
1612 ),
1613 'email' => array(
1614 'required' => false,
1615 'type' => 'string',
1616 'sanitize_callback' => 'sanitize_email',
1617 'description' => __( 'Email.', 'mainwp' ),
1618 ),
1619 'description' => array(
1620 'required' => false,
1621 'type' => 'string',
1622 'sanitize_callback' => 'sanitize_text_field',
1623 'description' => __( 'Description.', 'mainwp' ),
1624 ),
1625 )
1626 );
1627 }
1628
1629 /**
1630 * Create and edit users fields allowed fields.
1631 *
1632 * @return array
1633 */
1634 private function create_edit_users_fields_allowed_fields() {
1635 $roles = array( 'administrator', 'subscriber', 'editor', 'author', 'contributor' );
1636
1637 return array(
1638 'password' => array(
1639 'required' => false,
1640 'type' => 'string',
1641 'sanitize_callback' => function ( $value ) {
1642 return is_string( $value ) ? $value : '';
1643 },
1644 'description' => __( 'Password.', 'mainwp' ),
1645 ),
1646 'role' => array(
1647 'required' => false,
1648 'type' => 'string',
1649 'sanitize_callback' => $this->make_enum_sanitizer( $roles, 'string' ),
1650 'validate_callback' => $this->make_enum_validator( $roles, 'string' ),
1651 'description' => __( 'Role.', 'mainwp' ),
1652 ),
1653 'first_name' => array(
1654 'required' => false,
1655 'type' => 'string',
1656 'sanitize_callback' => 'sanitize_text_field',
1657 'description' => __( 'First name.', 'mainwp' ),
1658 ),
1659 'last_name' => array(
1660 'required' => false,
1661 'type' => 'string',
1662 'sanitize_callback' => 'sanitize_text_field',
1663 'description' => __( 'Last name.', 'mainwp' ),
1664 ),
1665 'user_url' => array(
1666 'required' => false,
1667 'type' => 'string',
1668 'sanitize_callback' => 'sanitize_url',
1669 'description' => __( 'User website.', 'mainwp' ),
1670 ),
1671 );
1672 }
1673
1674 /**
1675 * Update admin password fields allowed fields.
1676 *
1677 * @return array
1678 */
1679 public function update_admin_password_fields_allowed_fields() {
1680 return array(
1681 'password' => array(
1682 'required' => true,
1683 'type' => 'string',
1684 'sanitize_callback' => function ( $value ) {
1685 return is_string( $value ) ? $value : '';
1686 },
1687 'description' => __( 'Password.', 'mainwp' ),
1688 ),
1689 'clients' => array(
1690 'required' => false,
1691 'type' => 'string',
1692 'description' => __( 'Search by clients.', 'mainwp' ),
1693 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1694 'validate_callback' => array( $this, 'validate_clients' ),
1695 ),
1696 'groups' => array(
1697 'required' => false,
1698 'type' => 'string',
1699 'description' => __( 'Search by tags.', 'mainwp' ),
1700 'sanitize_callback' => array( $this, 'sanitize_groups_text_field' ),
1701 'validate_callback' => array( $this, 'validate_groups' ),
1702 ),
1703 'websites' => array(
1704 'required' => false,
1705 'type' => 'string',
1706 'description' => __( 'Websites child site field IDs.', 'mainwp' ),
1707 'sanitize_callback' => array( $this, 'sanitize_text_field_to_array' ),
1708 'validate_callback' => array( $this, 'validate_site_ids' ),
1709 ),
1710 );
1711 }
1712
1713 /**
1714 * Import users fields allowed fields.
1715 *
1716 * @return array
1717 */
1718 public function import_users_fields_allowed_fields() {
1719 return array(
1720 'csv_file' => array(
1721 'required' => false,
1722 'type' => 'file',
1723 'description' => __( 'CSV file containing user data.', 'mainwp' ),
1724 ),
1725 'has_header' => array(
1726 'required' => false,
1727 'type' => 'boolean',
1728 'default' => true,
1729 'sanitize_callback' => 'rest_sanitize_boolean',
1730 'description' => __( 'Whether the CSV file has a header row.', 'mainwp' ),
1731 ),
1732 );
1733 }
1734
1735 /**
1736 * Get allowed fields for users.
1737 *
1738 * @return array
1739 */
1740 private function allowed_user_id_field() {
1741 return array(
1742 'user_id' => array(
1743 'required' => true,
1744 'description' => __( 'User ID.', 'mainwp' ),
1745 'type' => 'integer',
1746 'sanitize_callback' => 'absint',
1747 ),
1748 );
1749 }
1750
1751 /**
1752 * Get allowed fields for posts.
1753 *
1754 * @return array
1755 */
1756 private function allowed_id_domain_field() {
1757 return array(
1758 'id_domain' => array(
1759 'required' => true,
1760 'description' => __( 'Site ID (number) or domain (string).', 'mainwp' ),
1761 'type' => 'string',
1762 'sanitize_callback' => 'sanitize_text_field',
1763 ),
1764 );
1765 }
1766
1767 /**
1768 * Get request body.
1769 *
1770 * @param WP_REST_Request $request Full details about the request.
1771 * @return array|WP_Error
1772 */
1773 private function get_request_body( $request ) { // phpcs:ignore -- NOSONAR - complex.
1774 // Get request body filed.
1775 $body = $request->get_body_params();
1776 if ( ! empty( $body ) && is_array( $body ) ) {
1777 return $body;
1778 }
1779
1780 // Get request body from body.
1781 $body = $request->get_json_params();
1782 if ( ! empty( $body ) && is_array( $body ) ) {
1783 return $body;
1784 }
1785
1786 // Get request body from raw.
1787 $body = $request->get_body();
1788 if ( ! empty( $body ) && is_string( $body ) ) {
1789 $body = json_decode( $body, true );
1790 if ( ! is_array( $body ) ) {
1791 return new WP_Error(
1792 'invalid_json',
1793 __( 'Request body contains invalid JSON.', 'mainwp' ),
1794 array( 'status' => 400 )
1795 );
1796 }
1797 return $body;
1798 }
1799 // Return error.
1800 return new WP_Error(
1801 'empty_body',
1802 __( 'Request body is empty.', 'mainwp' ),
1803 );
1804 }
1805
1806 /**
1807 * Validate content type.
1808 *
1809 * @param WP_REST_Request $request Full details about the request.
1810 * @return bool|WP_Error
1811 */
1812 private function validate_content_type( $request ) {
1813 $content_type = (string) $request->get_header( 'content-type' );
1814 if ( false === stripos( $content_type, 'application/json' ) ) {
1815 return new WP_Error(
1816 'invalid_content_type',
1817 __( 'Invalid content type. Expected application/json.', 'mainwp' ),
1818 );
1819 }
1820 return true;
1821 }
1822
1823 /**
1824 * Get monitor by.
1825 *
1826 * @param WP_REST_Request $request Full details about the request.
1827 *
1828 * @uses MainWP_DB_Uptime_Monitoring::instance()->get_monitor_by()
1829 *
1830 * @return WP_Error|mixed Item.
1831 */
1832 private function get_request_website_by_id_domain_item( $request ) { // phpcs:ignore -- NOSONAR
1833 // Get id or domain raw value.
1834 $raw = (string) $request->get_param( 'id_domain' );
1835 $raw = trim( $raw );
1836
1837 if ( empty( $raw ) ) {
1838 return new WP_Error( 'id_domain_not_found', __( 'Site id or domain not found.', 'mainwp' ) );
1839 }
1840
1841 // Get monitor by monitor id.
1842 if ( ctype_digit( $raw ) ) {
1843 $website_id = (int) $raw;
1844 return $this->get_site_by( 'id', $website_id );
1845 }
1846
1847 // Get monitor by domain.
1848 $domain = strtolower( rtrim( rawurldecode( $raw ), '/' ) );
1849 $website = $this->get_site_by( 'domain', $domain );
1850 if ( empty( $website ) ) {
1851 return new WP_Error( 'website_not_found', __( 'Website not found.', 'mainwp' ) );
1852 }
1853
1854 return $website;
1855 }
1856
1857 /**
1858 * Get user by id.
1859 *
1860 * @param object $website Website.
1861 * @param WP_REST_Request $request Full details about the request.
1862 *
1863 * @return array|WP_Error
1864 */
1865 private function get_request_user_by_id( $website, $request ) { // phpcs:ignore -- NOSONAR
1866 $user_id = (int) $request->get_param( 'user_id' );
1867 if ( empty( $user_id ) ) {
1868 return new WP_Error( 'user_id_not_found', __( 'User id not found.', 'mainwp' ) );
1869 }
1870
1871 try {
1872
1873 /**
1874 * Action: mainwp_before_user_action
1875 *
1876 * Fires before user edit/delete/update_user/update_password actions.
1877 *
1878 * @since 4.1
1879 */
1880 do_action( 'mainwp_before_user_action', 'edit', $user_id, '', '', 1, $website );
1881
1882 $information = MainWP_Connect::fetch_url_authed(
1883 $website,
1884 'user_action',
1885 array(
1886 'action' => 'edit',
1887 'id' => $user_id,
1888 'extra' => '',
1889 'user_pass' => '',
1890 'optimize' => 1,
1891 )
1892 );
1893 if ( is_array( $information ) && isset( $information['status'] ) && ( 'SUCCESS' === $information['status'] ) ) {
1894 $data = isset( $information['other_data']['users_data'] ) ? $information['other_data']['users_data'] : array(); // user actions data.
1895
1896 /**
1897 * Fires immediately after user action.
1898 *
1899 * @since 4.5.1.1
1900 */
1901 do_action( 'mainwp_user_action', $website, 'update_user', $data, '', 1 );
1902
1903 return $information;
1904 }
1905
1906 return new WP_Error( 'user_not_found', __( 'User not found.', 'mainwp' ) );
1907 } catch ( MainWP_Exception $e ) {
1908 return new WP_Error( 'get_user_error', MainWP_Error_Helper::get_error_message( $e ) );
1909 }
1910 }
1911
1912 /**
1913 * Get the API keys schema, conforming to JSON Schema.
1914 *
1915 * @since 5.2
1916 * @return array
1917 */
1918 public function get_item_schema() { // phpcs:ignore -- NOSONAR - long schema.
1919 return array(
1920 '$schema' => 'http://json-schema.org/draft-04/schema#',
1921 'title' => 'user',
1922 'type' => 'object',
1923 'properties' => array(
1924 'id' => array(
1925 'type' => 'integer',
1926 'description' => __( 'User ID.', 'mainwp' ),
1927 'context' => array( 'view', 'edit' ),
1928 ),
1929 'website_id' => array(
1930 'type' => 'integer',
1931 'description' => __( 'Website ID.', 'mainwp' ),
1932 'context' => array( 'view' ),
1933 'readonly' => true,
1934 ),
1935 'username' => array(
1936 'type' => 'string',
1937 'description' => __( 'Username.', 'mainwp' ),
1938 'context' => array( 'view', 'edit' ),
1939 ),
1940 'name' => array(
1941 'type' => 'string',
1942 'description' => __( 'Name.', 'mainwp' ),
1943 'context' => array( 'view', 'edit' ),
1944 ),
1945 'email' => array(
1946 'type' => 'string',
1947 'description' => __( 'Email.', 'mainwp' ),
1948 'context' => array( 'view', 'edit' ),
1949 ),
1950 'role' => array(
1951 'type' => 'string',
1952 'description' => __( 'Role.', 'mainwp' ),
1953 'context' => array( 'view', 'edit' ),
1954 ),
1955 'posts' => array(
1956 'type' => 'integer',
1957 'description' => __( 'Posts.', 'mainwp' ),
1958 'context' => array( 'view', 'edit' ),
1959 ),
1960 ),
1961 );
1962 }
1963
1964 // phpcs:enable Generic.Metrics.CyclomaticComplexity
1965 }
1966