PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.3
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.3
6.3 6.2.3 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 All 157 releases
← All changes | includes/abilities/class-mainwp-abilities-clients.php +13 -226 6.3 → 6.2.3 View file →
@@ -159,12 +159,8 @@
159 159 * @param array $input Validated input parameters.
160 160 * @return array|\WP_Error Result array or error.
161 161 */
162 162 public static function execute_list_clients( array $input ) {
163 - // Restricted callers see the clients whose sites overlap their
164 - // allowed set instead of a blanket denial.
165 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
166 -
167 163 $input = MainWP_Abilities_Util::normalize_input(
168 164 $input,
169 165 array(
170 166 'page' => 1,
@@ -198,26 +194,8 @@
198 194 if ( ! empty( $input['exclude'] ) ) {
199 195 $args['exclude'] = $input['exclude'];
200 196 }
201 197
202 - if ( 'all' !== $allowed_sites ) {
203 - $accessible = MainWP_Abilities_Util::get_accessible_client_ids( $allowed_sites );
204 - $accessible = empty( $args['include'] )
205 - ? $accessible
206 - : array_values( array_intersect( array_map( 'intval', (array) $args['include'] ), $accessible ) );
207 -
208 - if ( empty( $accessible ) ) {
209 - return array(
210 - 'items' => array(),
211 - 'page' => (int) $input['page'],
212 - 'per_page' => (int) $input['per_page'],
213 - 'total' => 0,
214 - );
215 - }
216 -
217 - $args['include'] = $accessible;
218 - }
219 -
220 198 $db = MainWP_DB_Client::instance();
221 199 $clients = $db->get_wp_clients( $args );
222 200 $total = $db->get_wp_clients( array_merge( $args, array( 'count_only' => true ) ) );
223 201
@@ -297,22 +275,10 @@
297 275 * @param array $input Validated input parameters (unused but required by ability signature).
298 276 * @return array|\WP_Error Result array or error.
299 277 */
300 278 public static function execute_count_clients( array $input ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found -- NOSONAR Required by ability signature.
301 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
279 + $total = MainWP_DB_Client::instance()->get_wp_clients( array( 'count_only' => true ) );
302 280
303 - $args = array( 'count_only' => true );
304 -
305 - if ( 'all' !== $allowed_sites ) {
306 - $accessible = MainWP_Abilities_Util::get_accessible_client_ids( $allowed_sites );
307 - if ( empty( $accessible ) ) {
308 - return array( 'total' => 0 );
309 - }
310 - $args['include'] = $accessible;
311 - }
312 -
313 - $total = MainWP_DB_Client::instance()->get_wp_clients( $args );
314 -
315 281 return array(
316 282 'total' => (int) $total,
317 283 );
318 284 }
@@ -384,14 +350,8 @@
384 350 if ( is_wp_error( $client ) ) {
385 351 return $client;
386 352 }
387 353
388 - // Any-site overlap grants read access.
389 - $access = MainWP_Abilities_Util::check_client_access( $client, $input );
390 - if ( is_wp_error( $access ) ) {
391 - return $access;
392 - }
393 -
394 354 return MainWP_Abilities_Util::format_client_for_output( $client );
395 355 }
396 356
397 357 /**
@@ -408,9 +368,9 @@
408 368 'category' => 'mainwp-clients',
409 369 'input_schema' => self::get_add_client_input_schema(),
410 370 'output_schema' => self::get_client_output_schema(),
411 371 'execute_callback' => array( self::class, 'execute_add_client' ),
412 - 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/add-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
372 + 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
413 373 'meta' => array(
414 374 'show_in_rest' => true,
415 375 'annotations' => array(
416 376 'instructions' => '',
@@ -458,44 +418,8 @@
458 418 );
459 419 }
460 420
461 421 /**
462 - * Reject selected_sites entries outside the caller's allowed set.
463 - *
464 - * A scoped user must not re-home sites they cannot access onto clients
465 - * they control, so any out-of-scope ID fails the whole call rather than
466 - * being silently dropped.
467 - *
468 - * @param array $input Ability input, possibly carrying selected_sites.
469 - * @return true|\WP_Error True when in scope, WP_Error otherwise.
470 - */
471 - public static function check_selected_sites_scope( array $input ) {
472 - if ( empty( $input['selected_sites'] ) || ! is_array( $input['selected_sites'] ) ) {
473 - return true;
474 - }
475 -
476 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
477 - if ( 'all' === $allowed_sites ) {
478 - return true;
479 - }
480 -
481 - $selected = array_map( 'intval', $input['selected_sites'] );
482 - $out_of_scope = array_diff( $selected, $allowed_sites );
483 -
484 - if ( ! empty( $out_of_scope ) ) {
485 - // Only a restricted caller reaches this leg, so the unseen site reads as missing.
486 - return new \WP_Error(
487 - 'mainwp_site_not_found',
488 - /* translators: %d: site ID */
489 - sprintf( __( 'No site found with ID %d.', 'mainwp' ), (int) reset( $out_of_scope ) ),
490 - array( 'status' => 404 )
491 - );
492 - }
493 -
494 - return true;
495 - }
496 -
497 - /**
498 422 * Execute add-client ability.
499 423 *
500 424 * @param array $input Validated input parameters.
501 425 * @return array|\WP_Error Result array or error.
@@ -500,15 +424,8 @@
500 424 * @param array $input Validated input parameters.
501 425 * @return array|\WP_Error Result array or error.
502 426 */
503 427 public static function execute_add_client( array $input ) {
504 - // Creation stays open to scoped callers, but they cannot attach
505 - // sites outside their allowed set.
506 - $scope_check = self::check_selected_sites_scope( $input );
507 - if ( is_wp_error( $scope_check ) ) {
508 - return $scope_check;
509 - }
510 -
511 428 if ( empty( $input['name'] ) ) {
512 429 return new \WP_Error(
513 430 'mainwp_invalid_input',
514 431 __( 'Client name is required.', 'mainwp' ),
@@ -567,9 +484,9 @@
567 484 'category' => 'mainwp-clients',
568 485 'input_schema' => self::get_update_client_input_schema(),
569 486 'output_schema' => self::get_client_output_schema(),
570 487 'execute_callback' => array( self::class, 'execute_update_client' ),
571 - 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/update-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
488 + 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
572 489 'meta' => array(
573 490 'show_in_rest' => true,
574 491 'annotations' => array(
575 492 'instructions' => '',
@@ -630,46 +547,8 @@
630 547 if ( is_wp_error( $client ) ) {
631 548 return $client;
632 549 }
633 550
634 - // Any-site overlap grants the update; attaching sites outside the
635 - // caller's allowed set is rejected outright.
636 - $access = MainWP_Abilities_Util::check_client_access( $client, $input );
637 - if ( is_wp_error( $access ) ) {
638 - return $access;
639 - }
640 -
641 - $scope_check = self::check_selected_sites_scope( $input );
642 - if ( is_wp_error( $scope_check ) ) {
643 - return $scope_check;
644 - }
645 -
646 - // Preserve memberships the caller cannot see. update_selected_sites_for_client
647 - // replaces the client's whole roster, so a restricted caller submitting
648 - // selected_sites would silently detach the client's sites outside their allowed
649 - // set. Fold those existing out-of-scope sites back in: a partial manager can add
650 - // or remove only sites within their own scope, never detach ones they can't see.
651 - if ( isset( $input['selected_sites'] ) && is_array( $input['selected_sites'] ) ) {
652 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
653 - if ( 'all' !== $allowed_sites ) {
654 - $current_site_ids = array_map(
655 - static fn( $site ) => (int) $site->id,
656 - (array) MainWP_DB_Client::instance()->get_websites_by_client_ids(
657 - (int) $client->client_id,
658 - array( 'no_perm_check' => true )
659 - )
660 - );
661 - $unseen = array_diff( $current_site_ids, $allowed_sites );
662 - if ( ! empty( $unseen ) ) {
663 - $input['selected_sites'] = array_values(
664 - array_unique(
665 - array_merge( array_map( 'intval', $input['selected_sites'] ), $unseen )
666 - )
667 - );
668 - }
669 - }
670 - }
671 -
672 551 $data = $input;
673 552 $data['client_id'] = $client->client_id;
674 553
675 554 unset( $data['client_id_or_email'] );
@@ -715,9 +594,9 @@
715 594 'category' => 'mainwp-clients',
716 595 'input_schema' => self::get_delete_client_input_schema(),
717 596 'output_schema' => self::get_delete_client_output_schema(),
718 597 'execute_callback' => array( self::class, 'execute_delete_client' ),
719 - 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/delete-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
598 + 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
720 599 'meta' => array(
721 600 'show_in_rest' => true,
722 601 'annotations' => array(
723 602 'instructions' => 'Destructive operation requires confirm: true. Only call when user explicitly requests deletion.',
@@ -806,12 +685,9 @@
806 685 'dry_run' => false,
807 686 )
808 687 );
809 688
810 - $dry_run = MainWP_Abilities_Util::get_boolean_input( $input, 'dry_run' );
811 - $confirm = MainWP_Abilities_Util::get_boolean_input( $input, 'confirm' );
812 -
813 - if ( $dry_run && $confirm ) {
689 + if ( $input['dry_run'] && $input['confirm'] ) {
814 690 return new \WP_Error(
815 691 'mainwp_invalid_input',
816 692 __( 'Cannot specify both dry_run and confirm. Use dry_run for preview, confirm for actual deletion.', 'mainwp' ),
817 693 array( 'status' => 400 )
@@ -823,17 +699,9 @@
823 699 if ( is_wp_error( $client ) ) {
824 700 return $client;
825 701 }
826 702
827 - // Deleting removes the client for every site it covers, so the
828 - // caller's allowed set must cover all of them. Any-site overlap is
829 - // read access only.
830 - $access = MainWP_Abilities_Util::check_client_access( $client, $input, true );
831 - if ( is_wp_error( $access ) ) {
832 - return $access;
833 - }
834 -
835 - if ( $dry_run ) {
703 + if ( $input['dry_run'] ) {
836 704 $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id );
837 705 $sites_count = is_array( $sites ) ? count( $sites ) : 0;
838 706
839 707 $warnings = array();
@@ -860,9 +728,9 @@
860 728 'warnings' => $warnings,
861 729 );
862 730 }
863 731
864 - if ( ! $confirm ) {
732 + if ( ! $input['confirm'] ) {
865 733 return new \WP_Error(
866 734 'mainwp_confirmation_required',
867 735 __( 'This is a destructive operation. Set confirm: true or use dry_run: true for preview.', 'mainwp' ),
868 736 array( 'status' => 400 )
@@ -952,29 +820,13 @@
952 820 if ( is_wp_error( $client ) ) {
953 821 return $client;
954 822 }
955 823
956 - $access = MainWP_Abilities_Util::check_client_access( $client, $input );
957 - if ( is_wp_error( $access ) ) {
958 - return $access;
959 - }
824 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id );
960 825
961 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
962 - $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids(
963 - $client->client_id,
964 - array(
965 - 'no_perm_check' => true,
966 - 'where' => 'wp.is_staging = 0',
967 - )
968 - );
969 -
970 826 $items = array();
971 827 if ( is_array( $sites ) ) {
972 828 foreach ( $sites as $site ) {
973 - // A restricted caller sees only their slice of the client.
974 - if ( 'all' !== $allowed_sites && ! in_array( (int) $site->id, $allowed_sites, true ) ) {
975 - continue;
976 - }
977 829 $items[] = MainWP_Abilities_Util::format_site_for_output( $site );
978 830 }
979 831 }
980 832
@@ -1054,32 +906,11 @@
1054 906 if ( is_wp_error( $client ) ) {
1055 907 return $client;
1056 908 }
1057 909
1058 - $access = MainWP_Abilities_Util::check_client_access( $client, $input );
1059 - if ( is_wp_error( $access ) ) {
1060 - return $access;
1061 - }
910 + $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids( $client->client_id );
911 + $count = is_array( $sites ) ? count( $sites ) : 0;
1062 912
1063 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
1064 - $sites = MainWP_DB_Client::instance()->get_websites_by_client_ids(
1065 - $client->client_id,
1066 - array(
1067 - 'no_perm_check' => true,
1068 - 'where' => 'wp.is_staging = 0',
1069 - )
1070 - );
1071 -
1072 - $count = 0;
1073 - if ( is_array( $sites ) ) {
1074 - foreach ( $sites as $site ) {
1075 - if ( 'all' !== $allowed_sites && ! in_array( (int) $site->id, $allowed_sites, true ) ) {
1076 - continue;
1077 - }
1078 - ++$count;
1079 - }
1080 - }
1081 -
1082 913 return array(
1083 914 'total' => $count,
1084 915 );
1085 916 }
@@ -1181,13 +1012,8 @@
1181 1012 if ( is_wp_error( $client ) ) {
1182 1013 return $client;
1183 1014 }
1184 1015
1185 - $access = MainWP_Abilities_Util::check_client_access( $client, $input );
1186 - if ( is_wp_error( $access ) ) {
1187 - return $access;
1188 - }
1189 -
1190 1016 if ( ! class_exists( 'MainWP\Dashboard\Module\CostTracker\Cost_Tracker_DB' ) ) {
1191 1017 return new \WP_Error(
1192 1018 'mainwp_internal_error',
1193 1019 __( 'Cost Tracker database class is not available.', 'mainwp' ),
@@ -1195,38 +1021,13 @@
1195 1021 );
1196 1022 }
1197 1023
1198 1024 $cost_db = \MainWP\Dashboard\Module\CostTracker\Cost_Tracker_DB::get_instance();
1199 - // The producer returns a client_id => cost[] map, not a flat list.
1200 - // get_cost_sites makes it stamp cost_sites_ids with the sites+groups+
1201 - // clients expansion the scope filter below needs.
1202 - $costs_map = $cost_db->get_all_cost_trackers_by_clients( array( $client->client_id ), array( 'get_cost_sites' => true ) );
1203 - $costs = is_array( $costs_map ) ? ( $costs_map[ (int) $client->client_id ] ?? array() ) : array();
1025 + $costs = $cost_db->get_all_cost_trackers_by_clients( array( $client->client_id ) );
1204 1026
1205 - // Any-site overlap grants the client read, but cost aggregates span
1206 - // all the client's sites. A partial manager only sees cost items
1207 - // backed by at least one site they can access, and company-wide
1208 - // items with no site attachment stay unrestricted-only.
1209 - $allowed_sites = MainWP_Abilities_Util::get_allowed_site_ids();
1210 -
1211 1027 $items = array();
1212 1028 if ( is_array( $costs ) ) {
1213 1029 foreach ( $costs as $cost ) {
1214 - if ( 'all' !== $allowed_sites ) {
1215 - // A cost reached through a group or client attachment
1216 - // carries an empty sites column; cost_sites_ids holds the
1217 - // resolved site list. Fall back to the raw column only if
1218 - // the producer did not stamp it.
1219 - if ( isset( $cost->cost_sites_ids ) && is_array( $cost->cost_sites_ids ) ) {
1220 - $cost_sites = array_map( 'intval', $cost->cost_sites_ids );
1221 - } else {
1222 - $cost_sites = ! empty( $cost->sites ) ? json_decode( $cost->sites, true ) : array();
1223 - $cost_sites = is_array( $cost_sites ) ? array_map( 'intval', $cost_sites ) : array();
1224 - }
1225 - if ( empty( array_intersect( $cost_sites, $allowed_sites ) ) ) {
1226 - continue;
1227 - }
1228 - }
1229 1030 $items[] = MainWP_Abilities_Util::format_cost_for_output( $cost );
1230 1031 }
1231 1032 }
1232 1033
@@ -1250,9 +1051,9 @@
1250 1051 'category' => 'mainwp-clients',
1251 1052 'input_schema' => self::get_suspend_client_input_schema(),
1252 1053 'output_schema' => self::get_client_output_schema(),
1253 1054 'execute_callback' => array( self::class, 'execute_suspend_client' ),
1254 - 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/suspend-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1055 + 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1255 1056 'meta' => array(
1256 1057 'show_in_rest' => true,
1257 1058 'annotations' => array(
1258 1059 'instructions' => '',
@@ -1292,15 +1093,8 @@
1292 1093 if ( is_wp_error( $client ) ) {
1293 1094 return $client;
1294 1095 }
1295 1096
1296 - // Suspension affects every site the client covers: full coverage
1297 - // required, any-site overlap is read access only.
1298 - $access = MainWP_Abilities_Util::check_client_access( $client, $input, true );
1299 - if ( is_wp_error( $access ) ) {
1300 - return $access;
1301 - }
1302 -
1303 1097 $updated = MainWP_DB_Client::instance()->update_client(
1304 1098 array(
1305 1099 'client_id' => $client->client_id,
1306 1100 'suspended' => 1,
@@ -1342,9 +1136,9 @@
1342 1136 'category' => 'mainwp-clients',
1343 1137 'input_schema' => self::get_unsuspend_client_input_schema(),
1344 1138 'output_schema' => self::get_client_output_schema(),
1345 1139 'execute_callback' => array( self::class, 'execute_unsuspend_client' ),
1346 - 'permission_callback' => MainWP_Abilities_Util::make_operation_permission_callback( 'mainwp/unsuspend-client-v1', array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ) ),
1140 + 'permission_callback' => array( MainWP_Abilities_Util::class, 'check_manage_sites_permission' ),
1347 1141 'meta' => array(
1348 1142 'show_in_rest' => true,
1349 1143 'annotations' => array(
1350 1144 'instructions' => '',
@@ -1382,15 +1176,8 @@
1382 1176 $client = MainWP_Abilities_Util::resolve_client( $input['client_id_or_email'] );
1383 1177
1384 1178 if ( is_wp_error( $client ) ) {
1385 1179 return $client;
1386 - }
1387 -
1388 - // Same full-coverage rule as suspend: the flip affects every site
1389 - // the client covers.
1390 - $access = MainWP_Abilities_Util::check_client_access( $client, $input, true );
1391 - if ( is_wp_error( $access ) ) {
1392 - return $access;
1393 1180 }
1394 1181
1395 1182 $updated = MainWP_DB_Client::instance()->update_client(
1396 1183 array(